[T02] Number Stock View
Summary
Current stock view of all RaceNumbers. Filter by NumberType / subtype / state; KPI tiles per state; drill-down to per-number audit log. Bulk-select hands off to T04 for flag-unfit / dispose, or to T03 for return.
The hub of the D2b tenant inventory area: T03 stock-return is launched from here (via the global "Return" sidebar entry or by selecting rows and choosing Return), and T04 bulk flag/dispose is launched from selections here.
T02 v1 ships without tenant scoping on RaceNumber reads. Tenant isolation for RaceNumber / NumberType is a separate cross-cutting workstream — see Out of Scope below and orchestration journal CD-7.
|
Actor & Context
Actor: tenant admin, stock operator.
Frequency: weekly+ during event seasons; less often otherwise.
Precondition: user has TENANT_ADMIN permission; sidebar tenant scope active (see C01 § sidebar — added in Thread A). Production today is single-tenant (WPCA only); see Out of Scope for the tenant-isolation limitation in v1.
Entry point: Tenant-admin sidebar Inventory ▸ Stock (ADO-541-sidebar-tenant-scope).
Layout
T02 renders inside C01’s shell — sidebar (tenant scope) + topbar are C01’s. T02 owns the main content area.
| Region | Content |
|---|---|
Topbar |
Breadcrumb |
Filter strip |
NumberType (single-select) + subtype (single-select, scoped to selected NumberType) + state (multi-select with chip removal) + free-text search |
KPI tile strip |
One tile per state: |
Table |
Columns: number (bold) · type · subtype · state pill · person (link if |
Bulk-select toolbar |
Checkbox column on the left. When ≥1 row is selected, the toolbar materialises above the table with the selected count and three actions: Return → opens T03 with the selected ids pre-loaded; Flag UNFIT → opens T04 confirm dialog; Dispose → opens T04 confirm dialog. Flag UNFIT and Dispose obey the row-state gating below (see Row-state action gating — ADR-0003). |
Drill-down side panel |
Row click opens a right-rail panel showing the per-number audit log (transition reason, actor, timestamp, optional note, |
Main Flow
-
Render shell (C01) with sidebar in
tenantscope, breadcrumbTenant ▸ Inventory ▸ Stock. -
Resolve filter state from query string (deep-linkable filters); empty state defaults to no filters and the full stock view.
-
Call
GET /api/race-numbers/stats?groupBy=state(CD-4 — flat enum-keyed map) and render the KPI tile strip. -
Call
GET /api/race-numbers/stock?…&page=0&size=50&sort=…(CD-6 —RaceNumberResourceEx#stockView, criteria-object) and render the table. -
Operator narrows by filter strip / KPI tile / search; URL query string updates so the view is shareable / bookmarkable.
-
Operator clicks a row → drill-down side panel loads
GET /api/race-numbers/{id}/state-log(CD-5 —List<RaceNumberStateLogDTO>orderedcreatedDate DESC). -
Operator selects rows; bulk-select toolbar appears; chosen action navigates to T03 (Return) or opens the T04 confirm dialog (Flag UNFIT / Dispose) inline.
-
After T03 / T04 returns control, the table refresh is automatic (re-fetches the current page + KPI strip).
Row-state action gating (ADR-0003 enforcement at row level)
Per ADR-0003, UNFIT_FOR_SERVICE is an admin-owned fitness verdict that nothing outside this screen / T04 may flip. The race-number lifecycle gives UNFIT_FOR_SERVICE exactly two valid forward transitions: → DESTROYED (via dispose) and → UNFIT_FOR_SERVICE (return — clears person_id, state preserved with "recommend disposal" note).
The bulk-select toolbar therefore gates per-row actions by the selected rows' states:
| Selected row state | Return | Flag UNFIT | Dispose | Notes |
|---|---|---|---|---|
|
no-op (idempotent) |
✅ enabled |
❌ disabled |
Already in stock; Return produces NO_OP rows in T03 summary. Dispose is rejected — only UNFIT may dispose. |
|
✅ enabled |
✅ enabled |
❌ disabled |
Standard return path. Flag UNFIT keeps |
|
✅ enabled |
✅ enabled |
❌ disabled |
Same as ISSUED — |
|
✅ enabled (preserves UNFIT, clears |
❌ disabled — already UNFIT |
✅ enabled |
The |
|
❌ disabled — terminal |
❌ disabled — terminal |
❌ disabled — terminal |
Read-only. Row renders quieter (greyed pill); checkbox is uncheckable. |
Mixed selections (e.g. UNFIT + ISSUED rows) constrain to the intersection — Dispose is enabled only if every selected row is UNFIT; Flag UNFIT is enabled only if no selected row is already UNFIT or DESTROYED. Disabled buttons render with tooltips per the project rule on disabled-affordance disclosure (see project memory feedback_explain_disabled_ui.md):
-
Dispose disabled tooltip — "Only UNFIT numbers can be disposed. Filter to UNFIT rows or remove non-UNFIT items from the selection."
-
Flag UNFIT disabled tooltip when an UNFIT row is in the selection — "Some selected numbers are already UNFIT. Remove them from the selection to flag the rest."
-
Return disabled tooltip on a DESTROYED-only selection — "DESTROYED numbers are terminal — they cannot be returned to stock."
Disabled-with-tooltip is preferred over hidden — operators must see the policy.
Alternative Flows
-
AF-1 — UNFIT row selected, operator hovers
Dispose: button enabled with helper text "These N UNFIT numbers will be marked DESTROYED. This is irreversible." (Confirm dialog handled by T04.) -
AF-2 — DESTROYED rows in the table: rendered greyed, checkbox uncheckable, row click does NOT open the drill-down panel (read-only). Mouse hover shows tooltip "Terminal state — record retained for audit only."
-
AF-3 — Empty stock for selected filters: empty-state card with "No numbers match these filters" + a
Clear filtersaction that resets the strip and re-fetches. -
AF-4 — Drill-down endpoint returns 5xx: side panel renders a one-line "Couldn’t load audit log — try again" with a retry link; no error toast (the rest of T02 stays usable).
-
AF-5 — Sidebar
tenantscope not active (user is not admin): T02 route is hidden by C01’s role gate; a deep-link to/inventory/numberswhile non-admin redirects to E05 with an info toast "Tenant inventory requires admin permission." -
AF-6 — Bulk-action returns partial success (some rows skipped/rejected): T04’s per-id outcome summary handles the visualisation; T02 just refreshes the table when the operator dismisses the summary.
-
AF-7 — KPI tile click while a multi-state selection is active in the filter strip: the click replaces the multi-state selection with the single tile state. (Pre-emptive call-out: the alternative — toggling — is rejected because the tile is meant as a quick-narrow shortcut, not a filter modifier.)
Acceptance Criteria
-
T02 renders inside C01’s shell with the
tenantscope sidebar active. -
Filter strip exposes NumberType / subtype / state / free-text
q; reset clears all four; URL query string updates as filters change so the view is shareable. -
KPI tile strip shows counts for the five operational states (
MANUFACTUREDomitted); tile click narrows the table to that single state. -
Table renders the eight columns; default sort is type asc + sequence asc; pagination is server-driven at page size 50.
-
Row-state action gating per the table above; disabled actions render with tooltips per
feedback_explain_disabled_ui.md. -
DESTROYED rows are visually quieted, uncheckable, and the drill-down does not open on click.
-
Bulk-select toolbar materialises only when ≥1 row is selected; selection survives pagination up to a soft cap of 200 rows; over the cap, surface an inline "select-all-matching not yet supported" notice.
-
After T03 / T04 returns, the table + KPI strip auto-refresh; URL filters preserved.
-
Drill-down side panel renders the per-number audit log from
GET /api/race-numbers/{id}/state-log(entries newest first); an error from that endpoint surfaces as an inline retry-link in the panel only, never a global toast.
API Surface
| Call | Purpose |
|---|---|
|
Filtered + paginated stock list. Existing |
|
KPI tile counts. CD-4: returns a flat enum-keyed map keyed by |
|
Audit log for the drill-down side panel. CD-5: returns |
|
Used indirectly via T03 / T04. T02 itself only forwards the selected ids; the request bodies + response handling live on the bulk screens. |
Out of Scope
-
Stock-take return workflow — T03.
-
Bulk flag/dispose actions — T04.
-
Reports — T05.
-
Pre-assignment for events — E07.
-
Manufactured-numbers onboarding (C02 / T06 / T07) — Track 3 (Thread E).
-
Auto-assignment UX (Feature #403 dependent) — explicitly out of scope this round per the plan.
-
Customer notification when a number is flagged UNFIT — out of scope this round.
-
Tenant-scoped reads on
RaceNumber/NumberType— T02 v1 ships unscoped. The schema gap (noorganisation_idonNumberTypeorRaceNumber) is a multi-PR, cross-repo workstream carved out as a separate ADO Bug under Epic #35 per orchestration journal CD-7. Acceptable for v1 because (a) production today is single-tenant (WPCA only); (b) Thread A’s sidebar entries are admin-role gated, so the only multi-tenant exposure path is via OrgPermission, not raw API; (c) the carved-out workstream is the proper fix and is filed. Implementation US should add// TODO: tenant-scope per ADO-<bug-id>comments at the controller / query-service level.
Design Anchors
-
C01 — sidebar (
tenantscope added by Thread A) -
E05 — sibling list-style portfolio screen; visual reference for the KPI strip + table density
-
ADR-0001 — single state-log writer
-
ADR-0003 — UNFIT verdict immutability (the row-level gating above is its UI manifestation)
-
Race Number Lifecycle (DRAFT)
Design Decisions
-
KPI tile strip omits
MANUFACTURED(2026-05-07).MANUFACTUREDis onboarding-transient; including it would inflate the strip with a state operators don’t act on at T02. T02 is a stock-operations view, not an onboarding view (C02 / T06 / T07 cover onboarding). If aMANUFACTUREDcount is ever needed it surfaces in T05 reports. -
Filter URL state (2026-05-07). Filter selections, the active KPI tile, and the current page are all reflected in the URL query string so the view is shareable / bookmarkable. Same pattern as
tanstack-event-participant-listadopts (see plan’s "second adopter of the Tanstack filter substrate" framing). Implementation must NOT modifyapp/shared/filter/to support this — if a substrate gap is found, fix inprototypes/filterable-list/first per the filter journal’s mirror rule. -
Row-state action gating mirrors ADR-0003 + state machine (2026-05-07). The gating table above is the UI projection of the four valid transitions documented in the lifecycle journal’s Updated Transitions block (sessions 2/3). Disabled actions render with tooltips explaining the policy — this is required by project memory
feedback_explain_disabled_ui.md(lesson from the WPCA stock-take incident class). -
Drill-down panel ships in v1 (2026-05-07). All four T02 components — filter strip, KPI strip, table, bulk-select toolbar, drill-down panel — run against endpoints that exist (or are scoped to ship in this wave per CD-5). The earlier "drill-down is gated" caveat is closed.
-
Bulk-select selection model (2026-05-07). Selection is held in component state and persists across pagination up to a soft cap (200 rows). Beyond that, the toolbar surfaces a "More than 200 rows selected — select-all-matching is not yet supported" inline notice; operator must narrow the filter. This avoids the unbounded-selection-overhead trap and matches the EP list’s behaviour. Reviewable cap value during implementation.
-
Tenant scoping deferred to a separate workstream (2026-05-07, CD-7). The schema gap is real but multi-PR; T02 v1 ships unscoped with a documented limitation. Implementation US must add a
// TODO: tenant-scope per ADO-<bug-id>comment at the read sites so the deferral is visible in code review.
Open Questions
-
q(free-text search) — Thread B’s criteria sweep determines whetherqis in pattern onRaceNumberCriteria. If not, T02 drops the search box from v1; filter strip + KPI tile narrowing remains sufficient. (Sole remaining open question after CD-4/-5/-6/-7.)
Notes
Backend WS1a + WS1b shipped; T02 surfaces existing endpoints. Hub for the rest of D2b — T03 / T04 launch from selections here.
|
Status: |
Appendix A: Claude Design Prompts
|
Prompts persisted for audit trail. Most recent first. The v1 prompt is the active hand-off prompt; it will be re-emitted if Coordination Backlog resolutions change the API surface materially. |
v1 — 2026-05-07 — derived from this .adoc
Status: paste-ready. Source: this .adoc at :status: handoff-ready. Locked API shapes per CD-4 (stats = flat enum-keyed map) and CD-5 (state-log = List<RaceNumberStateLogDTO> ordered DESC). Tenant scoping deferred per CD-7.
I'm designing a screen for the EMS admin portal — a Spring Boot + Angular SPA
admin tool used by event operators (race directors) and tenant admins to manage
participants, events, and operational data. Visual language matches existing
portal screens you've designed in this project (C01, C03, E01, E02, E05) —
JHipster 8 / Angular 16 / PrimeNG / ng-bootstrap stack; match fonts, spacing,
palette, density.
Design **T02 — the Number Stock View**: a tenant-scoped current stock view of
all race numbers (bib numbers, boards). The tenant admin / stock operator
uses it to see what's in stock, what's been issued, what's flagged unfit, and
to launch bulk actions (return, flag UNFIT, dispose).
T02 sits inside C01's shell. Sidebar is in the `tenant` scope (a new third
scope alongside `portfolio` and `event`, added in this design round) with an
"Inventory" group: Stock (this screen), Return, Bulk, Onboard, Manufacturer
export. T02 owns the main content area only.
A cross-cutting design rule: any disabled, hidden-by-policy, or limited
affordance MUST surface its reason inline (tooltip on hover, helper text,
empty-state copy). Operators should never have to guess why a control is
unavailable — this is a hard project rule.
============================================================
Layout (top to bottom inside the main content area)
============================================================
1. Filter strip — sticky to the top of the main area when the table scrolls.
- NumberType (single-select dropdown).
- Subtype (single-select; options scoped to the selected NumberType;
disabled with tooltip "Pick a NumberType to filter by subtype" when
no type is selected).
- State (multi-select chip picker over five states: IN_STOCK, ISSUED,
IN_USE, UNFIT_FOR_SERVICE, DESTROYED). MANUFACTURED is omitted.
- Free-text search q (a single text input).
- Reset (right-aligned ghost button) — clears all four filters and
unsets any active KPI tile.
2. KPI tile strip — five tiles, one per operational state:
IN_STOCK · ISSUED · IN_USE · UNFIT_FOR_SERVICE · DESTROYED
Each tile shows the state name, the count, and an icon. The active
tile renders in the accent treatment (indigo, matching C01 / E05).
Clicking a tile narrows the table to that single state — it REPLACES
any multi-state selection in the chip picker, not toggles. KPI counts
come from a server endpoint (see "API surface" below); they should
re-fetch when filters change.
3. Table — main content.
Columns (left to right):
- checkbox (bulk-select)
- number (bold)
- type
- subtype
- state pill (coloured per state; UNFIT in amber, DESTROYED in
quieter grey, IN_STOCK in green, ISSUED in indigo, IN_USE in
blue)
- person (link if person_id set, em-dash if blank)
- last_used (date, em-dash if null)
- sequence (right-aligned numeric)
- tag-mate barcode (monospace, em-dash if null)
Default sort: type asc, sequence asc. Sortable on every column except
state and tag-mate. Pagination: server-driven, page size 50, standard
PrimeNG paginator.
Row treatment:
- Hover: subtle background tint, row click opens the drill-down
panel (right rail).
- DESTROYED rows: greyed text, checkbox uncheckable (greyed),
click does NOT open the drill-down (terminal — read-only).
- Selected rows: accent-tinted background, checkbox checked.
4. Bulk-select toolbar — materialises ABOVE the table (between filter
strip and table) when ≥1 row is selected.
- Left: "{N} selected" label + a "Clear selection" link.
- Right: three buttons: Return · Flag UNFIT · Dispose
Buttons are gated by the row states in the current selection:
- Return: enabled unless every selected row is DESTROYED.
Tooltip on disabled: "DESTROYED numbers are terminal — they
cannot be returned to stock."
- Flag UNFIT: enabled only if NO selected row is currently UNFIT
or DESTROYED.
Tooltip on disabled (UNFIT in selection): "Some selected
numbers are already UNFIT. Remove them from the selection to
flag the rest."
Tooltip on disabled (DESTROYED in selection): "DESTROYED
numbers cannot be flagged."
- Dispose: enabled only if EVERY selected row is UNFIT.
Tooltip on disabled: "Only UNFIT numbers can be disposed.
Filter to UNFIT rows or remove non-UNFIT items from the
selection."
On click:
- Return → navigates to T03 (the Stock Return screen) with the
selected ids passed in route state.
- Flag UNFIT → opens the T04 confirm dialog inline (modal)
pre-loaded with the selected ids.
- Dispose → opens the T04 confirm dialog inline (modal)
pre-loaded with the selected ids; this dialog must show the
irreversible-DESTROYED warning copy from T04.
Selection persists across pagination up to a soft cap of 200 rows;
beyond that, render an inline notice ("More than 200 rows selected
— select-all-matching is not yet supported") and disable further
row checks.
5. Drill-down side panel — right rail, opens on row click for any
non-DESTROYED row.
- Header: "Audit log — bib {number}" + close (X).
- Body: per-transition list, newest first (server returns
`List<RaceNumberStateLogDTO>` ordered by createdDate DESC).
Each entry shows:
- state transition (e.g. "ISSUED → IN_USE") rendered as two
state pills with an arrow.
- reason code (e.g. "RETURNED", "RESULT", "ASSIGNED",
"REASSIGNED", "FLAG_UNFIT", "DISPOSE", "STOCK_TAKE_SCAN").
- actor (principal login).
- timestamp.
- optional note (italic, indented).
- optional batch link ("Print batch #PB-2026-04-22-001" or
"Stock-take #ST-2026-05-01-001") — clickable when present.
- Empty state when no log entries: "No audit log entries yet."
(Defensive — number was created without state transitions.)
- Error state (5xx from /state-log): one-line "Couldn't load
audit log — try again" with a retry link inside the panel; do
NOT raise a global error toast (rest of T02 stays usable).
============================================================
Filter & state behaviour
============================================================
- All filter selections + the active KPI tile + the current page +
current sort are reflected in the URL query string so the view is
shareable. Format follows the JHipster Tanstack convention (e.g.
`?stateIn=S,F&typeIdEquals=78&q=H10&page=2&sort=sequence,asc`).
- After a bulk action returns control to T02 (T03 navigation back, or
T04 dialog close), the table and KPI strip refresh automatically.
- Empty state: if the filtered query returns zero rows, render an
empty-state card with "No numbers match these filters" and a
"Clear filters" action that resets the strip.
============================================================
API surface
============================================================
GET /api/race-numbers/stock
Query params (criteria-object):
stateIn, typeIdEquals, subTypeIdEquals, personIdSpecified,
sequenceGreaterOrEqualThan, sequenceLessOrEqualThan,
lastUsedGreaterOrEqualThan, lastUsedLessOrEqualThan, q
+ standard Spring Pageable: page, size, sort
Returns: paginated page of RaceNumberDTO with state, type, subtype,
number, sequence, personId, personName, lastUsed, tagBarcode.
GET /api/race-numbers/stats?groupBy=state
Returns: flat enum-keyed map of state → count for the KPI tiles.
{ "IN_STOCK": 1234, "ISSUED": 567, "IN_USE": 890,
"UNFIT_FOR_SERVICE": 12, "DESTROYED": 5 }
Keys are the canonical RaceNumberState enum names.
GET /api/race-numbers/{id}/state-log
Returns: List<RaceNumberStateLogDTO> ordered by createdDate DESC.
Each entry: { id, raceNumberId, transitionFromState, transitionToState,
reason, note?, createdBy, createdDate, batchId? }.
============================================================
Output
============================================================
For each of these screen states:
- default view (no filters, all rows, KPI strip showing all states)
- filtered to UNFIT_FOR_SERVICE (KPI tile click)
- bulk-select with mixed selection (Dispose disabled with tooltip)
- bulk-select with UNFIT-only selection (Dispose enabled,
irreversible warning visible in the confirm dialog mock)
- drill-down panel open on a row with rich audit log (≥3 entries)
- empty-state (filters yield zero rows)
Provide:
- HTML/JSX + matching styles per screen state.
- Screen README explaining what T02 does, the row-state action
gating matrix, the URL-state contract, and the API endpoints
consumed.
- Cross-screen note: T02 hands off to T03 (route navigation) and T04
(modal launch). The T03 and T04 screens are designed separately
in this same round; T02 must not duplicate their internals.