[C04] Reassignment Dialog
Summary
Cross-cutting, reusable dialog used wherever an event participant holds a race number and the operator needs to swap it. First caller: E02 Event Participants. Future callers: E07 Pre-assignment.
The dialog provides the operator’s explicit signal for what should happen to the old number X. Without it, X can sit in ISSUED indefinitely after a physical replacement — ghost inventory.
Actor & Context
Actor: event organiser, tenant admin, or stock operator.
Frequency: ad-hoc; surfaces whenever a number swap is needed (damaged at sign-in, lost in transit, operator override).
Precondition: an EP exists with an assigned number; user has permission to reassign.
Entry point: Reassign number action on E02 row; Assign inline action on E01 finalisation readiness panel; future per-EP screens.
Main Flow
-
Dialog opens with EP context (event, name, current number).
-
New-number picker — pre-populated from the WS1b last-used pick list for
person_id(same subtype first; pool fall-through). -
Swap-reason picker (required):
Damaged,Lost,Other+ optional free-text note. -
Temporary checkbox —
Temporary — do not cascade to future events(default unchecked). -
Cascade preview — "This change will also detach N future EP(s)." Backed by
POST …/reassign-number-preview(US #559); before that endpoint existed the cascade was only knowable after the swap, so the dialog could report it but never warn about it. The same call supplies theassignableverdict, so an unusable bib is caught while the operator can still pick another. -
Submit: a single
POST /api/event-participants/{id}/reassign-number. TheswapReasonon the request decides the old number’s fate server-side, in the same transaction as the swap — the client chains nothing. See Old-number disposition is server-side (US #559, 2026-08-27). -
Success banner reports the cascade summary: originating EP swapped, N future EPs detached, temporary y/n, and what became of the old bib (
oldNumberDisposition).
Alternative Flows
-
AF-1: Retired 2026-08-27 by Old-number disposition is server-side (US #559, 2026-08-27). This flow described the swap succeeding while the chained action on X failed, leaving X stranded and the admin told to fix it by hand. The disposition now runs inside the reassignment’s transaction, so the two cannot diverge: either both land or neither does. The dialog no longer needs a partial-failure surface.
-
AF-2: Cascade preview shows zero future EPs — proceed without warning.
-
AF-3: Permission denied — dialog closes with a clear error.
Acceptance Criteria
-
Dialog renders per Claude Design pass.
-
Component is reusable (no E02-specific imports).
-
All three swap-reason chains behave per Session 9 of the journal.
-
Cascade summary surfaced after submit.
-
Failure-handling path tested.
-
Each reason’s consequence line matches the
oldNumberDispositionthe server reports back — no line claims an outcome the server did not perform.
API Surface
|
Corrected 2026-08-23. Two paths in the previous revision did not exist in admin-service and were verified against the code:
The v3 hand-off prompt in the appendix still carries both errors and must not be used verbatim. |
| Call | Purpose |
|---|---|
|
Last-used pick list for the new-number picker (WS1b). Implemented by
|
|
Primary reassignment (US #505). Body is
|
|
Read-only dry-run of the above (US #559). Same request body, writes nothing.
Returns |
|
No longer chained by C04. Still the correct endpoint for a deliberate
flag-unfit from T04. |
|
No longer chained by C04, and it could never have worked here. It resolves the
number from the EP’s current |
Old-number disposition is server-side (US #559, 2026-08-27)
The dialog exists to capture the operator’s signal for what happens to the old number
X. Until this change that signal decided nothing: swapReason was a free-form string
that reached the audit note and stopped there. reassignNumber repointed the EP and
left X ISSUED with its person_id intact, attached to no EventParticipant — the
exact ghost inventory the dialog was created to prevent.
The client was supposed to close that gap by chaining a second call. Against the real endpoints, only one of the three reasons could:
| Reason | Consequence line shown to the operator | What actually happened |
|---|---|---|
|
X flagged Unfit, removed from the pool |
Correct — |
|
X marked Lost |
Wrong bib. The only LOST endpoint is EP-scoped and reads the EP’s current number, which by then is the replacement. |
|
"X returns to the available pool" |
Nothing. No chained call existed. X stayed |
Two of the three consequence lines were false, and the consequence line is the one thing the operator reads to confirm what they are authorising.
Decision. swapReason becomes the discriminator for X’s fate, applied by
admin-service in the same transaction as the swap:
-
DAMAGED→ flag XUNFIT_FOR_SERVICE -
LOST→ record a LOST marker against X explicitly, never resolved from the EP -
OTHER→ return X to stock (ISSUED→IN_STOCK) -
anything unrecognised, including absent → X untouched, exactly as before
The last row is load-bearing: swapReason has always been an open string, so callers
predating this change may send labels that mean nothing to the enum. They keep the
behaviour they were written against rather than having a disposition guessed for them.
Why server-side rather than a client chain. It is the only way LOST can name the
right bib without the portal reordering its calls in a way that inverts AF-1; one
transaction removes AF-1 as a reachable state instead of warning about it; and E07,
the E01 readiness panel and the import paths inherit correct disposal without each
re-implementing the chain.
Side effect worth knowing. Damaged and Other route through the existing
flag-unfit / return primitives, which attach to an OPEN OperationalBatch of the
matching type (auto-opening one if none exists). Reassignment-driven flags and returns
therefore appear in FLAG_UNFIT / RETURN batch manifests and T05 stock reports
alongside deliberate ones — which is the point: a bib pulled from circulation by a
mid-event swap should not be invisible to stock-take reconciliation.
PRINT_BATCH — the envelope-printing group — is not affected. It is gated by
isPrintRelevant, which admits ASSIGNED and REASSIGNED only, so the single row a
swap contributes to it is the REASSIGNED on the new number: the assigned number,
which is what the envelope needs. The old bib’s disposition lands in a different batch
type and cannot enter a print batch; LOST attaches no batch at all. The only residual
is that Other may auto-open a RETURN batch holding a single reassignment-driven
return — the same shape as any T03 return, and invisible to envelope printing.
Out of Scope
-
Changing the event participant’s category. Decided 2026-08-23: that is a separate dialog, C08, so C04 keeps its single job — the fate of the old bib — and stays reusable for E07 and the E01 readiness panel.
-
Bulk reassignment across many EPs — single-EP only for v1.
-
Auto-disposal chain from
Damaged— admin runs dispose as a separate step. -
SMS notification when X is flagged — separate Feature.
Design Anchors
-
design-journal/2026-03/number-tag-management.adoc— Session 9 (DETACH-and-reconcile) -
E02 Event Participants — primary caller
Design Decisions
-
Per-reason consequence-on-X — inline, live-updating (2026-04-29). Below the swap-reason picker, a single line updates live as the operator changes the radio:
-
Damaged→ "Bib X will be flagged Unfit For Service and removed from the available pool." -
Lost→ "Bib X will be marked Lost." -
Other→ "Bib X returns to the available pool."Rationale: explicit signal for what happens to X is the entire reason the dialog exists (avoids ghost inventory in
ISSUED); hiding it behind a confirm step demotes the most important decision in the dialog. Implies the swap-reason picker is radios, not a dropdown, so the consequence line is always visible alongside the choice.
-
-
Single-EP scope (2026-04-29). Confirmed: dialog handles one EP / one number swap at a time. Bulk reassignment remains out of scope per existing spec.
-
"About this dialog" information panel (2026-04-29). Persistent collapsible panel near the top of the dialog with a short purpose paragraph + bulleted operational details (the same plain-language explanations of the three reason consequences). Default expanded on first open per browser, collapsed thereafter (state persisted in
localStorage). Trains operators in-flow without forcing them to read external docs. Cross-cutting pattern — applied symmetrically in C05 and E06; candidate for promotion to UI Design Principles.
Notes
Reuses backend behaviour from US #505 (shipped). UI candidate for @ems/shared-ui migration once a second consumer surfaces.
|
Design complete. The Claude Design pass is done and linked above as
The v3 prompt in the appendix was never used: it carries the two incorrect API
paths corrected above, and the design was authored directly against this
Repaired 2026-08-26. The design canvas had been broken since the C05
reorganisation — it loaded |
Appendix A: Claude Design Prompts
|
Prompts persisted for audit trail. Most recent first. The v3 prompt is the active hand-off prompt; earlier versions are retained for lessons-learned reference. |
v3 — 2026-04-29 — derived from this .adoc
|
Superseded 2026-08-23. Contains the two incorrect API paths corrected above.
Regenerate from the current |
Status: superseded. Source: this .adoc at :status: handoff-ready.
I'm designing a modal dialog for the EMS admin portal — same Spring Boot
+ Angular stack, same visual language as the screens you've designed
in this project (C01, C03, E01, E05). Match modal-width, header
treatment, button placement, and footer toolbar to C01.
Design **C04 — the Reassignment Dialog**: a cross-cutting reusable
modal where an event operator swaps one race number for another on an
event participant (EP). Single-EP, single-swap. First caller is E02
(event participants list); future callers include E07 (pre-assignment)
and an event-finalisation readiness panel — the dialog must have NO
caller-specific imports.
The dialog's central job: capture the operator's explicit signal for
what should happen to the OLD number X. Without this signal, X can sit
in `ISSUED` indefinitely after a physical replacement, creating ghost
inventory.
============================================================
Dialog structure (top to bottom)
============================================================
1. Header
- Title: "Reassign number".
- Sub-title: EP context — "<event> · <participant name> · currently
assigned bib <X>".
- Close (X) button top-right.
2. "About this dialog" information panel
- Collapsible, default expanded on first open per browser, collapsed
thereafter (state persisted in localStorage).
- Short purpose paragraph: "Swap a participant's race number and
decide what happens to the old number."
- Bulleted operational details — the same plain-language explanations
of the three reason consequences below, restated in plain prose.
3. New-number picker
- Pre-populated from GET /api/persons/{personId}/number-pickList
(returns the WS1b last-used pick list — same subtype first; pool
fall-through).
- Filterable dropdown (same component as C05 cell-mapping; filter
auto-appears above ~10 options).
4. Swap-reason picker — RADIOS (not a dropdown)
Three options:
- `Damaged`
- `Lost`
- `Other`
Plus an optional free-text "Operator note" field below.
5. Per-reason consequence-on-X line — INLINE, live-updating
Single line directly beneath the radios that updates as the operator
changes selection:
- `Damaged` → "Bib <X> will be flagged Unfit For Service and removed
from the available pool."
- `Lost` → "Bib <X> will be marked Lost."
- `Other` → "Bib <X> returns to the available pool."
6. Temporary checkbox
- Label: "Temporary — do not cascade to future events"
- Default: unchecked.
7. Cascade preview
- One-line message: "This change will also detach N future EP(s)."
- Hidden if N is zero.
8. Footer toolbar
- "Cancel" (left) — closes dialog, no API calls.
- "Confirm" (right, primary) — fires the API chain below.
============================================================
Submit behaviour
============================================================
On Confirm:
- Always: POST /api/event-participants/{id}/reassign-number
(request includes new-number-id, swapReason, temporary flag,
optional note).
- Then chain on the OLD bib X based on swapReason:
- `Damaged` → POST /api/race-numbers/flag-unfit
- `Lost` → POST /api/race-numbers/mark-lost
- `Other` → no chained call.
Success — close the dialog and show a success banner on the parent
screen with the cascade summary: originating EP swapped + N future EPs
detached + temporary y/n.
Failure modes:
- AF-1: Reassignment succeeds, chained X-side action fails → success
banner for the swap PLUS a warning toast that X needs manual attention.
Primary swap NOT rolled back.
- AF-3: Permission denied → close dialog, show error toast on parent.
============================================================
Output
============================================================
- HTML/JSX + matching styles for the dialog.
- README explaining:
- The three reason chains and why the consequence line is inline
(it's the operator's only signal for what happens to X).
- The reusability contract — no E02-specific imports; the dialog
receives EP context as a prop and emits a result event.
- The "About this dialog" information-panel pattern — and that the
same pattern should appear on full-screen flows like C05/E06 (right
rail rather than top-of-modal).
v1 — 2026-04-29 — discarded
Status: discarded. Reason: drafted from session memory without reading this .adoc. Invented four swap reasons (Lost / Damaged / Mis-issued / Operator override) instead of the canonical three (Damaged / Lost / Other); missed the Temporary checkbox for cascade control; missed the Cascade preview line; didn’t reference the WS1b last-used pick list for the new-number picker.
Continuing the EMS admin portal design language. C04 is a dialog (modal)
used cross-cuttingly anywhere in the portal where an operator needs to
swap one entity for another — most concretely, race-number reassignment
during the event-day operations workflow.
Context: an event participant has been assigned race-number 142.
Mid-event the operator needs to swap them to race-number 187 (lost bib,
duplicate issue, deliberate move). The dialog must:
- Show: current assignment ("Bib 142 → Jane Doe, F35-39, Started 06:42").
- Ask for the swap target: "Assign Jane Doe to bib ___" with autocomplete
on available bibs (filtered by the participant's category — bib 187 must
be in the F35-39 category's number range).
- Ask for a reason: dropdown of {Lost, Damaged/Unfit, Mis-issued, Operator
override}. The reason picker chains the right downstream consequences:
- Lost / Damaged → mark the OLD bib as UNFIT_FOR_SERVICE (don't
auto-reissue; operator does stock-take later).
- Mis-issued / Operator override → mark the OLD bib IN_STOCK (back
to the available pool).
- Show consequences inline as the operator picks a reason: "Bib 142
will be marked UNFIT_FOR_SERVICE and removed from the available pool"
vs "Bib 142 will return to the available pool". This is critical —
operators get this wrong on paper currently.
- Free-text "operator note" (optional, for audit trail).
- Confirm + Cancel buttons. Confirm fires the swap and closes the dialog.
An undo toast at the bottom of the parent page gives 10 seconds to
reverse if it was a mistake.
Edge case to design for: target bib is already assigned to someone else.
The dialog should detect this and show a conflict warning with the option
to swap them too (chained reassignment) or pick a different bib.
Style: matches the portal modal patterns established by C01 (user/tenant
switcher) — same modal width, header treatment, button placement, footer
toolbar.
Output: HTML/JSX + styles, README explaining how this dialog is reused
across at least 3 contexts (race-number reassignment, operational batch
re-print, person merge confirmation). README also describes the
"undo toast" pattern so it can be standardised across the portal.