176 lines
7.1 KiB
HTML
176 lines
7.1 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}Issues Aging{% endblock %}
|
||
{% block content %}
|
||
|
||
{# ── Sub-nav ── #}
|
||
{% include 'reports/_subnav.html' %}
|
||
|
||
<div class="d-flex justify-content-between align-items-start mb-4">
|
||
<div>
|
||
<h2><i class="bi bi-clock-history text-danger me-2"></i>Issues Aging</h2>
|
||
<p class="text-muted mb-0">All currently open issues grouped by how long they have been waiting.</p>
|
||
</div>
|
||
<a href="{{ url_for('reports.export_issues_aging', severity=severity_filter, facility_id=facility_id_filter or '') }}"
|
||
class="btn btn-sm btn-outline-success">
|
||
<i class="bi bi-file-earmark-excel me-1"></i>Export Excel
|
||
</a>
|
||
</div>
|
||
|
||
{# ── Filters ── #}
|
||
<div class="card shadow-sm mb-4">
|
||
<div class="card-body py-2">
|
||
<form method="get" class="row g-2 align-items-end">
|
||
<div class="col-md-2">
|
||
<label class="form-label small mb-1">Severity</label>
|
||
<select name="severity" class="form-select form-select-sm">
|
||
<option value="">All</option>
|
||
{% for s in ['critical','high','medium','low'] %}
|
||
<option value="{{ s }}" {{ 'selected' if severity_filter == s }}>{{ s|title }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div class="col-md-4">
|
||
<label class="form-label small mb-1">Facility</label>
|
||
<select name="facility_id" class="form-select form-select-sm">
|
||
<option value="">All Facilities</option>
|
||
{% for f in facilities %}
|
||
<option value="{{ f.id }}" {{ 'selected' if facility_id_filter == f.id }}>{{ f.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div class="col-auto">
|
||
<button class="btn btn-sm btn-primary">Apply</button>
|
||
<a href="{{ url_for('reports.issues_aging') }}" class="btn btn-sm btn-outline-secondary">Clear</a>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
{# ── KPI row ── #}
|
||
<div class="row g-3 mb-4">
|
||
<div class="col-6 col-md-4">
|
||
<div class="card shadow-sm border-0 h-100" style="background:#fff7ed;">
|
||
<div class="card-body">
|
||
<div class="small fw-semibold text-muted mb-1">Total Open</div>
|
||
<div class="fs-1 fw-bold" style="color:#ea580c;">{{ total }}</div>
|
||
<div class="small text-muted">unresolved issues</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-md-4">
|
||
<div class="card shadow-sm border-0 h-100" style="background:#fef2f2;">
|
||
<div class="card-body">
|
||
<div class="small fw-semibold text-muted mb-1">SLA Breached</div>
|
||
<div class="fs-1 fw-bold text-danger">{{ sla_breached }}</div>
|
||
<div class="small text-muted">past resolution deadline</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-6 col-md-4">
|
||
<div class="card shadow-sm border-0 h-100" style="background:#fefce8;">
|
||
<div class="card-body">
|
||
<div class="small fw-semibold text-muted mb-1">SLA At Risk</div>
|
||
<div class="fs-1 fw-bold text-warning">{{ sla_at_risk }}</div>
|
||
<div class="small text-muted">approaching deadline</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{# ── Bucket accordions ── #}
|
||
<div class="accordion" id="agingAccordion">
|
||
{% for label in bucket_labels %}
|
||
{% set bucket = buckets[label] %}
|
||
{% set bucket_id = 'bucket-' ~ loop.index %}
|
||
{% set is_danger = label in ['>4 weeks', '1–4 weeks'] %}
|
||
{% set is_warning = label == '3–7 days' %}
|
||
<div class="accordion-item mb-2 shadow-sm border-0">
|
||
<h2 class="accordion-header">
|
||
<button class="accordion-button {{ 'collapsed' if loop.index > 1 else '' }} fw-semibold"
|
||
type="button" data-bs-toggle="collapse"
|
||
data-bs-target="#{{ bucket_id }}">
|
||
<span class="badge rounded-pill me-2
|
||
{{ 'bg-danger' if is_danger else 'bg-warning text-dark' if is_warning else 'bg-secondary' }}">
|
||
{{ bucket|length }}
|
||
</span>
|
||
{{ label }}
|
||
{% if is_danger and bucket|length > 0 %}
|
||
<span class="ms-2 badge bg-danger bg-opacity-25 text-danger" style="font-size:.7rem;">Needs attention</span>
|
||
{% endif %}
|
||
</button>
|
||
</h2>
|
||
<div id="{{ bucket_id }}" class="accordion-collapse collapse {{ 'show' if loop.index == 1 else '' }}"
|
||
data-bs-parent="#agingAccordion">
|
||
<div class="accordion-body p-0">
|
||
{% if bucket %}
|
||
<div class="table-responsive">
|
||
<table class="table table-hover table-sm mb-0">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>#</th>
|
||
<th>Age</th>
|
||
<th>Severity</th>
|
||
<th>Facility / Area</th>
|
||
<th>Description</th>
|
||
<th>Status</th>
|
||
<th>SLA</th>
|
||
<th>Assigned</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for item in bucket %}
|
||
{% set issue = item.issue %}
|
||
{% set sla = item.sla %}
|
||
<tr class="{{ 'table-danger' if sla == 'breached' else 'table-warning' if sla == 'at_risk' else '' }}">
|
||
<td class="text-muted small">#{{ issue.id }}</td>
|
||
<td class="text-nowrap small">{{ item.age_h }}h</td>
|
||
<td>
|
||
<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>
|
||
<td class="small">
|
||
{{ issue.resolved_facility.name if issue.resolved_facility else '—' }}<br>
|
||
<span class="text-muted">{{ issue.area.name if issue.area else '—' }}</span>
|
||
</td>
|
||
<td class="small">{{ issue.description[:70] }}{% if issue.description|length > 70 %}…{% endif %}</td>
|
||
<td>
|
||
<span class="badge bg-{{ 'info text-dark' if issue.status == 'pending_verification' else 'warning text-dark' if issue.status == 'in_progress' else 'secondary' }}">
|
||
{{ issue.status|replace('_',' ')|title }}
|
||
</span>
|
||
</td>
|
||
<td>
|
||
{% 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">At Risk</span>
|
||
{% elif sla == 'ok' %}<span class="badge bg-secondary">OK</span>
|
||
{% else %}<span class="text-muted small">—</span>{% endif %}
|
||
</td>
|
||
<td class="small">{{ issue.assigned_user.display_name if issue.assigned_user else '—' }}</td>
|
||
<td>
|
||
<a href="{{ url_for('issues.view', issue_id=issue.id) }}" class="btn btn-sm btn-outline-secondary">
|
||
<i class="bi bi-eye"></i>
|
||
</a>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<div class="p-3 text-muted small">No issues in this age range.</div>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
{% if total == 0 %}
|
||
<div class="alert alert-success mt-3">
|
||
<i class="bi bi-check-circle me-2"></i>No open issues match the selected filters.
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% endblock %}
|