Jul 16 - Fill the gaps between Single-tenant mode and Multi-tenant mode - MT3
This commit is contained in:
@@ -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>
|
||||
@@ -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 & 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 & 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 %}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user