Initial commit: add compliance_checks table, per-check metadata on assets, and compliance audit trail

This commit is contained in:
2026-04-24 07:14:27 +03:00
commit e63b486ec2
58 changed files with 6468 additions and 0 deletions

View File

@@ -0,0 +1,453 @@
{% extends 'base.html' %}
{% block title %}{{ asset.serial_number }} IT Asset Management{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('assets.index') }}">Assets</a></li>
<li class="breadcrumb-item active">{{ asset.serial_number }}</li>
{% endblock %}
{% block content %}
<div class="page-header d-flex align-items-center justify-content-between mb-4">
<h1>
<i class="bi bi-laptop me-2"></i>{{ asset.brand or '' }} {{ asset.model or '' }}
<span class="badge badge-{{ asset.status }} fs-6 align-middle ms-2">{{ asset.status | title }}</span>
</h1>
<div class="d-flex gap-2">
<a href="{{ url_for('assets.edit', asset_id=asset.id) }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil me-1"></i>Edit
</a>
{% if asset.status == 'available' %}
<a href="{{ url_for('assignments.create', asset_id=asset.id) }}" class="btn btn-sm btn-outline-success">
<i class="bi bi-plus-circle me-1"></i>Assign
</a>
{% endif %}
{% if asset.current_user %}
<a href="{{ url_for('paperwork.create', asset_id=asset.id, user_id=asset.current_user.id) }}"
class="btn btn-sm btn-outline-info">
<i class="bi bi-file-earmark-plus me-1"></i>New Doc
</a>
{% endif %}
</div>
</div>
<div class="row g-3">
<!-- Asset details -->
<div class="col-md-4">
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white fw-semibold py-3">
<i class="bi bi-info-circle me-2 text-primary"></i>Asset Details
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-5 text-muted small">Type</dt>
<dd class="col-7"><span class="badge bg-secondary">{{ asset.asset_type }}</span></dd>
<dt class="col-5 text-muted small">Brand</dt>
<dd class="col-7">{{ asset.brand or '—' }}</dd>
<dt class="col-5 text-muted small">Model</dt>
<dd class="col-7">{{ asset.model or '—' }}</dd>
<dt class="col-5 text-muted small">Serial No.</dt>
<dd class="col-7"><code>{{ asset.serial_number }}</code></dd>
<dt class="col-5 text-muted small">Service Tag</dt>
<dd class="col-7"><code>{{ asset.service_tag or '—' }}</code></dd>
<dt class="col-5 text-muted small">Asset Tag</dt>
<dd class="col-7">{{ asset.asset_tag or '—' }}</dd>
<dt class="col-5 text-muted small">OS</dt>
<dd class="col-7">{{ asset.operating_system or '—' }}</dd>
{% if asset.processor %}
<dt class="col-5 text-muted small">CPU</dt>
<dd class="col-7">{{ asset.processor }}</dd>
{% endif %}
{% if asset.ram_gb %}
<dt class="col-5 text-muted small">RAM</dt>
<dd class="col-7">{{ asset.ram_gb }} GB</dd>
{% endif %}
{% if asset.storage_gb %}
<dt class="col-5 text-muted small">Storage</dt>
<dd class="col-7">{{ asset.storage_gb }} GB</dd>
{% endif %}
{% if asset.mac_address %}
<dt class="col-5 text-muted small">MAC</dt>
<dd class="col-7"><code>{{ asset.mac_address }}</code></dd>
{% endif %}
</dl>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header bg-white fw-semibold py-3">
<i class="bi bi-receipt me-2 text-primary"></i>Procurement
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-5 text-muted small">Purchased</dt>
<dd class="col-7">{{ asset.purchase_date.strftime('%d/%m/%Y') if asset.purchase_date else '—' }}</dd>
<dt class="col-5 text-muted small">Warranty</dt>
<dd class="col-7">{{ asset.warranty_expiry.strftime('%d/%m/%Y') if asset.warranty_expiry else '—' }}</dd>
<dt class="col-5 text-muted small">Price</dt>
<dd class="col-7">{{ '%.2f'|format(asset.purchase_price) if asset.purchase_price else '—' }}</dd>
<dt class="col-5 text-muted small">Supplier</dt>
<dd class="col-7">{{ asset.supplier or '—' }}</dd>
<dt class="col-5 text-muted small">PO #</dt>
<dd class="col-7">{{ asset.po_number or '—' }}</dd>
<dt class="col-5 text-muted small">Location</dt>
<dd class="col-7">{{ asset.location or '—' }}</dd>
</dl>
{% if asset.notes %}
<hr class="my-2">
<p class="small text-muted mb-0">{{ asset.notes }}</p>
{% endif %}
</div>
</div>
</div>
<!-- History + Docs -->
<div class="col-md-8">
<!-- Assignment history -->
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white fw-semibold py-3 d-flex justify-content-between align-items-center">
<span><i class="bi bi-clock-history me-2 text-primary"></i>Assignment History</span>
</div>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<thead class="table-light">
<tr>
<th>User</th>
<th>Windows ID</th>
<th>From</th>
<th>Until</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{% for a in history %}
<tr {% if a.user.is_masked %}class="masked-row"{% endif %}>
<td>{{ a.user.display_name }}</td>
<td><code>{{ a.user.windows_id }}</code></td>
<td>{{ a.assigned_date.strftime('%d/%m/%Y') if a.assigned_date else '—' }}</td>
<td>{{ a.returned_date.strftime('%d/%m/%Y') if a.returned_date else '—' }}</td>
<td>
{% if a.is_active %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-secondary">Returned</span>
{% endif %}
</td>
<td>
{% if a.is_active %}
<button class="btn btn-sm btn-outline-warning py-0 px-2"
data-bs-toggle="modal" data-bs-target="#returnModal{{ a.id }}">
<i class="bi bi-arrow-return-left"></i> Return
</button>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="6" class="text-center text-muted py-3">No assignment history.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Documents -->
<div class="card border-0 shadow-sm">
<div class="card-header bg-white fw-semibold py-3 d-flex justify-content-between align-items-center">
<span><i class="bi bi-file-earmark-text me-2 text-primary"></i>Documents</span>
{% if asset.current_user %}
<a href="{{ url_for('paperwork.create', asset_id=asset.id, user_id=asset.current_user.id) }}"
class="btn btn-sm btn-outline-info py-0 px-2">
<i class="bi bi-plus"></i> New
</a>
{% endif %}
</div>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<thead class="table-light">
<tr><th>Title</th><th>Type</th><th>User</th><th>Date</th><th></th></tr>
</thead>
<tbody>
{% for d in docs %}
<tr>
<td><a href="{{ url_for('paperwork.detail', doc_id=d.id) }}">{{ d.title }}</a></td>
<td><span class="badge bg-info text-dark">{{ d.doc_type_label }}</span></td>
<td>{{ d.user.display_name }}</td>
<td>{{ d.created_at.strftime('%d/%m/%Y') if d.created_at else '—' }}</td>
<td>
{% if d.pdf_filename %}
<a href="{{ url_for('paperwork.download', doc_id=d.id) }}"
class="btn btn-sm btn-outline-secondary py-0 px-2">
<i class="bi bi-download"></i>
</a>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="5" class="text-center text-muted py-3">No documents.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% if asset.asset_type in ('Laptop', 'Desktop') %}
<!-- Compliance card — Laptop / Desktop only -->
<div class="card border-0 shadow-sm mt-3">
<div class="card-header bg-white fw-semibold py-3 d-flex justify-content-between align-items-center">
<span><i class="bi bi-shield-check me-2 text-success"></i>IT Compliance &amp; Inventory</span>
<button class="btn btn-sm btn-outline-primary" type="button"
data-bs-toggle="collapse" data-bs-target="#complianceEdit">
<i class="bi bi-pencil me-1"></i>Edit
</button>
</div>
<!-- Read-only summary -->
<div class="card-body pb-2">
<div class="row g-2">
<div class="col-md-4 col-6">
<div class="small text-muted">Inventory #</div>
<div class="fw-semibold">{{ asset.inventory_number or '—' }}</div>
</div>
<div class="col-md-4 col-6">
<div class="small text-muted">AD Device Name</div>
<div class="fw-semibold"><code>{{ asset.ad_device_name or '—' }}</code></div>
</div>
<div class="col-md-4 col-12">
<div class="small text-muted">Location Note</div>
<div class="fw-semibold">{{ asset.location_note or '—' }}</div>
</div>
<div class="col-md-4 col-4 mt-2">
{% if asset.encryption_checked %}
<span class="badge bg-success"><i class="bi bi-lock-fill me-1"></i>Encrypted</span>
{% else %}
<span class="badge bg-danger"><i class="bi bi-lock me-1"></i>Not Encrypted</span>
{% endif %}
{% if asset.encryption_checked_by %}
<div class="small text-muted mt-1">
by <strong>{{ asset.encryption_checked_by.username }}</strong>
{% if asset.encryption_checked_at %}
&mdash; {{ asset.encryption_checked_at.strftime('%d/%m/%Y %H:%M') }}
{% endif %}
</div>
{% endif %}
</div>
<div class="col-md-4 col-4 mt-2">
{% if asset.backup_checked %}
<span class="badge bg-success"><i class="bi bi-cloud-check me-1"></i>Backup OK</span>
{% else %}
<span class="badge bg-warning text-dark"><i class="bi bi-cloud me-1"></i>No Backup</span>
{% endif %}
{% if asset.backup_checked_by %}
<div class="small text-muted mt-1">
by <strong>{{ asset.backup_checked_by.username }}</strong>
{% if asset.backup_checked_at %}
&mdash; {{ asset.backup_checked_at.strftime('%d/%m/%Y %H:%M') }}
{% endif %}
</div>
{% endif %}
</div>
<div class="col-md-4 col-4 mt-2">
{% if asset.hr_notified %}
<span class="badge bg-success"><i class="bi bi-person-check me-1"></i>HR Notified</span>
{% else %}
<span class="badge bg-secondary"><i class="bi bi-person me-1"></i>HR Pending</span>
{% endif %}
{% if asset.hr_notified_by %}
<div class="small text-muted mt-1">
by <strong>{{ asset.hr_notified_by.username }}</strong>
{% if asset.hr_notified_at %}
&mdash; {{ asset.hr_notified_at.strftime('%d/%m/%Y %H:%M') }}
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
<!-- Collapsible edit form -->
<div class="collapse" id="complianceEdit">
<div class="card-body border-top pt-3">
<form method="POST" action="{{ url_for('assets.update_compliance', asset_id=asset.id) }}">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label small fw-semibold">Inventory Number</label>
<input type="text" name="inventory_number" class="form-control form-control-sm"
value="{{ asset.inventory_number or '' }}" placeholder="INV-0001">
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold">AD Device Name</label>
<input type="text" name="ad_device_name" class="form-control form-control-sm"
value="{{ asset.ad_device_name or '' }}" placeholder="DESKTOP-AB1234">
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold">Current User in AD</label>
{% if asset.current_user %}
<div class="form-control form-control-sm bg-light text-muted">
{{ asset.current_user.display_name }} ({{ asset.current_user.windows_id }})
</div>
{% else %}
<div class="form-control form-control-sm bg-light text-muted">Not assigned</div>
{% endif %}
</div>
<div class="col-12">
<label class="form-label small fw-semibold">Location Note</label>
<textarea name="location_note" class="form-control form-control-sm" rows="2"
placeholder="e.g. Building A, Room 102, Desk 5">{{ asset.location_note or '' }}</textarea>
</div>
<div class="col-12">
<div class="d-flex gap-4 flex-wrap mt-1">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="encryption_checked"
id="chkEncrypt" value="1"
{% if asset.encryption_checked %}checked{% endif %}>
<label class="form-check-label fw-semibold" for="chkEncrypt">
<i class="bi bi-lock-fill me-1 text-success"></i>Encryption Verified
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="backup_checked"
id="chkBackup" value="1"
{% if asset.backup_checked %}checked{% endif %}>
<label class="form-check-label fw-semibold" for="chkBackup">
<i class="bi bi-cloud-check me-1 text-primary"></i>Backup Configured
</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="hr_notified"
id="chkHR" value="1"
{% if asset.hr_notified %}checked{% endif %}>
<label class="form-check-label fw-semibold" for="chkHR">
<i class="bi bi-person-check me-1 text-warning"></i>HR Send / Notified
</label>
</div>
</div>
</div>
<div class="col-12">
<label class="form-label small fw-semibold" for="compliance_notes">
<i class="bi bi-chat-left-text me-1 text-secondary"></i>Note
<span class="text-muted fw-normal">(reason for check / uncheck — saved with each change)</span>
</label>
<textarea name="compliance_notes" id="compliance_notes"
class="form-control form-control-sm" rows="2"
placeholder="e.g. BitLocker verified on site visit, backup re-enabled after restore…"></textarea>
</div>
</div>
<div class="d-flex gap-2 mt-3">
<button type="submit" class="btn btn-sm btn-success">
<i class="bi bi-check2 me-1"></i>Save Changes
</button>
<button type="button" class="btn btn-sm btn-outline-secondary"
data-bs-toggle="collapse" data-bs-target="#complianceEdit">
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Compliance change history -->
{% if compliance_log %}
<div class="card border-0 shadow-sm mt-2">
<div class="card-header bg-white fw-semibold py-3 d-flex justify-content-between align-items-center">
<!-- Compliance per-check history -->
{% if check_history %}
<div class="card border-0 shadow-sm mt-2">
<div class="card-header bg-white fw-semibold py-3 d-flex justify-content-between align-items-center">
<span><i class="bi bi-shield-exclamation me-2 text-secondary"></i>Compliance Check History</span>
<button class="btn btn-sm btn-outline-secondary" type="button"
data-bs-toggle="collapse" data-bs-target="#checkHistory">
Show / Hide
</button>
</div>
<div class="collapse" id="checkHistory">
<div class="table-responsive">
<table class="table table-sm table-hover mb-0 small">
<thead class="table-light">
<tr>
<th style="width:160px">Date &amp; Time</th>
<th style="width:140px">Check</th>
<th style="width:90px">Result</th>
<th style="width:130px">Performed by</th>
<th>Note</th>
</tr>
</thead>
<tbody>
{% for entry in check_history %}
<tr>
<td class="text-nowrap">{{ entry.performed_at.strftime('%d/%m/%Y %H:%M') }}</td>
<td>{{ entry.check_type_label }}</td>
<td>
{% if entry.checked %}
<span class="badge bg-success">Verified</span>
{% else %}
<span class="badge bg-danger">Cleared</span>
{% endif %}
</td>
<td>
{% if entry.performed_by %}
<span class="fw-semibold">{{ entry.performed_by.username }}</span>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td>{{ entry.notes or '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
{% endif %}{# end asset_type in Laptop/Desktop #}
<!-- Return modals -->
{% for a in history %}{% if a.is_active %}
<div class="modal fade" id="returnModal{{ a.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Return Asset</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST" action="{{ url_for('assignments.return_asset', assignment_id=a.id) }}">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Return Date</label>
<input type="date" name="returned_date" class="form-control"
value="{{ today_date }}" required>
</div>
<div class="mb-3">
<label class="form-label">Notes (optional)</label>
<textarea name="return_notes" class="form-control" rows="2"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-warning">Confirm Return</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}{% endfor %}
{% endblock %}

View File

@@ -0,0 +1,160 @@
{% extends 'base.html' %}
{% block title %}{{ 'Edit Asset' if asset else 'New Asset' }} IT Asset Management{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('assets.index') }}">Assets</a></li>
<li class="breadcrumb-item active">{{ 'Edit' if asset else 'New Asset' }}</li>
{% endblock %}
{% block content %}
<div class="page-header mb-4">
<h1><i class="bi bi-laptop me-2"></i>{{ 'Edit Asset' if asset else 'Add Asset' }}</h1>
</div>
<div class="card border-0 shadow-sm" style="max-width:800px;">
<div class="card-body">
{% if not asset and prefill and prefill.service_tag %}
<div class="alert alert-info py-2 mb-3 small">
<i class="bi bi-cloud-check me-1"></i>
Pre-filled from Dell service tag <strong>{{ prefill.service_tag }}</strong>. Review the details below before saving.
</div>
{% endif %}
<form method="POST" action="{{ url_for('assets.edit', asset_id=asset.id) if asset else url_for('assets.create') }}">
<h6 class="text-uppercase text-muted mb-3 small">Identifiers</h6>
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label">Serial Number <span class="text-danger">*</span></label>
<input type="text" name="serial_number" class="form-control"
value="{{ asset.serial_number if asset else (prefill.serial_number if prefill else '') }}" required>
</div>
<div class="col-md-4">
<label class="form-label">Service Tag</label>
<input type="text" name="service_tag" class="form-control"
value="{{ asset.service_tag or '' if asset else (prefill.service_tag if prefill else '') }}"
placeholder="e.g. Dell service tag">
</div>
<div class="col-md-4">
<label class="form-label">Asset Tag</label>
<input type="text" name="asset_tag" class="form-control"
value="{{ asset.asset_tag or '' if asset else '' }}"
placeholder="Internal barcode/tag">
</div>
</div>
<h6 class="text-uppercase text-muted mb-3 small">Classification</h6>
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label">Asset Type <span class="text-danger">*</span></label>
<select name="asset_type" class="form-select" required>
{% for t in asset_types %}
<option value="{{ t }}"
{% if asset and asset.asset_type == t %}selected
{% elif not asset and prefill and prefill.asset_type == t %}selected
{% elif not asset and not prefill and t == 'Laptop' %}selected
{% endif %}>{{ t }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="form-label">Brand</label>
<input type="text" name="brand" class="form-control"
value="{{ asset.brand or '' if asset else (prefill.brand if prefill else '') }}"
placeholder="e.g. Dell, Lenovo, HP">
</div>
<div class="col-md-4">
<label class="form-label">Model</label>
<input type="text" name="model" class="form-control"
value="{{ asset.model or '' if asset else (prefill.model if prefill else '') }}"
placeholder="e.g. Latitude 5540">
</div>
</div>
<h6 class="text-uppercase text-muted mb-3 small">Technical Specs</h6>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label">Processor</label>
<input type="text" name="processor" class="form-control"
value="{{ asset.processor or '' if asset else '' }}"
placeholder="e.g. Intel Core i5-1345U">
</div>
<div class="col-md-3">
<label class="form-label">RAM (GB)</label>
<input type="number" name="ram_gb" class="form-control" min="0"
value="{{ asset.ram_gb or '' if asset else '' }}">
</div>
<div class="col-md-3">
<label class="form-label">Storage (GB)</label>
<input type="number" name="storage_gb" class="form-control" min="0"
value="{{ asset.storage_gb or '' if asset else '' }}">
</div>
<div class="col-md-4">
<label class="form-label">Operating System</label>
<input type="text" name="operating_system" class="form-control"
value="{{ asset.operating_system or '' if asset else (prefill.operating_system if prefill else 'Windows 11 Pro') }}"
placeholder="e.g. Windows 11 Pro">
</div>
<div class="col-md-4">
<label class="form-label">MAC Address</label>
<input type="text" name="mac_address" class="form-control"
value="{{ asset.mac_address or '' if asset else '' }}">
</div>
<div class="col-md-4">
<label class="form-label">Location</label>
<input type="text" name="location" class="form-control"
value="{{ asset.location or '' if asset else '' }}">
</div>
</div>
<h6 class="text-uppercase text-muted mb-3 small">Procurement</h6>
<div class="row g-3 mb-3">
<div class="col-md-3">
<label class="form-label">Purchase Date</label>
<input type="date" name="purchase_date" class="form-control"
value="{{ asset.purchase_date.isoformat() if asset and asset.purchase_date else (prefill.purchase_date if prefill else '') }}">
</div>
<div class="col-md-3">
<label class="form-label">Warranty Expiry</label>
<input type="date" name="warranty_expiry" class="form-control"
value="{{ asset.warranty_expiry.isoformat() if asset and asset.warranty_expiry else (prefill.warranty_expiry if prefill else '') }}">
</div>
<div class="col-md-3">
<label class="form-label">Purchase Price</label>
<input type="number" name="purchase_price" class="form-control" step="0.01" min="0"
value="{{ asset.purchase_price or '' if asset else '' }}">
</div>
<div class="col-md-3">
<label class="form-label">Status</label>
<select name="status" class="form-select">
{% for val, label in asset_statuses %}
<option value="{{ val }}" {% if asset and asset.status == val %}selected{% elif not asset and val == 'available' %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="form-label">Supplier</label>
<input type="text" name="supplier" class="form-control"
value="{{ asset.supplier or '' if asset else '' }}">
</div>
<div class="col-md-4">
<label class="form-label">PO Number</label>
<input type="text" name="po_number" class="form-control"
value="{{ asset.po_number or '' if asset else '' }}">
</div>
<div class="col-12">
<label class="form-label">Notes</label>
<textarea name="notes" class="form-control" rows="2">{{ asset.notes or '' if asset else '' }}</textarea>
</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg me-1"></i>{{ 'Save Changes' if asset else 'Create Asset' }}
</button>
<a href="{{ url_for('assets.detail', asset_id=asset.id) if asset else url_for('assets.index') }}"
class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,300 @@
{% extends 'base.html' %}
{% block title %}Assets IT Asset Management{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item active">Assets</li>
{% endblock %}
{% block content %}
<div class="page-header d-flex align-items-center justify-content-between mb-4">
<h1><i class="bi bi-laptop me-2"></i>Assets</h1>
<a href="{{ url_for('assets.create') }}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-circle me-1"></i>Add Asset
</a>
</div>
<!-- Dell Service Tag Quick Import -->
<div class="card border-0 shadow-sm mb-4" id="dellLookupCard">
<div class="card-body py-3">
<div class="d-flex align-items-center gap-2 flex-wrap">
<span class="fw-semibold text-nowrap">
<i class="bi bi-search me-1 text-primary"></i>Dell Quick Import
</span>
<span class="text-muted small text-nowrap">Enter a service tag to open the asset form pre-filled:</span>
<div class="input-group input-group-sm" style="max-width:220px;">
<input type="text" id="dellTagInput" class="form-control text-uppercase"
placeholder="e.g. ABC1234" maxlength="20"
style="text-transform:uppercase; letter-spacing:.05em;">
<button class="btn btn-outline-primary" id="dellLookupBtn" type="button">
<i class="bi bi-cloud-download me-1"></i>Lookup
</button>
</div>
<div id="dellLookupSpinner" class="spinner-border spinner-border-sm text-primary d-none" role="status">
<span class="visually-hidden">Loading…</span>
</div>
<span class="text-muted small ms-auto">
<i class="bi bi-info-circle me-1"></i>
Full auto-fill available with a
<a href="https://tdm.dell.com" target="_blank" class="text-decoration-none">free Dell TechDirect API key</a>
</span>
</div>
<!-- Result preview (hidden until data arrives) -->
<div id="dellLookupResult" class="mt-3 d-none">
<div class="alert mb-2 py-2 d-flex align-items-start gap-3" id="dellResultBody">
<i class="bi bi-pc-display-horizontal fs-4 flex-shrink-0 mt-1" id="dellResultIcon"></i>
<div class="flex-grow-1">
<div class="fw-semibold mb-1" id="dellResultTitle"></div>
<div class="row row-cols-2 row-cols-md-4 g-1 small" id="dellResultMeta"></div>
</div>
<div class="d-flex flex-column gap-1 flex-shrink-0">
<a id="dellCreateBtn" href="#" class="btn btn-sm btn-primary">
<i class="bi bi-plus-circle me-1"></i>Create Asset
</a>
<a id="dellSupportLink" href="#" target="_blank" class="btn btn-sm btn-outline-secondary d-none">
<i class="bi bi-box-arrow-up-right me-1"></i>View on Dell
</a>
</div>
</div>
</div>
<!-- Error area -->
<div id="dellLookupError" class="alert alert-warning mt-2 py-2 d-none small mb-0"></div>
</div>
</div>
<!-- Filters -->
<form method="GET" class="row g-2 mb-3">
<div class="col-md-4">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" name="q" class="form-control" placeholder="Search SN, service tag, brand, model…" value="{{ q }}">
</div>
</div>
<div class="col-md-2">
<select name="status" class="form-select form-select-sm">
<option value="">All statuses</option>
{% for val, label in asset_statuses %}
<option value="{{ val }}" {% if status_filter == val %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<select name="asset_type" class="form-select form-select-sm">
<option value="">All types</option>
{% for t in asset_types %}
<option value="{{ t }}" {% if type_filter == t %}selected{% endif %}>{{ t }}</option>
{% endfor %}
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary">Filter</button>
<a href="{{ url_for('assets.index') }}" class="btn btn-sm btn-outline-secondary">Clear</a>
</div>
</form>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Type</th>
<th>Brand / Model</th>
<th>Serial Number</th>
<th>Service Tag</th>
<th>Status</th>
<th>Assigned To</th>
<th>Warranty</th>
<th></th>
</tr>
</thead>
<tbody>
{% for a in pagination.items %}
<tr>
<td><span class="badge bg-secondary">{{ a.asset_type }}</span></td>
<td>{{ a.brand or '' }} {{ a.model or '' }}</td>
<td><code>{{ a.serial_number }}</code></td>
<td><code>{{ a.service_tag or '—' }}</code></td>
<td>
<span class="badge badge-{{ a.status }}">{{ a.status | title }}</span>
</td>
<td>
{% if a.current_user %}
<a href="{{ url_for('users.detail', user_id=a.current_user.id) }}">
{{ a.current_user.display_name }}
</a>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td>
{% if a.warranty_expiry %}
<span class="{% if a.warranty_expiry < today %}text-danger{% else %}text-success{% endif %}">
{{ a.warranty_expiry.strftime('%d/%m/%Y') }}
</span>
{% else %}—{% endif %}
</td>
<td>
<a href="{{ url_for('assets.detail', asset_id=a.id) }}"
class="btn btn-sm btn-outline-secondary py-0 px-2">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
{% else %}
<tr><td colspan="8" class="text-center text-muted py-4">No assets found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if pagination.pages > 1 %}
<div class="card-footer bg-white d-flex justify-content-between align-items-center py-2">
<small class="text-muted">Showing {{ pagination.first }}{{ pagination.last }} of {{ pagination.total }}</small>
<nav>
<ul class="pagination pagination-sm mb-0">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('assets.index', page=pagination.prev_num, q=q, status=status_filter, asset_type=type_filter) }}"></a>
</li>
{% endif %}
{% for p in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
{% if p %}
<li class="page-item {% if p == pagination.page %}active{% endif %}">
<a class="page-link" href="{{ url_for('assets.index', page=p, q=q, status=status_filter, asset_type=type_filter) }}">{{ p }}</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link"></span></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('assets.index', page=pagination.next_num, q=q, status=status_filter, asset_type=type_filter) }}"></a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
</div>
{% endblock %}
{% block extra_head %}
<script>
// make today available for warranty colour
const today = new Date().toISOString().slice(0, 10);
</script>
{% endblock %}
{% block extra_js %}
<script>
(function () {
const input = document.getElementById('dellTagInput');
const btn = document.getElementById('dellLookupBtn');
const spinner = document.getElementById('dellLookupSpinner');
const result = document.getElementById('dellLookupResult');
const errBox = document.getElementById('dellLookupError');
const title = document.getElementById('dellResultTitle');
const meta = document.getElementById('dellResultMeta');
const createBtn = document.getElementById('dellCreateBtn');
function setLoading(on) {
btn.disabled = on;
spinner.classList.toggle('d-none', !on);
}
function showError(msg) {
errBox.textContent = msg;
errBox.classList.remove('d-none');
result.classList.add('d-none');
}
function doLookup() {
const tag = input.value.trim().toUpperCase();
if (!tag) { input.focus(); return; }
result.classList.add('d-none');
errBox.classList.add('d-none');
setLoading(true);
fetch(`{{ url_for('assets.dell_lookup') }}?tag=${encodeURIComponent(tag)}`)
.then(r => r.json().then(d => ({ ok: r.ok, status: r.status, data: d })))
.then(({ ok, status, data }) => {
setLoading(false);
if (!ok) {
if (status === 409 && data.existing_id) {
errBox.innerHTML = `${data.error} — <a href="/assets/${data.existing_id}">View asset</a>`;
errBox.classList.remove('d-none');
} else {
showError(data.error || 'Lookup failed.');
}
return;
}
// Build preview
const isPartial = data.source === 'partial';
const resultBody = document.getElementById('dellResultBody');
const resultIcon = document.getElementById('dellResultIcon');
const supportLink = document.getElementById('dellSupportLink');
resultBody.className = `alert mb-2 py-2 d-flex align-items-start gap-3 ${isPartial ? 'alert-warning' : 'alert-info'}`;
resultIcon.className = `bi bi-pc-display-horizontal fs-4 flex-shrink-0 mt-1 ${isPartial ? 'text-warning' : 'text-primary'}`;
if (isPartial) {
// Auto-open Dell's warranty page in a new tab so the user can read model + warranty
if (data.support_url) window.open(data.support_url, '_blank', 'noopener');
title.innerHTML = `Dell service tag <strong>${data.service_tag}</strong> &mdash; Dell&rsquo;s page opened in a new tab. Copy model &amp; warranty date into the form below.`;
meta.innerHTML = `
<div><span class="fw-medium">Brand:</span> Dell</div>
<div><span class="fw-medium">OS:</span> ${data.operating_system}</div>
<div><span class="fw-medium">Model:</span> <em class="text-muted">fill from Dell tab &rarr;</em></div>
<div><span class="fw-medium">Warranty:</span> <em class="text-muted">fill from Dell tab &rarr;</em></div>`;
supportLink.href = data.support_url;
supportLink.classList.remove('d-none');
createBtn.textContent = '';
createBtn.innerHTML = '<i class="bi bi-plus-circle me-1"></i>Open Form';
} else {
title.textContent = `Dell ${data.model || data.service_tag}`;
supportLink.href = data.support_url || '#';
supportLink.classList.remove('d-none');
const fields = [
['Type', data.asset_type],
['Service Tag', data.service_tag],
['Serial', data.serial_number || '—'],
['Warranty', data.warranty_expiry || '—'],
['Purchased', data.purchase_date || '—'],
['OS', data.operating_system],
];
meta.innerHTML = fields
.map(([k, v]) => `<div><span class="fw-medium">${k}:</span> ${v || '—'}</div>`)
.join('');
}
// Build "Create Asset" URL with pre-filled params
const params = new URLSearchParams({
service_tag: data.service_tag || '',
serial_number: data.serial_number || '',
brand: data.brand || 'Dell',
model: data.model || '',
asset_type: data.asset_type || '',
operating_system: data.operating_system || '',
warranty_expiry: data.warranty_expiry || '',
purchase_date: data.purchase_date || '',
});
createBtn.href = `{{ url_for('assets.create') }}?${params.toString()}`;
result.classList.remove('d-none');
})
.catch(() => {
setLoading(false);
showError('Network error could not reach the server.');
});
}
btn.addEventListener('click', doLookup);
input.addEventListener('keydown', e => { if (e.key === 'Enter') doLookup(); });
input.addEventListener('input', () => {
input.value = input.value.toUpperCase();
});
})();
</script>
{% endblock %}

View File

@@ -0,0 +1,133 @@
{% extends 'base.html' %}
{% block title %}Assign Asset IT Asset Management{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('assignments.index') }}">Assignments</a></li>
<li class="breadcrumb-item active">New Assignment</li>
{% endblock %}
{% block content %}
<div class="page-header mb-4">
<h1><i class="bi bi-arrow-left-right me-2"></i>Assign Asset to User</h1>
</div>
<div class="card border-0 shadow-sm" style="max-width:600px;">
<div class="card-body">
<form method="POST" action="{{ url_for('assignments.create') }}">
<div class="mb-3">
<label class="form-label">User <span class="text-danger">*</span></label>
<input type="hidden" name="user_id" id="userId" value="{{ preselect_user_id or '' }}">
<input type="text" id="userSearch" class="form-control"
placeholder="Search by name or Windows ID…"
value="" autocomplete="off">
<div id="userDropdown" class="list-group position-absolute shadow" style="z-index:1000;display:none;min-width:350px;"></div>
<div id="userDisplay" class="form-text text-success fw-semibold"></div>
</div>
<div class="mb-3">
<label class="form-label">Asset <span class="text-danger">*</span></label>
<input type="hidden" name="asset_id" id="assetId" value="{{ preselect_asset_id or '' }}">
<input type="text" id="assetSearch" class="form-control"
placeholder="Search by serial number or service tag…"
value="" autocomplete="off">
<div id="assetDropdown" class="list-group position-absolute shadow" style="z-index:1000;display:none;min-width:350px;"></div>
<div id="assetDisplay" class="form-text text-success fw-semibold"></div>
</div>
<div class="mb-3">
<label class="form-label">Assigned Date</label>
<input type="date" name="assigned_date" class="form-control" id="assignedDate">
</div>
<div class="mb-4">
<label class="form-label">Notes</label>
<textarea name="notes" class="form-control" rows="2"></textarea>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg me-1"></i>Create Assignment
</button>
<a href="{{ url_for('assignments.index') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Set today's date as default
document.getElementById('assignedDate').value = new Date().toISOString().slice(0,10);
// ── Generic live-search helper ───────────────────────────────────
function liveSearch(inputId, dropdownId, hiddenId, displayId, endpoint, labelField) {
const input = document.getElementById(inputId);
const dropdown = document.getElementById(dropdownId);
const hidden = document.getElementById(hiddenId);
const display = document.getElementById(displayId);
let timer;
input.addEventListener('input', () => {
clearTimeout(timer);
timer = setTimeout(() => {
const q = input.value.trim();
if (q.length < 2) { dropdown.style.display = 'none'; return; }
fetch(`${endpoint}?q=${encodeURIComponent(q)}`)
.then(r => r.json())
.then(items => {
dropdown.innerHTML = '';
if (!items.length) { dropdown.style.display = 'none'; return; }
items.forEach(item => {
const a = document.createElement('a');
a.className = 'list-group-item list-group-item-action py-2';
a.textContent = item.text || item[labelField];
// colour disabled assets
if (item.status && item.status !== 'available') {
a.className += ' text-muted';
a.textContent += ` [${item.status}]`;
}
a.addEventListener('click', () => {
hidden.value = item.id;
input.value = item.text || item[labelField];
display.textContent = '✓ Selected: ' + (item.windows_id || item.serial_number || '');
dropdown.style.display = 'none';
});
dropdown.appendChild(a);
});
dropdown.style.display = 'block';
});
}, 250);
});
document.addEventListener('click', e => {
if (!input.contains(e.target)) dropdown.style.display = 'none';
});
}
liveSearch('userSearch', 'userDropdown', 'userId', 'userDisplay',
'{{ url_for("users.search") }}', 'text');
liveSearch('assetSearch', 'assetDropdown', 'assetId', 'assetDisplay',
'{{ url_for("assets.search") }}', 'text');
// Pre-fill labels if IDs were passed via URL
{% if preselect_user_id %}
fetch('{{ url_for("users.search") }}?q={{ preselect_user_id }}')
.then(r => r.json()).then(items => {
if (items.length) {
document.getElementById('userSearch').value = items[0].text;
document.getElementById('userDisplay').textContent = '✓ Pre-selected';
}
});
{% endif %}
{% if preselect_asset_id %}
fetch('{{ url_for("assets.search") }}?q={{ preselect_asset_id }}')
.then(r => r.json()).then(items => {
if (items.length) {
document.getElementById('assetSearch').value = items[0].text;
document.getElementById('assetDisplay').textContent = '✓ Pre-selected';
}
});
{% endif %}
</script>
{% endblock %}

View File

@@ -0,0 +1,144 @@
{% extends 'base.html' %}
{% block title %}Assignments IT Asset Management{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item active">Assignments</li>
{% endblock %}
{% block content %}
<div class="page-header d-flex align-items-center justify-content-between mb-4">
<h1><i class="bi bi-arrow-left-right me-2"></i>Assignments</h1>
<a href="{{ url_for('assignments.create') }}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-circle me-1"></i>Assign Asset
</a>
</div>
<form method="GET" class="row g-2 mb-3">
<div class="col-auto">
<div class="form-check form-check-inline mt-1">
<input class="form-check-input" type="checkbox" name="active" value="0" id="chkAll"
{% if not active_only %}checked{% endif %} onchange="this.form.submit()">
<label class="form-check-label" for="chkAll">Show returned</label>
</div>
</div>
</form>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>User</th>
<th>Windows ID</th>
<th>Asset</th>
<th>Serial Number</th>
<th>Assigned</th>
<th>Returned</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{% for a in pagination.items %}
<tr {% if a.user.is_masked %}class="masked-row"{% endif %}>
<td>
<a href="{{ url_for('users.detail', user_id=a.user.id) }}">{{ a.user.display_name }}</a>
</td>
<td><code>{{ a.user.windows_id }}</code></td>
<td>
<a href="{{ url_for('assets.detail', asset_id=a.asset.id) }}">
{{ a.asset.brand or '' }} {{ a.asset.model or '' }}
</a>
</td>
<td><code>{{ a.asset.serial_number }}</code></td>
<td>{{ a.assigned_date.strftime('%d/%m/%Y') if a.assigned_date else '—' }}</td>
<td>{{ a.returned_date.strftime('%d/%m/%Y') if a.returned_date else '—' }}</td>
<td>
{% if a.is_active %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-secondary">Returned</span>
{% endif %}
</td>
<td>
{% if a.is_active %}
<button class="btn btn-sm btn-outline-warning py-0 px-2"
data-bs-toggle="modal" data-bs-target="#returnModal{{ a.id }}">
<i class="bi bi-arrow-return-left"></i>
</button>
{% endif %}
<a href="{{ url_for('paperwork.create', assignment_id=a.id, user_id=a.user.id, asset_id=a.asset.id) }}"
class="btn btn-sm btn-outline-info py-0 px-2" title="Create document">
<i class="bi bi-file-earmark-plus"></i>
</a>
</td>
</tr>
{% else %}
<tr><td colspan="8" class="text-center text-muted py-4">No assignments found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if pagination.pages > 1 %}
<div class="card-footer bg-white d-flex justify-content-between align-items-center py-2">
<small class="text-muted">Showing {{ pagination.first }}{{ pagination.last }} of {{ pagination.total }}</small>
<nav>
<ul class="pagination pagination-sm mb-0">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('assignments.index', page=pagination.prev_num, active='0' if not active_only else '1') }}"></a>
</li>
{% endif %}
{% for p in pagination.iter_pages() %}
{% if p %}
<li class="page-item {% if p == pagination.page %}active{% endif %}">
<a class="page-link" href="{{ url_for('assignments.index', page=p, active='0' if not active_only else '1') }}">{{ p }}</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link"></span></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('assignments.index', page=pagination.next_num, active='0' if not active_only else '1') }}"></a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
</div>
<!-- Return modals -->
{% for a in pagination.items %}{% if a.is_active %}
<div class="modal fade" id="returnModal{{ a.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Return Asset</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="POST" action="{{ url_for('assignments.return_asset', assignment_id=a.id) }}">
<div class="modal-body">
<p>Returning <strong>{{ a.asset.serial_number }}</strong> from
<strong>{{ a.user.display_name }}</strong>.</p>
<div class="mb-3">
<label class="form-label">Return Date</label>
<input type="date" name="returned_date" class="form-control" required>
</div>
<div class="mb-0">
<label class="form-label">Notes</label>
<textarea name="return_notes" class="form-control" rows="2"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-warning">Confirm Return</button>
</div>
</form>
</div>
</div>
</div>
{% endif %}{% endfor %}
{% endblock %}

View File

@@ -0,0 +1,99 @@
{% extends 'base.html' %}
{% block title %}Audit Log IT Asset Management{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item active">Audit Log</li>
{% endblock %}
{% block content %}
<div class="page-header mb-4">
<h1><i class="bi bi-shield-check me-2"></i>Audit Log</h1>
</div>
<form method="GET" class="row g-2 mb-3">
<div class="col-md-2">
<select name="table" class="form-select form-select-sm" onchange="this.form.submit()">
<option value="">All tables</option>
{% for t in tables %}
<option value="{{ t }}" {% if table_filter == t %}selected{% endif %}>{{ t }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<select name="action" class="form-select form-select-sm" onchange="this.form.submit()">
<option value="">All actions</option>
{% for a in actions %}
<option value="{{ a }}" {% if action_filter == a %}selected{% endif %}>{{ a }}</option>
{% endfor %}
</select>
</div>
<div class="col-auto">
<a href="{{ url_for('audit.index') }}" class="btn btn-sm btn-outline-secondary">Clear</a>
</div>
</form>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<thead class="table-light">
<tr>
<th>Date/Time</th>
<th>Performed By</th>
<th>Action</th>
<th>Table</th>
<th>Record</th>
<th>Description</th>
<th>IP</th>
</tr>
</thead>
<tbody>
{% for e in pagination.items %}
<tr>
<td class="text-nowrap">{{ e.performed_at.strftime('%d/%m/%Y %H:%M') if e.performed_at else '—' }}</td>
<td>{{ e.performed_by.username if e.performed_by else '<system>' }}</td>
<td>
{% set colours = {'create':'success','update':'primary','delete':'danger','mask':'purple','assign':'info','return':'warning','import':'secondary'} %}
<span class="badge bg-{{ colours.get(e.action, 'secondary') }}">{{ e.action }}</span>
</td>
<td><code>{{ e.table_name }}</code></td>
<td>{{ e.record_id or '—' }}</td>
<td>{{ e.description or '—' }}</td>
<td><small class="text-muted">{{ e.ip_address or '—' }}</small></td>
</tr>
{% else %}
<tr><td colspan="7" class="text-center text-muted py-4">No audit entries found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if pagination.pages > 1 %}
<div class="card-footer bg-white d-flex justify-content-between align-items-center py-2">
<small class="text-muted">Showing {{ pagination.first }}{{ pagination.last }} of {{ pagination.total }}</small>
<nav>
<ul class="pagination pagination-sm mb-0">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('audit.index', page=pagination.prev_num, table=table_filter, action=action_filter) }}"></a>
</li>
{% endif %}
{% for p in pagination.iter_pages() %}
{% if p %}
<li class="page-item {% if p == pagination.page %}active{% endif %}">
<a class="page-link" href="{{ url_for('audit.index', page=p, table=table_filter, action=action_filter) }}">{{ p }}</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link"></span></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('audit.index', page=pagination.next_num, table=table_filter, action=action_filter) }}"></a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login IT Asset Management</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
body { background: #1a3a5c; min-height: 100vh; display:flex; align-items:center; justify-content:center; }
.login-card { width: 380px; border-radius: .8rem; border: none;
box-shadow: 0 8px 32px rgba(0,0,0,.3); }
.login-brand { font-size: 1.1rem; font-weight: 700; color: #1a3a5c; }
</style>
</head>
<body>
<div class="card login-card p-4">
<div class="text-center mb-4">
<i class="bi bi-hdd-rack-fill text-primary" style="font-size:2.5rem;"></i>
<div class="login-brand mt-2">IT Asset Management</div>
<small class="text-muted">Sign in to continue</small>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}{% for cat, msg in messages %}
<div class="alert alert-{{ 'danger' if cat == 'error' else cat }} py-2">{{ msg }}</div>
{% endfor %}{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('auth.login') }}">
<div class="mb-3">
<label class="form-label">Username</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-person"></i></span>
<input type="text" name="username" class="form-control" required autofocus>
</div>
</div>
<div class="mb-4">
<label class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-lock"></i></span>
<input type="password" name="password" class="form-control" required>
</div>
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-box-arrow-in-right me-1"></i> Sign In
</button>
</form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

223
app/templates/base.html Normal file
View File

@@ -0,0 +1,223 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}IT Asset Management{% endblock %}</title>
<!-- Bootstrap 5 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
:root {
--sidebar-bg: #1a3a5c;
--sidebar-text: #cce0f5;
--sidebar-active: #2e6da4;
--accent: #2e86de;
}
body { background: #f0f4f8; font-size: .92rem; }
/* Sidebar */
#sidebar {
min-height: 100vh;
width: 240px;
background: var(--sidebar-bg);
position: fixed;
top: 0; left: 0;
display: flex; flex-direction: column;
z-index: 1000;
transition: width .2s;
}
#sidebar .brand {
padding: 1.1rem 1.2rem;
font-size: 1.05rem;
font-weight: 700;
color: #fff;
border-bottom: 1px solid rgba(255,255,255,.12);
letter-spacing: .02em;
}
#sidebar .brand small { font-weight: 400; font-size: .7rem; color: var(--sidebar-text); display:block; }
#sidebar .nav-section {
font-size: .68rem;
text-transform: uppercase;
letter-spacing: .08em;
color: rgba(255,255,255,.4);
padding: .75rem 1.2rem .25rem;
}
#sidebar .nav-link {
color: var(--sidebar-text);
padding: .45rem 1.2rem;
border-radius: 0;
display: flex; align-items: center; gap: .6rem;
font-size: .88rem;
}
#sidebar .nav-link:hover,
#sidebar .nav-link.active {
background: var(--sidebar-active);
color: #fff;
}
#sidebar .nav-link i { font-size: 1rem; width: 1.2rem; text-align: center; }
#sidebar .sidebar-footer {
margin-top: auto;
padding: .8rem 1.2rem;
border-top: 1px solid rgba(255,255,255,.12);
font-size: .8rem;
color: var(--sidebar-text);
}
/* Main content */
#main-wrapper { margin-left: 240px; min-height: 100vh; display: flex; flex-direction: column; }
#topbar {
background: #fff;
border-bottom: 1px solid #dde3ea;
padding: .55rem 1.5rem;
display: flex; align-items: center; justify-content: space-between;
position: sticky; top: 0; z-index: 900;
box-shadow: 0 1px 4px rgba(0,0,0,.06);
}
#topbar .breadcrumb { margin: 0; background: none; padding: 0; font-size: .85rem; }
#page-content { flex: 1; padding: 1.5rem; }
/* Cards */
.stat-card { border: none; border-radius: .6rem; box-shadow: 0 2px 8px rgba(0,0,0,.07); }
.stat-card .card-body { padding: 1.1rem 1.3rem; }
.stat-card .stat-icon { font-size: 2rem; opacity: .85; }
.stat-card .stat-value { font-size: 2rem; font-weight: 700; line-height: 1; }
.stat-card .stat-label { font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; opacity: .8; }
/* Badges */
.badge-available { background:#198754 !important; }
.badge-assigned { background:#0d6efd !important; }
.badge-maintenance{ background:#ffc107 !important; color:#000 !important; }
.badge-retired { background:#6c757d !important; }
.badge-lost { background:#dc3545 !important; }
.badge-masked { background:#6f42c1 !important; }
/* Tables */
.table-hover tbody tr:hover { background:#f5f8ff; }
/* Masked row */
tr.masked-row { opacity: .65; font-style: italic; }
/* Search bar */
.search-bar { max-width: 360px; }
/* Page header */
.page-header h1 { font-size: 1.35rem; font-weight: 700; color: #1a3a5c; margin: 0; }
</style>
{% block extra_head %}{% endblock %}
</head>
<body>
<!-- ===== SIDEBAR ===== -->
<nav id="sidebar">
<div class="brand">
<i class="bi bi-hdd-rack-fill me-2"></i>IT Assets
<small>Hardware Management</small>
</div>
<div class="nav-section">Main</div>
<a href="{{ url_for('dashboard.index') }}"
class="nav-link {% if request.endpoint == 'dashboard.index' %}active{% endif %}">
<i class="bi bi-speedometer2"></i> Dashboard
</a>
<div class="nav-section">People</div>
<a href="{{ url_for('users.index') }}"
class="nav-link {% if request.blueprint == 'users' %}active{% endif %}">
<i class="bi bi-people-fill"></i> Users
</a>
<a href="{{ url_for('users.import_page') }}"
class="nav-link {% if request.endpoint == 'users.import_page' %}active{% endif %}">
<i class="bi bi-cloud-download"></i> Import Users
</a>
<div class="nav-section">Hardware</div>
<a href="{{ url_for('assets.index') }}"
class="nav-link {% if request.blueprint == 'assets' %}active{% endif %}">
<i class="bi bi-laptop"></i> Assets
</a>
<a href="{{ url_for('assets.create') }}"
class="nav-link {% if request.endpoint == 'assets.create' %}active{% endif %}">
<i class="bi bi-plus-circle"></i> Add Asset
</a>
<div class="nav-section">Assignments</div>
<a href="{{ url_for('assignments.index') }}"
class="nav-link {% if request.blueprint == 'assignments' %}active{% endif %}">
<i class="bi bi-arrow-left-right"></i> Assignments
</a>
<a href="{{ url_for('assignments.create') }}"
class="nav-link {% if request.endpoint == 'assignments.create' %}active{% endif %}">
<i class="bi bi-plus-circle"></i> Assign Asset
</a>
<div class="nav-section">Documents</div>
<a href="{{ url_for('paperwork.index') }}"
class="nav-link {% if request.blueprint == 'paperwork' %}active{% endif %}">
<i class="bi bi-file-earmark-text"></i> Paperwork
</a>
<a href="{{ url_for('paperwork.create') }}"
class="nav-link {% if request.endpoint == 'paperwork.create' %}active{% endif %}">
<i class="bi bi-file-earmark-plus"></i> New Document
</a>
<a href="{{ url_for('doc_templates.index') }}"
class="nav-link {% if request.blueprint == 'doc_templates' %}active{% endif %}">
<i class="bi bi-file-earmark-word"></i> Templates
</a>
<div class="nav-section">System</div>
<a href="{{ url_for('audit.index') }}"
class="nav-link {% if request.blueprint == 'audit' %}active{% endif %}">
<i class="bi bi-shield-check"></i> Audit Log
</a>
<a href="{{ url_for('settings.index') }}"
class="nav-link {% if request.blueprint == 'settings' %}active{% endif %}">
<i class="bi bi-gear"></i> Settings
</a>
<div class="sidebar-footer">
<i class="bi bi-person-circle me-1"></i>
<strong>{{ current_user.username }}</strong>
<a href="{{ url_for('auth.logout') }}" class="ms-2 text-warning text-decoration-none">
<i class="bi bi-box-arrow-right"></i>
</a>
</div>
</nav>
<!-- ===== MAIN WRAPPER ===== -->
<div id="main-wrapper">
<!-- Topbar -->
<div id="topbar">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">{% block breadcrumb %}<li class="breadcrumb-item active">Home</li>{% endblock %}</ol>
</nav>
<div class="d-flex align-items-center gap-3">
<span class="text-muted" style="font-size:.8rem;">
<i class="bi bi-calendar3"></i>
{{ now.strftime('%d %b %Y') if now else '' }}
</span>
</div>
</div>
<!-- Flash messages -->
<div id="page-content">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for cat, msg in messages %}
<div class="alert alert-{{ 'danger' if cat == 'error' else cat }} alert-dismissible fade show mb-3" role="alert">
{{ msg }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,169 @@
{% extends 'base.html' %}
{% block title %}Dashboard IT Asset Management{% endblock %}
{% block breadcrumb %}<li class="breadcrumb-item active">Dashboard</li>{% endblock %}
{% block content %}
<div class="page-header mb-4">
<h1><i class="bi bi-speedometer2 me-2"></i>Dashboard</h1>
</div>
<!-- ── Stat Cards ──────────────────────────────────────────────── -->
<div class="row g-3 mb-4">
<!-- Users -->
<div class="col-6 col-md-3">
<div class="card stat-card text-white" style="background:#1a3a5c;">
<div class="card-body d-flex align-items-center gap-3">
<i class="bi bi-people-fill stat-icon"></i>
<div>
<div class="stat-value">{{ stats.active_users }}</div>
<div class="stat-label">Active Users</div>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card stat-card text-white" style="background:#6f42c1;">
<div class="card-body d-flex align-items-center gap-3">
<i class="bi bi-eye-slash-fill stat-icon"></i>
<div>
<div class="stat-value">{{ stats.masked_users }}</div>
<div class="stat-label">Masked Records</div>
</div>
</div>
</div>
</div>
<!-- Assets -->
<div class="col-6 col-md-3">
<div class="card stat-card text-white" style="background:#198754;">
<div class="card-body d-flex align-items-center gap-3">
<i class="bi bi-laptop stat-icon"></i>
<div>
<div class="stat-value">{{ stats.available_assets }}</div>
<div class="stat-label">Available Assets</div>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card stat-card text-white" style="background:#0d6efd;">
<div class="card-body d-flex align-items-center gap-3">
<i class="bi bi-arrow-left-right stat-icon"></i>
<div>
<div class="stat-value">{{ stats.assigned_assets }}</div>
<div class="stat-label">Assigned Assets</div>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card stat-card text-white" style="background:#ffc107; color:#000 !important;">
<div class="card-body d-flex align-items-center gap-3">
<i class="bi bi-tools stat-icon"></i>
<div>
<div class="stat-value">{{ stats.maintenance_assets }}</div>
<div class="stat-label">In Maintenance</div>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card stat-card text-white" style="background:#6c757d;">
<div class="card-body d-flex align-items-center gap-3">
<i class="bi bi-hdd-fill stat-icon"></i>
<div>
<div class="stat-value">{{ stats.total_assets }}</div>
<div class="stat-label">Total Assets</div>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card stat-card text-white" style="background:#0dcaf0; color:#000 !important;">
<div class="card-body d-flex align-items-center gap-3">
<i class="bi bi-file-earmark-text stat-icon"></i>
<div>
<div class="stat-value">{{ stats.total_paperwork }}</div>
<div class="stat-label">Documents</div>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card stat-card text-white" style="background:#fd7e14;">
<div class="card-body d-flex align-items-center gap-3">
<i class="bi bi-person-badge stat-icon"></i>
<div>
<div class="stat-value">{{ stats.active_assignments }}</div>
<div class="stat-label">Open Assignments</div>
</div>
</div>
</div>
</div>
</div>
<!-- ── Quick Actions ──────────────────────────────────────────── -->
<div class="row g-3 mb-4">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body py-2">
<span class="text-muted me-3" style="font-size:.8rem;">QUICK ACTIONS</span>
<a href="{{ url_for('assets.create') }}" class="btn btn-sm btn-outline-primary me-2">
<i class="bi bi-plus-circle me-1"></i>Add Asset
</a>
<a href="{{ url_for('assignments.create') }}" class="btn btn-sm btn-outline-success me-2">
<i class="bi bi-arrow-left-right me-1"></i>Assign Asset
</a>
<a href="{{ url_for('paperwork.create') }}" class="btn btn-sm btn-outline-info me-2">
<i class="bi bi-file-earmark-plus me-1"></i>New Document
</a>
<a href="{{ url_for('users.import_page') }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-cloud-download me-1"></i>Import Users
</a>
</div>
</div>
</div>
</div>
<!-- ── Recent Assignments ─────────────────────────────────────── -->
<div class="card border-0 shadow-sm">
<div class="card-header bg-white fw-semibold py-3">
<i class="bi bi-clock-history me-2 text-primary"></i>Current Assignments (latest 10)
</div>
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>User</th>
<th>Windows ID</th>
<th>Asset</th>
<th>Serial / Service Tag</th>
<th>Since</th>
<th></th>
</tr>
</thead>
<tbody>
{% for a in recent_assignments %}
<tr {% if a.user.is_masked %}class="masked-row"{% endif %}>
<td>{{ a.user.display_name }}</td>
<td><code>{{ a.user.windows_id }}</code></td>
<td>{{ a.asset.brand or '' }} {{ a.asset.model or '' }}</td>
<td>
<code>{{ a.asset.serial_number }}</code>
{% if a.asset.service_tag %}<br><small class="text-muted">{{ a.asset.service_tag }}</small>{% endif %}
</td>
<td>{{ a.assigned_date.strftime('%d/%m/%Y') if a.assigned_date else '—' }}</td>
<td>
<a href="{{ url_for('assets.detail', asset_id=a.asset.id) }}"
class="btn btn-xs btn-outline-secondary btn-sm py-0 px-2">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
{% else %}
<tr><td colspan="6" class="text-center text-muted py-3">No active assignments.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,155 @@
{% extends 'base.html' %}
{% block title %}{{ tpl.name }} Templates{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('doc_templates.index') }}">Templates</a></li>
<li class="breadcrumb-item active">{{ tpl.name }}</li>
{% endblock %}
{% block content %}
<div class="page-header d-flex align-items-center justify-content-between mb-4">
<h1><i class="bi bi-file-earmark-word me-2"></i>{{ tpl.name }}</h1>
<div class="d-flex gap-2">
<a href="{{ url_for('doc_templates.download', tpl_id=tpl.id) }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-download me-1"></i>Download .docx
</a>
<a href="{{ url_for('doc_templates.edit', tpl_id=tpl.id) }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil me-1"></i>Edit details
</a>
<form method="POST" action="{{ url_for('doc_templates.rescan', tpl_id=tpl.id) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-arrow-clockwise me-1"></i>Re-scan variables
</button>
</form>
<button class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">
<i class="bi bi-trash me-1"></i>Delete
</button>
</div>
</div>
<div class="row g-4">
<!-- Metadata -->
<div class="col-md-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white fw-semibold small text-uppercase text-muted">Details</div>
<div class="card-body">
<dl class="row small mb-0">
<dt class="col-5">Category</dt>
<dd class="col-7">
{% if tpl.category %}
<span class="badge bg-secondary">{{ dict(doc_types)[tpl.category] if tpl.category in dict(doc_types) else tpl.category }}</span>
{% else %}<span class="text-muted"></span>{% endif %}
</dd>
<dt class="col-5">File</dt>
<dd class="col-7"><code>{{ tpl.filename }}</code></dd>
<dt class="col-5">Uploaded</dt>
<dd class="col-7">{{ tpl.created_at.strftime('%d %b %Y %H:%M') }}</dd>
<dt class="col-5">By</dt>
<dd class="col-7">{{ tpl.created_by.username if tpl.created_by else '—' }}</dd>
<dt class="col-5">Docs generated</dt>
<dd class="col-7">{{ tpl.paperwork_docs.count() }}</dd>
</dl>
{% if tpl.description %}
<hr class="my-2">
<p class="small text-muted mb-0">{{ tpl.description }}</p>
{% endif %}
</div>
</div>
</div>
<!-- Variables -->
<div class="col-md-8">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white fw-semibold small text-uppercase text-muted d-flex justify-content-between align-items-center">
Detected Variables
<span class="badge bg-primary rounded-pill">{{ tpl.variables | length }}</span>
</div>
{% set vars = tpl.variables %}
{% if vars %}
<div class="card-body">
<p class="small text-muted mb-3">
These placeholders were detected in the template file.
They will be filled automatically when generating a document.
<strong class="text-danger">PII variables</strong> (name, email, phone)
are replaced with <code>[MASKED]</code> when a user's record is erased.
</p>
{% set pii = ['user_name','user_email','user_phone'] %}
<div class="row row-cols-2 row-cols-md-3 g-2">
{% for v in vars %}
<div class="col">
<span class="badge {% if v in pii %}bg-danger{% else %}bg-light text-dark border{% endif %} w-100 text-start p-2">
{% if v in pii %}<i class="bi bi-shield-x me-1"></i>{% else %}<i class="bi bi-braces me-1"></i>{% endif %}
{{ v }}
</span>
</div>
{% endfor %}
</div>
<div class="mt-3 small">
<span class="badge bg-danger me-1">PII</span> masked on departure &nbsp;
<span class="badge bg-light text-dark border me-1">other</span> retained
</div>
</div>
{% else %}
<div class="card-body text-muted small">
No variables detected. Make sure your template uses <code>&#123;&#123; variable_name &#125;&#125;</code> syntax
and click <strong>Re-scan</strong>.
</div>
{% endif %}
</div>
</div>
</div>
<!-- Documents generated from this template -->
{% set recent_docs = tpl.paperwork_docs.order_by('created_at desc').limit(10).all() %}
{% if recent_docs %}
<div class="card border-0 shadow-sm mt-4">
<div class="card-header bg-white fw-semibold small text-uppercase text-muted">Recently Generated Documents</div>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0 small">
<thead class="table-light">
<tr><th>Title</th><th>User</th><th>Created</th><th>Signed</th><th></th></tr>
</thead>
<tbody>
{% for doc in recent_docs %}
<tr>
<td>{{ doc.title }}</td>
<td>{{ doc.user.display_name if doc.user else '—' }}</td>
<td>{{ doc.created_at.strftime('%d/%m/%Y') }}</td>
<td>{% if doc.is_signed %}<i class="bi bi-check-circle text-success"></i>{% else %}<span class="text-muted"></span>{% endif %}</td>
<td><a href="{{ url_for('paperwork.detail', doc_id=doc.id) }}" class="btn btn-sm btn-outline-secondary py-0 px-2"><i class="bi bi-eye"></i></a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- Delete modal -->
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete Template</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
Delete <strong>{{ tpl.name }}</strong>?
{% if tpl.paperwork_docs.count() > 0 %}
<div class="alert alert-danger mt-2 small">
Cannot delete — {{ tpl.paperwork_docs.count() }} document(s) were generated from this template.
</div>
{% endif %}
</div>
<div class="modal-footer">
<button class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
{% if tpl.paperwork_docs.count() == 0 %}
<form method="POST" action="{{ url_for('doc_templates.delete', tpl_id=tpl.id) }}">
<button class="btn btn-danger btn-sm" type="submit">Delete</button>
</form>
{% endif %}
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,41 @@
{% extends 'base.html' %}
{% block title %}Edit {{ tpl.name }} Templates{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('doc_templates.index') }}">Templates</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('doc_templates.detail', tpl_id=tpl.id) }}">{{ tpl.name }}</a></li>
<li class="breadcrumb-item active">Edit</li>
{% endblock %}
{% block content %}
<div class="page-header mb-4">
<h1><i class="bi bi-pencil me-2"></i>Edit Template</h1>
</div>
<div class="card border-0 shadow-sm" style="max-width:600px;">
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label class="form-label fw-semibold">Name <span class="text-danger">*</span></label>
<input type="text" name="name" class="form-control" value="{{ tpl.name }}" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Category</label>
<select name="category" class="form-select">
<option value="">— no category —</option>
{% for val, label in doc_types %}
<option value="{{ val }}" {% if tpl.category == val %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="mb-4">
<label class="form-label fw-semibold">Description</label>
<textarea name="description" class="form-control" rows="3">{{ tpl.description or '' }}</textarea>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Save</button>
<a href="{{ url_for('doc_templates.detail', tpl_id=tpl.id) }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,77 @@
{% extends 'base.html' %}
{% block title %}Document Templates IT Asset Management{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item active">Document Templates</li>
{% endblock %}
{% block content %}
<div class="page-header d-flex align-items-center justify-content-between mb-4">
<h1><i class="bi bi-file-earmark-word me-2"></i>Document Templates</h1>
<a href="{{ url_for('doc_templates.upload') }}" class="btn btn-primary btn-sm">
<i class="bi bi-upload me-1"></i>Upload Template
</a>
</div>
<div class="alert alert-info small mb-4">
<i class="bi bi-info-circle me-1"></i>
Upload <strong>.docx</strong> Word files with <code>&#123;&#123; variable_name &#125;&#125;</code> placeholders.
When creating paperwork, the system fills them automatically from user / asset data.
All generated documents can be regenerated with masked PII if a user leaves the company.
</div>
{% if templates %}
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-3 g-3">
{% for tpl in templates %}
<div class="col">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body">
<div class="d-flex align-items-start justify-content-between mb-2">
<h6 class="mb-0 fw-semibold">
<i class="bi bi-file-earmark-word text-primary me-1"></i>{{ tpl.name }}
</h6>
{% if tpl.category %}
<span class="badge bg-secondary ms-2">{{ dict(doc_types)[tpl.category] if tpl.category in dict(doc_types) else tpl.category }}</span>
{% endif %}
</div>
{% if tpl.description %}
<p class="text-muted small mb-2">{{ tpl.description }}</p>
{% endif %}
<div class="small text-muted mb-3">
<i class="bi bi-braces me-1"></i>
{% set vars = tpl.variables %}
{% if vars %}
{{ vars | length }} variable(s):
{% for v in vars[:5] %}<code class="me-1">{{ v }}</code>{% endfor %}
{% if vars | length > 5 %}<em>+{{ vars | length - 5 }} more</em>{% endif %}
{% else %}
No variables detected
{% endif %}
</div>
<div class="d-flex gap-1 align-items-center">
<a href="{{ url_for('doc_templates.detail', tpl_id=tpl.id) }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye me-1"></i>View
</a>
<a href="{{ url_for('doc_templates.download', tpl_id=tpl.id) }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-download me-1"></i>Download
</a>
<span class="ms-auto text-muted small">
{{ tpl.paperwork_docs.count() }} doc(s) generated
</span>
</div>
</div>
<div class="card-footer bg-white text-muted small">
Uploaded {{ tpl.created_at.strftime('%d %b %Y') }}
{% if tpl.created_by %} by {{ tpl.created_by.username }}{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center text-muted py-5">
<i class="bi bi-file-earmark-word display-4 d-block mb-3"></i>
No templates yet. <a href="{{ url_for('doc_templates.upload') }}">Upload your first template</a>.
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,96 @@
{% extends 'base.html' %}
{% block title %}Upload Template IT Asset Management{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('doc_templates.index') }}">Templates</a></li>
<li class="breadcrumb-item active">Upload</li>
{% endblock %}
{% block content %}
<div class="page-header mb-4">
<h1><i class="bi bi-upload me-2"></i>Upload Document Template</h1>
</div>
<div class="row">
<div class="col-lg-7">
<div class="card border-0 shadow-sm mb-4">
<div class="card-body">
<form method="POST" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label fw-semibold">Template Name <span class="text-danger">*</span></label>
<input type="text" name="name" class="form-control" placeholder="e.g. Equipment Handover Receipt" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Category</label>
<select name="category" class="form-select">
<option value="">— no category —</option>
{% for val, label in doc_types %}
<option value="{{ val }}">{{ label }}</option>
{% endfor %}
</select>
<div class="form-text">Used to pre-select this template when creating paperwork of that type.</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">Description</label>
<textarea name="description" class="form-control" rows="2" placeholder="Optional notes about this template…"></textarea>
</div>
<div class="mb-4">
<label class="form-label fw-semibold">.docx Template File <span class="text-danger">*</span></label>
<input type="file" name="docx_file" class="form-control" accept=".docx" required>
<div class="form-text">Word document (.docx) with <code>&#123;&#123; variable_name &#125;&#125;</code> placeholders.</div>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-upload me-1"></i>Upload
</button>
<a href="{{ url_for('doc_templates.index') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-5">
<div class="card border-0 shadow-sm">
<div class="card-header fw-semibold small text-uppercase text-muted bg-white">
Available Variables
</div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0 small">
<thead class="table-light"><tr><th>Variable</th><th>Value</th></tr></thead>
<tbody>
{% set var_docs = [
('user_name','Full name (masked if user left)'),
('user_email','Email address'),
('user_phone','Phone number'),
('user_department','Department (retained after masking)'),
('user_job_title','Job title'),
('user_location','Office location'),
('user_windows_id','Windows / AD ID — never masked'),
('asset_serial','Asset serial number'),
('asset_service_tag','Dell / vendor service tag'),
('asset_brand','Brand (e.g. Dell)'),
('asset_model','Model name'),
('asset_type','Type (Laptop / Desktop / …)'),
('asset_os','Operating system'),
('asset_warranty_expiry','Warranty expiry date'),
('assignment_date','Date asset was assigned'),
('return_date','Date asset was returned'),
('document_date','Today\'s date'),
('document_number','Document / paperwork ID'),
('company_name','Your company name'),
('company_address','Your company address'),
] %}
{% for var, desc in var_docs %}
<tr>
<td><code>&#123;&#123; {{ var }} &#125;&#125;</code></td>
<td class="text-muted">{{ desc }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,238 @@
{% extends 'base.html' %}
{% block title %}{{ doc.title }} IT Asset Management{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('paperwork.index') }}">Paperwork</a></li>
<li class="breadcrumb-item active">{{ doc.title }}</li>
{% endblock %}
{% block content %}
<div class="page-header d-flex align-items-center justify-content-between mb-4">
<h1><i class="bi bi-file-earmark-text me-2"></i>{{ doc.title }}</h1>
<div class="d-flex gap-2 flex-wrap">
{% if doc.pdf_filename %}
<a href="{{ url_for('paperwork.download', doc_id=doc.id) }}" class="btn btn-sm btn-primary">
<i class="bi bi-filetype-pdf me-1"></i>Download PDF
</a>
{% endif %}
{% if doc.docx_filename %}
<a href="{{ url_for('paperwork.download_docx', doc_id=doc.id) }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-file-earmark-word me-1"></i>Download .docx
</a>
{% endif %}
<form method="POST" action="{{ url_for('paperwork.regenerate', doc_id=doc.id) }}" class="d-inline">
<button type="submit" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-arrow-clockwise me-1"></i>Regenerate
</button>
</form>
</div>
</div>
<div class="row g-3">
<!-- Left column: meta -->
<div class="col-md-4">
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white fw-semibold py-3">
<i class="bi bi-info-circle me-2 text-primary"></i>Document Info
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-5 text-muted small">Type</dt>
<dd class="col-7"><span class="badge bg-info text-dark">{{ doc.doc_type_label }}</span></dd>
<dt class="col-5 text-muted small">User</dt>
<dd class="col-7">
<a href="{{ url_for('users.detail', user_id=doc.user.id) }}">{{ doc.user.display_name }}</a>
<br><code class="small">WID: {{ doc.user.windows_id }}</code>
</dd>
{% if doc.asset %}
<dt class="col-5 text-muted small">Asset</dt>
<dd class="col-7">
<a href="{{ url_for('assets.detail', asset_id=doc.asset.id) }}">
{{ doc.asset.brand or '' }} {{ doc.asset.model or '' }}
</a>
<br><code class="small">{{ doc.asset.serial_number }}</code>
</dd>
{% endif %}
{% if doc.template %}
<dt class="col-5 text-muted small">Template</dt>
<dd class="col-7">
<a href="{{ url_for('doc_templates.detail', template_id=doc.template.id) }}">{{ doc.template.name }}</a>
</dd>
{% endif %}
<dt class="col-5 text-muted small">Created</dt>
<dd class="col-7">{{ doc.created_at.strftime('%d/%m/%Y %H:%M') if doc.created_at else '—' }}</dd>
<dt class="col-5 text-muted small">Created by</dt>
<dd class="col-7">{{ doc.created_by.username if doc.created_by else '—' }}</dd>
<dt class="col-5 text-muted small">PDF</dt>
<dd class="col-7">
{% if doc.pdf_filename %}<span class="badge bg-success">Generated</span>
{% else %}<span class="badge bg-secondary">Not generated</span>{% endif %}
</dd>
<dt class="col-5 text-muted small">Word doc</dt>
<dd class="col-7">
{% if doc.docx_filename %}<span class="badge bg-primary">Available</span>
{% else %}<span class="badge bg-secondary">None</span>{% endif %}
</dd>
<dt class="col-5 text-muted small">Signed</dt>
<dd class="col-7">
{% if doc.is_signed %}
<span class="badge bg-success">
<i class="bi bi-pen me-1"></i>{{ doc.signed_by_name }}
</span>
<br><span class="small text-muted">{{ doc.signed_at.strftime('%d/%m/%Y %H:%M') }}</span>
{% else %}
<span class="badge bg-warning text-dark">Unsigned</span>
{% endif %}
</dd>
</dl>
</div>
</div>
<!-- Signature card -->
{% if doc.is_signed %}
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white fw-semibold py-3 d-flex justify-content-between">
<span><i class="bi bi-pen me-2 text-success"></i>Signature</span>
<form method="POST" action="{{ url_for('paperwork.unsign', doc_id=doc.id) }}" class="d-inline">
<button class="btn btn-sm btn-outline-danger" onclick="return confirm('Remove signature?')">Remove</button>
</form>
</div>
<div class="card-body text-center">
{% if doc.signature_data %}
<img src="{{ doc.signature_data }}" alt="Signature" class="img-fluid border rounded"
style="max-height:80px; background:#fff;">
{% endif %}
<p class="mb-0 mt-2 small text-muted">
Signed by <strong>{{ doc.signed_by_name }}</strong><br>
{{ doc.signed_at.strftime('%d/%m/%Y at %H:%M') }}
</p>
</div>
</div>
{% else %}
<!-- Sign document card -->
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white fw-semibold py-3">
<i class="bi bi-pen me-2 text-warning"></i>Sign Document
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('paperwork.sign', doc_id=doc.id) }}">
<div class="mb-2">
<label class="form-label small">Signer's full name <span class="text-danger">*</span></label>
<input type="text" name="signed_by_name" class="form-control form-control-sm"
placeholder="{{ doc.user.display_name }}" required>
</div>
<div class="mb-2">
<label class="form-label small">Draw signature (optional)</label>
<canvas id="sigCanvas" width="260" height="80"
class="border rounded d-block"
style="background:#fff; cursor:crosshair; touch-action:none;"></canvas>
<input type="hidden" name="signature_data" id="sigData">
<div class="d-flex gap-2 mt-1">
<button type="button" class="btn btn-xs btn-outline-secondary btn-sm" id="clearSig">Clear</button>
</div>
</div>
<button type="submit" class="btn btn-sm btn-success w-100" id="signBtn">
<i class="bi bi-pen me-1"></i>Sign Document
</button>
</form>
</div>
</div>
{% endif %}
</div>
<!-- Right column: notes + merge vars -->
<div class="col-md-8">
{% if doc.notes %}
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white fw-semibold py-3">
<i class="bi bi-chat-left-text me-2 text-primary"></i>Notes
</div>
<div class="card-body">
<p class="mb-0">{{ doc.notes }}</p>
</div>
</div>
{% endif %}
{% if merge_vars %}
<div class="card border-0 shadow-sm">
<div class="card-header bg-white fw-semibold py-3 d-flex justify-content-between align-items-center">
<span><i class="bi bi-braces me-2 text-primary"></i>Merge Variables Used</span>
<button class="btn btn-sm btn-outline-secondary" type="button"
data-bs-toggle="collapse" data-bs-target="#mergeVarsBody">
Toggle
</button>
</div>
<div id="mergeVarsBody" class="collapse show">
<div class="card-body p-0">
<table class="table table-sm table-striped mb-0 small">
<thead class="table-light"><tr><th>Variable</th><th>Value</th></tr></thead>
<tbody>
{% set PII = ['user_name','user_email','user_phone'] %}
{% for k, v in merge_vars.items()|sort %}
<tr {% if k in PII %}class="table-danger"{% endif %}>
<td><code>{{ '{{' }} {{ k }} {{ '}}' }}</code>
{% if k in PII %}<span class="badge bg-danger ms-1 small">PII</span>{% endif %}
</td>
<td>{{ v or '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block extra_js %}
{% if not doc.is_signed %}
<script>
(function () {
const canvas = document.getElementById('sigCanvas');
const ctx = canvas.getContext('2d');
let drawing = false;
function getPos(e) {
const r = canvas.getBoundingClientRect();
const src = e.touches ? e.touches[0] : e;
return { x: src.clientX - r.left, y: src.clientY - r.top };
}
canvas.addEventListener('mousedown', e => { drawing = true; const p = getPos(e); ctx.beginPath(); ctx.moveTo(p.x, p.y); });
canvas.addEventListener('mousemove', e => { if (!drawing) return; const p = getPos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); });
canvas.addEventListener('mouseup', () => { drawing = false; });
canvas.addEventListener('touchstart', e => { e.preventDefault(); drawing = true; const p = getPos(e); ctx.beginPath(); ctx.moveTo(p.x, p.y); });
canvas.addEventListener('touchmove', e => { e.preventDefault(); if (!drawing) return; const p = getPos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); });
canvas.addEventListener('touchend', () => { drawing = false; });
ctx.strokeStyle = '#1a1a1a';
ctx.lineWidth = 2;
ctx.lineJoin = 'round';
ctx.lineCap = 'round';
document.getElementById('clearSig').addEventListener('click', () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
});
document.querySelector('form[action*="/sign"]').addEventListener('submit', () => {
// Only attach non-empty canvas
const blank = document.createElement('canvas');
blank.width = canvas.width; blank.height = canvas.height;
if (canvas.toDataURL() !== blank.toDataURL()) {
document.getElementById('sigData').value = canvas.toDataURL('image/png');
}
});
})();
</script>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,168 @@
{% extends 'base.html' %}
{% block title %}New Document IT Asset Management{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('paperwork.index') }}">Paperwork</a></li>
<li class="breadcrumb-item active">New Document</li>
{% endblock %}
{% block content %}
<div class="page-header mb-4">
<h1><i class="bi bi-file-earmark-plus me-2"></i>Create Document</h1>
</div>
<div class="card border-0 shadow-sm" style="max-width:740px;">
<div class="card-body">
<form method="POST" action="{{ url_for('paperwork.create') }}">
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label">Document Type <span class="text-danger">*</span></label>
<select name="document_type" class="form-select" id="docType">
{% for val, label in doc_types %}
<option value="{{ val }}">{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label class="form-label">Title</label>
<input type="text" name="title" class="form-control"
placeholder="Leave blank to auto-generate">
</div>
</div>
<!-- Word Template selector -->
{% if all_templates %}
<div class="mb-3">
<label class="form-label">
Word Template
<span class="text-muted small">(optional — generates an editable .docx)</span>
</label>
<select name="template_id" id="templateSelect" class="form-select">
<option value="">— No template (PDF only) —</option>
{% for tpl in all_templates %}
<option value="{{ tpl.id }}">{{ tpl.name }}{% if tpl.category %} [{{ tpl.category }}]{% endif %}</option>
{% endfor %}
</select>
<!-- Variable preview loaded via AJAX -->
<div id="tplVarsBox" class="mt-2" style="display:none">
<div class="small text-muted mb-1">Variables auto-filled from this template:</div>
<div id="tplVarsList" class="d-flex flex-wrap gap-1"></div>
</div>
</div>
{% endif %}
<!-- User search -->
<div class="mb-3">
<label class="form-label">User <span class="text-danger">*</span></label>
<input type="hidden" name="user_id" id="userId" value="{{ preselect_user_id or '' }}">
<input type="text" id="userSearch" class="form-control"
placeholder="Type name or Windows ID…" autocomplete="off">
<div id="userDropdown" class="list-group position-absolute shadow" style="z-index:1000;display:none;min-width:350px;"></div>
<div id="userDisplay" class="form-text text-success fw-semibold"></div>
</div>
<!-- Asset search (optional) -->
<div class="mb-3">
<label class="form-label">Asset <span class="text-muted small">(optional)</span></label>
<input type="hidden" name="asset_id" id="assetId" value="{{ preselect_asset_id or '' }}">
<input type="text" id="assetSearch" class="form-control"
placeholder="Serial number or service tag…" autocomplete="off">
<div id="assetDropdown" class="list-group position-absolute shadow" style="z-index:1000;display:none;min-width:350px;"></div>
<div id="assetDisplay" class="form-text text-success fw-semibold"></div>
</div>
{% if preselect_assignment_id %}
<input type="hidden" name="assignment_id" value="{{ preselect_assignment_id }}">
{% endif %}
<div class="mb-3">
<label class="form-label">Notes</label>
<textarea name="notes" class="form-control" rows="2"></textarea>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-file-earmark-check me-1"></i>Generate Document
</button>
<a href="{{ url_for('paperwork.index') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
{% if all_templates %}
<div class="mt-3">
<a href="{{ url_for('doc_templates.index') }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-file-earmark-word me-1"></i>Manage Templates
</a>
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
<script>
function liveSearch(inputId, dropdownId, hiddenId, displayId, endpoint) {
const input = document.getElementById(inputId);
const dropdown = document.getElementById(dropdownId);
const hidden = document.getElementById(hiddenId);
const display = document.getElementById(displayId);
let timer;
input.addEventListener('input', () => {
clearTimeout(timer);
timer = setTimeout(() => {
const q = input.value.trim();
if (q.length < 2) { dropdown.style.display = 'none'; return; }
fetch(`${endpoint}?q=${encodeURIComponent(q)}`)
.then(r => r.json())
.then(items => {
dropdown.innerHTML = '';
if (!items.length) { dropdown.style.display = 'none'; return; }
items.forEach(item => {
const a = document.createElement('a');
a.className = 'list-group-item list-group-item-action py-2';
a.textContent = item.text;
a.addEventListener('click', () => {
hidden.value = item.id;
input.value = item.text;
display.textContent = '✓ Selected';
dropdown.style.display = 'none';
});
dropdown.appendChild(a);
});
dropdown.style.display = 'block';
});
}, 250);
});
document.addEventListener('click', e => { if (!input.contains(e.target)) dropdown.style.display = 'none'; });
}
liveSearch('userSearch', 'userDropdown', 'userId', 'userDisplay', '{{ url_for("users.search") }}');
liveSearch('assetSearch', 'assetDropdown', 'assetId', 'assetDisplay', '{{ url_for("assets.search") }}');
// Template variable preview
const tplSelect = document.getElementById('templateSelect');
if (tplSelect) {
const PII_VARS = new Set(['user_name', 'user_email', 'user_phone']);
tplSelect.addEventListener('change', () => {
const id = tplSelect.value;
const box = document.getElementById('tplVarsBox');
const list = document.getElementById('tplVarsList');
if (!id) { box.style.display = 'none'; return; }
fetch(`/doc-templates/${id}/variables.json`)
.then(r => r.json())
.then(data => {
list.innerHTML = '';
(data.variables || []).forEach(v => {
const badge = document.createElement('span');
badge.className = 'badge ' + (PII_VARS.has(v) ? 'bg-danger' : 'bg-secondary');
badge.title = PII_VARS.has(v) ? 'PII — will be masked on user departure' : '';
badge.textContent = '{{ ' + v + ' }}';
list.appendChild(badge);
});
box.style.display = 'block';
})
.catch(() => { box.style.display = 'none'; });
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,110 @@
{% extends 'base.html' %}
{% block title %}Paperwork IT Asset Management{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item active">Paperwork</li>
{% endblock %}
{% block content %}
<div class="page-header d-flex align-items-center justify-content-between mb-4">
<h1><i class="bi bi-file-earmark-text me-2"></i>Paperwork</h1>
<a href="{{ url_for('paperwork.create') }}" class="btn btn-primary btn-sm">
<i class="bi bi-file-earmark-plus me-1"></i>New Document
</a>
</div>
<form method="GET" class="row g-2 mb-3">
<div class="col-md-3">
<select name="doc_type" class="form-select form-select-sm" onchange="this.form.submit()">
<option value="">All document types</option>
{% for val, label in doc_types %}
<option value="{{ val }}" {% if doc_type_filter == val %}selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-auto">
<a href="{{ url_for('paperwork.index') }}" class="btn btn-sm btn-outline-secondary">Clear</a>
</div>
</form>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Title</th>
<th>Type</th>
<th>User</th>
<th>Asset SN</th>
<th>Created</th>
<th>PDF</th>
<th></th>
</tr>
</thead>
<tbody>
{% for d in pagination.items %}
<tr>
<td><a href="{{ url_for('paperwork.detail', doc_id=d.id) }}">{{ d.title }}</a></td>
<td><span class="badge bg-info text-dark">{{ d.doc_type_label }}</span></td>
<td>
<a href="{{ url_for('users.detail', user_id=d.user.id) }}">{{ d.user.display_name }}</a>
</td>
<td>{{ d.asset.serial_number if d.asset else '—' }}</td>
<td>{{ d.created_at.strftime('%d/%m/%Y') if d.created_at else '—' }}</td>
<td>
{% if d.pdf_filename %}
<span class="badge bg-success"><i class="bi bi-check"></i> Ready</span>
{% else %}
<span class="badge bg-secondary"></span>
{% endif %}
</td>
<td>
<a href="{{ url_for('paperwork.detail', doc_id=d.id) }}"
class="btn btn-sm btn-outline-secondary py-0 px-2">
<i class="bi bi-eye"></i>
</a>
{% if d.pdf_filename %}
<a href="{{ url_for('paperwork.download', doc_id=d.id) }}"
class="btn btn-sm btn-outline-primary py-0 px-2">
<i class="bi bi-download"></i>
</a>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="7" class="text-center text-muted py-4">No documents found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% if pagination.pages > 1 %}
<div class="card-footer bg-white d-flex justify-content-between align-items-center py-2">
<small class="text-muted">Showing {{ pagination.first }}{{ pagination.last }} of {{ pagination.total }}</small>
<nav>
<ul class="pagination pagination-sm mb-0">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('paperwork.index', page=pagination.prev_num, doc_type=doc_type_filter) }}"></a>
</li>
{% endif %}
{% for p in pagination.iter_pages() %}
{% if p %}
<li class="page-item {% if p == pagination.page %}active{% endif %}">
<a class="page-link" href="{{ url_for('paperwork.index', page=p, doc_type=doc_type_filter) }}">{{ p }}</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link"></span></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('paperwork.index', page=pagination.next_num, doc_type=doc_type_filter) }}"></a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,126 @@
{% extends 'base.html' %}
{% block title %}Settings IT Asset Management{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item active">Settings</li>
{% endblock %}
{% block content %}
<div class="page-header mb-4">
<h1><i class="bi bi-gear me-2"></i>Settings</h1>
</div>
<div class="row g-4">
<!-- Admin users -->
<div class="col-md-7">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white fw-semibold py-3">
<i class="bi bi-person-gear me-2 text-primary"></i>Admin Users
</div>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<thead class="table-light">
<tr><th>Username</th><th>Full Name</th><th>Email</th><th>Role</th><th>Last Login</th><th>Active</th><th></th></tr>
</thead>
<tbody>
{% for a in admins %}
<tr>
<td><strong>{{ a.username }}</strong></td>
<td>{{ a.full_name or '—' }}</td>
<td>{{ a.email }}</td>
<td><span class="badge bg-secondary">{{ a.role }}</span></td>
<td>{{ a.last_login.strftime('%d/%m/%Y') if a.last_login else '—' }}</td>
<td>
{% if a.is_active %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-secondary">Inactive</span>
{% endif %}
</td>
<td>
{% if a.id != current_user.id %}
<form method="POST" action="{{ url_for('settings.toggle_admin', admin_id=a.id) }}" class="d-inline">
<button type="submit" class="btn btn-xs btn-sm btn-outline-{{ 'warning' if a.is_active else 'success' }} py-0 px-2">
{{ 'Deactivate' if a.is_active else 'Activate' }}
</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Add admin form -->
<div class="card-footer bg-white">
<h6 class="fw-semibold mb-3 mt-1">Add Admin User</h6>
<form method="POST" action="{{ url_for('settings.create_admin') }}">
<div class="row g-2">
<div class="col-md-3">
<input type="text" name="username" class="form-control form-control-sm" placeholder="Username" required>
</div>
<div class="col-md-3">
<input type="text" name="full_name" class="form-control form-control-sm" placeholder="Full Name">
</div>
<div class="col-md-3">
<input type="email" name="email" class="form-control form-control-sm" placeholder="Email" required>
</div>
<div class="col-md-2">
<input type="password" name="password" class="form-control form-control-sm" placeholder="Password" required minlength="8">
</div>
<div class="col-md-1">
<button type="submit" class="btn btn-sm btn-primary w-100">Add</button>
</div>
</div>
</form>
</div>
</div>
</div>
<!-- LDAP config info -->
<div class="col-md-5">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white fw-semibold py-3">
<i class="bi bi-diagram-3 me-2 text-primary"></i>LDAP / AD Configuration
</div>
<div class="card-body">
<p class="text-muted small mb-3">
LDAP settings are managed via environment variables (see <code>.env</code> file).
Restart the application after changing these values.
</p>
<table class="table table-sm table-bordered mb-0">
<tbody>
<tr><th class="bg-light">LDAP_SERVER</th><td><code>{{ config.LDAP_SERVER or '(not set)' }}</code></td></tr>
<tr><th class="bg-light">LDAP_PORT</th><td>{{ config.LDAP_PORT }}</td></tr>
<tr><th class="bg-light">LDAP_USE_SSL</th><td>{{ config.LDAP_USE_SSL }}</td></tr>
<tr><th class="bg-light">LDAP_BASE_DN</th><td><code>{{ config.LDAP_BASE_DN or '(not set)' }}</code></td></tr>
<tr><th class="bg-light">LDAP_BIND_USER</th><td>{{ config.LDAP_BIND_USER or '(not set)' }}</td></tr>
<tr><th class="bg-light">Windows ID attr</th><td><code>{{ config.LDAP_WINDOWS_ID_ATTR }}</code></td></tr>
</tbody>
</table>
<div class="mt-3">
<a href="{{ url_for('users.import_page') }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-arrow-repeat me-1"></i>Go to Import / Sync
</a>
</div>
</div>
</div>
<div class="card border-0 shadow-sm mt-3">
<div class="card-header bg-white fw-semibold py-3">
<i class="bi bi-building me-2 text-primary"></i>Company Info (for PDFs)
</div>
<div class="card-body">
<table class="table table-sm table-bordered mb-0">
<tbody>
<tr><th class="bg-light">COMPANY_NAME</th><td>{{ config.COMPANY_NAME or '(not set)' }}</td></tr>
<tr><th class="bg-light">COMPANY_ADDRESS</th><td>{{ config.COMPANY_ADDRESS or '(not set)' }}</td></tr>
</tbody>
</table>
<p class="small text-muted mt-2 mb-0">Edit these in <code>.env</code> and restart.</p>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,211 @@
{% extends 'base.html' %}
{% block title %}{{ user.display_name }} IT Asset Management{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('users.index') }}">Users</a></li>
<li class="breadcrumb-item active">{{ user.display_name }}</li>
{% endblock %}
{% block content %}
<div class="page-header d-flex align-items-center justify-content-between mb-4">
<h1>
<i class="bi bi-person-circle me-2"></i>
{{ user.display_name }}
{% if user.is_masked %}<span class="badge badge-masked fs-6 align-middle ms-2">MASKED</span>{% endif %}
</h1>
<div class="d-flex gap-2">
{% if not user.is_masked %}
<a href="{{ url_for('users.edit', user_id=user.id) }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil me-1"></i>Edit
</a>
<a href="{{ url_for('assignments.create', user_id=user.id) }}" class="btn btn-sm btn-outline-success">
<i class="bi bi-plus-circle me-1"></i>Assign Asset
</a>
<a href="{{ url_for('paperwork.create', user_id=user.id) }}" class="btn btn-sm btn-outline-info">
<i class="bi bi-file-earmark-plus me-1"></i>New Document
</a>
<!-- Mask button -->
<button class="btn btn-sm btn-outline-danger" data-bs-toggle="modal" data-bs-target="#maskModal">
<i class="bi bi-eye-slash me-1"></i>Mask User
</button>
{% endif %}
</div>
</div>
<div class="row g-3">
<!-- Info card -->
<div class="col-md-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white fw-semibold py-3">
<i class="bi bi-info-circle me-2 text-primary"></i>User Information
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-5 text-muted small">Windows ID</dt>
<dd class="col-7"><code>{{ user.windows_id }}</code></dd>
<dt class="col-5 text-muted small">Full Name</dt>
<dd class="col-7">{{ user.display_name }}</dd>
<dt class="col-5 text-muted small">Email</dt>
<dd class="col-7" style="word-break:break-all;">{{ user.display_email }}</dd>
<dt class="col-5 text-muted small">Phone</dt>
<dd class="col-7">{{ user.display_phone }}</dd>
<dt class="col-5 text-muted small">Department</dt>
<dd class="col-7">{{ user.department or '—' }}</dd>
<dt class="col-5 text-muted small">Job Title</dt>
<dd class="col-7">{{ user.job_title or '—' }}</dd>
<dt class="col-5 text-muted small">Location</dt>
<dd class="col-7">{{ user.location or '—' }}</dd>
<dt class="col-5 text-muted small">Source</dt>
<dd class="col-7"><span class="badge bg-secondary">{{ user.import_source }}</span></dd>
<dt class="col-5 text-muted small">Status</dt>
<dd class="col-7">
{% if user.is_masked %}
<span class="badge badge-masked">Masked</span>
<br><small class="text-muted">{{ user.masked_at.strftime('%d/%m/%Y') if user.masked_at else '' }}</small>
{% elif user.is_active %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-warning text-dark">Inactive</span>
{% endif %}
</dd>
<dt class="col-5 text-muted small">Added</dt>
<dd class="col-7">{{ user.created_at.strftime('%d/%m/%Y') if user.created_at else '—' }}</dd>
</dl>
</div>
</div>
</div>
<!-- Assignments -->
<div class="col-md-8">
<div class="card border-0 shadow-sm mb-3">
<div class="card-header bg-white fw-semibold py-3 d-flex justify-content-between align-items-center">
<span><i class="bi bi-arrow-left-right me-2 text-primary"></i>Asset History</span>
{% if not user.is_masked %}
<a href="{{ url_for('assignments.create', user_id=user.id) }}" class="btn btn-xs btn-sm btn-outline-success py-0 px-2">
<i class="bi bi-plus"></i> Assign
</a>
{% endif %}
</div>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<thead class="table-light">
<tr>
<th>Asset</th>
<th>SN / Service Tag</th>
<th>From</th>
<th>Until</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{% for a in assignments %}
<tr>
<td>{{ a.asset.brand or '' }} {{ a.asset.model or '' }}</td>
<td>
<code>{{ a.asset.serial_number }}</code>
{% if a.asset.service_tag %}<br><small>{{ a.asset.service_tag }}</small>{% endif %}
</td>
<td>{{ a.assigned_date.strftime('%d/%m/%Y') if a.assigned_date else '—' }}</td>
<td>{{ a.returned_date.strftime('%d/%m/%Y') if a.returned_date else '<span class="badge bg-primary">current</span>' | safe }}</td>
<td>
{% if a.is_active %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-secondary">Returned</span>
{% endif %}
</td>
<td>
<a href="{{ url_for('assets.detail', asset_id=a.asset.id) }}"
class="btn btn-sm btn-outline-secondary py-0 px-2">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
{% else %}
<tr><td colspan="6" class="text-center text-muted py-3">No assignments.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Documents -->
<div class="card border-0 shadow-sm">
<div class="card-header bg-white fw-semibold py-3 d-flex justify-content-between align-items-center">
<span><i class="bi bi-file-earmark-text me-2 text-primary"></i>Documents</span>
{% if not user.is_masked %}
<a href="{{ url_for('paperwork.create', user_id=user.id) }}"
class="btn btn-xs btn-sm btn-outline-info py-0 px-2">
<i class="bi bi-plus"></i> New
</a>
{% endif %}
</div>
<div class="table-responsive">
<table class="table table-sm table-hover mb-0">
<thead class="table-light">
<tr><th>Title</th><th>Type</th><th>Asset</th><th>Date</th><th></th></tr>
</thead>
<tbody>
{% for d in docs %}
<tr>
<td><a href="{{ url_for('paperwork.detail', doc_id=d.id) }}">{{ d.title }}</a></td>
<td><span class="badge bg-info text-dark">{{ d.doc_type_label }}</span></td>
<td>{{ d.asset.serial_number if d.asset else '—' }}</td>
<td>{{ d.created_at.strftime('%d/%m/%Y') if d.created_at else '—' }}</td>
<td>
{% if d.pdf_filename %}
<a href="{{ url_for('paperwork.download', doc_id=d.id) }}"
class="btn btn-sm btn-outline-secondary py-0 px-2">
<i class="bi bi-download"></i>
</a>
{% endif %}
</td>
</tr>
{% else %}
<tr><td colspan="5" class="text-center text-muted py-3">No documents.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Mask confirmation modal -->
{% if not user.is_masked %}
<div class="modal fade" id="maskModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title text-danger"><i class="bi bi-eye-slash me-2"></i>Mask User Record</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>This will <strong>permanently erase all PII</strong> (name, email, phone) for
<strong>{{ user.display_name }}</strong> (WID: <code>{{ user.windows_id }}</code>).</p>
<p class="mb-0 text-muted">Asset history and assignments will be retained, linked only to the Windows ID.
This action <strong>cannot be undone</strong>.</p>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<form method="POST" action="{{ url_for('users.mask', user_id=user.id) }}" class="d-inline">
<button type="submit" class="btn btn-danger">
<i class="bi bi-eye-slash me-1"></i>Confirm Mask
</button>
</form>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,92 @@
{% extends 'base.html' %}
{% block title %}{{ user.display_name if user else 'New User' }} IT Asset Management{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('users.index') }}">Users</a></li>
<li class="breadcrumb-item active">{{ 'Edit' if user else 'New User' }}</li>
{% endblock %}
{% block content %}
<div class="page-header mb-4">
<h1><i class="bi bi-person-{{ 'pencil' if user else 'plus' }} me-2"></i>
{{ 'Edit User' if user else 'Add User' }}
</h1>
</div>
<div class="card border-0 shadow-sm" style="max-width:700px;">
<div class="card-body">
<form method="POST" action="{{ url_for('users.edit', user_id=user.id) if user else url_for('users.create') }}">
<h6 class="text-uppercase text-muted mb-3 small">Identity</h6>
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label">Windows ID <span class="text-danger">*</span></label>
<input type="text" name="windows_id" class="form-control"
value="{{ user.windows_id if user else '' }}"
{% if user %}readonly{% endif %} required>
<div class="form-text">Numeric ID e.g. 408525</div>
</div>
<div class="col-md-4">
<label class="form-label">First Name</label>
<input type="text" name="first_name" class="form-control"
value="{{ user.first_name or '' if user else '' }}">
</div>
<div class="col-md-4">
<label class="form-label">Last Name</label>
<input type="text" name="last_name" class="form-control"
value="{{ user.last_name or '' if user else '' }}">
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-control"
value="{{ user.email or '' if user else '' }}">
</div>
<div class="col-md-6">
<label class="form-label">Phone</label>
<input type="text" name="phone" class="form-control"
value="{{ user.phone or '' if user else '' }}">
</div>
</div>
<hr class="my-3">
<h6 class="text-uppercase text-muted mb-3 small">Organisation</h6>
<div class="row g-3 mb-3">
<div class="col-md-4">
<label class="form-label">Department</label>
<input type="text" name="department" class="form-control"
value="{{ user.department or '' if user else '' }}">
</div>
<div class="col-md-4">
<label class="form-label">Job Title</label>
<input type="text" name="job_title" class="form-control"
value="{{ user.job_title or '' if user else '' }}">
</div>
<div class="col-md-4">
<label class="form-label">Location / Office</label>
<input type="text" name="location" class="form-control"
value="{{ user.location or '' if user else '' }}">
</div>
</div>
{% if user %}
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="is_active" id="isActive"
{% if user.is_active %}checked{% endif %}>
<label class="form-check-label" for="isActive">Active employee</label>
</div>
</div>
{% endif %}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg me-1"></i>{{ 'Save Changes' if user else 'Create User' }}
</button>
<a href="{{ url_for('users.detail', user_id=user.id) if user else url_for('users.index') }}"
class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,89 @@
{% extends 'base.html' %}
{% block title %}Import Users IT Asset Management{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item"><a href="{{ url_for('users.index') }}">Users</a></li>
<li class="breadcrumb-item active">Import</li>
{% endblock %}
{% block content %}
<div class="page-header mb-4">
<h1><i class="bi bi-cloud-download me-2"></i>Import Users</h1>
</div>
<div class="row g-4">
<!-- CSV Import -->
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white fw-semibold py-3">
<i class="bi bi-filetype-csv me-2 text-success"></i>Import from CSV
</div>
<div class="card-body">
<p class="text-muted small mb-3">
Upload a CSV file with employee data. The file must include a
<code>windows_id</code> column. Additional columns are matched by common aliases.
</p>
<div class="bg-light rounded p-2 mb-3" style="font-size:.78rem;">
<strong>Recognised column names:</strong><br>
<code>windows_id</code>, <code>first_name</code>, <code>last_name</code>,
<code>email</code>, <code>department</code>, <code>job_title</code>,
<code>phone</code>, <code>location</code>
<br><span class="text-muted">(case-insensitive, spaces or underscores)</span>
</div>
<form method="POST" action="{{ url_for('users.import_csv') }}" enctype="multipart/form-data">
<div class="mb-3">
<label class="form-label">CSV File</label>
<input type="file" name="csv_file" class="form-control" accept=".csv" required>
</div>
<button type="submit" class="btn btn-success w-100">
<i class="bi bi-upload me-1"></i>Import CSV
</button>
</form>
</div>
</div>
</div>
<!-- LDAP / AD Sync -->
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white fw-semibold py-3">
<i class="bi bi-diagram-3 me-2 text-primary"></i>Sync from Active Directory
</div>
<div class="card-body">
<p class="text-muted small mb-3">
Connects to the LDAP/AD server configured in Settings and upserts all
matching user accounts. Masked users are never overwritten.
</p>
<div class="alert alert-info py-2 small mb-3">
<i class="bi bi-info-circle me-1"></i>
Existing non-masked users will be updated with fresh AD data.
New accounts will be created. Masked records are skipped.
</div>
<form method="POST" action="{{ url_for('users.import_ldap') }}">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-arrow-repeat me-1"></i>Sync from AD Now
</button>
</form>
<hr>
<small class="text-muted d-block">
Configure LDAP server, bind credentials and base DN in
<a href="{{ url_for('settings.index') }}">Settings</a>.
</small>
</div>
</div>
</div>
</div>
<!-- CSV template download hint -->
<div class="card border-0 shadow-sm mt-4" style="max-width:500px;">
<div class="card-body py-3">
<h6 class="fw-semibold mb-2"><i class="bi bi-file-earmark-spreadsheet me-2"></i>CSV Template</h6>
<p class="small text-muted mb-2">Your CSV should look like this:</p>
<pre class="bg-light rounded p-2 small mb-0">windows_id,first_name,last_name,email,department,job_title,location
408525,John,Doe,john.doe@company.com,IT,Engineer,HQ
408526,Jane,Smith,jane.smith@company.com,HR,Manager,HQ</pre>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,125 @@
{% extends 'base.html' %}
{% block title %}Users IT Asset Management{% endblock %}
{% block breadcrumb %}
<li class="breadcrumb-item"><a href="{{ url_for('dashboard.index') }}">Home</a></li>
<li class="breadcrumb-item active">Users</li>
{% endblock %}
{% block content %}
<div class="page-header d-flex align-items-center justify-content-between mb-4">
<h1><i class="bi bi-people-fill me-2"></i>Users</h1>
<div class="d-flex gap-2">
<a href="{{ url_for('users.import_page') }}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-cloud-download me-1"></i>Import
</a>
<a href="{{ url_for('users.create') }}" class="btn btn-primary btn-sm">
<i class="bi bi-person-plus me-1"></i>Add User
</a>
</div>
</div>
<!-- Filters -->
<form method="GET" class="row g-2 mb-3">
<div class="col-md-5">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" name="q" class="form-control" placeholder="Search name, email, WID, dept…" value="{{ q }}">
</div>
</div>
<div class="col-auto">
<div class="form-check form-check-inline mt-1">
<input class="form-check-input" type="checkbox" name="masked" value="1" id="chkMasked"
{% if show_masked %}checked{% endif %} onchange="this.form.submit()">
<label class="form-check-label" for="chkMasked">Show masked users</label>
</div>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-primary">Search</button>
<a href="{{ url_for('users.index') }}" class="btn btn-sm btn-outline-secondary">Clear</a>
</div>
</form>
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Windows ID</th>
<th>Name</th>
<th>Email</th>
<th>Department</th>
<th>Job Title</th>
<th>Source</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{% for u in pagination.items %}
<tr {% if u.is_masked %}class="masked-row"{% endif %}>
<td><code>{{ u.windows_id }}</code></td>
<td>
<a href="{{ url_for('users.detail', user_id=u.id) }}">{{ u.display_name }}</a>
{% if u.is_masked %}<span class="badge badge-masked ms-1">MASKED</span>{% endif %}
</td>
<td>{{ u.display_email }}</td>
<td>{{ u.department or '—' }}</td>
<td>{{ u.job_title or '—' }}</td>
<td>
<span class="badge bg-secondary">{{ u.import_source }}</span>
</td>
<td>
{% if not u.is_masked and u.is_active %}
<span class="badge bg-success">Active</span>
{% elif not u.is_masked %}
<span class="badge bg-warning text-dark">Inactive</span>
{% else %}
<span class="badge badge-masked">Masked</span>
{% endif %}
</td>
<td>
<a href="{{ url_for('users.detail', user_id=u.id) }}" class="btn btn-sm btn-outline-secondary py-0 px-2">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
{% else %}
<tr><td colspan="8" class="text-center text-muted py-4">No users found.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if pagination.pages > 1 %}
<div class="card-footer bg-white d-flex justify-content-between align-items-center py-2">
<small class="text-muted">
Showing {{ pagination.first }}{{ pagination.last }} of {{ pagination.total }}
</small>
<nav>
<ul class="pagination pagination-sm mb-0">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('users.index', page=pagination.prev_num, q=q, masked='1' if show_masked else '0') }}"></a>
</li>
{% endif %}
{% for p in pagination.iter_pages(left_edge=1, right_edge=1, left_current=2, right_current=2) %}
{% if p %}
<li class="page-item {% if p == pagination.page %}active{% endif %}">
<a class="page-link" href="{{ url_for('users.index', page=p, q=q, masked='1' if show_masked else '0') }}">{{ p }}</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link"></span></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('users.index', page=pagination.next_num, q=q, masked='1' if show_masked else '0') }}"></a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
</div>
{% endblock %}