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,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 %}