Appearance
[UX] tt-time-tracker — Admin dashboard
Draft from /ux-audit on 2026-07-30 (unattended batch run). Not filed. Repo: Dr-Wade/tt-time-tracker · Branch:
develop@bb3238c· Files reviewed: 12 Patterns: data-display/statistics
Summary
The admin landing page (admin-dashboard, AdminDashboard.vue) is a KPI strip plus an approvals queue, active-punch list and recent-invoices cards. The approvals Table is wired well — it has a real error surface with retry and a proper empty state, and the headline counts come from dedicated aggregate endpoints rather than a truncated page (so the "aggregate from one page of data" theme does not apply here). The weak spot is the rest of the overview: the KPI tiles and the first-run detection ignore the error branch entirely, so a failed data load is rendered as an empty, freshly-created org — the single most important issue. Two other genuine defects: the "Ce mois" tile silently tracks a shared cross-page filter, and the page has no <h1>.
Findings
1. A failed data load is shown as a brand-new empty org ("Bienvenue") — Medium · MSG-06 (proposed)
Where: services/client/src/views/Admin/AdminDashboard.vue:298-305 (with :41-105) What: isFirstRun is computed from dataLoaded = !users.loading && !projects.loading plus the lists being empty. It never consults users.error / projects.error. On a failed fetch, loading flips to false and the lists stay [], so activeProjectCount and teamCount are 0, isFirstRun becomes true, and the admin of an established organisation is shown the welcome panel: « Votre organisation est prête. Voici par où commencer » with "create a project / invite your team" CTAs. The KPI tiles themselves (:45-105) have no loading skeleton and no error state at all — on error they render 0 under real labels, and the activeSessions poll (:279-287) likewise degrades silently to "0 au travail". Only the approvals Table (:167) surfaces its error. This is the same class as the project-level MSG-06 note but a distinct instance: the user Dashboard.vue swallows errors into "Aucune entrée"; here the admin dashboard swallows them into a misleading onboarding screen. Why it matters: An admin whose network blipped is told their populated org is empty and nudged to re-onboard; the real failure is invisible and there is no retry for the overview. The statistics pattern flags "ignoring non-happy states" as a top anti-pattern for exactly this surface. Fix: Gate isFirstRun on a successful load (&& !users.error && !projects.error), and give the KPI region a loading skeleton and an error+retry state, matching the approvals Table already on the page.
2. "Ce mois" tile shows whatever the shared filter holds, under a fixed label — Medium · DATA-TRUTHFUL-AGGREGATE (proposed)
Where: services/client/src/views/Admin/AdminDashboard.vue:95-104; value is hours(allEntries.total), and allEntries filters by the shared useFilters store (stores/organization/allEntries.store.ts:46-74,107-110). What: The tile is hard-labelled « Ce mois » / « cumulé », but allEntries.total is the aggregate for whatever period and user/project/status currently sit in the app-wide useFilters store — the same store the Overview screen writes to (filters.store.ts). The dashboard never resets it on mount. Set year 2024 or a single user/project on Overview, return to the dashboard, and "Ce mois" reports that other slice while still claiming "this month". Why it matters: A KPI that looks authoritative can silently show a different period or a single employee's hours under a "this month, cumulative" label — the admin has no way to tell the number has narrowed. Fix: Compute this tile from a dashboard-owned current-month aggregate independent of the shared filter store, or change the label to name what it actually covers.
3. No <h1>; the page heading starts at <h2> — Medium · A11Y-04
Where: services/client/src/components/Layout/LayoutMain.vue:6 and :74 (the #title slot renders inside <h2>); confirmed no <h1> on the route (grep '<h1' over the admin shell returns nothing). What: The page title « Tableau de bord » is an <h2>, and every section heading on the dashboard (« Heures à approuver », « Factures récentes », the first-run « Bienvenue ») is also <h2>. There is no <h1> anywhere on the route, so the heading outline begins at level 2. Why it matters: Screen-reader users get no top-level page heading and a skipped level — the standard "jump to h1" shortcut lands nowhere. This lives in the shared LayoutMain shell, so it affects every admin feature, not just this one; fixing it there fixes them all. Fix: Render the #title slot as <h1> in LayoutMain (demote section headings to <h2> as needed), so each admin route has exactly one <h1>.
Unverified
- A11Y-01 (contrast) — the tiles use CSS-var tones (
--text-secondary,--ink-700,success/warning/danger-bg) and lightsurface-*/primary-50backgrounds; several caption/label pairs look low-contrast but this needs a contrast tool on rendered colours. - A11Y-06 (short viewport / mobile) — the KPI grid is
grid-cols-2 lg:grid-cols-4and section grids stack; not verifiable from source. - FORM-06 — the bulk Approve/Reject buttons are PrimeVue
Button :loading(AdminDashboard.vue:148-163). PrimeVue mapsloading→disabled, which drops focus to<body>on the just-clicked control;node_modulesis not installed, so the internal behaviour cannot be asserted here. - MSG-01 — success/failure feedback goes through
layout.showSuccess/showError(PrimeVue Toast); whether it carriesrole="status"/alertis a library internal, unverifiable from source.
Baseline additions
- MSG-06 (proposed, reconcile) — a failed load must not be rendered as an empty/first-run/success state; every data region needs a distinct error surface with a recovery path. (Finding 1.)
- DATA-TRUTHFUL-AGGREGATE (proposed, reconcile with project-level candidate) — a displayed total/count/aggregate must either cover the whole set implied by its label or state what it covers; a KPI must not inherit a cross-page filter under a fixed label. (Finding 2.)
- CONTENT-01 — not-applicable — see project-level i18n finding.
- NAV-03 — fails project-wide (
index.html:17is « Tim » for every route); see PROJECT-LEVEL.md. Not re-filed here.
Cross-project note
Finding 1 (loading gate ignores the error branch) is the same shape as members' DonsProgress.vue / objectifs.store.ts and customer-portal's dashboard hero counts — likely present in members and customer-portal. Finding 3 (title in <h2>, no <h1>) is a shared-shell issue and worth checking in whichever shell each project's admin pages use; customer-portal's auth/onboarding shells already have a confirmed <h1> gap. Finding 2 (KPI bound to a shared filter store) is specific to how tt-time-tracker shares useFilters across Overview and dashboard.