diff --git a/.claude/settings.json b/.claude/settings.json index 59b344b..6162dc8 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -13,7 +13,8 @@ "Bash(python -c \"print\\(len\\('phase47_sched_acknowledged'\\)\\)\")", "Bash(python -c \"import ast,io; ast.parse\\(io.open\\('app/routes/scheduled_inspections.py',encoding='utf-8'\\).read\\(\\)\\); print\\('route OK'\\)\")", "Bash(python -c \"import ast,io; ast.parse\\(io.open\\('app/utils/notifications.py',encoding='utf-8'\\).read\\(\\)\\); print\\('notif OK'\\)\")", - "Bash(SECRET_KEY=x DATABASE_URL=sqlite:///:memory: DIGEST_SECRET=x MAIL_SERVER=localhost MAIL_USERNAME=x MAIL_PASSWORD=x MAIL_PORT=587 APP_BASE_URL=http://localhost MAIL_DEFAULT_SENDER=x@x.com python -c ' *)" + "Bash(SECRET_KEY=x DATABASE_URL=sqlite:///:memory: DIGEST_SECRET=x MAIL_SERVER=localhost MAIL_USERNAME=x MAIL_PASSWORD=x MAIL_PORT=587 APP_BASE_URL=http://localhost MAIL_DEFAULT_SENDER=x@x.com python -c ' *)", + "Bash(python -c \"import ast,io; ast.parse\\(io.open\\('app/routes/dashboard.py',encoding='utf-8'\\).read\\(\\)\\); print\\('dashboard route OK'\\)\")" ] } } diff --git a/app/routes/dashboard.py b/app/routes/dashboard.py index dc747d6..1e7a278 100644 --- a/app/routes/dashboard.py +++ b/app/routes/dashboard.py @@ -294,6 +294,23 @@ def index(): stale_q = stale_q.filter(False) stale_in_progress = stale_q.count() + # ── In-progress inspections (all, not just stale) — "In Progress" tile ───── + inprog_q = Inspection.query.filter(Inspection.status == 'in_progress') + if is_inspector: + if not inspector_facility_ids: + inprog_q = inprog_q.filter(False) + else: + inprog_q = inprog_q.filter( + Inspection.facility_id.in_(inspector_facility_ids), + Inspection.inspector_id == current_user.id, + ) + elif is_customer: + if customer_facility_ids: + inprog_q = inprog_q.filter(Inspection.facility_id.in_(customer_facility_ids)) + else: + inprog_q = inprog_q.filter(False) + in_progress_total = inprog_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( @@ -361,6 +378,7 @@ def index(): # ── Scheduled inspections (phase36): upcoming / overdue ────────────── sched_upcoming = [] sched_overdue_count = 0 + sched_total = 0 sched_open_inspections = {} if not is_customer: from app.models.scheduled_inspection import ScheduledInspection @@ -370,6 +388,7 @@ def index(): if is_inspector: _sq = _sq.filter(ScheduledInspection.inspector_id == current_user.id) _all_sched = _sq.order_by(ScheduledInspection.next_due_date.asc()).all() + sched_total = len(_all_sched) # active scheduled inspection plans ("On Schedules") sched_overdue_count = sum(1 for s in _all_sched if s.next_due_date < _today) # Upcoming = due today through the next 7 days (overdue shown separately) sched_upcoming = [ @@ -383,7 +402,9 @@ def index(): 'dashboard.html', sched_upcoming = sched_upcoming, sched_overdue_count = sched_overdue_count, + sched_total = sched_total, sched_open_inspections = sched_open_inspections, + in_progress_total = in_progress_total, submitted_this_week = submitted_this_week, completed_today = completed_today, open_issues = open_issues, diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 73db548..4e620b5 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -3,8 +3,34 @@ {% block content %} -{# Handler ("Handled by") breakdown chips for an issue KPI card. `base` is a - dict of extra issues.index query params identifying the card's scope. #} +{# ── Grouped dashboard styling (colored section cards + white stat tiles) ──── #} + + +{# Handler ("Handled by") breakdown chips for an issue KPI tile. `base` is a + dict of extra issues.index query params identifying the tile's scope. #} {% macro handler_chips(bd, base) %}
Handled by: @@ -20,6 +46,36 @@
{% endmacro %} +{# One white stat tile: label (+ optional subtitle OR handler chips) left, big + colored number right. Numbers render two-digit (01, 02, …) to match the design. + When chips_bd is given and the value is > 0, the handler chips replace the + subtitle; because the chips are themselves links, the whole tile is NOT + wrapped in an anchor (nested is invalid) — only the number links. #} +{% macro stat_tile(label, value, accent, href=None, subtitle=None, chips_bd=None, chips_base=None) %} +{% set has_chips = chips_bd is not none and value and value > 0 %} +
+ {% if href and not has_chips %}{% endif %} +
+
+
{{ label }}
+ {% if has_chips %} + {{ handler_chips(chips_bd, chips_base) }} + {% elif subtitle %} +
{{ subtitle }}
+ {% endif %} +
+
+ {% if href and has_chips %} + {{ '%02d'|format(value) }} + {% else %} + {{ '%02d'|format(value) if value is number else value }} + {% endif %} +
+
+ {% if href and not has_chips %}{% endif %} +
+{% endmacro %} +

Welcome, {{ current_user.display_name }}!

@@ -29,302 +85,157 @@
-{# ── Scheduled inspections: upcoming / overdue (phase36) ─────────────────── #} -{% if current_user.role != 'customer' and (sched_upcoming or sched_overdue_count) %} -
-
-
- Scheduled Inspections - View all -
- {% if sched_overdue_count %} -
- - {{ sched_overdue_count }} scheduled inspection{{ 's' if sched_overdue_count != 1 }} - {{ 'are' if sched_overdue_count != 1 else 'is' }} overdue. -
- {% endif %} - {% if sched_upcoming %} -
- - - - - - {% for s in sched_upcoming %} - - - - - - - - - {% endfor %} - -
FacilityTemplateInspectorRepeatsDue
{{ s.facility.name if s.facility else '—' }}{{ s.template.name if s.template else '—' }}{{ s.inspector.display_name if s.inspector else '—' }}{{ s.recurrence_label }}{{ s.next_due_date.strftime('%b %d') }} - {# Start is shown only to the assignee — the inspection is theirs to do. #} - {% if s.inspector_id and s.inspector_id == current_user.id %} - {# Receipt confirmation (phase47) — assignee confirms, or shows confirmed. #} - {% if s.is_acknowledged %} - Confirmed - {% else %} -
- - -
- {% endif %} - {% set open_id = sched_open_inspections.get(s.id) %} - {% if open_id %} - - Continue - {% else %} - Start - {% endif %} - {% endif %} -
-
- {% else %} -

No inspections due in the next 7 days.

- {% endif %} +{# ══════════════════════════════════════════════════════════════════════════ + A. SCHEDULED INSPECTION (blue) + ══════════════════════════════════════════════════════════════════════════ #} +{% if current_user.role != 'customer' %} +
+
+ A. Scheduled Inspection + View all
+ + {% if sched_overdue_count %} +
+ + {{ sched_overdue_count }} scheduled inspection{{ 's' if sched_overdue_count != 1 }} + {{ 'are' if sched_overdue_count != 1 else 'is' }} overdue. +
+ {% endif %} + + {% if sched_upcoming %} +
+ + + + + + + + + {% for s in sched_upcoming %} + + + + + + + + + {% endfor %} + +
Facility NameInspection TemplateInspector NameHow Often?Due
{{ s.facility.name if s.facility else '—' }}{{ s.template.name if s.template else '—' }}{{ s.inspector.display_name if s.inspector else '—' }}{{ s.recurrence_label }}{{ s.next_due_date.strftime('%b %d') }} + {# Start is shown only to the assignee — the inspection is theirs to do. #} + {% if s.inspector_id and s.inspector_id == current_user.id %} + {# Receipt confirmation (phase47) — assignee confirms, or shows confirmed. #} + {% if s.is_acknowledged %} + Confirmed + {% else %} +
+ + +
+ {% endif %} + {% set open_id = sched_open_inspections.get(s.id) %} + {% if open_id %} + + Continue + {% else %} + Start + {% endif %} + {% endif %} +
+
+ {% else %} +
+ No inspections due in the next 7 days. +
+ {% endif %}
{% endif %} -{# ── Inspections section ─────────────────────────────────────────────────── #} -
- - Inspections -
-
-
- -
- -
-
-
- Submitted Today - -
-
{{ completed_today }}
-
Fully completed & submitted today
-
-
-
+{# ══════════════════════════════════════════════════════════════════════════ + B. QUICK REVIEW (green) + ══════════════════════════════════════════════════════════════════════════ #} +{% set G = '#4e8a2f' %} +
+
B. Quick Review
+
+ {{ stat_tile('Submitted Today', completed_today, G, + url_for('inspections.index', status='completed', date_from=today_str, date_to=today_str), + 'Completed & submitted today') }} + {{ stat_tile('Submitted This Week', submitted_this_week, G, + url_for('inspections.index', status='completed', date_from=week_start_str, date_to=today_str), + 'Completed & submitted this week') }} + {% if current_user.role != 'customer' %} + {{ stat_tile('In Progress', in_progress_total, G, + url_for('inspections.index', status='in_progress'), + (stale_in_progress ~ ' stale >24h') if stale_in_progress else 'Started, not yet submitted') }} + {{ stat_tile('Pending Follow-ups', pending_followups, G, + url_for('inspections.index', status='follow_up'), + 'Flagged for re-inspection') }} + {{ stat_tile('On Schedules', sched_total, G, + url_for('scheduled_inspections.index'), + 'Active scheduled plans') }} + {% endif %}
- - - - {% if current_user.role != 'customer' %} - - - - {% endif %} -
-{# ── Issues section ──────────────────────────────────────────────────────── #} -
- - Issues -
-
-
- - {# Open issues split into three cards by who handles them. #} -
- -
-
-
- Open · Janitorial - -
-
{{ handler_breakdown.internal }}
-
Open issues handled by Janitorial crew
-
-
-
+{# ══════════════════════════════════════════════════════════════════════════ + C. REVIEW ISSUES (orange) + ══════════════════════════════════════════════════════════════════════════ #} +{% set O = '#d5761a' %} +
+
C. Review Issues
+
+ {{ stat_tile('Open · Janitorial', handler_breakdown.internal, O, + url_for('issues.index', status='open', handler_type='internal'), + 'Handled by our crew') }} + {{ stat_tile('Open · Facility Staff', handler_breakdown.facility, O, + url_for('issues.index', status='open', handler_type='facility'), + 'Handled by facility staff') }} + {{ stat_tile('Open · Vendors', handler_breakdown.vendor, O, + url_for('issues.index', status='open', handler_type='vendor'), + 'Handled by outside vendor') }} + {% if current_user.role != 'customer' %} + {{ stat_tile('Open · Unassigned', unassigned_open, O, + url_for('issues.index', status='open', unassigned='1'), + 'No one assigned yet', + chips_bd=unassigned_handler, chips_base={'status': 'open', 'unassigned': '1'}) }} + {{ stat_tile('Pending · Verification', pending_verification, O, + url_for('issues.index', status='pending_verification'), + 'Awaiting sign-off') }} + {% endif %} + {# All the original Issues cards are kept here in the same colored group. #} + {{ stat_tile('Issues Opened Today', issues_opened_today, O, + url_for('issues.index', date_from=today_str, date_to=today_str), + 'New issues reported today', + chips_bd=opened_today_handler, chips_base={'date_from': today_str, 'date_to': today_str}) }} + {{ stat_tile('Resolved Today', resolved_today, O, + url_for('issues.index', status='resolved', date_from=today_str, date_to=today_str), + 'Issues closed today') }}
- - - - - -
-
-
-
- Issues Opened Today - -
- -
{{ issues_opened_today }}
-
- {% if issues_opened_today > 0 %} - {{ handler_chips(opened_today_handler, {'date_from': today_str, 'date_to': today_str}) }} - {% else %} -
New issues reported today
- {% endif %} -
-
-
- - - - {% if current_user.role != 'customer' %} - - -
-
-
-
- Unassigned Open - -
- -
{{ unassigned_open }}
-
- {% if unassigned_open > 0 %} - {{ handler_chips(unassigned_handler, {'status': 'open', 'unassigned': '1'}) }} - {% else %} -
Open issues with no one assigned
- {% endif %} -
-
-
- {% endif %} -
-{# ── SLA Summary ─────────────────────────────────────────────────────────── #} +{# ══════════════════════════════════════════════════════════════════════════ + D. SLA ALERT (grey) + ══════════════════════════════════════════════════════════════════════════ #} +{# Section D shows only when there is at least one at-risk or breached issue. #} {% if sla_breached > 0 or sla_at_risk > 0 %} -
- {% if sla_breached > 0 %} -
- -
-
-
-
SLA Breached
-
{{ sla_breached }}
-
- -
-
-
+{% set R = '#cc0000' %} +
+
D. SLA Alert
+
+ {{ stat_tile('SLA At Risk', sla_at_risk, R, + url_for('issues.index', sla='at_risk'), + 'Approaching their SLA deadline') }} + {{ stat_tile('SLA Alert', sla_breached, R, + url_for('issues.index', sla='breached'), + 'Past their SLA deadline') }}
- {% endif %} - {% if sla_at_risk > 0 %} - - {% endif %}
{% endif %} @@ -594,4 +505,4 @@ {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %}