Initial commit: add compliance_checks table, per-check metadata on assets, and compliance audit trail
This commit is contained in:
211
app/templates/users/detail.html
Normal file
211
app/templates/users/detail.html
Normal 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 %}
|
||||
92
app/templates/users/form.html
Normal file
92
app/templates/users/form.html
Normal 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 %}
|
||||
89
app/templates/users/import.html
Normal file
89
app/templates/users/import.html
Normal 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 %}
|
||||
125
app/templates/users/index.html
Normal file
125
app/templates/users/index.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user