Wahab customer request (relay #748 v3 next-wave). 3a. Fix 'Tech #N' display: - FM dashboard's "Technician Workload" card was built from `Tech #${t.assigned_to}`; it now reads Ticket.assigned_technician_name (falling back to 'Unassigned', matching /tickets). - Template sweep confirms no other `Tech #` placeholder exists. 3b. Technician performance dashboard: - New bearer-gated GET /api/tickets/tech-performance aggregating existing ticket columns only (no schema change): per-technician total assigned, completed, in progress, escalated, cancelled, pending, open tasks, completion rate, and created_at -> closed_at resolution time with explicit counts for finished tasks lacking a timestamp. Rows key on user id so same-name technicians stay separate; labels are real names. Fleet totals and an unassigned-ticket count are included. - New /dashboard/tech-performance page (FM + CEO nav and links) with sortable table, completion bars and empty states; same-origin vendored assets only. - Bucket map, service, schemas and tests in app/services/ticket.py, app/schemas/ticket.py, tests/test_tech_performance.py. Tests: 111 passed (101 existing + 10 new).
57 lines
2.0 KiB
Python
57 lines
2.0 KiB
Python
"""Frontend page routes — serve Jinja2 HTML templates for the SPA-like dashboard."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, Request
|
|
from fastapi.responses import HTMLResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
|
|
router = APIRouter(tags=["pages"])
|
|
|
|
templates = Jinja2Templates(directory=str(Path(__file__).resolve().parent.parent / "templates"))
|
|
|
|
|
|
@router.get("/login", response_class=HTMLResponse)
|
|
async def login_page(request: Request):
|
|
return templates.TemplateResponse(request, "login.html")
|
|
|
|
|
|
@router.get("/dashboard/cs", response_class=HTMLResponse)
|
|
async def cs_dashboard(request: Request):
|
|
return templates.TemplateResponse(request, "dashboard/cs.html")
|
|
|
|
|
|
@router.get("/dashboard/fm", response_class=HTMLResponse)
|
|
async def fm_dashboard(request: Request):
|
|
return templates.TemplateResponse(request, "dashboard/fm.html")
|
|
|
|
|
|
@router.get("/dashboard/ceo", response_class=HTMLResponse)
|
|
async def ceo_dashboard(request: Request):
|
|
return templates.TemplateResponse(request, "dashboard/ceo.html")
|
|
|
|
|
|
@router.get("/dashboard/tech-performance", response_class=HTMLResponse)
|
|
async def tech_performance_dashboard(request: Request):
|
|
"""Technician performance report (FM + CEO). Data comes from
|
|
``GET /api/tickets/tech-performance``; the page itself follows the same
|
|
client-side auth pattern as the other dashboards."""
|
|
return templates.TemplateResponse(request, "dashboard/tech-performance.html")
|
|
|
|
|
|
@router.get("/tickets", response_class=HTMLResponse)
|
|
async def ticket_list(request: Request):
|
|
return templates.TemplateResponse(request, "tickets/list.html")
|
|
|
|
|
|
@router.get("/tickets/new", response_class=HTMLResponse)
|
|
async def create_ticket_page(request: Request):
|
|
return templates.TemplateResponse(request, "tickets/new.html")
|
|
|
|
|
|
@router.get("/tickets/{ticket_id}", response_class=HTMLResponse)
|
|
async def ticket_detail_page(request: Request, ticket_id: int):
|
|
return templates.TemplateResponse(request, "tickets/detail.html", context={"ticket_id": ticket_id})
|