06/12 Add features: 1. Vendor/Contractor assignment 2. Period-over-period comparison 3. Trend Alerts (cron)

This commit is contained in:
2026-06-12 16:27:09 -04:00
parent cf22fe87fe
commit 0fe5954794
12 changed files with 447 additions and 2 deletions
+6
View File
@@ -414,6 +414,12 @@ def view(issue_id):
issue.result_notes = form.result_notes.data or None
# Vendor / contractor assignment — admin, director, project_manager only
if current_user.role in ('admin', 'director', 'project_manager'):
issue.vendor_name = form.vendor_name.data.strip() or None
issue.vendor_contact = form.vendor_contact.data.strip() or None
issue.vendor_notes = form.vendor_notes.data.strip() or None
from app.routes.inspections import _save_photo
new_photos = []
for file_obj in request.files.getlist('result_photos'):
+41 -1
View File
@@ -265,4 +265,44 @@ def cleanup_tokens():
db.session.commit()
logger.info('TOKEN CLEANUP | deleted=%s expired/revoked rows', deleted)
return jsonify({'ok': True, 'deleted': deleted})
return jsonify({'ok': True, 'deleted': deleted})
# ── Score trend alert trigger (called by cron) ────────────────────────────────
@bp.route('/check-score-trends', methods=['POST'])
@csrf.exempt
def check_score_trends():
"""Scan facility score trends and dispatch alerts for significant drops.
Compares each active facility's avg inspection score for the last 30 days
against the prior 30-day period. Alerts fire when the drop exceeds the
configured threshold (default: 5 percentage points).
Protected by the same DIGEST_SECRET token used by the other cron endpoints.
Recommended cron schedule — once per day is sufficient:
0 8 * * * curl -s -X POST https://yourdomain.com/notifications/check-score-trends \\
-d "token=YOUR_DIGEST_SECRET"
Optional param:
threshold=<float> Override the default 5.0-point drop threshold.
"""
token = request.form.get('token') or request.args.get('token')
expected = current_app.config.get('DIGEST_SECRET')
if not expected or token != expected:
logger.warning('SCORE TREND CHECK REJECTED | bad or missing token')
abort(403)
threshold = request.form.get('threshold', type=float) or None
from app.utils.sla import send_score_alerts
kwargs = {}
if threshold is not None:
kwargs['threshold'] = threshold
sent = send_score_alerts(**kwargs)
logger.info('SCORE TREND CHECK TRIGGERED | alerts_sent=%s', sent)
return jsonify({'ok': True, 'alerts_sent': sent})
+36 -1
View File
@@ -162,6 +162,35 @@ def index():
facility_scores = fac_score_q.group_by(Facility.id, Facility.name)\
.order_by(func.avg(Inspection.overall_score).desc()).all()
# Prior-period facility scores for period-over-period delta badges
period_len = end - start
prior_end = start
prior_start = start - period_len
prior_fac_q = db.session.query(
Facility.name,
func.avg(Inspection.overall_score).label('avg_score'),
).join(Inspection, Facility.id == Inspection.facility_id)\
.filter(
Inspection.inspection_date >= prior_start,
Inspection.inspection_date <= prior_end,
Inspection.status == 'completed',
Inspection.overall_score.isnot(None),
)
if inspector_filter:
prior_fac_q = prior_fac_q.filter(Inspection.inspector_id == inspector_filter)
if customer_facility_ids is not None:
prior_fac_q = prior_fac_q.filter(
Facility.id.in_(customer_facility_ids) if customer_facility_ids else False
)
prior_scores_raw = prior_fac_q.group_by(Facility.id, Facility.name).all()
prior_scores_map = {r.name: round(float(r.avg_score), 2) for r in prior_scores_raw}
# Build delta map keyed by facility name: positive = improved, negative = declined
facility_deltas = {}
for row in facility_scores:
prior = prior_scores_map.get(row.name)
if prior is not None:
facility_deltas[row.name] = round(float(row.avg_score) - prior, 1)
# Score trend — daily averages (line chart)
daily_q = db.session.query(
func.date(Inspection.inspection_date).label('day'),
@@ -226,13 +255,19 @@ def index():
inspectors = User.query.filter_by(role='inspector', active=True)\
.order_by(User.full_name, User.username).all()
facility_scores_list = [{'name': r.name, 'avg_score': round(float(r.avg_score), 2), 'count': r.count} for r in facility_scores]
# Attach prior avg and delta to each facility score dict for the template table
for row in facility_scores_list:
row['prior_avg'] = prior_scores_map.get(row['name'])
row['delta'] = facility_deltas.get(row['name'])
return render_template('reports/index.html',
start=start, end=end,
total_inspections=total_inspections,
completed=completed,
flagged=flagged,
avg_score=round(float(avg_score), 2) if avg_score else None,
facility_scores=[{'name': r.name, 'avg_score': round(float(r.avg_score), 2), 'count': r.count} for r in facility_scores],
facility_scores=facility_scores_list,
daily_scores=[{'day': str(r.day), 'avg': round(float(r.avg), 2), 'count': r.count} for r in daily_scores],
issue_severity=[{'severity': r.severity, 'count': r.count} for r in issue_severity],
issue_status=[{'status': r.status, 'count': r.count} for r in issue_status],