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