Initial commit: add compliance_checks table, per-check metadata on assets, and compliance audit trail
This commit is contained in:
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 %}
|
||||
Reference in New Issue
Block a user