Appearance
[UX] members — Settings
Draft from /ux-audit on 2026-07-30 (unattended batch run). Not filed. Repo: bcc-nancy/members · Branch:
develop@2c22f5a· Files reviewed: 5 Patterns: none consulted (feature maps cleanly to baseline; MCP budget preserved)
Summary
The Settings view (admin-settings, views/Admin/Settings.vue) is a two-card config form (sub-organisation + organisation) with no client-side validation and no error surface on load. The most serious issue is a data-loss path: a failed initial fetch silently renders an empty but fully editable form, and pressing « Enregistrer » then PATCHes blank values over the real record. Remaining issues are label association, an unnamed icon-only delete, and no unsaved-changes guard — several of them living in the shared Field.vue primitive rather than this view.
Findings
1. Failed load renders an empty editable form that can overwrite the real record — High · MSG-06 (proposed)
Where: views/Admin/Settings.vue:207-220 (load), consumed at :230-243 (saveSubOrg) What: load() wraps the fetch in try/finally with no catch. On a failed GET /api/org/settings, loading is cleared in finally, the v-else block (:15) renders, and the refs stay at their empty defaults (subOrg = { Nom:"", Slug:"", Modules:[] }, settings = {}, years = []). Nothing tells the user the load failed. If they then press « Enregistrer », saveSubOrg PATCHes Nom: "", Modules: [], and an empty Settings object — blanking the live sub-organisation name, disabling every module, and wiping the camp-cost years. Why it matters: A transient network error turns a read screen into a silent destructive write of organisation-wide configuration. There is no confirmation and no way for the user to know the form they are editing is not the real data. Fix: Add a catch that sets an error state and renders a retry surface instead of the form; do not render the editable cards until a successful load has populated the refs. Separately, block save when required fields (Nom) are empty. This is the members "loading gate ignores the error branch" theme (PROJECT-LEVEL.md, members) applied to an editable admin form, where the consequence escalates from dead-end to data loss.
2. Labels are not programmatically associated with their inputs — Medium · FORM-01
Where: components/ui/Field.vue:3-9 (shared); bare labels at Settings.vue:38, 53, 60, 74, 90What: Field.vue renders a <label> with no for attribute directly above a <slot>, and the BccInput/BccCheckbox inside carry no matching id. The hand-rolled <label class="text-xs text-neutral-600"> blocks in this view (Modules actifs, Objectif « à jour », Cutoff Pennylane, adhesion lock date, Années) are likewise unassociated. Why it matters: Clicking a label does not focus its control, and screen readers do not reliably announce the label when the field gains focus (WCAG 1.3.1 / 4.1.2). Affects every form built on Field.vue across the admin SPA, not just Settings. Fix: Generate an id in Field.vue, put it on the <label for>, and pass it down to the slotted control (e.g. via :id/aria-labelledby). Replace the bare <label> blocks here with <Field>.
3. Icon-only "delete year" button has no accessible name — Medium · A11Y-05
Where: views/Admin/Settings.vue:113-119What: The row-delete BccButton passes :icon="piIcon('trash')" with variant="tertiary" and no label or aria-label — unlike every other BccButton in the view, which supplies a label. Why it matters: A screen-reader user reaches an unnamed button and cannot tell it removes a camp-cost year (A11Y-05). The action mutates persisted config on save. Fix: Add an accessible name, e.g. aria-label="Supprimer l'année" (whether BccButton derives a name from the icon alone is unverifiable without the library source — see Unverified).
4. No warning when navigating away with unsaved edits — Medium · FORM-12 (proposed): warn before discarding entered form data
Where: views/Admin/Settings.vue (whole view — no beforeRouteLeave / beforeunload guard) What: All fields, module checkboxes and year rows mutate local refs; nothing is persisted until the per-card « Enregistrer » is pressed. Leaving the route (sidebar nav, back) discards every change with no prompt. Why it matters: On a dense config form it is easy to edit several fields, get distracted, navigate away, and lose the lot with no signal. The two separate save buttons make it easy to save one card and forget the other. Fix: Track a dirty flag and add a beforeRouteLeave confirm (and beforeunload) when edits are pending.
5. Save buttons are disabled during submission — Medium · FORM-06
Where: views/Admin/Settings.vue:124-128 and :158-162 (:disabled="saving") What: Both « Enregistrer » buttons bind :disabled to the in-flight saving flag from useMutationWithToast. Why it matters: Per FORM-06, disabling a just-activated button drops focus to <body>, stranding keyboard/AT users mid-action, and gives no busy affordance. Whether BccButton renders a native disabled (which causes the focus drop) vs. aria-busy cannot be confirmed without library source — see Unverified. Fix: Keep the button enabled and use aria-busy + an in-handler re-entrancy guard instead of :disabled.
6. Number fields expose no constraints and no validation — Low · FORM-04
Where: views/Admin/Settings.vue:54-57 (Target %), :103-112 (Year / Total_U18) What: The percentage target and camp-cost inputs are bare type="number" with no min/max/step, no hint, and no validation. Nothing prevents a negative percentage, a target over 100, or a nonsensical year. Why it matters: Constraints are only discoverable by failure (and here not even then — bad values save). Low because the values are numeric and staff-facing. Fix: Add min/max/step and a short hint stating the expected range before the user types.
7. Raw error message surfaced in the save toast — Low · MSG-02
Where: composables/useMutationWithToast.ts (detail: e instanceof Error ? e.message : ""), used by both saves What: The toast summary is a friendly French sentence, but detail echoes the raw Error.message from apiRequest, which can be an HTTP <status>-style string. Why it matters: Users occasionally see technical error prose. Minor because the primary summary is human-readable. Shared helper, not specific to Settings. Fix: Map known statuses to human sentences; drop the raw detail or gate it behind a dev flag.
Unverified
- A11Y-01 (contrast) —
text-neutral-500/600on light cards and thetext-red-500/600label/error colours need a contrast tool; not assessable from classes. - A11Y-06 (responsive / short viewport) — the fixed
grid-cols-2/grid-cols-3layouts (:25, :39, :51, :143) have no breakpoints and may crowd on a narrow or short viewport; needs a rendered page. - FORM-06 mechanism — whether
BccButtonmapsdisabledto a nativedisabledattribute (focus drop) is in@bcc-code/component-library-vue, whose source is not installed (node_modulesabsent per PROJECT-LEVEL.md). - A11Y-05 mechanism — whether
BccButtonderives an accessible name from:iconalone: same library, unverifiable from source. - MSG-01 — whether the PrimeVue-backed
useToastoutput carriesrole="status"/role="alert": library internals, unverifiable.
Baseline additions
- FORM-12 (proposed): warn before discarding entered form data. A view with unsaved edits must prompt before route change / unload. (Finding 4. Note:
FORM-12is already colliding across drafts per PROJECT-LEVEL.md — orchestrator to renumber.) - Finding 1 cites the MSG-06 theme (failed load must not render as populated/empty success state); here it needs the stronger clause that an editable form must not render on an unpopulated failed load, because save then destroys data.
Cross-project note
- Load-error-as-empty-form (Finding 1): the members "loading gate ignores the error branch" theme is already confirmed in members widgets (PROJECT-LEVEL.md) and as MSG-06 in customer-portal and tt-time-tracker. The editable-form-overwrites-real-data escalation is worth checking on any other members admin edit view (e.g. #25 Users & roles, #29 Superadmin) and on customer-portal/tt settings-style forms.
- FORM-01 unassociated labels in a shared field wrapper (Finding 2): check customer-portal's
packages/uifield primitive and tt-time-tracker's form wrappers for the same missingfor/id. - CONTENT-01: not-applicable — see project-level i18n finding (members has no i18n layer by design).
- NAV-03: fails project-wide — see PROJECT-LEVEL.md (members: static « BCC Nancy Admin » title for all routes).