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 ──────────────────────────────────────────────