Skip to content

[UX] members — Familles ​

Draft from /ux-audit on 2026-07-30 (unattended batch run). Not filed. Repo: bcc-nancy/members · Branch: develop @ 2c22f5a · Files reviewed: 6 Patterns: none consulted (baseline sufficient); data-display/tree-view and data-display/table considered

Summary ​

Familles is a two-tab admin view (Familles.vue) built on the shared TableData + ModalFamille + SelectMembers + ModalBase stack. It works for the happy path, but the create/edit modal is not screen-reader-labelled, closes optimistically before the save resolves (so a failed edit silently discards entered data), and — most seriously — the "Membres sans famille" tab turns a failed fetch into the reassuring message « Tous les membres actifs sont rattachés à une famille », actively telling staff the work is done when the load in fact errored. The rendering choice (flat table over a tree-view for a two-level parent/child hierarchy) is reasonable and not flagged.

Findings ​

1. Failed load of "Membres sans famille" reads as a done-message — High · MSG-06 (proposed) ​

Where: admin/src/client/views/Admin/Familles.vue:61-70 (custom #empty slot) over TableData.vue:118-140What: TableData never inspects an isError state — its v-else-if branch renders whenever getRowCount() === 0, whether the collection is empty or the fetch failed (this is the project-level TableData defect). Familles makes the generic case worse by overriding #empty with « Tous les membres actifs sont rattachés à une famille. » So a network/API failure on pmo.membersWithoutFamily presents as an affirmative success statement, not an error. Why it matters: Staff whose real intent is to find and group unattached members are told there are none to group, and stop — a silent false-negative on a data-integrity workflow. The generic « Aucun élement » on the Familles tab has the same root cause but is less actively misleading. Fix: Thread an error/retry state through TableData (project-level fix) and, until then, do not phrase the empty slot as a positive assertion — keep it neutral or guard it on a known-success load state.

2. Parents/Enfants selectors have no programmatic label — Medium · FORM-01 ​

Where: admin/src/client/components/app/forms/SelectMembers.vue:3-6; same shape in SelectFamily.vue:4-8What: The visible label is a bare <label>{{ label }}</label> with no for attribute, sitting as a sibling of BccAutoComplete — nothing associates the two. The label wraps no control, so the association is impossible regardless of any id the library renders internally (assertable, not merely unverified). Why it matters: In the family create/edit modal (ModalFamille), a screen-reader user reaching the Parents and Enfants comboboxes hears an unlabelled autocomplete and cannot tell which is which. Clicking the label also does not focus the field. Fix: Give BccAutoComplete an id and point the <label for> at it (or wrap the input in the label). Verify whether @bcc-code/component-library-vue exposes an inputId/aria-labelledby prop — node_modules is not installed here, so the exact prop name is Unverified.

3. Member-chip remove button is icon-only with no accessible name — Medium · A11Y-05 ​

Where: admin/src/client/components/app/forms/SelectMembers.vue:16-24What: Each selected-member chip carries a <button type="button"> whose only content is <i class="pi pi-times">. No aria-label, no text. Why it matters: In the family modal a screen-reader user hears an unnamed button next to each parent/child and cannot tell it removes that member; the target is also a ~10px glyph. Fix: Add aria-label="Retirer {{ m.Nom }}" to the button (and ensure a ≥24px hit area, see A11Y-02).

4. Family modal closes before the save resolves, discarding edits on failure — Medium · MSG-06 (proposed) / MSG-01 ​

Where: admin/src/client/components/app/modals/ModalBase.vue:50-53 (shared; reached by ModalFamille) What: handleSave emits save and immediately sets model.value = false. The dialog closes optimistically; it has no busy state and surfaces no error inline. If props.data.update/add.mutateAsync rejects, the modal is already gone and the operator's typed Parents/Enfants selections are lost. Why it matters: On a save failure the family edit silently vanishes with nothing to retry from — the user must reconstruct the parent/child membership from scratch, assuming they even noticed it did not save. Fix: Await the mutation, keep the dialog open on rejection, show the error in-dialog (role="alert"), and only close on success. Shared ModalBase — recurs across every members create/edit modal; fix once.

5. Deleting a family confirms generically, without naming what is lost — Medium · MSG-05 ​

Where: admin/src/client/components/app/tables/TableData.vue:191-214; reachable on the Familles tab (no hide-delete-action, unlike the Membres tab at Familles.vue:47) What: The delete confirm dialog says « Êtes-vous sûr ? » / « Cette action est irréversible. » with no reference to which family or how many members it regroups. Why it matters: A staff member right-clicking the wrong row gets no chance to notice — the confirm names nothing, so it does not prevent deleting the wrong household. Fix: Interpolate the family name / member count into the confirm copy. (The right-click-only path to this action is the project-level TableData A11Y-03 defect — see below.)

6. Bulk-action buttons disable themselves mid-action, dropping focus — Low · FORM-06 ​

Where: admin/src/client/views/Admin/Familles.vue:28 (« Faire passer les adultes… »), :56 (« Créer une famille ») What: Both buttons bind :disabled="… || busy". Activating them flips busy true, disabling the just-clicked control, which moves focus to <body>. Why it matters: A keyboard user loses their place after each bulk operation and must tab back in from the top. Fix: Keep the button enabled and convey progress with aria-busy, guarding re-entry in the handler instead of via disabled.

Unverified ​

  • A11Y-01 (contrast) — the faint greys used throughout (text-neutral-400/500, text-2xs/text-3xs chip text at Familles.vue:162,174,193) need a contrast tool against BCC OKLch tokens; cannot be settled from class names.
  • A11Y-06 (short viewport / mobile keyboard) — needs a rendered page; the two-tab + sticky-header + modal layout is untested here.
  • BccAutoComplete / BccDialog / BccContextMenu internals — node_modules not installed, so keyboard operability of the autocomplete listbox, the dialog's focus trap and aria-modal, and whether the label props exist (finding 2) are Unverified.

Baseline additions ​

  • MSG-06 (proposed, already in flight batch-wide): a failed data load must render a distinct error+retry state, never an empty/idle state — and never a positive "nothing to do" message. This feature is a strong exhibit (finding 1). The orchestrator will renumber; noted as colliding per PROJECT-LEVEL.md.

Cross-project note ​

  • MSG-06 empty-state-on-error and A11Y-03 mouse-only rows/sort/actions are the shared TableData.vue defects already confirmed project-wide (PROJECT-LEVEL.md, members) and mirrored by customer-portal's DataTable cluster and tt-time-tracker's inconsistently-wired ListErrorState — findings 1 and 5 are members instances, not new.
  • CONTENT-01 — not-applicable, see project-level i18n finding (members has no i18n layer by design).
  • NAV-03 — fails project-wide (static « BCC Nancy Admin » title for all 24 routes); this route inherits it. See PROJECT-LEVEL.md.
  • FORM-01 unlabelled autocomplete (finding 2) and the optimistic modal close (finding 4) live in shared SelectMembers/ModalBase, so they recur on every members admin view that edits an entity — check customer-portal's PrimeVue AutoComplete/dialog wrappers for the same shapes.