Skip to content

[UX] members — B-Active dashboard ​

Draft from /ux-audit on 2026-07-30 (unattended batch run). Not filed. Repo: bcc-nancy/members · Branch: develop @ 2c22f5a · Files reviewed: 8 Patterns: data-display/chart (consulted); data-display/statistics, data-display/dashboard (baseline-covered, not fetched)

Summary ​

A read-only staff dashboard of stat tiles, progress rings and bar charts for one program-year. It is mostly clean — no forms, no destructive actions, currency is formatted correctly (admin useCurrency.ts uses maximumFractionDigits: 2) and the Directus-access path computes its aggregates from full, non-paginated collections (useApiData fetches T[] with no limit), so it is not affected by the cross-project "aggregate from one page" theme. The two real problems are that the whole view has no loading or error surface — a failed fetch renders as authoritative zeros — and that the figures shown to dashboard-role users are pinned to the calendar year while the header labels them with a different, user-chosen year.

Findings ​

1. Failed or in-flight loads render as authoritative zeros — High · MSG-06 (proposed) ​

Where: admin/src/client/views/BActive/Dashboard.vue:94 (and the store reads at :104,109,127,140…); TileClubs.vue:8-12What: The view destructures only data from useDashboardTotals (const { data: apiTotals } = …) and never reads its isError/isFetching. Every tile falls back to apiTotals.value?.… ?? 0 (and the Directus path reads bactive.*.list, which useApiData seeds with initialData: []). There is no skeleton, spinner or error block anywhere on the page. So while the request is in flight — and permanently if it 403s or fails — the dashboard shows « 0 membres », « 0 € » dons/parrainages/cotisations, and empty progress rings, all styled identically to real data. TileClubs compounds this: it shows the empty state « Aucune donnée disponible » (:8-12) whenever clubStats is empty, which includes the loading and error cases, so a failed member fetch reads as "this program has no members". TileDonsProgress has its own loading/error branch, but it is dead here because the parent passes props, disabling its internal fetch. Why it matters: Staff make dues/donation decisions off these totals. A transient or permission failure silently misinforms — zero dons looks like "no one donated", not "the data did not load" — and there is no retry or even an indication anything is wrong. The chart pattern names this exact anti-pattern ("Ignoring non-happy states… design empty, partial, stale, and failed results up front"). Fix: Gate the grid on the query/stores' isFetching (show skeleton tiles) and isError (show an error card with a retry that calls refetch); distinguish a genuine empty result from a not-yet-loaded/failed one in TileClubs. This is the members-wide MSG-06 shape (see PROJECT-LEVEL.md) recurring on this feature.

2. Year label can disagree with the figures shown — Medium · DATA-LABEL-SCOPE (proposed) ​

Where: admin/src/client/views/BActive/Dashboard.vue:93 vs :10; TileDonsProgress.vue:51What: The subheading renders « Année {{ filters.year }} » — the global, localStorage-persisted year set by the sidebar picker (SidebarContent.vue:45, v-model="filters.year"). But useDashboardTotals(year) is called with year = new Date().getFullYear(), captured once and non-reactively. For dashboard-role users (who read totals from the API, not from the year-scoped Directus stores), the tiles therefore always show the current calendar year while the header may say a different year, and changing the picker updates the label but never refetches the figures. TileDonsProgress independently pins to new Date().getFullYear() too. Why it matters: The page asserts a year for financial figures that can belong to a different year — a truthfulness defect, not just staleness. A user reviewing 2025 sees a header claiming 2025 over 2026 numbers, with no cue they diverge. (Directus-access staff are consistent because their stores fetch on filters.year; the split makes it easy to miss in testing.) Fix: Drive useDashboardTotals and TileDonsProgress from a reactive filters.year (pass a getter / ref so the query key updates), or label the tiles with the calendar year the data actually covers. One source of truth for "the year" on this view.

3. Heading levels skip from h1 to h3 — Low · A11Y-04 ​

Where: admin/src/client/views/BActive/Dashboard.vue:6 (<h1>), then :30 and :50 (<h3> « Dons et parrainages » / « Camps et objectifs ») What: The section headings jump h1 → h3 with no h2. The tile card titles are <p>, not headings, so the outline is just h1 then two h3s. Why it matters: Screen-reader users navigating by heading level hit a skipped level (WCAG 1.3.1). Minor here because the page is short. Fix: Make the section headings <h2> (and optionally promote tile titles to <h3> for a real outline).

4. Charts carry no text alternative or programmatic label — Low · A11Y-05 (adjacent, proposed A11Y-08) ​

Where: TileProgress.vue:9-38 (SVG donut), TileCampFinancing.vue:8-21 (segmented bar), TileClubs.vue:22-27 (per-club bars) What: The SVG ring and the div-based bars have no role="img"/aria-label or <title>; they are decorative to assistive tech. This is not a hard failure because each chart sits beside visible text conveying the same numbers (the ring shows « {{percentage}}% » and « X sur Y reçus »; the camp bar has a text legend with amounts; club bars show name + count), so a screen-reader user still gets the data — but the visual encoding itself is unlabelled, and the camp bar's three segments are distinguished by colour, mitigated only by the adjacent text legend (chart pattern: "do not rely on colour alone"). Why it matters: The graphic adds nothing for non-visual users and would fail if the adjacent text were ever removed. Baseline has no non-text-content rule, so proposing one. Fix: Add role="img" + a summarising aria-label (e.g. « Cotisations : 8 200 € reçus sur 10 000 €, 82% ») to each chart container, or mark them aria-hidden and rely explicitly on the text.

Unverified ​

  • A11Y-01 (contrast): faint text — text-neutral-400 « Chargement… », text-neutral-500 subheading, text-brand-700 on white, and the orange-500/amber-300 chart fills — needs a contrast tool. Not asserted.
  • A11Y-06 (responsive / short viewport): the grid-cols-1 md:grid-cols-2 lg:grid-cols-3 layout, the row-span-2 clubs tile and the fixed 24×24 (w-24 h-24) ring need a rendered viewport to judge. Not asserted.
  • @bcc-code/component-library-vue internals: none are used on this view (tiles are hand-rolled), so nothing is deferred to the library here; node_modules is absent per PROJECT-LEVEL.md regardless.

Baseline additions ​

  • MSG-06 (proposed, members-wide) — a load must expose its error/pending states; a failed or in-flight fetch must never render as an empty state or as zeroed data. Cited in finding 1; reconcile with the existing MSG-06 proposals in PROJECT-LEVEL.md.
  • DATA-LABEL-SCOPE (proposed) — a displayed figure's scope label (year, filter, account) must match the data actually shown, and must react when the user changes that scope. Cited in finding 2; relative of the DATA-TRUTHFUL-AGGREGATE family.
  • A11Y-08 (proposed) — non-text data visualisations expose a text alternative (role="img" + label, or an equivalent adjacent summary). Cited in finding 4.

Not applicable / checked:

  • CONTENT-01 — not-applicable, see project-level i18n finding (members has no i18n layer).
  • NAV-03 — fails project-wide (static « BCC Nancy Admin » title for all routes), see PROJECT-LEVEL.md; not re-filed.
  • A11Y-02 / A11Y-03 — no interactive controls exist on this view (no buttons, links, inputs), so keyboard/target-size rules have nothing to bite on here.
  • DATA-01 (undeclared suborg prop) — not applicable: this is a routed admin view, not a host-mounted widget, so there are no mount attributes to drop.
  • Currency precision — passes: admin useCurrency.ts uses maximumFractionDigits: 2; no toFixed/Math.round/parseInt on money on this surface.

Cross-project note ​

Finding 1 (loading/error branch ignored → empty/zeroed state shown as real) is the members instance of the batch-wide MSG-06 theme already confirmed in customer-portal and tt-time-tracker (whose own Dashboard swallows entry-list errors into « Aucune entrée »); worth checking members #19 Home and #20 Objectives, which share the dashboard/statistics patterns. Finding 2's year/label divergence is a specific case of the "aggregate/label must state what it covers" theme flagged for customer-portal and tt-time-tracker dashboards.