Jul 10 - Update codes to catch up with the single tenant project
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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'):
|
||||
|
||||
Reference in New Issue
Block a user