192 lines
7.3 KiB
HTML
192 lines
7.3 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="robots" content="noindex, nofollow">
|
|
<title>{{ facility.name }} — Facility Status</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
|
<style>
|
|
body { background:#f1f5f9; color:#1f2937; }
|
|
.fq-wrap { max-width:640px; margin:2rem auto; padding:0 1rem; }
|
|
.stat-tile { background:#fff; border-radius:.5rem; padding:.9rem .5rem; text-align:center;
|
|
box-shadow:0 1px 2px rgba(0,0,0,.06); height:100%; }
|
|
.stat-tile .val { font-size:1.6rem; font-weight:700; line-height:1.2; }
|
|
.stat-tile .lbl { font-size:.72rem; color:#64748b; text-transform:uppercase;
|
|
letter-spacing:.03em; margin-top:.15rem; }
|
|
.sev-critical{background:#dc2626}.sev-high{background:#ea580c}
|
|
.sev-medium{background:#d97706}.sev-low{background:#64748b}
|
|
.trend-up { color:#15803d; }
|
|
.trend-down { color:#dc2626; }
|
|
.trend-flat { color:#64748b; }
|
|
@media (max-width:576px){ .fq-wrap{ margin:1rem auto; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="fq-wrap">
|
|
|
|
{% if request.args.get('reported') == '1' %}
|
|
<div class="alert alert-success d-flex align-items-center gap-2 mb-3" role="alert">
|
|
<i class="bi bi-check-circle-fill fs-5"></i>
|
|
<div><strong>Report submitted.</strong> Our team has been notified and will follow up.</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="d-flex align-items-center gap-2 mb-3">
|
|
<i class="bi bi-building fs-3 text-primary"></i>
|
|
<div>
|
|
<div class="fw-bold">{{ facility.name }}</div>
|
|
<div class="text-muted small">
|
|
{% if contract %}{{ contract.name }}{% endif %}
|
|
{% if contract and facility.address %} · {% endif %}
|
|
{{ facility.address or '' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# ── Summary stat tiles (90 days) ── #}
|
|
<div class="row g-2 mb-3">
|
|
<div class="col-3">
|
|
<div class="stat-tile">
|
|
<div class="val">{% if avg_90 is not none %}{{ '%.1f'|format(avg_90) }}%{% else %}—{% endif %}</div>
|
|
<div class="lbl">Avg Score<br>90 days</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-3">
|
|
<div class="stat-tile">
|
|
<div class="val">{{ total_90 }}</div>
|
|
<div class="lbl">Inspections<br>90 days</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-3">
|
|
<div class="stat-tile">
|
|
<div class="val">{{ open_total }}</div>
|
|
<div class="lbl">Open<br>Issues</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-3">
|
|
<div class="stat-tile">
|
|
<div class="val">{{ resolved_90 }}</div>
|
|
<div class="lbl">Resolved<br>90 days</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# ── Score trend ── #}
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-body py-2 d-flex align-items-center justify-content-between">
|
|
<div class="text-muted small text-uppercase">Score trend — 30 days vs prior 30</div>
|
|
{% if trend_delta is not none %}
|
|
{% if trend_delta > 0.5 %}
|
|
<div class="trend-up fw-semibold">
|
|
<i class="bi bi-arrow-up-right"></i> Improving
|
|
(+{{ '%.1f'|format(trend_delta) }} pts, {{ '%.1f'|format(avg_prior) }}% → {{ '%.1f'|format(avg_cur) }}%)
|
|
</div>
|
|
{% elif trend_delta < -0.5 %}
|
|
<div class="trend-down fw-semibold">
|
|
<i class="bi bi-arrow-down-right"></i> Declining
|
|
({{ '%.1f'|format(trend_delta) }} pts, {{ '%.1f'|format(avg_prior) }}% → {{ '%.1f'|format(avg_cur) }}%)
|
|
</div>
|
|
{% else %}
|
|
<div class="trend-flat fw-semibold">
|
|
<i class="bi bi-arrow-right"></i> Steady ({{ '%.1f'|format(avg_cur) }}%)
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<div class="text-muted small">Not enough data yet</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{# ── Open issues by severity + SLA state ── #}
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-header bg-white fw-semibold py-2">
|
|
<i class="bi bi-exclamation-triangle"></i> Open Issues
|
|
</div>
|
|
<div class="card-body py-3">
|
|
{% if open_total or pending_verification %}
|
|
<div class="d-flex flex-wrap gap-2 mb-2">
|
|
{% for sev in severity_order %}
|
|
{% if severity_counts[sev] %}
|
|
<span class="badge sev-{{ sev }} text-white">
|
|
{{ severity_counts[sev] }} {{ sev }}
|
|
</span>
|
|
{% endif %}
|
|
{% endfor %}
|
|
{% if pending_verification %}
|
|
<span class="badge bg-info text-dark">{{ pending_verification }} pending verification</span>
|
|
{% endif %}
|
|
</div>
|
|
{% if sla_breached %}
|
|
<div class="small text-danger fw-semibold">
|
|
<i class="bi bi-alarm"></i> {{ sla_breached }} issue{{ 's' if sla_breached != 1 }} past the response-time target
|
|
</div>
|
|
{% endif %}
|
|
{% if sla_at_risk %}
|
|
<div class="small text-warning-emphasis fw-semibold">
|
|
<i class="bi bi-hourglass-split"></i> {{ sla_at_risk }} issue{{ 's' if sla_at_risk != 1 }} approaching the response-time target
|
|
</div>
|
|
{% endif %}
|
|
{% if not sla_breached and not sla_at_risk and open_total %}
|
|
<div class="small text-muted">All open issues are within response-time targets.</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<div class="text-muted small"><i class="bi bi-check-circle text-success"></i> No open issues right now.</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{# ── Recent inspections ── #}
|
|
<div class="card shadow-sm mb-3">
|
|
<div class="card-header bg-white fw-semibold py-2">
|
|
<i class="bi bi-clipboard-check"></i> Recent Inspections
|
|
</div>
|
|
{% if recent %}
|
|
<div class="table-responsive">
|
|
<table class="table table-sm mb-0 align-middle">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th class="ps-3">Date</th>
|
|
<th>Checklist</th>
|
|
<th class="text-end pe-3">Score</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for ins in recent %}
|
|
<tr>
|
|
<td class="ps-3 text-nowrap">{{ ins.inspection_date.strftime('%b %d, %Y') }}</td>
|
|
<td class="text-muted small">{{ ins.template.name if ins.template else '—' }}</td>
|
|
<td class="text-end pe-3 fw-semibold">
|
|
{% if ins.overall_score is not none %}{{ '%.1f'|format(ins.overall_score) }}%{% else %}—{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="card-body py-3 text-muted small">No completed inspections yet.</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if can_view_full %}
|
|
<a href="{{ url_for('facilities.view_facility', facility_id=facility.id) }}"
|
|
class="btn btn-primary w-100 mb-3">
|
|
<i class="bi bi-box-arrow-in-right me-1"></i> Open Full Facility View
|
|
</a>
|
|
{% endif %}
|
|
|
|
{% with action = url_for('facility_qr.report', token=token) %}
|
|
{% include "facility_qr/_report_form.html" %}
|
|
{% endwith %}
|
|
|
|
<p class="text-center text-muted small mt-2 mb-1">
|
|
{% if last_date %}Last inspected {{ last_date.strftime('%b %d, %Y') }} · {% endif %}
|
|
Snapshot generated {{ generated_at.strftime('%b %d, %Y %I:%M %p') }} ET
|
|
</p>
|
|
<p class="text-center text-muted small">Janitorial QC — facility quality snapshot</p>
|
|
</div>
|
|
</body>
|
|
</html>
|