Skip to content

[UX] members — Objectives & group comparison ​

Draft from /ux-audit on 2026-07-30 (unattended batch run). Not filed. Repo: bcc-nancy/members · Branch: develop @ 2c22f5a · Files reviewed: 7 Patterns: data-display/comparison-table (consulted)

Summary ​

The objectifs list is a thin TableData wrapper and is mostly clean (its shared-table defects are the project-level TableData.vue cluster, not re-filed here). The custom bactive-groupes comparison screen is where the real defects sit: it deletes shared group definitions with no confirmation, silently swallows score-computation failures, ships label-less inputs and icon-only buttons with no accessible names, and carries a dead colour-coding branch that never runs. None are blockers, but the delete-without-confirm and the swallowed-error path both have real consequences for a treasurer acting on these numbers.

Findings ​

1. Deleting a group is instant and irreversible, with no confirmation — High · MSG-05 ​

Where: admin/src/client/views/BActive/ComparaisonGroupes.vue:112-121 (trash button) → :339 (deleteGroupe) What: The trash icon calls groupes.remove.mutateAsync({ id }) directly. There is no confirm step and no undo. These Groupes_Comparaison records are Directus-backed and shared across all admins (:275 comment), and each carries a Conditions expression that can be non-trivial to rebuild. Why it matters: A single misclick permanently destroys a group definition every other admin relies on, including its filter conditions. The button sits directly beside the (also destructive-adjacent) edit pencil, at a 24px-ish target, making the misclick plausible. Fix: Gate deletion behind a confirm dialog that names the group ("Supprimer le groupe « X » ?") and states it is shared and permanent, per MSG-05. The ModalTransferSurplus confirm/processing/error pattern already in this feature is a reusable model.

2. Score-computation failure is swallowed to the console — Medium · MSG-01 (+ project MSG-06) ​

Where: admin/src/client/views/BActive/ComparaisonGroupes.vue:395-399What: computeScores's catch does console.error(...) only; isComputing resets to false and groupeScores is left holding its previous value. So a failed /api/groupes-comparaison/:id/objectifs-stats call renders either stale scores from a prior tab/year (which look authoritative) or the "Sélectionnez au moins un groupe" empty state — never an error. No role="alert" / aria-live surface exists anywhere in the right pane. Why it matters: The percentages drive treasurer decisions about which group is behind on camp payments; a silently stale or blank comparison is acted on as if current. This is the members instance of the cross-project MSG-06 theme (see PROJECT-LEVEL.md). Fix: Track an error ref, render a visible error state with a retry in the results pane, and mark it role="alert". Do not leave the previous groupeScores on screen after a failed recompute.

3. Row edit/delete and popover triggers are icon-only with no accessible name — Medium · A11Y-05 ​

Where: admin/src/client/views/BActive/ComparaisonGroupes.vue:102-111 (pencil) and :112-121 (trash); admin/src/client/components/app/tables/objectifs/CellObjectifProgress.vue:8-17 (info/warning popover trigger) What: Each <button> contains only an icon (<component :is="piIcon(...)"> or a bare <i class="pi ...">) with no aria-label, title, or visually-hidden text. A screen-reader user hears "button" with no indication of edit vs delete vs "show transactions". Why it matters: The primary management actions of the comparison screen, and the per-row detail affordance in the objectives table, are unusable non-visually. @bcc-code BccButton uses in this feature pass because they set label; only these hand-rolled <button>s miss it. Fix: Add aria-label="Modifier le groupe" / "Supprimer le groupe" / "Voir les transactions" (interpolate the group/member name where available).

4. Group-name input has no label — placeholder is the only cue — Medium · FORM-01 ​

Where: admin/src/client/views/BActive/ComparaisonGroupes.vue:61-66 (edit) and :129-134 (new) What: A bare <input v-model="editDraft.Nom" placeholder="Nom du groupe"> with no associated <label>. The placeholder disappears the moment the user types, and there is no programmatic label for assistive tech. @keydown.enter.prevent also swallows Enter, so the expected "type name, press Enter to save" does nothing. Why it matters: FORM-01 — placeholder-as-label. On edit of an existing group the field is pre-filled, so a screen-reader user tabbing in hears the value with no field name; on the new-group draft the label vanishes on first keystroke. The FormConditionsBuilder below it takes a label prop, so the name field is the only unlabeled control. Fix: Add a <label> (or the Field UI primitive) for the name input; let Enter trigger save instead of preventing it.

5. Score colour-coding is dead code — the intended low/high signal never renders — Low · A11Y-07(e) (selection/severity not colour-only) ​

Where: admin/src/client/views/BActive/ComparaisonGroupes.vue:410-415What: scoreColor unconditionally return "text-brand-800"; on its first line, so the pct >= 80 / >= 50 / else branches are unreachable. Every non-leader score renders the same brand colour regardless of value; the intended "a struggling group looks different" cue is silently absent. Why it matters: The screen is a comparison whose whole job is surfacing which groups lag, yet the only per-score differentiation (colour) is disabled. The leader card is distinguished by size + a rank number as well, so this is not colour-alone-fatal, but the design intent is lost. The comparison-table pattern explicitly warns against relying on colour alone for severity/completion — here even that colour is missing. Fix: Remove the leading return, or delete the dead branches and decide whether colour coding is wanted; if kept, pair it with a non-colour cue.

6. One pending delete disables every row's delete button — Low · FORM-06 ​

Where: admin/src/client/views/BActive/ComparaisonGroupes.vue:114 (:disabled="groupes.remove.isPending.value"); similarly :104/:106 disable all edit buttons while editingId !== nullWhat: The disabled flag is not scoped to the row being mutated, so activating one delete disables the trash button on every row. Disabling the just-clicked control drops keyboard focus to <body> (FORM-06). Why it matters: Minor with typically few groups, but a keyboard user loses their place after each delete, and unrelated rows briefly become non-interactive. Fix: Scope the pending/disabled state to the affected row id; prefer aria-busy over disabled on the activated control.

Unverified ​

  • A11Y-01 (contrast): not assessed from code. Candidates to check on a rendered page: text-neutral-400 empty-state and select-all copy (:45-47, :158-174), text-white/40 rank and text-white/60 count on the bg-brand-800 leader card (:220-241), and text-brand-500/600 on bg-brand-400 runner-up cards.
  • A11Y-02 (target size): the p-1 pencil/trash buttons (:102-121) and the text-xs "Tout sélectionner / désélectionner" text buttons (:162-174) look under 24×24 / closely packed but need measurement on a rendered viewport.
  • A11Y-06 (short viewport / responsive): the lg:grid-cols-[360px_1fr] two-pane layout (:28) and the score cards need a rendered check; not assessable from code.
  • Progress semantics: BarProgress wraps @bcc-code BccProgressBar; whether it emits role="progressbar"/aria-valuenow is in node_modules, which is not installed — the visible value / total and % text (BarProgress.vue:4-6) do give SR users the numbers regardless.

Baseline additions ​

  • A11Y-07(e) (referenced above) — selection/severity/completion state must not be conveyed by colour alone; already proposed in PROJECT-LEVEL.md's A11Y-07 collision list. No new ID proposed here.

Not-applicable ​

  • CONTENT-01 — not-applicable; see project-level i18n finding (members has no i18n layer by design).

Cross-project references (not re-filed) ​

  • objectifs renders through TableData.vue — inherits the project-level shared-table cluster (isError never read so a failed load shows « Aucun élement »; mouse-only sort/edit/row-actions). See PROJECT-LEVEL.md.
  • NAV-03 — fails project-wide (static « BCC Nancy Admin » title); see PROJECT-LEVEL.md.

Cross-project note ​

Finding 2 (non-throwing/failed load swallowed, shown as stale or empty rather than an error) is the MSG-06 shape confirmed in all four projects — playout, customer-portal, tt-time-tracker and members. Findings 3 and 4 (icon-only controls, placeholder-as-label) are recurring A11Y-05/FORM-01 shapes worth grepping for in the other three admin surfaces, especially any hand-rolled <button><i></button> and <input placeholder=…> outside the component library. Finding 1 (destructive action with no confirm) should be checked against the other projects' definition/config CRUD screens.