From 59218a671772e6d6fc047efde6fa4b10aac99bb2 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Thu, 9 Jul 2026 12:21:30 -0400 Subject: [PATCH] Jul 9 - Update Dashboard separate open issue by Handler (more viewable) --- CLAUDE.md | 12 ++++--- app/routes/dashboard.py | 26 +++++++++++----- app/routes/issues.py | 7 +++++ app/templates/dashboard.html | 57 ++++++++++++++++++++-------------- app/templates/issues/list.html | 4 +-- 5 files changed, 68 insertions(+), 38 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8a0fccc..96ec372 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1004,7 +1004,7 @@ Rendered in `dashboard.html` for `current_user.role == 'customer'`. Uses a Boots ### Issues List — ID Filter, Date Filter & PDF Export -`issues.index()` accepts three additional query params: `issue_id` (exact match on `Issue.id`), `date_from`, `date_to` (ISO date strings applied to `Issue.reported_at`). The `date_to` end is expanded to `23:59:59` so the whole day is included. +`issues.index()` accepts additional query params: `issue_id` (exact match on `Issue.id`), `date_from`, `date_to` (ISO date strings applied to `Issue.reported_at`; `date_to` expanded to `23:59:59`), `handler_type` (`internal`/`facility`/`vendor`), and `unassigned` (truthy → `Issue.assigned_to IS NULL`). `handler_type` and `unassigned` are both threaded through the pagination links and shared with `export_list_pdf()`. `GET /issues/export-list-pdf` — same scope + filter logic as `index()`, applies SLA post-filter for `?sla=` param (SLA is computed in Python, not stored). Calls `generate_issues_list_pdf()`. @@ -1029,13 +1029,15 @@ The dashboard cards are organised into two labelled sections separated by a divi 4. Pending Follow-ups — inspections with `follow_up_required=True`; links to `inspections.index?status=follow_up` **Issues section** (customers see first 3; staff see all 5): -1. Open Issues — `status=open` with severity breakdown badges (C/H/M/L) **and** a "Handled by" split (Janitorial Staff / Facility Staff / External Vendor) from `handler_breakdown`; each handler chip links to `issues.index?status=open&handler_type=...`. Both breakdowns are derived from the already-loaded `open_issues_all` list (no extra queries). The card is no longer a single wrapping anchor — the count and each handler chip are separate links (nested anchors are invalid). -2. Issues Opened Today — links to `issues.index` with `date_from=today&date_to=today` (uses the date filter added to `issues.index`) +1. Open Issues — `status=open` with severity breakdown badges (C/H/M/L) **and** a "Handled by" split (Janitorial Staff / Facility Staff / External Vendor) from `handler_breakdown`. +2. Issues Opened Today — `date_from=today&date_to=today`, with a "Handled by" split from `opened_today_handler`. 3. Resolved Today — `status=resolved` + today's date range 4. Pending Verification — `status=pending_verification` -5. Unassigned Open — `status=open` issues with no `assigned_to` +5. Unassigned Open — `status=open&unassigned=1`, with a "Handled by" split from `unassigned_handler`. -Each card has a subtitle line explaining what it counts. Section dividers use `d-flex align-items-center gap-2` with a `
` rule. +**"Handled by" splits (phase35+):** cards 1, 2, and 5 render a handler breakdown via the `handler_chips(bd, base)` **Jinja macro** at the top of `dashboard.html` — full-text labels ("Janitorial N / Facility N / Vendor N") that each link to `issues.index` with the card's own filter (`base`) plus `handler_type=`. Breakdowns come from `_handler_split(list)` over the already-loaded issue lists (`open_issues_all`, `opened_today_all`, `unassigned_all`) — **no extra queries**. These three cards are no longer a single wrapping anchor — the count and each chip are separate links (nested `` is invalid). `issues.index` gained an **`unassigned=1`** filter (`Issue.assigned_to.is_(None)`) so the Unassigned card and its chips link precisely; it is threaded through the list pagination links. + +Each card has a subtitle line (or the handler split) explaining what it counts. Section dividers use `d-flex align-items-center gap-2` with a `
` rule. **Inspector Activity table** follows the cards for admin/director/PM: all active inspectors, today's completed inspection count per inspector, progress bar scaled to `max_count`. Green row highlight if count > 0. diff --git a/app/routes/dashboard.py b/app/routes/dashboard.py index f7a8091..adde5aa 100644 --- a/app/routes/dashboard.py +++ b/app/routes/dashboard.py @@ -17,6 +17,16 @@ bp = Blueprint('dashboard', __name__) logger = logging.getLogger(__name__) +def _handler_split(issues): + """Count a list of Issues by handler_type (phase35). Rows default to + 'internal' when unset. Returns a dict keyed internal/facility/vendor.""" + return { + 'internal': sum(1 for i in issues if (i.handler_type or 'internal') == 'internal'), + 'facility': sum(1 for i in issues if i.handler_type == 'facility'), + 'vendor': sum(1 for i in issues if i.handler_type == 'vendor'), + } + + @bp.route('/') @bp.route('/dashboard') @login_required @@ -96,11 +106,7 @@ def index(): 'low': sum(1 for i in open_issues_all if i.severity == 'low'), } # Open issues split by who handles them (phase35) — same list, no extra query. - handler_breakdown = { - 'internal': sum(1 for i in open_issues_all if (i.handler_type or 'internal') == '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'), - } + handler_breakdown = _handler_split(open_issues_all) # ── Issues resolved today ───────────────────────────────────────────────── resolved_today_q = Issue.query.filter( @@ -235,7 +241,9 @@ def index(): Issue.facility_id.in_(customer_facility_ids), _AreaT.facility_id.in_(customer_facility_ids), )) - issues_opened_today = opened_today_q.count() + opened_today_all = opened_today_q.all() + issues_opened_today = len(opened_today_all) + opened_today_handler = _handler_split(opened_today_all) # ── Pending verification ────────────────────────────────────────────────── from app.models.facility import Area as _AreaV @@ -297,7 +305,9 @@ def index(): )) elif is_customer: unassigned_q = unassigned_q.filter(False) # not relevant for customers - unassigned_open = unassigned_q.count() + unassigned_all = unassigned_q.all() + unassigned_open = len(unassigned_all) + unassigned_handler = _handler_split(unassigned_all) # ── Inspector activity today (admin / director / PM only) ───────────────── inspector_activity = [] @@ -372,9 +382,11 @@ def index(): resolved_today = resolved_today, pending_followups = pending_followups, issues_opened_today = issues_opened_today, + opened_today_handler = opened_today_handler, pending_verification = pending_verification, stale_in_progress = stale_in_progress, unassigned_open = unassigned_open, + unassigned_handler = unassigned_handler, inspector_activity = inspector_activity, recent_inspections = recent_inspections, total_facilities = total_facilities, diff --git a/app/routes/issues.py b/app/routes/issues.py index 15f4a73..ae1b5a8 100644 --- a/app/routes/issues.py +++ b/app/routes/issues.py @@ -113,11 +113,14 @@ def export_list_pdf(): date_to_filter = request.args.get('date_to', '') reporter_filter = request.args.get('reporter_id', '') handler_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)) if handler_filter in ('internal', 'facility', 'vendor'): q = q.filter(Issue.handler_type == handler_filter) + if unassigned_filter: + q = q.filter(Issue.assigned_to.is_(None)) if severity_filter: q = q.filter(Issue.severity == severity_filter) if status_filter: @@ -247,11 +250,14 @@ def index(): date_to_filter = request.args.get('date_to', '') reporter_filter = request.args.get('reporter_id', '') handler_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)) if handler_filter in ('internal', 'facility', 'vendor'): q = q.filter(Issue.handler_type == handler_filter) + if unassigned_filter: + q = q.filter(Issue.assigned_to.is_(None)) if severity_filter: q = q.filter(Issue.severity == severity_filter) if status_filter: @@ -359,6 +365,7 @@ def index(): date_to_filter=date_to_filter, reporter_filter=reporter_filter, handler_filter=handler_filter, + unassigned_filter=unassigned_filter, facilities=facilities, projects=projects, staff=staff, diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 05a1f9e..54059c7 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -2,6 +2,24 @@ {% block title %}Dashboard{% endblock %} {% 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. #} +{% macro handler_chips(bd, base) %} +
+ Handled by: + {% if bd.internal > 0 %} + Janitorial {{ bd.internal }}{% endif %} + {% if bd.facility > 0 %} + Facility {{ bd.facility }}{% endif %} + {% if bd.vendor > 0 %} + Vendor {{ bd.vendor }}{% endif %} +
+{% endmacro %} +

Welcome, {{ current_user.display_name }}!

@@ -156,24 +174,7 @@ {% if severity_breakdown.low > 0 %}{{ severity_breakdown.low }}L{% endif %}
{# ── Split by who handles it (click to drill in) ── #} -
- Handled by: - {% if handler_breakdown.internal > 0 %} - - {{ handler_breakdown.internal }} - {% endif %} - {% if handler_breakdown.facility > 0 %} - - {{ handler_breakdown.facility }} - {% endif %} - {% if handler_breakdown.vendor > 0 %} - - {{ handler_breakdown.vendor }} - {% endif %} -
+ {{ handler_chips(handler_breakdown, {'status': 'open'}) }} {% else %}
Active issues not yet resolved
{% endif %} @@ -182,18 +183,22 @@
-
Issues Opened 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 %}
-
@@ -228,18 +233,22 @@
-
Unassigned Open
-
{{ 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 %} diff --git a/app/templates/issues/list.html b/app/templates/issues/list.html index ce8da54..a80c4ad 100644 --- a/app/templates/issues/list.html +++ b/app/templates/issues/list.html @@ -198,7 +198,7 @@ class="d-inline" title="Unfollow this issue"> - +