Jul 10 - Update codes to catch up with the single tenant project

This commit is contained in:
2026-07-10 12:29:44 -04:00
parent faab9fd008
commit e41998b561
12 changed files with 375 additions and 13 deletions
+7
View File
@@ -95,6 +95,12 @@ def index():
'medium': sum(1 for i in open_issues_all if i.severity == 'medium'),
'low': sum(1 for i in open_issues_all if i.severity == 'low'),
}
# Handler breakdown (phase39) — NULL and 'internal' both mean janitorial staff
handler_breakdown = {
'internal': sum(1 for i in open_issues_all if not i.handler_type or i.handler_type == 'internal'),
'facility': sum(1 for i in open_issues_all if i.handler_type == 'facility'),
'vendor': sum(1 for i in open_issues_all if i.handler_type == 'vendor'),
}
# ── Issues resolved today ─────────────────────────────────────────────────
resolved_today_q = Issue.query.filter(
@@ -343,6 +349,7 @@ def index():
completed_today = completed_today,
open_issues = open_issues,
severity_breakdown = severity_breakdown,
handler_breakdown = handler_breakdown,
resolved_today = resolved_today,
pending_followups = pending_followups,
issues_opened_today = issues_opened_today,
+51 -1
View File
@@ -25,7 +25,7 @@ In multi-tenant mode the printed URL is built from the tenant's own domain
import logging
from datetime import timedelta
from flask import Blueprint, render_template, abort
from flask import Blueprint, render_template, redirect, request, url_for, abort
from flask_login import current_user
from sqlalchemy import func, or_
@@ -135,6 +135,7 @@ def scan(token):
return render_template(
'facility_qr/view.html',
token = token,
facility = facility,
contract = facility.project,
total_90 = total_90,
@@ -154,3 +155,52 @@ def scan(token):
can_view_full = _can_view_full(facility),
generated_at = now,
)
@bp.route('/<token>/report', methods=['POST'])
@limiter.limit('5 per hour')
def report(token):
"""Public occupant issue report submitted from the QR scan page.
No login required — the unguessable QR token is the sole authorization.
A honeypot field silently rejects bot submissions. Creates an Issue with
reported_by=None so staff know it came from a public form.
"""
facility = Facility.query.filter_by(qr_token=token).first()
if facility is None or not facility.active:
abort(404)
# Honeypot — bots fill this field, humans leave it blank
if request.form.get('website', '').strip():
logger.warning('FACILITY QR REPORT | honeypot triggered | facility_id=%s', facility.id)
return redirect(url_for('facility_qr.scan', token=token) + '?reported=1')
description = request.form.get('description', '').strip()
severity = request.form.get('severity', 'medium')
if not description:
return redirect(url_for('facility_qr.scan', token=token))
if severity not in ('low', 'medium', 'high'):
severity = 'medium'
issue = Issue(
facility_id = facility.id,
severity = severity,
description = description,
status = 'open',
reported_by = None, # anonymous public submission
)
db.session.add(issue)
db.session.commit()
logger.info('FACILITY QR REPORT | facility_id=%s issue_id=%s severity=%s',
facility.id, issue.id, severity)
# Notify staff via the notification matrix (same event as Issues → Create)
try:
from app.utils.notifications import notify_by_matrix
notify_by_matrix('issue_created', issue_id=issue.id, facility_id=facility.id)
except Exception as exc:
logger.error('FACILITY QR REPORT | notify_failed | err=%s', exc)
return redirect(url_for('facility_qr.scan', token=token) + '?reported=1')
+36 -9
View File
@@ -235,15 +235,17 @@ def index():
)
)
issue_id_filter = request.args.get('issue_id', '').strip()
severity_filter = request.args.get('severity', '')
status_filter = request.args.get('status', '')
sla_filter = request.args.get('sla', '')
facility_filter = request.args.get('facility_id', '')
contract_filter = request.args.get('contract_id', '')
date_from_filter = request.args.get('date_from', '')
date_to_filter = request.args.get('date_to', '')
reporter_filter = request.args.get('reporter_id', '')
issue_id_filter = request.args.get('issue_id', '').strip()
severity_filter = request.args.get('severity', '')
status_filter = request.args.get('status', '')
sla_filter = request.args.get('sla', '')
facility_filter = request.args.get('facility_id', '')
contract_filter = request.args.get('contract_id', '')
date_from_filter = request.args.get('date_from', '')
date_to_filter = request.args.get('date_to', '')
reporter_filter = request.args.get('reporter_id', '')
handler_type_filter = request.args.get('handler_type', '')
unassigned_filter = request.args.get('unassigned', '')
if issue_id_filter.isdigit():
q = q.filter(Issue.id == int(issue_id_filter))
@@ -280,6 +282,17 @@ def index():
Area.facility_id == fid,
)
)
if handler_type_filter:
if handler_type_filter == 'internal':
# NULL handler_type means 'internal' (the default)
q = q.filter(db.or_(
Issue.handler_type == 'internal',
Issue.handler_type.is_(None),
))
else:
q = q.filter(Issue.handler_type == handler_type_filter)
if unassigned_filter:
q = q.filter(Issue.assigned_to.is_(None))
# SLA filter — SLA status is computed in Python (not a DB column).
# When active: load all matching rows, filter in Python, wrap in a
# single-page compatible object so the template interface is unchanged.
@@ -353,6 +366,8 @@ def index():
date_from_filter=date_from_filter,
date_to_filter=date_to_filter,
reporter_filter=reporter_filter,
handler_type_filter=handler_type_filter,
unassigned_filter=unassigned_filter,
facilities=facilities,
projects=projects,
staff=staff,
@@ -439,6 +454,18 @@ def view(issue_id):
issue.vendor_contact = form.vendor_contact.data.strip() or None
issue.vendor_notes = form.vendor_notes.data.strip() or None
# Handler type (phase39)
ht = form.handler_type.data or None
issue.handler_type = ht
if ht == 'facility':
issue.facility_handler_name = form.facility_handler_name.data.strip() or None
issue.facility_handler_contact = form.facility_handler_contact.data.strip() or None
issue.facility_handler_notes = form.facility_handler_notes.data.strip() or None
else:
issue.facility_handler_name = None
issue.facility_handler_contact = None
issue.facility_handler_notes = None
from app.routes.inspections import _save_photo
new_photos = []
for file_obj in request.files.getlist('result_photos'):