Skip to content

[UX] members — BCC reminders (relances) ​

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

Summary ​

views/BCC/Relances.vue is a thin, read-only TableData wrapper: it lists PMO members who have no BCC membership for the selected year (Relances.vue:21) so staff know who to chase. Despite the queue's forms/multi-select-input and user-feedback/notification patterns, the screen has no selection, no bulk action, no "send reminder" control and no toast — it is a diff list, not a relance-sending workflow, so both those patterns are effectively unimplemented here (observation, not a rule breach). The one feature-specific defect is real and matters: the loading gate watches the wrong store, so a slow or failed PMO load renders as "nobody needs a reminder." The rest are shared-TableData issues already tracked at project level.

Findings ​

1. Loading gate watches only the BCC store, so a pending/failed PMO load shows as an empty reminder list — High · MSG-06 (proposed) ​

Where: admin/src/client/views/BCC/Relances.vue:6 (:loading="bcc.members.isFetching"), rows derived at :21 from pmo.members.listWhat: The table body is built from pmo.members.list filtered against bcc.members.list, but the skeleton is gated solely on bcc.members.isFetching. Nothing consults pmo.members' fetch state or either store's error state (pmo.store.ts:8, bcc.store.ts:9). If the BCC request resolves first while the PMO members request is still in flight — or fails — loading is already false, so TableData leaves its skeleton and, with an empty derived list, falls through to the "Aucun élement" branch (TableData.vue:129-139, which never reads an error state — the project-wide TableData isError gap, see PROJECT-LEVEL.md). Why it matters: This is a chase-list whose entire job is to surface members who have not renewed. Presenting "no one to remind" when the data is merely loading or failed is a silent false negative: staff conclude everyone has renewed and skip real follow-ups. On a reminders worklist a false-empty is more harmful than a visible error, hence High rather than Medium. Fix: Gate on both sources and a real error branch, e.g. :loading="bcc.members.isFetching || pmo.members.isFetching", and surface pmo.members.isError || bcc.members.isError as a retryable error state rather than letting it collapse into the empty-state (fixing TableData's missing isError handling covers the second half project-wide).

2. Search field has no programmatic label — Medium · FORM-01 ​

Where: admin/src/client/components/app/tables/TableData.vue:44-51 (rendered here because Relances.vue:9 sets searchable) What: The search BccInput is passed only placeholder="Rechercher…" and a decorative icon — no label, no aria-label. Placeholder-only is exactly the FORM-01 failure. (Whether @bcc-code's BccInput fabricates a name from the placeholder is Unverified — node_modules is not installed — but the call site supplies none, and placeholder-as-name is itself the anti-pattern.) Why it matters: A screen-reader user tabbing to the field hears nothing stable ("search, edit text" at best); once text is entered the placeholder vanishes, so no accessible name remains. Shared component, so this affects every searchable admin table, not just relances. Fix: Add a visually-hidden <label> or aria-label="Rechercher" on the search input in TableData.

3. Sortable column headers are mouse-only and expose no sort state to assistive tech — Medium · A11Y-03 ​

Where: admin/src/client/components/app/tables/TableData.vue:91-114What: Each <th> toggles sorting via @click on a non-interactive element — no tabindex, role, aria-sort or key handler — and the sort direction is conveyed only by a pi-sort-up/pi-sort-down icon with no text alternative. This is the shared-TableData mouse-only-headers issue already noted project-wide (see PROJECT-LEVEL.md); flagged here only to add the specific aria-sort gap the table pattern's Accessibility section calls out. Why it matters: Keyboard and screen-reader users cannot sort the reminder list and cannot tell which column/direction is active. Fix: Make headers real <button>s (or th[tabindex=0] with Enter/Space) and set aria-sort="ascending|descending|none" on the header cell.

4. Empty state is a mis-spelled, uninformative "Aucun élement" — Low · CONTENT-04 ​

Where: admin/src/client/components/app/tables/TableData.vue:136What: The fallback empty row reads « Aucun élement » — missing accent (should be « élément ») and generic. For a relances list, empty legitimately means "everyone has renewed" (good news), but the copy neither says so nor distinguishes that from a filtered-to-nothing result. The table pattern lists "Missing Empty States" as an anti-pattern. Why it matters: Minor polish, but on this screen an unexplained blank reads as ambiguous rather than reassuring; the typo undercuts the "calm, trustworthy" tone CLAUDE.md asks for. Fix: Correct to « élément » and let callers pass a contextual empty message (the #empty slot already exists) — e.g. « Tous les membres sont à jour ».

Unverified ​

  • A11Y-01 (contrast): the text-neutral-500 header/count text and text-neutral-500 empty-state text on white need a contrast tool; not assertable from class names.
  • A11Y-06 (short viewport / mobile keyboard): the sticky header row and table-layout: fixed table need a rendered viewport to judge.
  • FORM-01 residual: whether BccInput synthesises an accessible name from placeholder cannot be confirmed — @bcc-code/component-library-vue internals are not installed.

Baseline additions ​

  • MSG-06 (already proposed batch-wide): a loading gate must reflect every data source a view derives its rows from, and a failed fetch must render a distinct, retryable error state — never collapse into the empty state. This feature is the clean minimal case (gate watches one of two stores).
  • No other new rule; findings 2–4 are covered by FORM-01 / A11Y-03 / CONTENT-04.

Cross-project note ​

Finding 1's shape (aggregate/derived list gated on one store while a second source can be pending or errored) is the same MSG-06 family already confirmed in customer-portal, tt-time-tracker and playout. The B-Active reminders screen (views/BActive/Relances.vue, queue #17) is near-certain to share findings 1–4 verbatim — same TableData, same two-store diff shape. Findings 2–4 live in the shared TableData.vue and therefore recur across members queue rows #9–#29.

CONTENT-01: not-applicable — see project-level i18n finding. NAV-03: fails project-wide (static « BCC Nancy Admin » title) — see PROJECT-LEVEL.md.