diff --git a/app/routes/dashboard.py b/app/routes/dashboard.py index 6e524d1..8c329d5 100644 --- a/app/routes/dashboard.py +++ b/app/routes/dashboard.py @@ -207,6 +207,120 @@ def index(): sla_breached = sum(1 for i in all_open_issues if sla_status(i) == 'breached') sla_at_risk = sum(1 for i in all_open_issues if sla_status(i) == 'at_risk') + # ── Issues opened today ─────────────────────────────────────────────────── + from app.models.facility import Area as _AreaT + opened_today_q = Issue.query.outerjoin(_AreaT, Issue.area_id == _AreaT.id).filter( + Issue.reported_at >= today_start, + Issue.reported_at < today_end, + ) + if is_inspector: + if not inspector_facility_ids: + opened_today_q = opened_today_q.filter(False) + else: + opened_today_q = opened_today_q.filter(db.or_( + Issue.facility_id.in_(inspector_facility_ids), + _AreaT.facility_id.in_(inspector_facility_ids), + )) + elif is_customer: + if not customer_facility_ids: + opened_today_q = opened_today_q.filter(False) + else: + opened_today_q = opened_today_q.filter(db.or_( + Issue.facility_id.in_(customer_facility_ids), + _AreaT.facility_id.in_(customer_facility_ids), + )) + issues_opened_today = opened_today_q.count() + + # ── Pending verification ────────────────────────────────────────────────── + from app.models.facility import Area as _AreaV + pv_q = Issue.query.outerjoin(_AreaV, Issue.area_id == _AreaV.id).filter( + Issue.status == 'pending_verification', + ) + if is_inspector: + if not inspector_facility_ids: + pv_q = pv_q.filter(False) + else: + pv_q = pv_q.filter(db.or_( + Issue.facility_id.in_(inspector_facility_ids), + _AreaV.facility_id.in_(inspector_facility_ids), + )) + elif is_customer: + if not customer_facility_ids: + pv_q = pv_q.filter(False) + else: + pv_q = pv_q.filter(db.or_( + Issue.facility_id.in_(customer_facility_ids), + _AreaV.facility_id.in_(customer_facility_ids), + )) + pending_verification = pv_q.count() + + # ── Stale in-progress inspections (started > 24h ago, not yet submitted) ── + stale_cutoff = now - timedelta(hours=24) + stale_q = Inspection.query.filter( + Inspection.status == 'in_progress', + Inspection.inspection_date < stale_cutoff, + ) + if is_inspector: + if not inspector_facility_ids: + stale_q = stale_q.filter(False) + else: + stale_q = stale_q.filter( + Inspection.facility_id.in_(inspector_facility_ids), + Inspection.inspector_id == current_user.id, + ) + elif is_customer: + if customer_facility_ids: + stale_q = stale_q.filter(Inspection.facility_id.in_(customer_facility_ids)) + else: + stale_q = stale_q.filter(False) + stale_in_progress = stale_q.count() + + # ── Unassigned open issues ──────────────────────────────────────────────── + from app.models.facility import Area as _AreaU + unassigned_q = Issue.query.outerjoin(_AreaU, Issue.area_id == _AreaU.id).filter( + Issue.status.in_(['open', 'in_progress']), + Issue.assigned_to.is_(None), + ) + if is_inspector: + if not inspector_facility_ids: + unassigned_q = unassigned_q.filter(False) + else: + unassigned_q = unassigned_q.filter(db.or_( + Issue.facility_id.in_(inspector_facility_ids), + _AreaU.facility_id.in_(inspector_facility_ids), + )) + elif is_customer: + unassigned_q = unassigned_q.filter(False) # not relevant for customers + unassigned_open = unassigned_q.count() + + # ── Inspector activity today (admin / director / PM only) ───────────────── + inspector_activity = [] + if is_privileged or is_project_manager: + active_inspectors = ( + User.query + .filter_by(role='inspector', active=True) + .order_by(User.full_name, User.username) + .all() + ) + today_counts = dict( + db.session.query( + Inspection.inspector_id, + func.count(Inspection.id), + ) + .filter( + Inspection.status == 'completed', + Inspection.inspection_date >= today_start, + Inspection.inspection_date < today_end, + ) + .group_by(Inspection.inspector_id) + .all() + ) + inspector_activity = sorted( + [{'name': u.display_name, 'count': today_counts.get(u.id, 0)} + for u in active_inspectors], + key=lambda x: (-x['count'], x['name']), + ) + # ── My open issues (inspector dashboard widget) ─────────────────────────── # Issues assigned to the current inspector that are not yet resolved, # ordered by SLA urgency (breached first, then at-risk, then ok). @@ -225,21 +339,26 @@ def index(): return render_template( 'dashboard.html', - today_inspections = today_inspections, - completed_today = completed_today, - open_issues = open_issues, - severity_breakdown = severity_breakdown, - resolved_today = resolved_today, - pending_followups = pending_followups, - recent_inspections = recent_inspections, - total_facilities = total_facilities, - total_templates = total_templates, - total_users = total_users, - sla_breached = sla_breached, - sla_at_risk = sla_at_risk, - customer_facilities = customer_facilities, - my_issues = my_issues, - today_str = now.strftime('%Y-%m-%d'), + today_inspections = today_inspections, + completed_today = completed_today, + open_issues = open_issues, + severity_breakdown = severity_breakdown, + resolved_today = resolved_today, + pending_followups = pending_followups, + issues_opened_today = issues_opened_today, + pending_verification = pending_verification, + stale_in_progress = stale_in_progress, + unassigned_open = unassigned_open, + inspector_activity = inspector_activity, + recent_inspections = recent_inspections, + total_facilities = total_facilities, + total_templates = total_templates, + total_users = total_users, + sla_breached = sla_breached, + sla_at_risk = sla_at_risk, + customer_facilities = customer_facilities, + my_issues = my_issues, + today_str = now.strftime('%Y-%m-%d'), ) diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index f499cda..ca7db77 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -90,6 +90,66 @@ {% endif %} +{# ── Second stat row ────────────────────────────────────────────────────── #} +
+
+ +
+
+
+
Issues Opened Today
+
{{ issues_opened_today }}
+
+ +
+
+
+
+ {% if current_user.role != 'customer' %} +
+ +
+
+
+
Pending Verification
+
{{ pending_verification }}
+
+ +
+
+
+
+
+ +
+
+
+
Stale In-Progress
+
{{ stale_in_progress }}
+
started >24h ago
+
+ +
+
+
+
+
+ +
+
+
+
Unassigned Open
+
{{ unassigned_open }}
+
no assignee
+
+ +
+
+
+
+ {% endif %} +
+ {# ── SLA Summary ─────────────────────────────────────────────────────────── #} {% if sla_breached > 0 or sla_at_risk > 0 %}
@@ -181,6 +241,49 @@ +{# ── Inspector activity today (admin / director / PM) ───────────────────── #} +{% if inspector_activity %} +
+
+ Inspector Activity Today + View all +
+
+ + + + + + + + + + {% for row in inspector_activity %} + + + + + + {% endfor %} + +
InspectorSubmitted Today
{{ row.name }} + {% if row.count > 0 %} + {{ row.count }} + {% else %} + + {% endif %} + +
+ {% set max_count = inspector_activity | map(attribute='count') | max %} + {% set pct = (row.count / max_count * 100) | int if max_count > 0 else 0 %} +
+
+
+
+
+{% endif %} + {# ── My open issues (inspector widget) ──────────────────────────────────── #} {% if my_issues %}