From 39feaa4704fc29fcdf5d39ab08f927fcddce355a Mon Sep 17 00:00:00 2001 From: NguyenND Date: Wed, 8 Jul 2026 13:27:49 -0400 Subject: [PATCH] Jul 8 - Implement Regenerate token and buik print QR codes --- CLAUDE.md | 2 +- app/routes/facilities.py | 60 +++++++++++++++++++++ app/templates/facilities/list.html | 6 +++ app/templates/facilities/qr.html | 19 +++++-- app/templates/facilities/qr_print_all.html | 62 ++++++++++++++++++++++ 5 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 app/templates/facilities/qr_print_all.html diff --git a/CLAUDE.md b/CLAUDE.md index 46938ef..c82d362 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -400,7 +400,7 @@ contract_notification_recipients: |---|---|---| | `auth` | `/auth` | `/login`, `/logout`, `/profile`, `/users/*`, `/notification-matrix` | | `dashboard` | `/` | `GET /`, `/facility-trend` (AJAX) | -| `facilities` | `/facilities` | CRUD + area management + QR code (`//qr` printable page, `//qr.png` image — staff only, customers 403) | +| `facilities` | `/facilities` | CRUD + area management + QR code: `//qr` printable page, `//qr.png` image, `POST //qr/regenerate` (admin/director — invalidates old printed code), `/qr/print-all[?contract_id=]` bulk sheet. All QR routes are staff-only (customers 403); inspectors scoped to contracted facilities. | | `public` | `/f` | **No login.** `GET /` occupant facility summary; `POST //report` occupant issue report (rate-limited `5/hour`, honeypot). Resolves ACTIVE facility by `public_token` or 404. | | `projects` | `/projects` | CRUD + customer assignment management + notification-recipient add/remove (`//notify-recipients/add`, `/notify-recipients//remove` — admin only) | | `customers` | `/customers` | list, invite, set-password, manage, import CSV | diff --git a/app/routes/facilities.py b/app/routes/facilities.py index 37eec67..984d1d8 100644 --- a/app/routes/facilities.py +++ b/app/routes/facilities.py @@ -151,6 +151,66 @@ def facility_qr_page(facility_id): return render_template('facilities/qr.html', facility=facility, public_url=public_url) + +@bp.route('//qr/regenerate', methods=['POST']) +@login_required +@supervisor_required +def facility_qr_regenerate(facility_id): + """Mint a NEW public token, invalidating any previously printed QR code.""" + facility = db.session.get(Facility, facility_id) + if facility is None: + abort(404) + + facility.public_token = Facility.generate_public_token() + db.session.commit() + + logger.info('FACILITIES | qr_regenerate | user=%s | facility_id=%s', + current_user.username, facility.id) + log_action(ACTION_UPDATE, 'Facility', facility.id, facility.name, + 'regenerated public QR token (old code invalidated)') + flash('QR code regenerated. Any previously printed codes for this facility no ' + 'longer work — reprint and repost.', 'warning') + return redirect(url_for('facilities.facility_qr_page', facility_id=facility.id)) + + +@bp.route('/qr/print-all') +@login_required +def facility_qr_print_all(): + """Printable sheet of QR codes for all facilities the user can see. + + Optional ?contract_id= limits the sheet to one contract. Customers have + no QR access (403); inspectors are scoped to their contracted facilities. + """ + if current_user.role == 'customer': + abort(403) + + contract_id = request.args.get('contract_id', type=int) + + if current_user.role == 'inspector': + fids = get_inspector_scope(current_user) or [] + query = Facility.query.filter(Facility.id.in_(fids), Facility.active == True) + else: + query = Facility.query.filter(Facility.active == True) + + if contract_id: + query = query.filter(Facility.project_id == contract_id) + + facilities = query.order_by(Facility.name).all() + + # Ensure every facility on the sheet has a token so its qr.png renders. + changed = False + for f in facilities: + if not f.public_token: + f.ensure_public_token() + changed = True + if changed: + db.session.commit() + + selected_contract = db.session.get(Project, contract_id) if contract_id else None + return render_template('facilities/qr_print_all.html', + facilities=facilities, + selected_contract=selected_contract) + @bp.route('//edit', methods=['GET', 'POST']) @login_required @supervisor_required diff --git a/app/templates/facilities/list.html b/app/templates/facilities/list.html index f69b509..e6c6d79 100644 --- a/app/templates/facilities/list.html +++ b/app/templates/facilities/list.html @@ -8,6 +8,12 @@

Facilities

+ {% if current_user.role != 'customer' %} + + Print All QR Codes + + {% endif %} {% if current_user.role in ['admin', 'director'] %} Add Facility diff --git a/app/templates/facilities/qr.html b/app/templates/facilities/qr.html index 676aeb4..502363e 100644 --- a/app/templates/facilities/qr.html +++ b/app/templates/facilities/qr.html @@ -16,9 +16,22 @@ class="btn btn-outline-secondary btn-sm"> Back to Facility - +
+ {% if current_user.role in ['admin', 'director'] %} +
+ + +
+ {% endif %} + +
diff --git a/app/templates/facilities/qr_print_all.html b/app/templates/facilities/qr_print_all.html new file mode 100644 index 0000000..6467187 --- /dev/null +++ b/app/templates/facilities/qr_print_all.html @@ -0,0 +1,62 @@ +{% extends "base.html" %} +{% block title %}Print QR Codes{% endblock %} + +{% block content %} + + +
+
+

Facility QR Codes

+
+ {% if selected_contract %}Contract: {{ selected_contract.name }} — {% endif %} + {{ facilities|length }} facilit{{ 'y' if facilities|length == 1 else 'ies' }} +
+
+
+ + Back + + +
+
+ +{% if facilities %} +
+ {% for f in facilities %} +
+
+ Scan for Facility Status +
+
{{ f.name }}
+ {% if f.project %} +
{{ f.project.name }}
+ {% endif %} +
+ QR code for {{ f.name }} +
+
Report a problem & view recent quality
+
+ {% endfor %} +
+{% else %} +
No active facilities to print QR codes for.
+{% endif %} + +

+ Tip: print, cut along the dashed lines, and post each code at its facility. +

+{% endblock %}