diff --git a/app/templates/dashboard/tech-performance.html b/app/templates/dashboard/tech-performance.html index 88da002..2161816 100644 --- a/app/templates/dashboard/tech-performance.html +++ b/app/templates/dashboard/tech-performance.html @@ -52,7 +52,7 @@
- Pending / Unstarted + Pending / In review
@@ -168,10 +168,15 @@ } let av = a[key]; let bv = b[key]; - // Missing resolution times sort last in either direction. + // Missing resolution times sort last in either direction; + // two missing values fall through to the name tiebreak. if (key === 'avg_resolution_hours') { - if (av === null || av === undefined) return 1; - if (bv === null || bv === undefined) return -1; + const aMissing = av === null || av === undefined; + const bMissing = bv === null || bv === undefined; + if (aMissing || bMissing) { + if (aMissing && bMissing) return a.name.localeCompare(b.name); + return aMissing ? 1 : -1; + } } av = av || 0; bv = bv || 0; diff --git a/tests/test_tech_performance.py b/tests/test_tech_performance.py index 0d68b5f..1ce0802 100644 --- a/tests/test_tech_performance.py +++ b/tests/test_tech_performance.py @@ -3,9 +3,9 @@ Anchors two Wahab-facing defects/requests: 1. The FM dashboard built its "Technician Workload" card from - ``Tech #`` instead of the technician's real name. The template must - use ``assigned_technician_name`` (the same field ``/tickets`` already uses), - and no template may reintroduce a ``Tech #`` placeholder. + ``Tech #`` instead of the technician's real name. The data sources + the card consumes must expose the technician's real name via + ``assigned_technician_name``, never an id placeholder. 2. ``GET /api/tickets/tech-performance`` reports per-technician workload and outcomes by real name: totals, per-status buckets, completion rate and ``created_at -> closed_at`` resolution times. Everything derives from @@ -15,7 +15,6 @@ Anchors two Wahab-facing defects/requests: from __future__ import annotations from datetime import datetime -from pathlib import Path import pytest from sqlalchemy import select @@ -26,8 +25,6 @@ from app.models.user import User pytestmark = pytest.mark.asyncio -TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "app" / "templates" - async def _login(client, email: str = "wahab@denya.com", password: str = "denya123") -> str: resp = await client.post("/api/auth/login", json={"email": email, "password": password}) @@ -80,24 +77,36 @@ def _ticket(status: str, assigned_to: int | None, *, created_at: datetime | None return spec -# ── 3a. 'Tech #N' regression ───────────────────────────────────────── -async def test_fm_dashboard_workload_uses_real_technician_names(client): - """The FM dashboard must build the workload card from the real name field.""" - resp = await client.get("/dashboard/fm") - assert resp.status_code == 200 - body = resp.text - assert "Tech #" not in body - assert "assigned_technician_name" in body +# ── 3a. Real technician names on the workload data paths ───────────── +async def test_workload_sources_report_real_technician_names(client): + """The FM workload card reads ``/api/tickets?page_size=200`` and the + performance report reads ``/api/tickets/tech-performance``. For an assigned + ticket both must expose the technician's real full name, never a + ``Tech #`` placeholder. + """ + prosper = await _user_id("prosper@denya.com") + await _seed_tickets([_ticket("In Progress", prosper), _ticket("Escalated", prosper)]) - -async def test_no_template_reintroduces_tech_id_placeholder(): - """Sweep every template for the ``Tech #`` placeholder.""" - offenders = [ - str(path.relative_to(TEMPLATES_DIR)) - for path in TEMPLATES_DIR.rglob("*.html") - if "Tech #" in path.read_text(encoding="utf-8") + token = await _login(client) + list_resp = await client.get("/api/tickets?page_size=200", headers=_auth(token)) + assert list_resp.status_code == 200 + active = [ + t for t in list_resp.json()["items"] + if t["status"] not in ("Closed", "Completed", "Cancelled") ] - assert offenders == [] + + # Same mapping the FM workload card builds from active assigned tickets. + workload: dict[int, str] = {} + for t in active: + if t["assigned_to"]: + workload.setdefault(t["assigned_to"], t["assigned_technician_name"] or "Unassigned") + assert workload == {prosper: "Prosper"} + + perf_resp = await client.get("/api/tickets/tech-performance", headers=_auth(token)) + assert perf_resp.status_code == 200 + rows = {t["technician_id"]: t for t in perf_resp.json()["technicians"]} + assert rows[prosper]["name"] == "Prosper" + assert all(not row["name"].startswith("Tech #") for row in rows.values()) # ── Page + route wiring ──────────────────────────────────────────────