Jul 10 - Update facility's area with QR code
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}QR Code — {{ area.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
@media print {
|
||||
.no-print { display: none !important; }
|
||||
.navbar, nav, footer { display: none !important; }
|
||||
.qr-card { border: none !important; box-shadow: none !important; }
|
||||
}
|
||||
.qr-card { max-width: 520px; margin: 0 auto; }
|
||||
</style>
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center mb-3 no-print">
|
||||
<a href="{{ url_for('facilities.view_facility', facility_id=facility.id) }}"
|
||||
class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> Back to Facility
|
||||
</a>
|
||||
<div class="d-flex gap-2">
|
||||
{% if current_user.role in ['admin', 'director', 'customer'] %}
|
||||
<form method="POST"
|
||||
action="{{ url_for('facilities.area_qr_regenerate', area_id=area.id) }}"
|
||||
onsubmit="return confirm('Regenerate this QR code? Any codes already printed and posted for {{ area.name }} will STOP working and must be reprinted.');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit" class="btn btn-outline-danger btn-sm"
|
||||
title="Invalidate the current code and generate a new one">
|
||||
<i class="bi bi-arrow-repeat"></i> Regenerate
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<button onclick="window.print()" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-printer"></i> Print
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm qr-card">
|
||||
<div class="card-body text-center p-4">
|
||||
<div class="text-muted text-uppercase small" style="letter-spacing:.08em;">
|
||||
Scan for Area Status
|
||||
</div>
|
||||
<h1 class="h3 fw-bold mt-1 mb-0">{{ area.name }}</h1>
|
||||
<div class="text-muted mb-3">{{ facility.name }}</div>
|
||||
|
||||
<img src="{{ url_for('facilities.area_qr_png', area_id=area.id) }}"
|
||||
alt="QR code for {{ area.name }}"
|
||||
style="width:260px;height:260px;max-width:100%;" class="mb-3">
|
||||
|
||||
<p class="mb-1">Scan this code with your phone camera to see this area's
|
||||
recent cleaning quality and to <strong>report a problem</strong>.</p>
|
||||
|
||||
{% if public_url %}
|
||||
<div class="small text-muted mt-3">
|
||||
Or visit:<br>
|
||||
<a href="{{ public_url }}" class="text-break">{{ public_url }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-center text-muted small mt-3 no-print">
|
||||
Tip: print this and post it inside the area itself (e.g. on the restroom door).
|
||||
</p>
|
||||
{% endblock %}
|
||||
@@ -135,6 +135,12 @@
|
||||
</td>
|
||||
<td>{{ area.inspections.count() }}</td>
|
||||
<td>
|
||||
{% if current_user.role in ['admin', 'director', 'project_manager', 'customer'] %}
|
||||
<a href="{{ url_for('facilities.area_qr_page', area_id=area.id) }}"
|
||||
class="btn btn-sm btn-outline-dark" title="Printable QR code for this area">
|
||||
<i class="bi bi-qr-code"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if current_user.role in ['admin', 'director'] %}
|
||||
<a href="{{ url_for('facilities.edit_area', area_id=area.id) }}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
<!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>{{ area.name }} — {{ facility.name }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background:#eef1f5; }
|
||||
.wrap { max-width: 640px; margin: 0 auto; padding: 16px; }
|
||||
.hpot { position:absolute !important; left:-9999px !important; width:1px; height:1px; overflow:hidden; }
|
||||
.card-soft { background:#fff; border:1px solid #e6e9ef; border-radius:.85rem; }
|
||||
.kpi-num { font-size: 1.9rem; font-weight: 700; line-height: 1; }
|
||||
.kpi-label { font-size: .68rem; letter-spacing:.05em; color:#8a93a2; text-transform:uppercase; }
|
||||
.kpi-sub { font-size: .62rem; letter-spacing:.05em; color:#aab1bd; text-transform:uppercase; }
|
||||
.fac-icon { width:40px; height:40px; border-radius:.6rem; background:#e7efff; color:#2563eb;
|
||||
display:flex; align-items:center; justify-content:center; font-size:1.3rem; flex:none; }
|
||||
.sec-title { font-weight:700; font-size:1.02rem; }
|
||||
.list-line { border-top:1px solid #eef0f4; }
|
||||
.list-line:first-child { border-top:0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ 'success' if category in ['success','info'] else category }} shadow-sm">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
{# ── Header ── #}
|
||||
<div class="d-flex align-items-start gap-3 mb-3 mt-1">
|
||||
<div class="fac-icon"><i class="bi bi-door-open"></i></div>
|
||||
<div>
|
||||
<h1 class="h4 fw-bold mb-1">{{ area.name }}</h1>
|
||||
<div class="text-muted small">
|
||||
{{ facility.name }}
|
||||
{% if area.area_type %} · {{ area.area_type|title }}{% endif %}
|
||||
{% if facility.address %}<br>{{ facility.address }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── KPI row ── #}
|
||||
<div class="row g-2 mb-3">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card-soft p-3 text-center h-100">
|
||||
<div class="kpi-num text-{{ rating_colour }}">
|
||||
{{ ('%.1f' % avg_score) ~ '%' if avg_score is not none else '—' }}
|
||||
</div>
|
||||
<div class="kpi-label mt-2">Avg Score</div>
|
||||
<div class="kpi-sub">90 Days</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card-soft p-3 text-center h-100">
|
||||
<div class="kpi-num">{{ inspections_90 }}</div>
|
||||
<div class="kpi-label mt-2">Inspections</div>
|
||||
<div class="kpi-sub">90 Days</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card-soft p-3 text-center h-100">
|
||||
<div class="kpi-num">{{ open_issue_count }}</div>
|
||||
<div class="kpi-label mt-2">Open</div>
|
||||
<div class="kpi-sub">Issues</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card-soft p-3 text-center h-100">
|
||||
<div class="kpi-num">{{ resolved_90 }}</div>
|
||||
<div class="kpi-label mt-2">Resolved</div>
|
||||
<div class="kpi-sub">90 Days</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Score trend (aggregate only) ── #}
|
||||
<div class="card-soft p-3 mb-3 d-flex align-items-center justify-content-between">
|
||||
<div class="kpi-label">Score Trend — 30 Days vs Prior 30</div>
|
||||
<div>
|
||||
{% if trend_delta is none %}
|
||||
<span class="text-muted small">Not enough data yet</span>
|
||||
{% elif trend_delta > 0 %}
|
||||
<span class="text-success fw-semibold"><i class="bi bi-arrow-up-right"></i> +{{ '%.1f' % trend_delta }} pts</span>
|
||||
{% elif trend_delta < 0 %}
|
||||
<span class="text-danger fw-semibold"><i class="bi bi-arrow-down-right"></i> {{ '%.1f' % trend_delta }} pts</span>
|
||||
{% else %}
|
||||
<span class="text-muted fw-semibold"><i class="bi bi-dash"></i> No change</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Cleaning quality rating ── #}
|
||||
<div class="card-soft p-3 mb-3 d-flex align-items-center justify-content-between">
|
||||
<div class="sec-title"><i class="bi bi-stars me-1 text-{{ rating_colour }}"></i> Cleaning Quality</div>
|
||||
<span class="badge bg-{{ rating_colour }} fs-6">{{ rating_label }}</span>
|
||||
</div>
|
||||
|
||||
{# ── Recent inspections (DATES ONLY — occupant-safe) ── #}
|
||||
{% if recent_dates %}
|
||||
<div class="card-soft p-3 mb-3">
|
||||
<div class="sec-title mb-2"><i class="bi bi-clipboard-check me-1"></i> Recent Inspections</div>
|
||||
{% for d in recent_dates %}
|
||||
<div class="list-line py-2 d-flex align-items-center justify-content-between">
|
||||
<span>{{ d.strftime('%b %d, %Y') }}</span>
|
||||
<span class="text-success small"><i class="bi bi-check-circle-fill"></i> Completed</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="text-muted small mt-2">This area is inspected regularly by our quality team.</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ── Report a problem ── #}
|
||||
<div class="card-soft p-4 mb-4">
|
||||
<h2 class="sec-title"><i class="bi bi-megaphone me-1"></i> Report a Problem</h2>
|
||||
<p class="text-muted small">
|
||||
Notice something in <strong>{{ area.name }}</strong> that needs attention?
|
||||
Let the cleaning team know.
|
||||
</p>
|
||||
|
||||
<form method="POST"
|
||||
action="{{ url_for('public.area_report_problem', token=token) }}"
|
||||
enctype="multipart/form-data" novalidate>
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
{# Honeypot — hidden from humans; bots that fill it are rejected #}
|
||||
<div class="hpot" aria-hidden="true">
|
||||
{{ form.website.label }} {{ form.website(tabindex="-1", autocomplete="off") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.area_label.label(class="form-label small fw-semibold") }}
|
||||
{{ form.area_label(class="form-control", placeholder="e.g. third stall from the door") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.description.label(class="form-label small fw-semibold") }}
|
||||
{{ form.description(class="form-control", rows="4",
|
||||
placeholder="Describe what you noticed…") }}
|
||||
{% for e in form.description.errors %}
|
||||
<div class="text-danger small mt-1">{{ e }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="row g-2">
|
||||
<div class="col-sm-6 mb-3">
|
||||
{{ form.reporter_name.label(class="form-label small fw-semibold") }}
|
||||
{{ form.reporter_name(class="form-control") }}
|
||||
</div>
|
||||
<div class="col-sm-6 mb-3">
|
||||
{{ form.reporter_contact.label(class="form-label small fw-semibold") }}
|
||||
{{ form.reporter_contact(class="form-control") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.photo.label(class="form-label small fw-semibold") }}
|
||||
{{ form.photo(class="form-control", accept="image/*") }}
|
||||
{% for e in form.photo.errors %}
|
||||
<div class="text-danger small mt-1">{{ e }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="bi bi-send"></i> Submit Report
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-muted small mb-4">
|
||||
Janitorial Quality Control
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user