Mar 03 2026: implemented SLA issue tracking & enhanced Dashboard

This commit is contained in:
2026-03-03 17:26:37 -05:00
parent e25471531c
commit e292213042
8 changed files with 874 additions and 229 deletions
+257 -149
View File
@@ -1,172 +1,280 @@
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<div class="row mb-4">
<div class="col-12">
<h2>Welcome, {{ current_user.username }}!</h2>
<p class="text-muted">
<span class="badge bg-{% if current_user.role == 'admin' %}danger{% elif current_user.role == 'supervisor' %}warning{% else %}info{% endif %}">
{{ current_user.role|title }}
</span>
</p>
</div>
<div class="row mb-3 align-items-center">
<div class="col">
<h2 class="mb-0">Welcome, {{ current_user.username }}!</h2>
<span class="badge bg-{% if current_user.role == 'admin' %}danger{% elif current_user.role == 'supervisor' %}warning{% else %}info{% endif %} mt-1">
{{ current_user.role|title }}
</span>
</div>
</div>
<div class="row">
<div class="col-md-3 mb-4">
<div class="card text-white bg-primary h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-title text-white-50">Today's Inspections</h6>
<h2 class="mb-0">{{ today_inspections }}</h2>
</div>
<div>
<i class="bi bi-clipboard-data" style="font-size: 3rem; opacity: 0.3;"></i>
</div>
</div>
</div>
{# ── Top stat cards ─────────────────────────────────────────────────────── #}
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="card text-white bg-primary h-100">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="small text-white-50 fw-semibold">Today's Inspections</div>
<div class="fs-2 fw-bold">{{ today_inspections }}</div>
</div>
<i class="bi bi-clipboard-data" style="font-size:2.5rem;opacity:.25;"></i>
</div>
</div>
<div class="col-md-3 mb-4">
<div class="card text-white bg-success h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-title text-white-50">Completed Today</h6>
<h2 class="mb-0">{{ completed_today }}</h2>
</div>
<div>
<i class="bi bi-check-circle" style="font-size: 3rem; opacity: 0.3;"></i>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card text-white bg-success h-100">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="small text-white-50 fw-semibold">Completed Today</div>
<div class="fs-2 fw-bold">{{ completed_today }}</div>
</div>
<i class="bi bi-check-circle" style="font-size:2.5rem;opacity:.25;"></i>
</div>
</div>
<div class="col-md-3 mb-4">
<div class="card text-white bg-warning h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-title text-white-50">Open Issues</h6>
<h2 class="mb-0">{{ open_issues }}</h2>
</div>
<div>
<i class="bi bi-exclamation-triangle" style="font-size: 3rem; opacity: 0.3;"></i>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card text-white bg-warning h-100">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="small text-white-50 fw-semibold">Open Issues</div>
<div class="fs-2 fw-bold">{{ open_issues }}</div>
</div>
<i class="bi bi-exclamation-triangle" style="font-size:2.5rem;opacity:.25;"></i>
</div>
</div>
<div class="col-md-3 mb-4">
<div class="card text-white bg-info h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<h6 class="card-title text-white-50">Avg Score (30d)</h6>
<h2 class="mb-0">{{ avg_score if avg_score else '--' }}%</h2>
</div>
<div>
<i class="bi bi-graph-up" style="font-size: 3rem; opacity: 0.3;"></i>
</div>
</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card text-white bg-info h-100">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="small text-white-50 fw-semibold">Avg Score (30d)</div>
<div class="fs-2 fw-bold">{{ avg_score if avg_score else '--' }}{% if avg_score %}%{% endif %}</div>
</div>
<i class="bi bi-graph-up" style="font-size:2.5rem;opacity:.25;"></i>
</div>
</div>
</div>
</div>
{% if current_user.role in ['admin', 'supervisor'] %}
<div class="row mb-4">
<div class="col-md-4">
<div class="card shadow-sm">
<div class="card-body text-center">
<i class="bi bi-building text-primary" style="font-size: 2.5rem;"></i>
<h3 class="mt-2">{{ total_facilities }}</h3>
<p class="text-muted mb-0">Active Facilities</p>
</div>
{# ── SLA Summary ─────────────────────────────────────────────────────────── #}
{% if sla_breached > 0 or sla_at_risk > 0 %}
<div class="row g-3 mb-4">
{% if sla_breached > 0 %}
<div class="col-6 col-md-3">
<a href="{{ url_for('issues.index', sla='breached') }}" class="text-decoration-none">
<div class="card border-danger h-100">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="small text-danger fw-semibold">SLA Breached</div>
<div class="fs-2 fw-bold text-danger">{{ sla_breached }}</div>
</div>
<i class="bi bi-alarm text-danger" style="font-size:2.5rem;opacity:.3;"></i>
</div>
</div>
<div class="col-md-4">
<div class="card shadow-sm">
<div class="card-body text-center">
<i class="bi bi-file-earmark-text text-success" style="font-size: 2.5rem;"></i>
<h3 class="mt-2">{{ total_templates }}</h3>
<p class="text-muted mb-0">Templates</p>
</div>
</div>
</a>
</div>
{% endif %}
{% if sla_at_risk > 0 %}
<div class="col-6 col-md-3">
<a href="{{ url_for('issues.index', sla='at_risk') }}" class="text-decoration-none">
<div class="card border-warning h-100">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<div class="small text-warning fw-semibold">SLA At Risk</div>
<div class="fs-2 fw-bold text-warning">{{ sla_at_risk }}</div>
</div>
<i class="bi bi-alarm text-warning" style="font-size:2.5rem;opacity:.3;"></i>
</div>
</div>
{% if current_user.role == 'admin' %}
<div class="col-md-4">
<div class="card shadow-sm">
<div class="card-body text-center">
<i class="bi bi-people text-warning" style="font-size: 2.5rem;"></i>
<h3 class="mt-2">{{ total_users }}</h3>
<p class="text-muted mb-0">Users</p>
</div>
</div>
</div>
{% endif %}
</div>
</a>
</div>
{% endif %}
</div>
{% endif %}
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="bi bi-clock-history"></i> Recent Activity</h5>
</div>
<div class="card-body">
{% if recent_inspections %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Date</th>
<th>Facility</th>
<th>Area</th>
<th>Inspector</th>
<th>Score</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for inspection in recent_inspections %}
<tr>
<td>{{ inspection.inspection_date.strftime('%Y-%m-%d %H:%M') }}</td>
<td>{{ inspection.facility.name }}</td>
<td>{{ inspection.area.name if inspection.area else 'N/A' }}</td>
<td>{{ inspection.inspector.username }}</td>
<td>
{% if inspection.overall_score %}
<span class="badge bg-{% if inspection.overall_score >= 90 %}success{% elif inspection.overall_score >= 70 %}warning{% else %}danger{% endif %}">
{{ inspection.overall_score }}%
</span>
{% else %}
<span class="text-muted">--</span>
{% endif %}
</td>
<td>
<span class="badge bg-{% if inspection.status == 'completed' %}success{% elif inspection.status == 'flagged' %}danger{% else %}secondary{% endif %}">
{{ inspection.status|title }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info mb-0">
<i class="bi bi-info-circle"></i> No recent inspections to display.
</div>
{% endif %}
</div>
</div>
{# ── System quick stats (admin/supervisor) ──────────────────────────────── #}
{% if current_user.role in ['admin', 'supervisor'] %}
<div class="row g-3 mb-4">
<div class="col-6 col-md-{% if current_user.role == 'admin' %}4{% else %}6{% endif %}">
<div class="card shadow-sm text-center">
<div class="card-body py-3">
<i class="bi bi-building text-primary" style="font-size:2rem;"></i>
<div class="fs-4 fw-bold mt-1">{{ total_facilities }}</div>
<div class="text-muted small">Active Facilities</div>
</div>
</div>
</div>
<div class="col-6 col-md-{% if current_user.role == 'admin' %}4{% else %}6{% endif %}">
<div class="card shadow-sm text-center">
<div class="card-body py-3">
<i class="bi bi-file-earmark-text text-success" style="font-size:2rem;"></i>
<div class="fs-4 fw-bold mt-1">{{ total_templates }}</div>
<div class="text-muted small">Templates</div>
</div>
</div>
</div>
{% if current_user.role == 'admin' %}
<div class="col-6 col-md-4">
<div class="card shadow-sm text-center">
<div class="card-body py-3">
<i class="bi bi-people text-warning" style="font-size:2rem;"></i>
<div class="fs-4 fw-bold mt-1">{{ total_users }}</div>
<div class="text-muted small">Users</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
{% endif %}
{# ── Score trend chart + Facility performance ───────────────────────────── #}
<div class="row g-3 mb-4">
<div class="col-lg-{% if current_user.role in ['admin','supervisor'] and facility_perf %}7{% else %}12{% endif %}">
<div class="card shadow-sm h-100">
<div class="card-header bg-light fw-semibold">
<i class="bi bi-graph-up me-1"></i>Inspection Score Trend (Last 30 Days)
</div>
<div class="card-body">
{% if trend_labels %}
<canvas id="trendChart" height="120"></canvas>
{% else %}
<div class="text-center text-muted py-4">
<i class="bi bi-bar-chart-line fs-2 d-block mb-2"></i>
No completed inspections with scores in the last 30 days.
</div>
{% endif %}
</div>
</div>
</div>
{% if current_user.role in ['admin', 'supervisor'] and facility_perf %}
<div class="col-lg-5">
<div class="card shadow-sm h-100">
<div class="card-header bg-light fw-semibold">
<i class="bi bi-building me-1"></i>Facility Performance (30d)
</div>
<div class="card-body p-0">
<table class="table table-sm table-hover mb-0">
<thead class="table-light">
<tr>
<th>Facility</th>
<th class="text-center">Inspections</th>
<th class="text-center">Avg Score</th>
</tr>
</thead>
<tbody>
{% for f in facility_perf %}
<tr>
<td class="small">{{ f.name }}</td>
<td class="text-center small">{{ f.count }}</td>
<td class="text-center">
<span class="badge bg-{% if f.avg >= 90 %}success{% elif f.avg >= 70 %}warning{% else %}danger{% endif %}">
{{ f.avg }}%
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
</div>
{# ── Recent activity ─────────────────────────────────────────────────────── #}
<div class="card shadow-sm">
<div class="card-header bg-light fw-semibold">
<i class="bi bi-clock-history me-1"></i>Recent Activity
</div>
<div class="card-body p-0">
{% if recent_inspections %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Date</th>
<th>Facility</th>
<th>Area</th>
{% if current_user.role != 'inspector' %}<th>Inspector</th>{% endif %}
<th>Score</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for insp in recent_inspections %}
<tr style="cursor:pointer;" onclick="window.location='{{ url_for('inspections.view', inspection_id=insp.id) }}'">
<td><small>{{ insp.inspection_date.strftime('%Y-%m-%d %H:%M') }}</small></td>
<td>{{ insp.facility.name }}</td>
<td>{{ insp.area.name if insp.area else '—' }}</td>
{% if current_user.role != 'inspector' %}<td>{{ insp.inspector.username }}</td>{% endif %}
<td>
{% if insp.overall_score %}
<span class="badge bg-{% if insp.overall_score >= 90 %}success{% elif insp.overall_score >= 70 %}warning{% else %}danger{% endif %}">
{{ insp.overall_score }}%
</span>
{% else %}
<span class="text-muted"></span>
{% endif %}
</td>
<td>
<span class="badge bg-{% if insp.status == 'completed' %}success{% elif insp.status == 'flagged' %}danger{% else %}secondary{% endif %}">
{{ insp.status|title }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-4 text-muted">
<i class="bi bi-inbox fs-2 d-block mb-2"></i>No recent inspections.
</div>
{% endif %}
</div>
</div>
{% if trend_labels %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script>
(function () {
const ctx = document.getElementById('trendChart').getContext('2d');
const labels = {{ trend_labels | tojson }};
const data = {{ trend_data | tojson }};
new Chart(ctx, {
type: 'line',
data: {
labels,
datasets: [{
label: 'Avg Score (%)',
data,
borderColor: '#2563eb',
backgroundColor: 'rgba(37,99,235,0.08)',
borderWidth: 2,
pointRadius: 4,
pointBackgroundColor: '#2563eb',
tension: 0.3,
fill: true,
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: { callbacks: { label: ctx => ' ' + ctx.parsed.y + '%' } }
},
scales: {
y: { min: 0, max: 100, ticks: { callback: v => v + '%' }, grid: { color: 'rgba(0,0,0,.05)' } },
x: { grid: { display: false } }
}
}
});
}());
</script>
{% endif %}
{% endblock %}