Aug 3 - Update the dashboard

This commit is contained in:
2026-08-03 14:09:10 -04:00
parent 991f242d12
commit d673d52f29
3 changed files with 221 additions and 288 deletions
+2 -1
View File
@@ -13,7 +13,8 @@
"Bash(python -c \"print\\(len\\('phase47_sched_acknowledged'\\)\\)\")",
"Bash(python -c \"import ast,io; ast.parse\\(io.open\\('app/routes/scheduled_inspections.py',encoding='utf-8'\\).read\\(\\)\\); print\\('route OK'\\)\")",
"Bash(python -c \"import ast,io; ast.parse\\(io.open\\('app/utils/notifications.py',encoding='utf-8'\\).read\\(\\)\\); print\\('notif OK'\\)\")",
"Bash(SECRET_KEY=x DATABASE_URL=sqlite:///:memory: DIGEST_SECRET=x MAIL_SERVER=localhost MAIL_USERNAME=x MAIL_PASSWORD=x MAIL_PORT=587 APP_BASE_URL=http://localhost MAIL_DEFAULT_SENDER=x@x.com python -c ' *)"
"Bash(SECRET_KEY=x DATABASE_URL=sqlite:///:memory: DIGEST_SECRET=x MAIL_SERVER=localhost MAIL_USERNAME=x MAIL_PASSWORD=x MAIL_PORT=587 APP_BASE_URL=http://localhost MAIL_DEFAULT_SENDER=x@x.com python -c ' *)",
"Bash(python -c \"import ast,io; ast.parse\\(io.open\\('app/routes/dashboard.py',encoding='utf-8'\\).read\\(\\)\\); print\\('dashboard route OK'\\)\")"
]
}
}
+21
View File
@@ -294,6 +294,23 @@ def index():
stale_q = stale_q.filter(False)
stale_in_progress = stale_q.count()
# ── In-progress inspections (all, not just stale) — "In Progress" tile ─────
inprog_q = Inspection.query.filter(Inspection.status == 'in_progress')
if is_inspector:
if not inspector_facility_ids:
inprog_q = inprog_q.filter(False)
else:
inprog_q = inprog_q.filter(
Inspection.facility_id.in_(inspector_facility_ids),
Inspection.inspector_id == current_user.id,
)
elif is_customer:
if customer_facility_ids:
inprog_q = inprog_q.filter(Inspection.facility_id.in_(customer_facility_ids))
else:
inprog_q = inprog_q.filter(False)
in_progress_total = inprog_q.count()
# ── Unassigned open issues ────────────────────────────────────────────────
from app.models.facility import Area as _AreaU
unassigned_q = Issue.query.outerjoin(_AreaU, Issue.area_id == _AreaU.id).filter(
@@ -361,6 +378,7 @@ def index():
# ── Scheduled inspections (phase36): upcoming / overdue ──────────────
sched_upcoming = []
sched_overdue_count = 0
sched_total = 0
sched_open_inspections = {}
if not is_customer:
from app.models.scheduled_inspection import ScheduledInspection
@@ -370,6 +388,7 @@ def index():
if is_inspector:
_sq = _sq.filter(ScheduledInspection.inspector_id == current_user.id)
_all_sched = _sq.order_by(ScheduledInspection.next_due_date.asc()).all()
sched_total = len(_all_sched) # active scheduled inspection plans ("On Schedules")
sched_overdue_count = sum(1 for s in _all_sched if s.next_due_date < _today)
# Upcoming = due today through the next 7 days (overdue shown separately)
sched_upcoming = [
@@ -383,7 +402,9 @@ def index():
'dashboard.html',
sched_upcoming = sched_upcoming,
sched_overdue_count = sched_overdue_count,
sched_total = sched_total,
sched_open_inspections = sched_open_inspections,
in_progress_total = in_progress_total,
submitted_this_week = submitted_this_week,
completed_today = completed_today,
open_issues = open_issues,
+198 -287
View File
@@ -3,8 +3,34 @@
{% block content %}
{# Handler ("Handled by") breakdown chips for an issue KPI card. `base` is a
dict of extra issues.index query params identifying the card's scope. #}
{# ── Grouped dashboard styling (colored section cards + white stat tiles) ──── #}
<style>
.group-card { border-radius:16px; padding:14px 18px 18px; margin-bottom:22px; }
.group-title { font-weight:800; font-size:1.4rem; letter-spacing:.2px; margin:2px 0 12px; }
.group-blue { background:#4a90d9; } /* A — solid blue */
.group-green { background:#d9ead3; } /* B — light green */
.group-orange { background:#fce5cd; } /* C — light orange */
.group-grey { background:#efefef; } /* D — light grey */
.gt-white { color:#ffffff; }
.gt-green { color:#38761d; }
.gt-orange { color:#b45f06; }
.gt-red { color:#cc0000; }
.stat-tile { background:#fff; border:2px solid #cbd3dd; border-radius:11px;
padding:12px 16px; height:100%; display:flex; align-items:center;
justify-content:space-between; gap:10px; transition:box-shadow .15s; }
a.tile-link:hover .stat-tile { box-shadow:0 3px 10px rgba(0,0,0,.12); }
.st-label { font-size:.9rem; font-weight:600; color:#3f4652; line-height:1.15; }
.st-sub { font-size:.68rem; color:#8a93a2; margin-top:3px; }
.st-value { font-size:1.9rem; font-weight:800; line-height:1; white-space:nowrap; }
/* Scheduled-inspection table inside the solid-blue section A */
.sched-table { background:#fff; border-radius:8px; overflow:hidden; margin-bottom:0; }
.sched-table thead th { background:#5b9bd5; color:#fff; border-color:#4a90d9; font-weight:600; }
</style>
{# Handler ("Handled by") breakdown chips for an issue KPI tile. `base` is a
dict of extra issues.index query params identifying the tile's scope. #}
{% macro handler_chips(bd, base) %}
<div class="mt-1 d-flex flex-wrap align-items-center gap-1" style="font-size:.7rem;">
<span style="opacity:.75;">Handled by:</span>
@@ -20,6 +46,36 @@
</div>
{% endmacro %}
{# One white stat tile: label (+ optional subtitle OR handler chips) left, big
colored number right. Numbers render two-digit (01, 02, …) to match the design.
When chips_bd is given and the value is > 0, the handler chips replace the
subtitle; because the chips are themselves links, the whole tile is NOT
wrapped in an anchor (nested <a> is invalid) — only the number links. #}
{% macro stat_tile(label, value, accent, href=None, subtitle=None, chips_bd=None, chips_base=None) %}
{% set has_chips = chips_bd is not none and value and value > 0 %}
<div class="col">
{% if href and not has_chips %}<a href="{{ href }}" class="text-decoration-none tile-link d-block h-100">{% endif %}
<div class="stat-tile" style="border-color:{{ accent }};">
<div class="st-text">
<div class="st-label">{{ label }}</div>
{% if has_chips %}
{{ handler_chips(chips_bd, chips_base) }}
{% elif subtitle %}
<div class="st-sub">{{ subtitle }}</div>
{% endif %}
</div>
<div class="st-value" style="color:{{ accent }};">
{% if href and has_chips %}
<a href="{{ href }}" class="text-decoration-none" style="color:inherit;">{{ '%02d'|format(value) }}</a>
{% else %}
{{ '%02d'|format(value) if value is number else value }}
{% endif %}
</div>
</div>
{% if href and not has_chips %}</a>{% endif %}
</div>
{% endmacro %}
<div class="row mb-3 align-items-center">
<div class="col">
<h2 class="mb-0">Welcome, {{ current_user.display_name }}!</h2>
@@ -29,302 +85,157 @@
</div>
</div>
{# ── Scheduled inspections: upcoming / overdue (phase36) ─────────────────── #}
{% if current_user.role != 'customer' and (sched_upcoming or sched_overdue_count) %}
<div class="card shadow-sm mb-4 border-0" style="border-left:4px solid #6366f1 !important;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="fw-bold"><i class="bi bi-calendar-check text-primary"></i> Scheduled Inspections</span>
<a href="{{ url_for('scheduled_inspections.index') }}" class="btn btn-sm btn-outline-primary">View all</a>
</div>
{% if sched_overdue_count %}
<div class="alert alert-danger py-2 mb-2">
<i class="bi bi-alarm-fill"></i>
<strong>{{ sched_overdue_count }}</strong> scheduled inspection{{ 's' if sched_overdue_count != 1 }}
{{ 'are' if sched_overdue_count != 1 else 'is' }} <strong>overdue</strong>.
</div>
{% endif %}
{% if sched_upcoming %}
<div class="table-responsive">
<table class="table table-sm table-hover mb-0 align-middle">
<thead class="table-light">
<tr><th>Facility</th><th>Template</th><th>Inspector</th><th>Repeats</th><th>Due</th><th></th></tr>
</thead>
<tbody>
{% for s in sched_upcoming %}
<tr>
<td>{{ s.facility.name if s.facility else '—' }}</td>
<td class="small">{{ s.template.name if s.template else '—' }}</td>
<td class="small">{{ s.inspector.display_name if s.inspector else '—' }}</td>
<td class="small text-muted">{{ s.recurrence_label }}</td>
<td class="small">{{ s.next_due_date.strftime('%b %d') }}</td>
<td class="text-end">
{# Start is shown only to the assignee — the inspection is theirs to do. #}
{% if s.inspector_id and s.inspector_id == current_user.id %}
{# Receipt confirmation (phase47) — assignee confirms, or shows confirmed. #}
{% if s.is_acknowledged %}
<span class="badge bg-success" title="You confirmed receipt"><i class="bi bi-check-circle"></i> Confirmed</span>
{% else %}
<form method="POST" class="d-inline" action="{{ url_for('scheduled_inspections.acknowledge', schedule_id=s.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-outline-success py-0" title="Confirm you received this request">
<i class="bi bi-check-lg"></i> Confirm</button>
</form>
{% endif %}
{% set open_id = sched_open_inspections.get(s.id) %}
{% if open_id %}
<a href="{{ url_for('inspections.execute', inspection_id=open_id) }}"
class="btn btn-sm btn-warning py-0" title="You already started this — resume it">
<i class="bi bi-pencil-square"></i> Continue</a>
{% else %}
<a href="{{ url_for('scheduled_inspections.start', schedule_id=s.id) }}"
class="btn btn-sm btn-success py-0"><i class="bi bi-play-fill"></i> Start</a>
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted small mb-0">No inspections due in the next 7 days.</p>
{% endif %}
{# ══════════════════════════════════════════════════════════════════════════
A. SCHEDULED INSPECTION (blue)
══════════════════════════════════════════════════════════════════════════ #}
{% if current_user.role != 'customer' %}
<div class="group-card group-blue">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="group-title gt-white mb-0">A. Scheduled Inspection</span>
<a href="{{ url_for('scheduled_inspections.index') }}" class="btn btn-sm btn-light">View all</a>
</div>
{% if sched_overdue_count %}
<div class="alert alert-danger py-2 mb-2">
<i class="bi bi-alarm-fill"></i>
<strong>{{ sched_overdue_count }}</strong> scheduled inspection{{ 's' if sched_overdue_count != 1 }}
{{ 'are' if sched_overdue_count != 1 else 'is' }} <strong>overdue</strong>.
</div>
{% endif %}
{% if sched_upcoming %}
<div class="table-responsive">
<table class="table table-hover align-middle sched-table">
<thead>
<tr>
<th>Facility Name</th><th>Inspection Template</th><th>Inspector Name</th>
<th>How Often?</th><th>Due</th><th class="text-end"></th>
</tr>
</thead>
<tbody>
{% for s in sched_upcoming %}
<tr>
<td>{{ s.facility.name if s.facility else '—' }}</td>
<td class="small">{{ s.template.name if s.template else '—' }}</td>
<td class="small">{{ s.inspector.display_name if s.inspector else '—' }}</td>
<td class="small text-muted">{{ s.recurrence_label }}</td>
<td class="small">{{ s.next_due_date.strftime('%b %d') }}</td>
<td class="text-end text-nowrap">
{# Start is shown only to the assignee — the inspection is theirs to do. #}
{% if s.inspector_id and s.inspector_id == current_user.id %}
{# Receipt confirmation (phase47) — assignee confirms, or shows confirmed. #}
{% if s.is_acknowledged %}
<span class="badge bg-success" title="You confirmed receipt"><i class="bi bi-check-circle"></i> Confirmed</span>
{% else %}
<form method="POST" class="d-inline" action="{{ url_for('scheduled_inspections.acknowledge', schedule_id=s.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-sm btn-outline-success py-0" title="Confirm you received this request">
<i class="bi bi-check-lg"></i> Confirm</button>
</form>
{% endif %}
{% set open_id = sched_open_inspections.get(s.id) %}
{% if open_id %}
<a href="{{ url_for('inspections.execute', inspection_id=open_id) }}"
class="btn btn-sm btn-warning py-0" title="You already started this — resume it">
<i class="bi bi-pencil-square"></i> Continue</a>
{% else %}
<a href="{{ url_for('scheduled_inspections.start', schedule_id=s.id) }}"
class="btn btn-sm btn-success py-0"><i class="bi bi-play-fill"></i> Start</a>
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="bg-white rounded p-3 text-muted small mb-0">
<i class="bi bi-info-circle me-1"></i>No inspections due in the next 7 days.
</div>
{% endif %}
</div>
{% endif %}
{# ── Inspections section ─────────────────────────────────────────────────── #}
<div class="d-flex align-items-center gap-2 mb-3">
<i class="bi bi-clipboard-data-fill text-primary"></i>
<span class="fw-bold text-uppercase" style="font-size:.78rem;letter-spacing:.07em;color:#64748b;">Inspections</span>
<div style="flex:1;height:1px;background:#e2e8f0;"></div>
</div>
<div class="row g-3 mb-4">
<div class="col-6 col-md">
<a href="{{ url_for('inspections.index', status='completed', date_from=today_str, date_to=today_str) }}" class="text-decoration-none">
<div class="card text-white bg-success h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small text-white-50 fw-semibold">Submitted Today</span>
<i class="bi bi-check-circle" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ completed_today }}</div>
<div class="mt-2" style="font-size:.72rem;opacity:.7;">Fully completed &amp; submitted today</div>
</div>
</div>
</a>
{# ══════════════════════════════════════════════════════════════════════════
B. QUICK REVIEW (green)
══════════════════════════════════════════════════════════════════════════ #}
{% set G = '#4e8a2f' %}
<div class="group-card group-green">
<div class="group-title gt-green">B. Quick Review</div>
<div class="row row-cols-2 row-cols-sm-3 row-cols-xl-5 g-3">
{{ stat_tile('Submitted Today', completed_today, G,
url_for('inspections.index', status='completed', date_from=today_str, date_to=today_str),
'Completed & submitted today') }}
{{ stat_tile('Submitted This Week', submitted_this_week, G,
url_for('inspections.index', status='completed', date_from=week_start_str, date_to=today_str),
'Completed & submitted this week') }}
{% if current_user.role != 'customer' %}
{{ stat_tile('In Progress', in_progress_total, G,
url_for('inspections.index', status='in_progress'),
(stale_in_progress ~ ' stale >24h') if stale_in_progress else 'Started, not yet submitted') }}
{{ stat_tile('Pending Follow-ups', pending_followups, G,
url_for('inspections.index', status='follow_up'),
'Flagged for re-inspection') }}
{{ stat_tile('On Schedules', sched_total, G,
url_for('scheduled_inspections.index'),
'Active scheduled plans') }}
{% endif %}
</div>
<div class="col-6 col-md">
<a href="{{ url_for('inspections.index', status='completed', date_from=week_start_str, date_to=today_str) }}" class="text-decoration-none">
<div class="card text-white bg-primary h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small text-white-50 fw-semibold">Submitted This Week</span>
<i class="bi bi-calendar-check" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ submitted_this_week }}</div>
<div class="mt-2" style="font-size:.72rem;opacity:.7;">Fully completed &amp; submitted this week</div>
</div>
</div>
</a>
</div>
{% if current_user.role != 'customer' %}
<div class="col-6 col-md">
<a href="{{ url_for('inspections.index', status='in_progress') }}" class="text-decoration-none">
<div class="card text-white h-100" style="background:#b45309;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small fw-semibold" style="color:rgba(255,255,255,.6);">Stale In-Progress</span>
<i class="bi bi-hourglass-split" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ stale_in_progress }}</div>
<div class="mt-2" style="font-size:.72rem;opacity:.7;">Started &gt;24 h ago, not yet submitted</div>
</div>
</div>
</a>
</div>
<div class="col-6 col-md">
<a href="{{ url_for('inspections.index', status='follow_up') }}" class="text-decoration-none">
<div class="card text-white h-100" style="background:#7c3aed;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small fw-semibold" style="color:rgba(255,255,255,.6);">Pending Follow-ups</span>
<i class="bi bi-arrow-repeat" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ pending_followups }}</div>
<div class="mt-2" style="font-size:.72rem;opacity:.7;">Flagged for a follow-up re-inspection</div>
</div>
</div>
</a>
</div>
{% endif %}
</div>
{# ── Issues section ──────────────────────────────────────────────────────── #}
<div class="d-flex align-items-center gap-2 mb-3">
<i class="bi bi-exclamation-triangle-fill text-danger"></i>
<span class="fw-bold text-uppercase" style="font-size:.78rem;letter-spacing:.07em;color:#64748b;">Issues</span>
<div style="flex:1;height:1px;background:#e2e8f0;"></div>
</div>
<div class="row g-3 mb-4">
{# Open issues split into three cards by who handles them. #}
<div class="col-6 col-md">
<a href="{{ url_for('issues.index', status='open', handler_type='internal') }}" class="text-decoration-none">
<div class="card text-white bg-primary h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small text-white-50 fw-semibold">Open · Janitorial</span>
<i class="bi bi-people" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ handler_breakdown.internal }}</div>
<div class="mt-2" style="font-size:.72rem;opacity:.7;">Open issues handled by Janitorial crew</div>
</div>
</div>
</a>
{# ══════════════════════════════════════════════════════════════════════════
C. REVIEW ISSUES (orange)
══════════════════════════════════════════════════════════════════════════ #}
{% set O = '#d5761a' %}
<div class="group-card group-orange">
<div class="group-title gt-orange">C. Review Issues</div>
<div class="row row-cols-2 row-cols-sm-3 row-cols-xl-5 g-3">
{{ stat_tile('Open · Janitorial', handler_breakdown.internal, O,
url_for('issues.index', status='open', handler_type='internal'),
'Handled by our crew') }}
{{ stat_tile('Open · Facility Staff', handler_breakdown.facility, O,
url_for('issues.index', status='open', handler_type='facility'),
'Handled by facility staff') }}
{{ stat_tile('Open · Vendors', handler_breakdown.vendor, O,
url_for('issues.index', status='open', handler_type='vendor'),
'Handled by outside vendor') }}
{% if current_user.role != 'customer' %}
{{ stat_tile('Open · Unassigned', unassigned_open, O,
url_for('issues.index', status='open', unassigned='1'),
'No one assigned yet',
chips_bd=unassigned_handler, chips_base={'status': 'open', 'unassigned': '1'}) }}
{{ stat_tile('Pending · Verification', pending_verification, O,
url_for('issues.index', status='pending_verification'),
'Awaiting sign-off') }}
{% endif %}
{# All the original Issues cards are kept here in the same colored group. #}
{{ stat_tile('Issues Opened Today', issues_opened_today, O,
url_for('issues.index', date_from=today_str, date_to=today_str),
'New issues reported today',
chips_bd=opened_today_handler, chips_base={'date_from': today_str, 'date_to': today_str}) }}
{{ stat_tile('Resolved Today', resolved_today, O,
url_for('issues.index', status='resolved', date_from=today_str, date_to=today_str),
'Issues closed today') }}
</div>
<div class="col-6 col-md">
<a href="{{ url_for('issues.index', status='open', handler_type='facility') }}" class="text-decoration-none">
<div class="card text-dark bg-info h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small fw-semibold" style="color:rgba(0,0,0,.55);">Open · Facility Staff</span>
<i class="bi bi-building" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ handler_breakdown.facility }}</div>
<div class="mt-2" style="font-size:.72rem;color:rgba(0,0,0,.6);">Open issues the facility staff handle</div>
</div>
</div>
</a>
</div>
<div class="col-6 col-md">
<a href="{{ url_for('issues.index', status='open', handler_type='vendor') }}" class="text-decoration-none">
<div class="card text-dark bg-warning h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small fw-semibold" style="color:rgba(0,0,0,.55);">Open · Vendor</span>
<i class="bi bi-person-gear" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ handler_breakdown.vendor }}</div>
<div class="mt-2" style="font-size:.72rem;color:rgba(0,0,0,.6);">Open issues an outside vendor handles</div>
</div>
</div>
</a>
</div>
<div class="col-6 col-md">
<div class="card text-white h-100" style="background:#ea580c;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small fw-semibold" style="color:rgba(255,255,255,.6);">Issues Opened Today</span>
<i class="bi bi-flag" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<a href="{{ url_for('issues.index', date_from=today_str, date_to=today_str) }}" class="text-white text-decoration-none">
<div class="fs-1 fw-bold lh-1">{{ issues_opened_today }}</div>
</a>
{% if issues_opened_today > 0 %}
{{ handler_chips(opened_today_handler, {'date_from': today_str, 'date_to': today_str}) }}
{% else %}
<div class="mt-2" style="font-size:.72rem;opacity:.7;">New issues reported today</div>
{% endif %}
</div>
</div>
</div>
<div class="col-6 col-md">
<a href="{{ url_for('issues.index', status='resolved', date_from=today_str, date_to=today_str) }}" class="text-decoration-none">
<div class="card text-white bg-info h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small text-white-50 fw-semibold">Resolved Today</span>
<i class="bi bi-check2-all" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ resolved_today }}</div>
<div class="mt-2" style="font-size:.72rem;opacity:.7;">Issues closed and resolved today</div>
</div>
</div>
</a>
</div>
{% if current_user.role != 'customer' %}
<div class="col-6 col-md">
<a href="{{ url_for('issues.index', status='pending_verification') }}" class="text-decoration-none">
<div class="card text-white h-100" style="background:#2563eb;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small fw-semibold" style="color:rgba(255,255,255,.6);">Pending Verification</span>
<i class="bi bi-clipboard2-check" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ pending_verification }}</div>
<div class="mt-2" style="font-size:.72rem;opacity:.7;">Resolved but awaiting supervisor sign-off</div>
</div>
</div>
</a>
</div>
<div class="col-6 col-md">
<div class="card text-white h-100" style="background:#16a34a;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small fw-semibold" style="color:rgba(255,255,255,.6);">Unassigned Open</span>
<i class="bi bi-person-dash" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<a href="{{ url_for('issues.index', status='open', unassigned='1') }}" class="text-white text-decoration-none">
<div class="fs-1 fw-bold lh-1">{{ unassigned_open }}</div>
</a>
{% if unassigned_open > 0 %}
{{ handler_chips(unassigned_handler, {'status': 'open', 'unassigned': '1'}) }}
{% else %}
<div class="mt-2" style="font-size:.72rem;opacity:.7;">Open issues with no one assigned</div>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
{# ── SLA Summary ─────────────────────────────────────────────────────────── #}
{# ══════════════════════════════════════════════════════════════════════════
D. SLA ALERT (grey)
══════════════════════════════════════════════════════════════════════════ #}
{# Section D shows only when there is at least one at-risk or breached issue. #}
{% 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>
</a>
{% set R = '#cc0000' %}
<div class="group-card group-grey">
<div class="group-title gt-red">D. SLA Alert</div>
<div class="row row-cols-1 row-cols-sm-2 g-3">
{{ stat_tile('SLA At Risk', sla_at_risk, R,
url_for('issues.index', sla='at_risk'),
'Approaching their SLA deadline') }}
{{ stat_tile('SLA Alert', sla_breached, R,
url_for('issues.index', sla='breached'),
'Past their SLA deadline') }}
</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>
</a>
</div>
{% endif %}
</div>
{% endif %}
@@ -594,4 +505,4 @@
</script>
{% endif %}
{% endblock %}
{% endblock %}