Appearance
[UX] tt-time-tracker — Organizations (superadmin)
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: forms/search-field
Summary
The superadmin Organizations list is a clickable card grid (not the assigned table), and its cards are bare <div @click> — the screen's primary action is mouse-only. The assigned organization-details route (stat cards, delete, settings) has no in-app entry point: clicking a card enters the org via admin-dashboard instead, so the superadmin org-management view is reachable only by hand-typing the URL. A failed list fetch is swallowed into the empty state, telling a superadmin "create your first organization" on a network blip.
Findings
1. Organization cards are click-only <div>s — High · A11Y-03
Where: services/client/src/views/SuperAdmin/Organizations.vue:19-24What: Each org is a <div … class="… cursor-pointer" @click="selectOrganization(t.id)"> with no tabindex, role="button", @keydown.enter/space, or accessible name. Why it matters: The list's sole purpose is picking an organization, and that action is unreachable by keyboard or screen reader — a hard barrier for the whole feature, not a single control. Fix: Make each card a <button> (or <a> if it navigates), or add role="button" tabindex="0" plus Enter/Space handlers and an accessible name (org name). Same click-only-row shape is a confirmed cross-project theme.
2. organization-details route has no in-app entry point — High · NAV-ENTRYPOINT (proposed)
Where: router/routes.ts:68 (route) vs Organizations.vue:93-97 (selectOrganization → router.push({ name: "admin-dashboard" })) What: Nothing in src/ navigates to name: "organization-details" (grep: zero references outside routes.ts). Clicking a card sets idFromChooser and routes to admin-dashboard, i.e. it enters the org rather than opening the superadmin detail view. That detail view — the only surface exposing per-org stats, Supprimer (delete), and Sauvegarder — is thus reachable only by manually typing /admin/organizations/:id. Why it matters: A whole management surface, including the destructive delete-organization control, is dead to normal navigation; a superadmin cannot delete or inspect an organization through the UI. Fix: Route card clicks (or a per-card overflow action) to organization-details, and separate "enter this org" from "manage this org".
3. Failed org-list fetch renders as an empty state — Medium · MSG-06 (project theme, see PROJECT-LEVEL.md)
Where: services/client/src/stores/admin/organizations.store.ts:9-14What: fetch() wraps the call in try { … } catch { list.value = []; }. A failed load produces an empty list, which Organizations.vue:40-67 renders as « Aucune organisation / Créez votre première organisation ». Why it matters: A superadmin who manages many orgs sees, on any transient error, a screen inviting them to create their first one — no error, no retry. Indistinguishable from a genuinely empty system. Fix: Track an error state and show a distinct error + retry surface (ListErrorState already exists in this repo) instead of collapsing to [].
4. Mobile header action buttons are icon-only with no accessible name — Medium · A11Y-05
Where: OrganizationDetails.vue:6-20 (<span class="hidden lg:inline"> labels) + components/Layout/LayoutMain.vue:109-119 (mobile CSS hides .p-button-label); also components/AddButton.vue (label "Ajouter" hidden on mobile). What: Below lg, Supprimer/Sauvegarder render as bare icons (MdiTrash / MdiContentSave) with their text hidden and no aria-label; the header slot CSS strips PrimeVue labels app-wide, so AddButton's pi-plus is likewise nameless on mobile. Why it matters: On the phone form factor these are unlabeled to screen readers — and one of them is a destructive delete. Fix: Add aria-label on the icon buttons (kept even when the visible text is hidden). Cross-feature: any LayoutMain page using header buttons is affected.
5. No <h1> on the page — Medium · A11Y-04
Where: components/Layout/LayoutMain.vue:6,74 (page title rendered as <h2>) What: The layout renders the page title (« Organisations », « Organisation : … ») inside <h2>; OrganizationSettings.vue sections are also <h2>. No <h1> exists on either route. Why it matters: Screen-reader users lose the top-level document landmark and heading-level order is broken from the top. Applies to every LayoutMain page. Fix: Promote the page-title slot to <h1> in LayoutMain; keep section headings at <h2>.
6. Search field and modal inputs lack associated labels — Medium · FORM-01
Where: components/Forms/Fields/FieldSearch.vue:4-10 (placeholder « Rechercher... » is the only label); components/Modals/ModalOrganization.vue:12-46 (« Id »/« Nom » <label>s have no for, inputs have no id). What: The org search box is placeholder-only. In the create modal the labels are visually present but not programmatically associated with their InputTexts. The forms/search-field pattern's own example wires <label for>, a Clear control, and a result status — none present here (no clear button, no result-count aria-live). Why it matters: Placeholder-only labels vanish on input and are inconsistently announced; unassociated modal labels aren't read on focus. Fix: Give FieldSearch a real (optionally visually-hidden) <label> and a clear button; add for/id pairs in the modal. Feature-flag checkboxes in the same modal already do this correctly — mirror that.
7. No last-admin guard when removing org administrators — Medium · last-admin hypothesis (PROJECT-LEVEL.md)
Where: components/OrganizationSettings.vue:269-286 (handleRemoveAdmin) What: Removing an admin only confirms (« Retirer les droits… ? ») then calls admins.remove. There is no client-side guard preventing removal of the last remaining administrator of an organization; server enforcement is Unverified (node_modules/API not read). Why it matters: Per the cross-project hypothesis, an org can be left with no org-level admin. Less severe here than self-lockout since a superadmin can re-add, but the guard is absent on the path. Fix: Block removing the final admin (disable + explain), and confirm the rule is enforced server-side.
8. Stat cards show "0" during load rather than a loading state — Low · CONTENT-04
Where: OrganizationDetails.vue:121-128What: usersCount/invoicesCount/hoursThisMonth default to "0" while their queries resolve, so the cards briefly assert « 0 Utilisateurs / 0 Factures » before the real figures arrive. Why it matters: A momentary authoritative-looking zero is misinformation, not a wait state. Fix: Render a skeleton/placeholder until each query settles.
Unverified
- A11Y-01 (contrast) —
text-surface-400/text-gray-500on white cards and the primary-500 avatar chip need a contrast tool; not settleable from code. - A11Y-06 (short viewport / mobile keyboard) — the create Drawer/Dialog (
SheetOrDialog.vue) and the settings cards need a rendered viewport. - FORM-06 / MSG-01 — PrimeVue
Button :loading→ nativedisabled(focus drop) and whetherlayout.showSuccess/showErrortoasts carryrole=alertcannot be read:node_modulesnot installed. - SEC (server enforcement) — last-admin removal and org delete/archive behaviour are client-only observations.
Baseline additions
- NAV-ENTRYPOINT (proposed): every routable, user-relevant surface has a discoverable in-app navigation path; a route reachable only by typing its URL is a defect. (Distinct from the NAV-06 async-guard cluster.)
- Note:
ModalOrganizationsupports an edit branch (isEditing, footer delete) but is only ever mounted in create mode (Organizations.vue:68, no:organization), andOrganizationSettingshas no name/id field — so an org's name cannot be changed after creation via the UI. Folded here rather than filed separately; symptomatic of the same half-wired CRUD as finding 2.
Cross-project note
- A11Y-03 click-only rows/cards — confirmed failing in all four projects; this is the tt-time-tracker card-grid instance.
- MSG-06 empty-on-error — matches the tt-time-tracker project-level theme and the members
TableData.vue/ customer-portal DataTable clusters. - A11Y-04 no
<h1>— LayoutMain-wide here; worth checking the shared shells in members and customer-portal (customer-portal'sPageLayoutalready passes). - NAV-ENTRYPOINT / orphaned routes — likely present wherever detail routes were added ahead of their list links; check the other three.
- Non-superadmins are silently redirected off superadmin routes with no forbidden surface — see PROJECT-LEVEL.md (do not re-file).
- CONTENT-01 — not-applicable — see project-level i18n finding.