Jul 9 - Update Dashboard separate open issue by Handler (more viewable)

This commit is contained in:
2026-07-09 12:21:30 -04:00
parent 53ba27370d
commit 59218a6717
5 changed files with 68 additions and 38 deletions
+7 -5
View File
@@ -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 `<div style="flex:1;height:1px;background:#e2e8f0;">` 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 `<a>` 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 `<div style="flex:1;height:1px;background:#e2e8f0;">` 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.
+19 -7
View File
@@ -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,
+7
View File
@@ -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,
+33 -24
View File
@@ -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) %}
<div class="mt-1 d-flex flex-wrap align-items-center gap-1" style="font-size:.7rem;">
<span style="opacity:.75;">Handled by:</span>
{% if bd.internal > 0 %}
<a href="{{ url_for('issues.index', handler_type='internal', **base) }}"
class="badge bg-primary text-decoration-none" title="Janitorial Staff — our crew">Janitorial {{ bd.internal }}</a>{% endif %}
{% if bd.facility > 0 %}
<a href="{{ url_for('issues.index', handler_type='facility', **base) }}"
class="badge bg-info text-dark text-decoration-none" title="Facility Staff — the facility's own on-site staff">Facility {{ bd.facility }}</a>{% endif %}
{% if bd.vendor > 0 %}
<a href="{{ url_for('issues.index', handler_type='vendor', **base) }}"
class="badge bg-light text-dark border text-decoration-none" title="External Vendor — an outside contractor">Vendor {{ bd.vendor }}</a>{% endif %}
</div>
{% endmacro %}
<div class="row mb-3 align-items-center">
<div class="col">
<h2 class="mb-0">Welcome, {{ current_user.display_name }}!</h2>
@@ -156,24 +174,7 @@
{% if severity_breakdown.low > 0 %}<span class="badge bg-secondary">{{ severity_breakdown.low }}L</span>{% endif %}
</div>
{# ── Split by who handles it (click to drill in) ── #}
<div class="mt-1 d-flex flex-wrap align-items-center gap-1" style="font-size:.7rem;">
<span style="opacity:.75;">Handled by:</span>
{% if handler_breakdown.internal > 0 %}
<a href="{{ url_for('issues.index', status='open', handler_type='internal') }}"
class="badge bg-primary text-decoration-none" title="Janitorial Staff">
<i class="bi bi-people"></i> {{ handler_breakdown.internal }}
</a>{% endif %}
{% if handler_breakdown.facility > 0 %}
<a href="{{ url_for('issues.index', status='open', handler_type='facility') }}"
class="badge bg-info text-dark text-decoration-none" title="Facility Staff">
<i class="bi bi-building"></i> {{ handler_breakdown.facility }}
</a>{% endif %}
{% if handler_breakdown.vendor > 0 %}
<a href="{{ url_for('issues.index', status='open', handler_type='vendor') }}"
class="badge bg-light text-dark border text-decoration-none" title="External Vendor">
<i class="bi bi-person-gear"></i> {{ handler_breakdown.vendor }}
</a>{% endif %}
</div>
{{ handler_chips(handler_breakdown, {'status': 'open'}) }}
{% else %}
<div class="mt-2" style="font-size:.72rem;opacity:.75;">Active issues not yet resolved</div>
{% endif %}
@@ -182,18 +183,22 @@
</div>
<div class="col-6 col-md">
<a href="{{ url_for('issues.index', date_from=today_str, date_to=today_str) }}" class="text-decoration-none">
<div class="card text-white h-100" style="background:#ea580c;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small fw-semibold" style="color:rgba(255,255,255,.6);">Issues Opened Today</span>
<i class="bi bi-flag" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ issues_opened_today }}</div>
<a href="{{ url_for('issues.index', date_from=today_str, date_to=today_str) }}" class="text-white text-decoration-none">
<div class="fs-1 fw-bold lh-1">{{ issues_opened_today }}</div>
</a>
{% if issues_opened_today > 0 %}
{{ handler_chips(opened_today_handler, {'date_from': today_str, 'date_to': today_str}) }}
{% else %}
<div class="mt-2" style="font-size:.72rem;opacity:.7;">New issues reported today</div>
{% endif %}
</div>
</div>
</a>
</div>
<div class="col-6 col-md">
@@ -228,18 +233,22 @@
</div>
<div class="col-6 col-md">
<a href="{{ url_for('issues.index', status='open') }}" class="text-decoration-none">
<div class="card text-white h-100" style="background:#16a34a;">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-1">
<span class="small fw-semibold" style="color:rgba(255,255,255,.6);">Unassigned Open</span>
<i class="bi bi-person-dash" style="font-size:1.4rem;opacity:.3;"></i>
</div>
<div class="fs-1 fw-bold lh-1">{{ unassigned_open }}</div>
<a href="{{ url_for('issues.index', status='open', unassigned='1') }}" class="text-white text-decoration-none">
<div class="fs-1 fw-bold lh-1">{{ unassigned_open }}</div>
</a>
{% if unassigned_open > 0 %}
{{ handler_chips(unassigned_handler, {'status': 'open', 'unassigned': '1'}) }}
{% else %}
<div class="mt-2" style="font-size:.72rem;opacity:.7;">Open issues with no one assigned</div>
{% endif %}
</div>
</div>
</a>
</div>
{% endif %}
+2 -2
View File
@@ -198,7 +198,7 @@
class="d-inline"
title="Unfollow this issue">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="next" value="{{ url_for('issues.index', page=issues.page, issue_id=issue_id_filter, severity=severity_filter, status=status_filter, contract_id=contract_filter, facility_id=facility_filter, date_from=date_from_filter, date_to=date_to_filter, reporter_id=reporter_filter, handler_type=handler_filter) }}">
<input type="hidden" name="next" value="{{ url_for('issues.index', page=issues.page, issue_id=issue_id_filter, severity=severity_filter, status=status_filter, contract_id=contract_filter, facility_id=facility_filter, date_from=date_from_filter, date_to=date_to_filter, reporter_id=reporter_filter, handler_type=handler_filter, unassigned=unassigned_filter) }}">
<button type="submit" class="btn btn-sm btn-outline-primary p-0 px-1 me-1"
title="Unfollow">
<i class="bi bi-bell-slash" style="font-size:.75rem;"></i>
@@ -239,7 +239,7 @@
{% if p %}
<li class="page-item {{ 'active' if p == issues.page }}">
<a class="page-link"
href="{{ url_for('issues.index', page=p, issue_id=issue_id_filter, severity=severity_filter, status=status_filter, sla=sla_filter, contract_id=contract_filter, facility_id=facility_filter, date_from=date_from_filter, date_to=date_to_filter, reporter_id=reporter_filter, handler_type=handler_filter) }}">{{ p }}</a>
href="{{ url_for('issues.index', page=p, issue_id=issue_id_filter, severity=severity_filter, status=status_filter, sla=sla_filter, contract_id=contract_filter, facility_id=facility_filter, date_from=date_from_filter, date_to=date_to_filter, reporter_id=reporter_filter, handler_type=handler_filter, unassigned=unassigned_filter) }}">{{ p }}</a>
</li>
{% else %}<li class="page-item disabled"><span class="page-link"></span></li>{% endif %}
{% endfor %}