Skip to content

[UX] members — B-Active reminders (relances) ​

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 (baseline sufficient; feature is a read-only wrapper over shared TableData)

Summary ​

bactive-relances is a ~60-line read-only view (views/BActive/Relances.vue) that renders one shared TableData of B-Active members who need a reminder. The worklist is computed client-side as pmo.members minus bactive.members, and the single most important defect follows directly from that: the loading state is gated on only one of the two sources, so the table can briefly (and, on a PMO error, permanently) claim "nobody needs a reminder" when it simply has not loaded. Note also that despite the feature name and its assigned multi-select-input / notification patterns, there is no control anywhere on the screen to actually send a relance — it is a list, not an action surface.

Findings ​

1. Loading gate ignores the PMO source — a lapsed-member worklist flashes "empty" — High · MSG-06 (proposed) ​

Where: admin/src/client/views/BActive/Relances.vue:6 (:loading="bactive.members.isFetching") vs :23 (relances derived from pmo.members.list) What: The row set is pmo.members.list.filter(... not in bactive.members.list). The :loading prop is wired to bactive.members.isFetching only. pmo.members is a separate TanStack query (pmo.store.ts:8) that fetches independently. While PMO is still loading (its list is the initialData: []), relances computes to an empty array; if bactive has already resolved, TableData leaves the skeleton and renders its empty state. Why it matters: On a reminder worklist, "empty" reads as "everyone is up to date, no one to chase" — the exact opposite of a still-loading set. Staff can act on a false all-clear. It then flips to a full list a moment later, which also erodes trust. Fix: Gate on both sources: :loading="pmo.members.isFetching || bactive.members.isFetching". Consider computing relances only once both have resolved.

2. A fetch failure also renders as "Aucun élement" (false all-clear) — High · project-level, see PROJECT-LEVEL.md (TableData never reads isError) ​

Where: admin/src/client/components/app/tables/TableData.vue:129-140What: Same mechanism as finding 1 but on the error path — TableData has no isError branch, so if either /api/membres or /api/adhesions-bactive fails, the view shows the same "Aucun élement" empty state. This is the project-level TableData defect; not re-filed here. Recorded only because the consequence on this feature is materially worse than on a normal list: a silent fetch failure tells staff that no member is overdue. Fixing the shared component (finding in PROJECT-LEVEL.md) resolves it here too.

3. No way to send a reminder — the feature lists who to chase but provides no action — Medium · ACTION-SURFACE-01 (proposed) ​

Where: admin/src/client/views/BActive/Relances.vue:1-11 (entire template) What: The view passes no selectable, renders no #actions slot, no send/notify button, and no per-row action. Two of the three patterns this feature was scoped against (forms/multi-select-input, user-feedback/notification) have no implementation. There is no in-app path from "here are the members to remind" to "remind them". Why it matters: Staff must copy names out and act out of band (email/spreadsheet), which is error-prone and defeats the point of a dedicated "relances" screen. Whether this is a deliberate worklist-only scope or an unfinished feature cannot be settled from code — flagging so the product decision is made explicitly. Fix: If sending is in scope, add selectable + an #actions "Envoyer une relance" button that batches the selected rows and surfaces a success/failure notification. If not, the feature and its inventory patterns should be re-scoped to "worklist only". Proposed rule ACTION-SURFACE-01: a view named for an action (send, approve, assign, remind) must expose that action in-app, not merely list its candidates.

4. Sort headers and the filter drawer are mouse-only — Medium · A11Y-03 — project-level, see PROJECT-LEVEL.md ​

Where: admin/src/client/components/app/tables/TableData.vue:91-114 (sortable <th @click> with no tabindex/role/aria-sort); FilterDrawer focus handling also broken per PROJECT-LEVEL.md What: All six columns get the sort toggle via a click-only <th>; there is no keyboard operability and no aria-sort announcing sort state. This is the project-level TableData A11Y-03 defect; not re-filed — one-line reference. It applies to this feature because Age / Date de naissance / Genre are the columns a staffer would most want to sort a chase-list by.

5. Search field has no visible label; accessible name unverifiable — Low · FORM-01 ​

Where: admin/src/client/components/app/tables/TableData.vue:44-51What: The search BccInput has placeholder="Rechercher…" and no label/aria-label prop. Placeholder-only labelling fails FORM-01 for sighted users once text is typed. Whether @bcc-code/component-library-vue's BccInput emits an accessible name from the icon/placeholder cannot be read (node_modules not installed) — see Unverified. Shared TableData, so a fix is repo-wide. Fix: Pass an explicit aria-label="Rechercher" (or visible label) on the shared input.

6. Unknown/undeclared gender renders an empty tag — Low · A11Y-05 ​

Where: admin/src/client/components/app/tables/adhesions/BadgeSexe.vue:22-26What: For any member whose Genre is null/other, label is "" and context is "neutral", so the Sexe cell shows an empty grey chip that conveys nothing and has no accessible text. Why it matters: Visual noise and an unlabeled control-shaped element in a column staff scan quickly. Fix: render nothing (v-if) or a real "—"/"Non renseigné" label.

7. Empty-state copy is misspelled and ambiguous on a worklist — Low · CONTENT-04 ​

Where: admin/src/client/components/app/tables/TableData.vue:136 (Aucun élement) What: "élement" is missing its accent (should be "élément"). Shared string, so it is wrong on every list. On this feature specifically, "Aucun élément" cannot distinguish "loading", "everyone reminded" and "fetch failed" (findings 1–2). Fix: correct the spelling in TableData; give reminder-style views a purpose-named empty state ("Aucun membre à relancer").

Unverified ​

  • A11Y-01 contrast — the uppercase text-xs text-neutral-500 column headers and the text-neutral-500 count next to the <h1> (TableData.vue:13,94) look light on white; needs a contrast tool. Not asserted.
  • A11Y-06 responsive / short viewport — fixed-layout <table> with table-layout: fixed and per-column pixel widths (TableData.vue:69-72); horizontal-scroll and short-viewport behaviour need a rendered page.
  • FORM-01 search accessible name — depends on BccInput internals in @bcc-code/component-library-vue, unreadable (node_modules absent, per PROJECT-LEVEL.md caveat).

Baseline additions ​

  • ACTION-SURFACE-01 (finding 3) — a view named for an action must expose that action in-app, not merely enumerate its candidates. Orchestrator to renumber.
  • Findings 1–2 both cite the MSG-06 family (a load that ignores one of several sources, or the error branch, renders as an authoritative empty state). Fold into the reconciled MSG-06 rather than a new ID.

Cross-project note ​

  • Finding 1 (multi-source loading gate) — the "worklist derived from set-difference of two independent queries, loading gated on one" shape is members-specific here, but the broader "failed/partial load shown as empty state" theme (MSG-06) is confirmed in customer-portal and tt-time-tracker; see the cross-project table in PROJECT-LEVEL.md.
  • Finding 3 (list with no action) — worth checking the sibling bcc-relances (queue #16), which shares patterns and almost certainly shares the same read-only shape.
  • CONTENT-01 — not-applicable; see project-level i18n finding (members has no i18n layer by design).