[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

  1. Dialog opens with EP context (event, name, current number).

  2. New-number picker — pre-populated from the WS1b last-used pick list for person_id (same subtype first; pool fall-through).

  3. Swap-reason picker (required): Damaged, Lost, Other + optional free-text note.

  4. Temporary checkboxTemporary — do not cascade to future events (default unchecked).

  5. Cascade preview — "This change will also detach N future EP(s)."

  6. Submit:

    1. DamagedPOST /api/event-participants/{id}/reassign-number then chain POST /api/race-numbers/flag-unfit for old X.

    2. Lost → reassignment then chain POST /api/race-numbers/mark-lost for old X.

    3. Other → reassignment only.

  7. Success banner reports cascade summary: originating EP swapped, N future EPs detached, temporary y/n.

Alternative Flows

  • AF-1: Reassignment succeeds, chained X-side action fails — surface error; admin told X needs manual attention. Primary swap not rolled back.

  • 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.

API Surface

Call Purpose

GET /api/persons/{personId}/number-pickList

Last-used pick list for the new-number picker (WS1b).

POST /api/event-participants/{id}/reassign-number

Primary reassignment with swapReason parameter (US #505).

POST /api/race-numbers/flag-unfit

Chained for Damaged.

POST /api/race-numbers/mark-lost

Chained for Lost.

Out of Scope

  • 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 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.

Active design iteration in progress. See admin-portal Screen Design Prompt Iteration for the broader handoff workflow. Next step: Claude Design pass on the v3 prompt persisted in the appendix below.

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

Status: drafted; ready for hand-off. Source: this .adoc at :status: in-design.

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.