Jul 16 - Fill the gaps between Single-tenant mode and Multi-tenant mode - MT3

This commit is contained in:
2026-07-16 17:17:44 -04:00
parent a60afc6c41
commit 02b030b1d2
12 changed files with 1294 additions and 90 deletions
+81
View File
@@ -0,0 +1,81 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>QR Code — {{ area.name }}</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; }
.qr-card { max-width:420px; margin:2rem auto; background:#fff; border-radius:.75rem;
box-shadow:0 1px 3px rgba(0,0,0,.12); padding:2rem; text-align:center; }
.qr-box svg { width:260px; height:260px; }
.scan-url { word-break:break-all; font-size:.72rem; color:#94a3b8; }
@media print {
body { background:#fff; }
.no-print { display:none !important; }
.qr-card { box-shadow:none; margin:0 auto; }
}
</style>
</head>
<body>
<div class="text-center mt-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>
<button onclick="window.print()" class="btn btn-primary btn-sm">
<i class="bi bi-printer"></i> Print
</button>
<a href="{{ url_for('facilities.qr_print_all', facility_id=facility.id, include_areas=1) }}"
class="btn btn-outline-primary btn-sm">
<i class="bi bi-grid-3x3-gap"></i> Print All Codes
</a>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% for category, message in messages %}
<div class="alert alert-{{ 'success' if category == 'success' else 'warning' }} mx-auto mt-3 no-print" style="max-width:420px;">
{{ message }}
</div>
{% endfor %}
{% endwith %}
<div class="qr-card">
<div class="text-muted text-uppercase small" style="letter-spacing:.08em;">Area</div>
<h4 class="mb-0">{{ area.name }}</h4>
<div class="text-muted small">{{ facility.name }}</div>
{% if area.area_type %}<div class="text-muted small mb-2">{{ area.area_type }}</div>{% endif %}
<div class="qr-box my-3">{{ svg | safe }}</div>
<div class="fw-semibold mb-1">
<i class="bi bi-phone"></i> Scan to report a problem in this area
</div>
<div class="text-muted small mb-2">
Recent scores, open issues, and quality trend for {{ area.name }}.
</div>
<div class="scan-url">{{ scan_url }}</div>
</div>
<p class="text-center text-muted small no-print">
Tip: post this inside the area itself (e.g. on the restroom door), not at the building entrance.
</p>
{% if current_user.role in ['admin', 'director', 'customer'] %}
<div class="text-center mb-4 no-print">
<form method="POST" action="{{ url_for('facilities.regenerate_area_qr', area_id=area.id) }}"
onsubmit="return confirm('Regenerate this QR code? Every previously printed poster for {{ area.name }} will stop working.');">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<button type="submit" class="btn btn-outline-danger btn-sm">
<i class="bi bi-arrow-repeat"></i> Regenerate QR Code
</button>
<div class="form-text">Use this if a printed poster leaked or was posted somewhere it shouldn't be.</div>
</form>
</div>
{% endif %}
</body>
</html>
+182
View File
@@ -0,0 +1,182 @@
{% extends "base.html" %}
{% block title %}Print QR Codes{% endblock %}
{% block content %}
<style>
.qr-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
.qr-item { position: relative; break-inside: avoid; page-break-inside: avoid;
text-align: center; cursor: pointer; }
.qr-item img { width: 220px; height: 220px; max-width: 100%; }
.qr-item.qr-selected { outline: 3px solid #2563eb; outline-offset: -1px; }
.qr-check { position: absolute; top: 10px; left: 10px; }
.qr-check .form-check-input { width: 1.25rem; height: 1.25rem; }
.qr-kind { font-size: .68rem; letter-spacing: .08em; }
@media print {
.no-print { display: none !important; }
.navbar, nav, footer { display: none !important; }
.qr-item { border: 1px dashed #bbb !important; cursor: default; }
.qr-item.qr-selected { outline: none !important; }
.qr-grid { gap: 8px; }
/* When printing a selection, hide the unselected cards. */
body.print-selected-only .qr-item:not(.qr-selected) { display: none !important; }
}
</style>
<div class="d-flex justify-content-between align-items-center mb-3 no-print">
<div>
<h2 class="h4 mb-0"><i class="bi bi-qr-code"></i> QR Codes</h2>
<div class="text-muted small">
{% if selected_contract %}Contract: {{ selected_contract.name }} — {% endif %}
{{ facilities|length }} facilit{{ 'y' if facilities|length == 1 else 'ies' }}
{% if include_areas %}(with areas){% endif %}
</div>
</div>
<a href="{{ url_for('facilities.list_facilities') }}" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-arrow-left"></i> Back
</a>
</div>
{# ── Filter bar (GET reload) ─────────────────────────────────────────────── #}
<form method="get" id="filterForm" class="card card-body mb-3 no-print">
<div class="row g-2 align-items-end">
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Contract</label>
<select name="contract_id" class="form-select form-select-sm"
onchange="document.getElementById('facilitySelect').value=''; this.form.submit();">
<option value="">All Contracts</option>
{% for c in contracts %}
<option value="{{ c.id }}" {{ 'selected' if selected_contract_id == c.id }}>{{ c.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-4">
<label class="form-label small fw-semibold mb-1">Facility</label>
<select name="facility_id" id="facilitySelect" class="form-select form-select-sm">
<option value="">All Facilities</option>
{% for f in facility_options %}
<option value="{{ f.id }}" {{ 'selected' if selected_facility_id == f.id }}>{{ f.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="include_areas" value="1"
id="includeAreas" {{ 'checked' if include_areas }}>
<label class="form-check-label small" for="includeAreas">Include area QR codes</label>
</div>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary btn-sm w-100">
<i class="bi bi-funnel"></i> Apply
</button>
</div>
</div>
</form>
{# ── Selection toolbar + export form ─────────────────────────────────────── #}
<form method="post" action="{{ url_for('facilities.qr_export_pdf') }}" id="qrForm">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="d-flex align-items-center gap-2 mb-3 no-print flex-wrap">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="selectAllQr(true)">Select All</button>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="selectAllQr(false)">Clear</button>
<span class="text-muted small" id="selCount">0 selected</span>
<div class="ms-auto d-flex gap-2">
<button type="button" class="btn btn-primary btn-sm" onclick="printSelected()">
<i class="bi bi-printer"></i> Print Selected
</button>
<button type="submit" class="btn btn-danger btn-sm">
<i class="bi bi-file-earmark-pdf"></i> Export Selected to PDF
</button>
</div>
</div>
{% if facilities %}
<div class="qr-grid">
{% for f in facilities %}
{# Facility QR card #}
<label class="qr-item card shadow-sm p-3 mb-0">
<div class="qr-check no-print">
<input type="checkbox" class="form-check-input qr-cb" name="facility_ids" value="{{ f.id }}">
</div>
<div class="text-muted text-uppercase qr-kind">Facility</div>
<div class="fw-bold">{{ f.name }}</div>
{% if f.project %}
<div class="text-muted small mb-1">{{ f.project.name }}</div>
{% endif %}
<div>
<img src="{{ url_for('facilities.facility_qr_png', facility_id=f.id) }}"
alt="QR code for {{ f.name }}" loading="lazy">
</div>
<div class="small">Report a problem &amp; view recent quality</div>
</label>
{% if include_areas %}
{% for a in areas_by_facility.get(f.id, []) %}
{# Area QR card #}
<label class="qr-item card shadow-sm p-3 mb-0">
<div class="qr-check no-print">
<input type="checkbox" class="form-check-input qr-cb" name="area_ids" value="{{ a.id }}">
</div>
<div class="text-muted text-uppercase qr-kind">Area</div>
<div class="fw-bold">{{ a.name }}</div>
<div class="text-muted small mb-1">{{ f.name }}</div>
<div>
<img src="{{ url_for('facilities.area_qr_png', area_id=a.id) }}"
alt="QR code for {{ a.name }}" loading="lazy">
</div>
<div class="small">Report a problem &amp; view recent quality</div>
</label>
{% endfor %}
{% endif %}
{% endfor %}
</div>
{% else %}
<div class="alert alert-info">No facilities match the selected filters.</div>
{% endif %}
</form>
<p class="text-muted small mt-3 no-print">
Tip: tick the codes you want, then <strong>Print Selected</strong> or
<strong>Export Selected to PDF</strong>. With nothing ticked, Print Selected prints them all.
</p>
<script>
(function () {
'use strict';
function checkboxes() { return document.querySelectorAll('.qr-cb'); }
function updateCount() {
var n = document.querySelectorAll('.qr-cb:checked').length;
document.getElementById('selCount').textContent = n + ' selected';
}
function syncCard(cb) {
var card = cb.closest('.qr-item');
if (card) { card.classList.toggle('qr-selected', cb.checked); }
}
window.selectAllQr = function (state) {
checkboxes().forEach(function (cb) { cb.checked = state; syncCard(cb); });
updateCount();
};
window.printSelected = function () {
var anySelected = document.querySelectorAll('.qr-cb:checked').length > 0;
if (anySelected) { document.body.classList.add('print-selected-only'); }
window.print();
setTimeout(function () {
document.body.classList.remove('print-selected-only');
}, 500);
};
// Toggling a checkbox inside its <label> card also fires on the label click.
checkboxes().forEach(function (cb) {
cb.addEventListener('change', function () { syncCard(cb); updateCount(); });
});
updateCount();
}());
</script>
{% endblock %}
+11 -1
View File
@@ -17,11 +17,15 @@
<i class="bi bi-graph-up-arrow"></i> Scorecard
</a>
{% endif %}
{% if current_user.role in ['admin', 'director', 'project_manager', 'auditor'] %}
{% if current_user.role != 'inspector' %}
<a href="{{ url_for('facilities.qr_card', facility_id=facility.id) }}"
class="btn btn-outline-secondary">
<i class="bi bi-qr-code"></i> QR Code
</a>
<a href="{{ url_for('facilities.qr_print_all', facility_id=facility.id, include_areas=1) }}"
class="btn btn-outline-secondary" title="Print or export this facility's codes, including every area">
<i class="bi bi-grid-3x3-gap"></i> All Codes
</a>
{% endif %}
{% if current_user.role in ['admin', 'director'] %}
<a href="{{ url_for('facilities.edit_facility', facility_id=facility.id) }}" class="btn btn-outline-primary">
@@ -135,6 +139,12 @@
</td>
<td>{{ area.inspections.count() }}</td>
<td>
{% if current_user.role != 'inspector' %}
<a href="{{ url_for('facilities.area_qr_card', area_id=area.id) }}"
class="btn btn-sm btn-outline-secondary" 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>