237 lines
9.3 KiB
HTML
237 lines
9.3 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Verification Queue{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h2><i class="bi bi-patch-check text-info me-2"></i>Verification Queue</h2>
|
|
<p class="text-muted mb-0">
|
|
Issues awaiting director sign-off before they are fully closed.
|
|
</p>
|
|
</div>
|
|
<div class="d-flex gap-2 align-items-center">
|
|
{% if total_pending > 0 %}
|
|
<span class="badge bg-info text-dark fs-6">{{ total_pending }} pending</span>
|
|
{% else %}
|
|
<span class="badge bg-success fs-6"><i class="bi bi-check-all me-1"></i>All clear</span>
|
|
{% endif %}
|
|
<a href="{{ url_for('issues.index') }}" class="btn btn-sm btn-outline-secondary">
|
|
<i class="bi bi-arrow-left"></i> All Issues
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
{# ── Bulk-verify toolbar — shown when at least one issue is pending ── #}
|
|
{% if grouped %}
|
|
<form method="POST" action="{{ url_for('issues.bulk_verify') }}" id="bulkVerifyForm">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
{# Issue checkboxes are rendered inside the per-facility tables below;
|
|
hidden inputs with their IDs are inserted here by JS on submission. #}
|
|
<div class="card bg-light border-0 mb-3 p-2 d-flex flex-row align-items-center gap-3 flex-wrap" id="bulkToolbar">
|
|
<div class="form-check mb-0">
|
|
<input class="form-check-input" type="checkbox" id="selectAllIssues">
|
|
<label class="form-check-label fw-semibold" for="selectAllIssues">Select all</label>
|
|
</div>
|
|
<span class="text-muted small" id="selectedCount">0 selected</span>
|
|
<button type="submit" class="btn btn-success btn-sm" id="bulkVerifyBtn" disabled
|
|
onclick="return injectBulkIds(this.form)">
|
|
<i class="bi bi-patch-check-fill me-1"></i>Verify Selected
|
|
</button>
|
|
</div>
|
|
</form>
|
|
{% endif %}
|
|
|
|
{% if not grouped %}
|
|
<div class="card shadow-sm">
|
|
<div class="card-body text-center py-5 text-muted">
|
|
<i class="bi bi-patch-check fs-1 d-block mb-3 opacity-25"></i>
|
|
<p class="mb-0 fs-5">No issues are currently awaiting verification.</p>
|
|
<p class="small mt-1">When inspectors request sign-off, their issues will appear here.</p>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
|
|
{# ── Per-facility groups ── #}
|
|
{% for facility, issues in grouped %}
|
|
<div class="card shadow-sm mb-4">
|
|
<div class="card-header d-flex justify-content-between align-items-center bg-light">
|
|
<span class="fw-semibold">
|
|
<i class="bi bi-building me-1 text-muted"></i>{{ facility.name }}
|
|
</span>
|
|
<span class="badge bg-info text-dark">
|
|
{{ issues|length }} issue{{ 's' if issues|length != 1 else '' }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="card-body p-0">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light" style="font-size:.82rem;">
|
|
<tr>
|
|
<th width="36"><span class="visually-hidden">Select</span></th>
|
|
<th width="60">ID</th>
|
|
<th width="90">Severity</th>
|
|
<th>Area / Description</th>
|
|
<th>Requested By</th>
|
|
<th>Reported</th>
|
|
<th>SLA</th>
|
|
<th width="220">Verify</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for issue in issues %}
|
|
{% set sla = sla_status(issue) %}
|
|
{% set hrs = sla_hours_remaining(issue) %}
|
|
<tr class="{{ 'table-danger' if sla == 'breached' else 'table-warning' if sla == 'at_risk' else '' }}">
|
|
|
|
{# Bulk-select checkbox #}
|
|
<td class="align-middle text-center">
|
|
<input class="form-check-input issue-checkbox" type="checkbox"
|
|
value="{{ issue.id }}" aria-label="Select issue #{{ issue.id }}">
|
|
</td>
|
|
|
|
{# ID #}
|
|
<td class="text-muted small align-middle">
|
|
<a href="{{ url_for('issues.view', issue_id=issue.id) }}"
|
|
class="text-decoration-none fw-semibold">#{{ issue.id }}</a>
|
|
</td>
|
|
|
|
{# Severity #}
|
|
<td class="align-middle">
|
|
<span class="badge bg-{{ 'danger' if issue.severity in ['critical','high'] else 'warning text-dark' if issue.severity == 'medium' else 'secondary' }}">
|
|
{{ issue.severity|title }}
|
|
</span>
|
|
</td>
|
|
|
|
{# Area / Description #}
|
|
<td class="align-middle">
|
|
<div class="fw-semibold small">{{ issue.area.name if issue.area else '—' }}</div>
|
|
<div class="text-muted small">
|
|
{{ issue.description[:80] }}{% if issue.description|length > 80 %}…{% endif %}
|
|
</div>
|
|
</td>
|
|
|
|
{# Requested by / assignee #}
|
|
<td class="align-middle small">
|
|
{% if issue.assigned_user %}
|
|
<i class="bi bi-person-circle text-muted me-1"></i>{{ issue.assigned_user.display_name }}
|
|
{% else %}
|
|
<span class="text-muted">— unassigned —</span>
|
|
{% endif %}
|
|
</td>
|
|
|
|
{# Reported date #}
|
|
<td class="align-middle small text-muted">
|
|
{{ issue.reported_at.strftime('%Y-%m-%d') }}<br>
|
|
<span style="font-size:.75rem;">{{ issue.reported_at.strftime('%H:%M') }}</span>
|
|
</td>
|
|
|
|
{# SLA indicator #}
|
|
<td class="align-middle">
|
|
{% if sla == 'breached' %}
|
|
<span class="badge bg-danger">
|
|
<i class="bi bi-alarm me-1"></i>Breached
|
|
</span>
|
|
{% elif sla == 'at_risk' %}
|
|
<span class="badge bg-warning text-dark">
|
|
<i class="bi bi-hourglass-split me-1"></i>
|
|
{% if hrs is not none %}{{ hrs|abs|round(1) }}h left{% else %}At Risk{% endif %}
|
|
</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">
|
|
{% if hrs is not none %}{{ hrs|round(1) }}h left{% else %}OK{% endif %}
|
|
</span>
|
|
{% endif %}
|
|
</td>
|
|
|
|
{# Inline verify form #}
|
|
<td class="align-middle">
|
|
<form method="POST"
|
|
action="{{ url_for('issues.verify', issue_id=issue.id) }}"
|
|
class="d-flex gap-1 align-items-center">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="text"
|
|
name="verification_note"
|
|
class="form-control form-control-sm"
|
|
placeholder="Note (optional)"
|
|
style="width:130px;">
|
|
<button type="submit" class="btn btn-sm btn-success flex-shrink-0"
|
|
title="Verify & close this issue"
|
|
onclick="return confirm('Verify and close Issue #{{ issue.id }}?')">
|
|
<i class="bi bi-patch-check"></i>
|
|
</button>
|
|
<a href="{{ url_for('issues.view', issue_id=issue.id) }}"
|
|
class="btn btn-sm btn-outline-secondary flex-shrink-0"
|
|
title="View full issue">
|
|
<i class="bi bi-eye"></i>
|
|
</a>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
|
|
{# ── Legend ── #}
|
|
<div class="card border-0 bg-light mt-2">
|
|
<div class="card-body py-2 px-3 d-flex gap-4 flex-wrap" style="font-size:.78rem;">
|
|
<span><span class="badge bg-danger me-1">Breached</span>Past SLA deadline</span>
|
|
<span><span class="badge bg-warning text-dark me-1">At Risk</span>>75% of SLA window elapsed</span>
|
|
<span><span class="badge bg-secondary me-1">OK</span>Within SLA</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
// ── Bulk-verify checkbox management ─────────────────────────────────────────
|
|
(function () {
|
|
const selectAll = document.getElementById('selectAllIssues');
|
|
const countEl = document.getElementById('selectedCount');
|
|
const verifyBtn = document.getElementById('bulkVerifyBtn');
|
|
|
|
function updateToolbar() {
|
|
const checked = document.querySelectorAll('.issue-checkbox:checked');
|
|
const n = checked.length;
|
|
if (countEl) countEl.textContent = n + ' selected';
|
|
if (verifyBtn) verifyBtn.disabled = n === 0;
|
|
if (selectAll) selectAll.indeterminate = n > 0 && n < document.querySelectorAll('.issue-checkbox').length;
|
|
if (selectAll) selectAll.checked = n > 0 && n === document.querySelectorAll('.issue-checkbox').length;
|
|
}
|
|
|
|
document.querySelectorAll('.issue-checkbox').forEach(cb => {
|
|
cb.addEventListener('change', updateToolbar);
|
|
});
|
|
|
|
if (selectAll) {
|
|
selectAll.addEventListener('change', function () {
|
|
document.querySelectorAll('.issue-checkbox').forEach(cb => { cb.checked = this.checked; });
|
|
updateToolbar();
|
|
});
|
|
}
|
|
|
|
updateToolbar();
|
|
}());
|
|
|
|
// ── Inject checked issue IDs into the bulk-verify form before submit ─────────
|
|
function injectBulkIds(form) {
|
|
// Remove any previously injected inputs
|
|
form.querySelectorAll('input[name="issue_ids"]').forEach(el => el.remove());
|
|
const checked = document.querySelectorAll('.issue-checkbox:checked');
|
|
if (!checked.length) { alert('Please select at least one issue.'); return false; }
|
|
if (!confirm('Verify and close ' + checked.length + ' selected issue(s)?')) return false;
|
|
checked.forEach(cb => {
|
|
const hidden = document.createElement('input');
|
|
hidden.type = 'hidden';
|
|
hidden.name = 'issue_ids';
|
|
hidden.value = cb.value;
|
|
form.appendChild(hidden);
|
|
});
|
|
return true;
|
|
}
|
|
</script>
|
|
{% endblock %}
|