[E02] Event Participants

Summary

The event-scoped participants list. Primary working surface for the operator within an event — filter, find, fix individual participants, and run bulk operations. First major consumer of the JHipster Filterable Data Table pattern (see related journal); design decisions made here cascade to E04, M02, and T02T05.

Primary callers and consumers:

  • Caller from: E01 (event overview), E05 (control centre Recent panel), E06 (import summary follow-up).

  • Caller to: C04 (number reassignment), E06 (import participants), E07 (pre-assignment).

Actor & Context

Actor: event organiser, race director, tenant admin. Frequency: many times per day during the registration phase; high-frequency on race day. Precondition: an event exists in the tenant; user has read access; pinned event context. Entry point: /events/{eventId}/participants — path-param-driven; the event id is structural, not a filter.

Main Flow

  1. Operator arrives at /events/{eventId}/participants.

  2. Page renders, top to bottom:

    1. Stats strip — chip metrics with click-to-filter behaviour.

    2. Active filter chips — quick-clear chips for currently-set filter dimensions; filter editor expands when the operator adds a filter.

    3. Top-bar action buttons and inactive toggle (with badge count of hidden inactive rows).

    4. Table — sortable headers, server-driven pagination, multi-select for bulk actions.

  3. Operator narrows the list using filters or stats-chip clicks.

  4. Inline cell interactions:

    1. Click Primary Number cell → opens C04 reassignment dialog.

    2. Click Category cell → opens multi-step impact dialog (see Future Use Cases — FU-1).

  5. Row "View" action (inline) → side panel slides in from the right with race assignments, start groups, assigned numbers, seeding. Table context is preserved. Browser-back closes the panel without leaving the page.

  6. Row overflow menu: Change category, Change number, Substitute, Active/Inactive (label adapts to current state).

  7. Bulk-action toolbar surfaces in the top bar when one or more rows are selected.

  8. Top-bar action buttons: Import participants (→ E06), Add participant (→ FU-3), Export, Pre-assignment (→ E07).

Alternative Flows

  • AF-1 — Empty state. No participants for this event: render an empty-state panel with an "Import participants" call-to-action linking to E06 and an "Add participant" affordance for the one-off case.

  • AF-2 — Inactive toggle off. Inactive participants are hidden; the toggle’s badge surfaces the hidden count (Show inactive (12)). Inactive participants render with greyed/strikethrough text when the toggle is on.

  • AF-3 — Stats chip vs filter conflict. Clicking a stats chip sets the corresponding filter; clicking again toggles it off. If incompatible filters are set, the most-recent action wins; the chip strip reflects current state.

  • AF-4 — Filter result empty. "No participants match the current filters" with a Clear filters action.

  • AF-5 — Permission denied on action. Action button or row action is disabled with a tooltip explaining the missing permission; bulk actions guard at the toolbar level.

Acceptance Criteria

  • Use-case page authored.

  • Status design-todo → in-design → handoff-ready after Claude Design pass.

  • :design-url: populated.

  • Path-param routing — /events/{id}/participants (not query-param).

  • Stats strip with click-to-filter.

  • Inactive toggle with hidden-count badge.

  • Click-to-change Primary Number opens C04.

  • Click-to-change Category opens the multi-step impact dialog (FU-1 stub acceptable in v1 if FU-1 backend is not yet ready).

  • "View" row action opens a slide-in side panel; browser-back closes the panel; full-page route at E09 exists for deep-linking.

  • Bulk-action toolbar appears in top bar when ≥2 rows are selected; row-level actions are disabled while bulk mode is active.

  • Multiple Order Numbers per row supported (chips with status icon + tooltip).

  • Filter state encoded in URL query params (refresh / share-link preserves filter set).

  • Cross-references C04 + E06 + E07 + E09.

API Surface

Call Purpose

GET /api/event-participants?eventId={id}&…​

List with filter dimensions per legacy EventParticipantFilter (event, person, eventCategory, team, number, tag, active, hasNoStartGroups). Server-side sort + pagination.

GET /api/event-participants/stats?eventId={id}

Stats strip metrics (Total, New, Returning, Unpaid, Unassigned numbers). New endpoint — hard prerequisite for the 5-chip strip. Tracked under US #623.

PATCH /api/event-participants/{id}

Inline category change (after FU-1 dry-run preview); active/inactive toggle.

POST /api/event-participants/{id}/category-change-preview

FU-1 dry-run — returns the race/start-group impact summary without committing. Future endpoint — tracked under US #624.

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

Number reassignment via C04 (US #505 backend, shipped).

POST /api/event-participants/bulk/…​

Bulk operations — endpoints TBD per FU-6. Tracked under US #625.

GET /api/event-participants/{id} + related endpoints

Side-panel detail view (races, start groups, numbers, seeding).

Out of Scope

  • Tenant-wide unpinned participants list — does not make practical operational sense; participants are always viewed within an event context.

  • Phase-aware column customisation — same default column set across registration / pre-race / race-day phases; operator does not switch column views.

  • Multi-step impact dialog for category change — captured as FU-1; surfaces a stub in v1 if backend dry-run isn’t ready.

  • Substitute flow — captured as FU-2; substantial flow with order-transfer + audit.

  • "Add participant" dialog flow — captured as FU-3; called from top-bar and from FU-2 step 2.

  • Saved filter views — FU-4.

  • In-row inline editing beyond click-to-change cells — FU-5.

  • Bulk-action repertoire — FU-6 lists the candidate operations; v1 ships a minimal set (Export selected, Delete with confirm).

Design Anchors

  • Portal Pattern

  • UI Design Principles

  • design-journal/2026-04/jhipster-filterable-data-table.adoc — table substrate pattern (active design)

  • E01 (event overview, handoff-ready) — visual language reference

  • E05 (control centre, handoff-ready) — visual language reference

  • C04 — number reassignment dialog (in-design)

  • C06 — order detail screen + cluster modal (in-design); E02’s Order chip click + hover summons C06’s invocation surfaces

  • C07 — Data Table component (in-design); E02 mounts <DataTable> rather than re-implementing rows / selection / bulk chrome

  • E06 — top-bar Import participants link

  • E07 — top-bar Pre-assignment link

  • E09 — side-panel Open detail destination (design-todo)

  • C01 — bulk-action placement was deferred to E02; resolved here as "top-bar toolbar surfaces when rows are selected"

Design Decisions

  • Pinned-context routing — path-param /events/{eventId}/participants (2026-04-29). Event id is structural in the route, not a filter. Sets the convention for the JHipster Filterable Data Table pattern; cascades to E04 / M02 / T02–T05. Resolves the "no path-param support" gap called out in design-journal/2026-04/jhipster-filterable-data-table.adoc.

  • Default column set — fixed, not phase-customised (2026-04-29). The same columns render across registration / pre-race / race-day phases. No "switch view" affordance. Operators reach for filters, not column configs.

    Columns (left to right):

    1. First Name

    2. Last Name

    3. Date of Birth

    4. Gender

    5. Age

    6. Team — column hidden when the event has no team-based registrations (legacy displayListField mechanic, generalised).

    7. Categoryclick-to-change (opens FU-1 multi-step impact dialog).

    8. Primary Numberclick-to-change (opens C04).

    9. Order Number(s) — one chip per order, each with a status icon (P Paid, U Unpaid, X Other) and tooltip carrying the full status text. Multiple orders supported per row (paid → upgrade → second order; bulk-payment edge cases; etc.).

      The Secondary Number column is deferred to v2 — it is bundled with the schema pivot in US #478 (number2 + tag ownership). Until that schema lands the data has no number2 slot to render. Re-introduce the column (and a second click-to-change → C04 path) once US #478 ships. C04 will need a slot parameter at that point so the swap knows which number to replace.

      Inactive participants: rendered with greyed text + strikethrough across all cells when the inactive toggle is on. When the toggle is off, they are hidden and the toggle’s badge shows the hidden count.

  • Top-bar contents (2026-04-29):

    Element Behaviour

    Inactive toggle

    Show inactive (N) — toggle button with badge count of currently-hidden inactive rows.

    Bulk-action toolbar

    Surfaces in the top bar when ≥2 rows are selected (single-row selection still uses row-level actions; the bulk toolbar is genuinely multi-row). While the toolbar is active, row-level action buttons (View, overflow menu, click-to-change cells) are disabled so only bulk operations can be executed. Resolves the C01 deferred question.

    Stats strip

    Chip metrics, all click-to-filter: Total, New (first-time entrant in this org), Returning, Unpaid, Unassigned numbers. Click toggles the corresponding filter on/off. Total is a no-op chip used as the headline anchor.

    Action buttons

    Import participants (→ E06) · Add participant (FU-3 — disabled in v1 with a "Coming soon" tooltip; do NOT route to a placeholder URL) · Export (CSV/XLSX of current filter result) · Pre-assignment (→ E07).

  • Row actions (2026-04-29):

    Surface Actions

    Inline cell click

    Primary Number → C04. Category → FU-1 multi-step impact dialog.

    Inline button

    View → side panel slides in from the right (races, start groups, assigned numbers, seeding); table context preserved. Browser-back closes the panel without leaving the page. Full-page route at E09 is available for deep-linking and is the destination of the side panel’s Open detail link.

    Overflow menu

    Change category (same dialog as click-to-change) · Change primary number (same dialog as click-to-change, i.e. C04) · Substitute (FU-2 — disabled in v1 with a "Coming soon" tooltip) · Active/Inactive — label adapts to current state ("Make inactive" / "Make active").

  • Stats endpoint is a hard prerequisite for v1 (2026-04-30 — supersedes 2026-04-29 minimum-viable fallback). The 5-chip strip ships in full. The /api/event-participants/stats endpoint (US #623) is a hard backend prerequisite — not a "nice to have for the last two chips". Rationale: a 2-chip-greyed-out strip looks broken to operators; better to land all 5 with the endpoint, or ship E02 v1 without the strip at all. Decision is the former.

  • Multiple order numbers — chip-per-order rendering (2026-04-29). Each EP may carry multiple orders (registration → upgrade → second order; bulk-payment edge cases; concurrent-registration timing). Render as small chips, one per order, each with a status icon and tooltip; clicking a chip drills into that order’s detail (destination route to be defined during the Claude Design session — tracked under US #626).

  • Filter state — URL-persisted via fromRoute adapter (2026-04-30). Filters set on the screen are encoded into the URL as query params (e.g. /events/123/participants?eventCategory=H21A&unpaid=true). Refresh and copy-link-to-colleague both restore the filter set. Implementation uses the fromRoute / toUrlQueryParams adapter pattern documented in design-journal/2026-04/jhipster-filterable-data-table.adoc. Path-param eventId and query-param filter state coexist cleanly under that pattern.

  • Side-panel browser-back closes the panel (2026-04-30). When the View side panel is open, pressing browser-back closes the panel without leaving the screen (dismissal-only, not a full-page navigation). Achieved via a routed-modal pattern — the side panel is mounted on a subroute (e.g. /events/123/participants(view:456)) so back/forward toggles the panel naturally without bespoke history-listener code.

  • No tenant-wide unpinned list (2026-04-29). E02 is exclusively pinned-by-event. Tenant-wide participant queries are addressed elsewhere (Person screens, M02 for membership members) and don’t belong here.

Future Use Cases

The following are forward-looking features. They are not part of the current UI phase. Captured here so the design intent isn’t lost and so v1 stubs in the right places.

FU-1 — Multi-step impact dialog for Category change

When the operator clicks the Category cell or overflow "Change category", the dialog must surface the downstream impact before commit. Steps:

  1. Pick — the new category (filterable dropdown of the event’s categories).

  2. Preview — backend dry-run returns the impact summary: "Removed from these races / start-groups: [list]; added to these: [list]".

  3. Confirm — commits the category change as a single transaction.

Backend support needed: POST /api/event-participants/{id}/category-change-preview returning the impact summary; existing PATCH /api/event-participants/{id} for the commit.

v1 stub: until the dry-run endpoint exists, the click-to-change Category interaction can fall through to a single-step dropdown commit (no preview), with a banner noting "Impact preview available in a future release."

FU-2 — Substitute flow

Triggered from row overflow "Substitute". One operator-perceived flow, multi-stage internally:

  1. Operator confirms substitution intent on the originating EP (Person A).

  2. Add participant dialog launches in substitute mode (consumes FU-3).

  3. Once Person B is created (or selected from existing), order details are transferred from Person A’s EP to Person B’s EP — atomic operation, audit-logged.

  4. Substitution fee, if applicable, generates an additional order on Person B’s EP.

  5. Originating EP (Person A) marked inactive.

  6. New EP (Person B) linked to original via a substitution-of relationship for audit trail.

Backend support needed: order-transfer endpoint, substitution-fee price-list item, EP-to-EP substitution-of relationship, audit log entries.

FU-3 — Add participant dialog

Triggered from top-bar "Add participant" or as the second step in FU-2. Standalone from E02. Inputs: person picker (existing Person) or new-Person creation, category, optional number assignment, order details.

Reusable dialog — candidate for promotion to a Cxx cross-cutting use case once a second caller surfaces.

FU-4 — Saved filter views

Operator-defined named filter combinations (e.g. "Unpaid", "Unassigned numbers", "DNF status"). Tenant-scoped, possibly per-event-type. Persisted via a new endpoint.

FU-5 — In-row inline editing beyond click-to-change cells

Quick edits to safe fields (phone, email, t-shirt size, emergency contact) without leaving the page. Requires a per-field "is this safe to inline-edit?" gate to avoid accidental mutation of identity-bearing fields.

FU-7 — Operator-controlled column visibility settings

A "Column settings" affordance (gear icon in the table header, or a top-bar utility) that lets the operator hide/reveal columns from the canonical set. Persisted per-user, per-screen (so the same operator can have a different column set on E02 vs E04 vs M02).

Specifically intended to:

  1. Re-introduce the Secondary Number column when US #478 schema pivot lands — defaulted hidden, operators who use bibs + ankle-strap RFID combinations reveal it.

  2. Make the Team column declarative-hidden default-on-by-event-shape mechanic (currently displayListField) explicit and operator-overridable — some operators want to see the empty Team column for upcoming events; some want to hide it even when populated.

  3. Generalises across the table-substrate consumers (E04, M02, T02–T05) — settings live with the table substrate, not per-screen.

Distinct from the declarative displayListField visibility hint: that’s structure-driven (column hidden when no event has team-based registrations); FU-7 is operator-driven (column hidden because I don’t want to see it).

FU-6 — Bulk-action repertoire

Candidate operations beyond v1’s minimal set (Export selected, Delete with confirm):

  • Bulk category change (with FU-1-style impact preview).

  • Bulk start-group assignment.

  • Bulk active/inactive toggle.

  • Bulk mark-paid (with caveat about audit trail).

  • Bulk number assignment / detachment.

  • Bulk message / email send.

  • Bulk export to specialised formats (timing system feed, sticker sheet, etc.).

Notes

Stage 1 skipped. The original delivery plan staged E02 across two milestones — a Stage 1 throwaway Bootstrap-table read-only list (US #607) plus a Stage 2 full headless-data-table implementation (Feature #608 / US #609 + #610). The Stage 1 ticket exists to unblock D2a (Feature #540) before the headless-table substrate was ready.

Following the 2026-04-30 review pass, the headless-data-table substrate at design-journal/2026-04/jhipster-filterable-data-table.adoc is far enough along that the implementation goes directly to Stage 2 (Feature #608). US #607 is superseded — the screen described here is the canonical target.

Mounts C07 as the table substrate. Rows, selection, the variant-E bulk treatment (1.5px indigo border + 4px outer glow + bonded DTBulkBar), and the dt-stop click-to-act mechanism all come from C07’s component contract. E02’s job is to declare its column set + caller-side state (selection, filters, sort, pagination) and pass them through C07’s prop surface. None of C07’s chrome is to be re-implemented inside E02.

E02 is the first major consumer of the JHipster Filterable Data Table pattern from design-journal/2026-04/jhipster-filterable-data-table.adoc — that journal entry is in active design, and decisions made on E02 cascade to E04, M02, and T02–T05. Particular load-bearing items resolved here:

  • Path-param-driven pinned context — was a gap in the legacy admin-ui pattern; E02 is the first screen to lock it down.

  • Declarative column metadata + visibility hint — generalises the legacy displayListField mechanic (e.g. Team column hidden when the event has no team-based registrations).

  • Server-side sort by header click — preserved from the legacy pattern.

  • Bulk-action surface placement — resolves the C01 deferred question (bulk-action toolbar surfaces in the top bar when rows are selected).

Legacy reference for filter dimensions and column shapes: admin-ui/src/main/webapp/app/entities/admin-service/event-participant/not the target visual or interaction model, but the data shape is authoritative.

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

Appendix A: Claude Design Prompts

Prompts persisted for audit trail. Most recent first. The active hand-off prompt is the topmost; earlier versions (where they exist) are retained for lessons-learned reference.

v2 — 2026-04-30 — supersedes v1 after review pass

Status: drafted; ready for hand-off. Source: this .adoc after the 2026-04-30 review-pass updates (Secondary Number column dropped, side-panel Edit replaced with E09 link, bulk threshold ≥2, filter URL persistence, side-panel browser-back, hard /stats prerequisite). Differences from v1: see Discussion Log entry 2026-04-30 in the iteration journal.

I'm designing the **Event Participants** screen for the EMS admin portal —
a Spring Boot + Angular SPA admin tool used by event operators (race
directors) to manage participants, events, and operational data. Visual
language matches the existing portal screens you've designed in this
project (C01 structure, C03 public landing, E01 event overview, E05
events control centre — these are handoff-ready). Match fonts, spacing,
palette, density.

E02 is the **event-scoped participants list** — the operator's primary
working surface for an event. They open an event, land here, and spend
most of their day filtering, finding, and fixing individual participants
plus running bulk operations.

This screen is the **first major consumer** of a new filterable-data-
table pattern (path-param pinned context, declarative columns, server-
side sort/filter/pagination, query-param URL persistence for filter
state). Decisions made here cascade to event results, membership
members, and tenant number/tag inventory screens. Get this right.

============================================================
Route
============================================================

  /events/{eventId}/participants

The event id is **structural** in the route, not a filter. Operators
arrive here from event overview, control centre, or import summary —
always inside an event context. There is no tenant-wide unpinned mode.

Filter state encodes into URL query params on top of the path-param
event id (e.g. /events/123/participants?eventCategory=H21A&unpaid=true).
Refresh and copy-link-to-colleague both restore the filter set.

============================================================
Page anatomy (top to bottom)
============================================================

[1] Page header
   - Page title: "Participants — <event name>".
   - Right side: action buttons (see Top-bar action buttons below).

[2] Stats strip — chip metrics, click-to-filter
   Five chips, horizontal:
   - **Total** — anchor: "<n> participants". Click is a no-op.
   - **New** — first-time entrants in this org. Click toggles
     "first-time only" filter.
   - **Returning** — have prior EPs in this org. Click toggles
     "returning only" filter.
   - **Unpaid** — at least one unpaid order. Click toggles
     "unpaid only" filter.
   - **Unassigned numbers** — no primary number assigned. Click toggles
     the unassigned filter.
   Each chip shows the metric value prominently and the label below;
   active state visually distinct (filter is on); clicking again clears.

[3] Active-filter chip strip + filter editor
   - Left: chips for currently-set filter dimensions (event-category,
     team, number, tag, hasNoStartGroups, etc.) with quick-clear (×)
     per chip and a "Clear all" link.
   - Right: a "+ Add filter" button that opens a small panel — pick
     dimension → pick comparator → pick value(s).
   - Filter dimensions available: eventCategory, team, number, tag,
     active, hasNoStartGroups, person.

[4] Top-bar utilities row
   Left:
   - **Inactive toggle** — "Show inactive (N)" button with badge of
     hidden inactive count. Off by default.
   - **Bulk-action toolbar** — appears next to the inactive toggle when
     **2 or more** rows are selected (single-row selection still uses
     row-level actions; the bulk toolbar is genuinely multi-row).
     Contains: "Export selected", "Delete selected (with confirm)".
     While the toolbar is active, **row-level action buttons (View,
     overflow menu, click-to-change cells) are disabled** — only bulk
     operations can be executed. The bulk toolbar replaces / overlays
     the action buttons on the right.
   Right (when bulk toolbar not active):
   - **Import participants** → navigates to E06 upload screen.
   - **Add participant** → disabled in v1 with a "Coming soon" tooltip.
     **Do not link to a placeholder route** — a 404 destination is worse
     UX than a clearly-disabled button.
   - **Export** → CSV/XLSX download of the current filter result.
   - **Pre-assignment** → navigates to E07 (event-scoped pre-assignment).

[5] Table
   Columns (left to right):
   - Multi-select checkbox.
   - First Name (sortable).
   - Last Name (sortable).
   - Date of Birth (sortable).
   - Gender (sortable).
   - Age (sortable).
   - Team (sortable; column hidden when the event has no team-based
     registrations — declarative visibility hint).
   - Category (sortable; **click cell to change** — opens a multi-step
     impact dialog, see Inline Cell Interactions below).
   - Primary Number (sortable; **click cell to change** — opens C04
     reassignment dialog).
   - Order Number(s) — render as one **chip per order** with a small
     status icon (P for Paid, U for Unpaid, X for Other) and a tooltip
     carrying the full status text. Multiple orders per row are
     supported (e.g. paid → upgrade → second order). Clicking a chip
     drills into that order's detail — destination route to be defined
     during this design session.
   - Row actions: inline "View" button + overflow (kebab) menu.

   Sort: clicking a sortable header toggles asc/desc; emits server-side
   sort param. Single-sort, not multi-sort.

   Pagination: server-driven, visible page size selector (25 / 50 / 100),
   prev/next + page number links.

   Inactive participants:
   - Rendered with greyed text + strikethrough across all cells.
   - Hidden when the inactive toggle is off; visible when on.

   Note: a **Secondary Number** column is intentionally NOT in v1. It
   ships when the schema pivot in US #478 (number2 + tag ownership)
   lands. Do not draw a placeholder column.

============================================================
Inline cell interactions
============================================================

Two cells are **click-to-change**:

a) **Primary Number** cell
   - Click opens C04 — the reassignment dialog (separately designed).
   - C04 captures swap reason (Damaged / Lost / Other), confirms, and
     fires the reassignment API + chained side-effects on the old number.
   - The same dialog is also reachable from the row overflow's
     "Change primary number" action — they are the same dialog.

b) **Category** cell
   - Click opens a **multi-step impact dialog** (FU-1 in the spec —
     not yet fully designed, but the entry point lives here):
     Step 1: Pick the new category (filterable dropdown of the event's
       categories).
     Step 2: Impact preview — "Removed from these races / start-groups:
       [list]; added to these: [list]".
     Step 3: Confirm.
   - **v1 stub**: until the backend dry-run endpoint is ready, the click
     falls through to a single-step dropdown commit (no preview), with
     a small banner: "Impact preview available in a future release."
   - The same dialog is reachable from the row overflow's
     "Change category" action — they are the same dialog.

Make these affordances **discoverable** — the cell value should look
clickable on hover (subtle background + cursor change) so the operator
notices the interaction without needing to be told.

============================================================
Row actions
============================================================

Inline (always visible at row's right edge):
- **View** — slides in a side panel from the right with the EP's
  detail (race assignments, start groups, assigned numbers, seeding).
  Table context preserved — operator sees which row is selected and
  can close the panel or click another row to swap content.
  - Browser-back closes the panel without leaving the screen
    (routed-modal pattern: panel mounts on a subroute, e.g.
    /events/123/participants(view:456), so back/forward toggles the
    panel naturally).
  - The side panel's "Open detail" link routes to E09
    (/events/{eventId}/participants/{epId}) for the full-page view.

Overflow (kebab) menu:
- **Change category** — same dialog as click-to-change Category.
- **Change primary number** — same dialog as click-to-change Number (C04).
- **Substitute** — opens the Substitute flow (FU-2 in the spec — not
  yet fully designed). For v1, **disable** with a "Coming soon" tooltip.
- **Active / Inactive** — label adapts to the current row state
  ("Make inactive" if active; "Make active" if inactive). Toggles via
  PATCH.

Both inline View and the overflow menu are disabled while the bulk-
action toolbar is active.

============================================================
Empty + edge states
============================================================

- No participants yet (new event): empty-state panel with "Import
  participants" call-to-action linking to E06 + an "Add participant"
  affordance (also disabled with "Coming soon" tooltip in v1).
- Filter result empty: "No participants match the current filters" +
  "Clear filters" action.
- Permission denied: action button or row action disabled with tooltip
  explaining the missing permission; bulk actions guard at toolbar.

============================================================
Side panel (View)
============================================================

Slides in from the right. Width: ~480px (enough for vertical sections,
not so wide it eats the table). Sections, top to bottom:
- EP header — name, category, primary number chip, active state.
- Races — list of races the EP is registered in, with status.
- Start groups — list of start groups the EP is assigned to.
- Assigned numbers — primary number, any historical reassignments.
- Seeding — current seed value + history.
- Bottom: **Open detail** (links to the full-page E09 route) + **Close**.

============================================================
Output
============================================================

- HTML/JSX + matching styles for E02.
- Screen README that explains:
  - The path-param-driven pinned-context routing + filter-URL-persistence
    pattern (this is a pattern contribution to the table substrate).
  - The declarative column-visibility pattern (Team column hidden when
    event has no teams).
  - The click-to-change cell pattern (Primary Number → C04, Category → FU-1).
  - The slide-in side panel for "View" + browser-back closes via the
    routed-modal pattern.
  - The bulk-action toolbar surfacing in the top-bar utilities row when
    ≥2 rows are selected, and the row-action disable behaviour.
  - The stats strip with click-to-filter behaviour (all 5 chips ship
    in v1; backend `/stats` endpoint is a hard prerequisite).
- Note any moments where the table substrate (filter editor, sort
  affordances, pagination control) needs new shared components — the
  substrate is in active design as the JHipster Filterable Data Table
  pattern; this screen is the first to consume it.
- Propose the destination route + screen for the order-chip drill-down;
  it's tracked under US #626 and design freedom is OK here.

v1 — 2026-04-30 — derived from this .adoc (superseded)

Status: superseded by v2 above. Source: this .adoc at :status: in-design before the 2026-04-30 review pass.

I'm designing the **Event Participants** screen for the EMS admin portal —
a Spring Boot + Angular SPA admin tool used by event operators (race
directors) to manage participants, events, and operational data. Visual
language matches the existing portal screens you've designed in this
project (C01 structure, C03 public landing, E01 event overview, E05
events control centre — these are handoff-ready). Match fonts, spacing,
palette, density.

E02 is the **event-scoped participants list** — the operator's primary
working surface for an event. They open an event, land here, and spend
most of their day filtering, finding, and fixing individual participants
plus running bulk operations.

This screen is the **first major consumer** of a new filterable-data-
table pattern (path-param pinned context, declarative columns, server-
side sort/filter/pagination). Decisions made here cascade to event
results, membership members, and tenant number/tag inventory screens.
Get this right.

============================================================
Route
============================================================

  /events/{eventId}/participants

The event id is **structural** in the route, not a filter. Operators
arrive here from event overview, control centre, or import summary —
always inside an event context. There is no tenant-wide unpinned mode.

============================================================
Page anatomy (top to bottom)
============================================================

[1] Page header
   - Page title: "Participants — <event name>".
   - Right side: action buttons (see Top-bar action buttons below).

[2] Stats strip — chip metrics, click-to-filter
   Five chips, horizontal:
   - **Total** — anchor: "<n> participants". Click is a no-op.
   - **New** — first-time entrants in this org. Click toggles
     "first-time only" filter.
   - **Returning** — have prior EPs in this org. Click toggles
     "returning only" filter.
   - **Unpaid** — at least one unpaid order. Click toggles
     "unpaid only" filter.
   - **Unassigned numbers** — no primary number assigned. Click toggles
     the unassigned filter.
   Each chip shows the metric value prominently and the label below;
   active state visually distinct (filter is on); clicking again clears.

[3] Active-filter chip strip + filter editor
   - Left: chips for currently-set filter dimensions (event-category,
     team, number, tag, hasNoStartGroups, etc.) with quick-clear (×)
     per chip and a "Clear all" link.
   - Right: a "+ Add filter" button that opens a small panel — pick
     dimension → pick comparator → pick value(s).
   - Filter dimensions available: eventCategory, team, number, tag,
     active, hasNoStartGroups, person.

[4] Top-bar utilities row
   Left:
   - **Inactive toggle** — "Show inactive (N)" button with badge of
     hidden inactive count. Off by default.
   - **Bulk-action toolbar** — appears next to the inactive toggle when
     ≥1 row is selected. Contains: "Export selected", "Delete selected
     (with confirm)". (Future bulk operations are designed-out in v1.)
     The bulk toolbar replaces / overlays the action buttons on the
     right while rows are selected.
   Right (when bulk toolbar not active):
   - **Import participants** → navigates to E06 upload screen.
   - **Add participant** → opens the Add Participant dialog (future;
     stub the button, link to a placeholder route).
   - **Export** → CSV/XLSX download of the current filter result.
   - **Pre-assignment** → navigates to E07 (event-scoped pre-assignment).

[5] Table
   Columns (left to right):
   - Multi-select checkbox.
   - First Name (sortable).
   - Last Name (sortable).
   - Date of Birth (sortable).
   - Gender (sortable).
   - Age (sortable).
   - Team (sortable; column hidden when the event has no team-based
     registrations — declarative visibility hint).
   - Category (sortable; **click cell to change** — opens a multi-step
     impact dialog, see Inline Cell Interactions below).
   - Primary Number (sortable; **click cell to change** — opens C04
     reassignment dialog).
   - Secondary Number (sortable; **click cell to change** — opens C04
     reassignment dialog).
   - Order Number(s) — render as one **chip per order** with a small
     status icon (P for Paid, U for Unpaid, X for Other) and a tooltip
     carrying the full status text. Multiple orders per row are
     supported (e.g. paid → upgrade → second order).
   - Row actions: inline "View" button + overflow (kebab) menu.

   Sort: clicking a sortable header toggles asc/desc; emits server-side
   sort param. Single-sort, not multi-sort.

   Pagination: server-driven, visible page size selector (25 / 50 / 100),
   prev/next + page number links.

   Inactive participants:
   - Rendered with greyed text + strikethrough across all cells.
   - Hidden when the inactive toggle is off; visible when on.

============================================================
Inline cell interactions
============================================================

Two cells are **click-to-change**:

a) **Number** cell (Primary or Secondary)
   - Click opens C04 — the reassignment dialog (separately designed).
   - C04 captures swap reason (Damaged / Lost / Other), confirms, and
     fires the reassignment API + chained side-effects on the old number.
   - The same dialog is also reachable from the row overflow's
     "Change number" action — they are the same dialog.

b) **Category** cell
   - Click opens a **multi-step impact dialog** (FU-1 in the spec —
     not yet fully designed, but the entry point lives here):
     Step 1: Pick the new category (filterable dropdown of the event's
       categories).
     Step 2: Impact preview — "Removed from these races / start-groups:
       [list]; added to these: [list]".
     Step 3: Confirm.
   - **v1 stub**: until the backend dry-run endpoint is ready, the click
     falls through to a single-step dropdown commit (no preview), with
     a small banner: "Impact preview available in a future release."
   - The same dialog is reachable from the row overflow's
     "Change category" action — they are the same dialog.

Make these affordances **discoverable** — the cell value should look
clickable on hover (subtle background + cursor change) so the operator
notices the interaction without needing to be told.

============================================================
Row actions
============================================================

Inline (always visible at row's right edge):
- **View** — slides in a side panel from the right with the EP's
  detail (race assignments, start groups, assigned numbers, seeding).
  Table context preserved — operator sees which row is selected and
  can close the panel or click another row to swap content.
- A full-page route exists too (`/events/{eventId}/participants/{epId}`)
  for deep-linking, but the default click is the side panel.

Overflow (kebab) menu:
- **Change category** — same dialog as click-to-change Category.
- **Change number** — same dialog as click-to-change Number (C04).
- **Substitute** — opens the Substitute flow (FU-2 in the spec — not
  yet fully designed). For v1, stub as a placeholder action with a
  "Coming soon" toast.
- **Active / Inactive** — label adapts to the current row state
  ("Make inactive" if active; "Make active" if inactive). Toggles via
  PATCH.

============================================================
Empty + edge states
============================================================

- No participants yet (new event): empty-state panel with "Import
  participants" call-to-action linking to E06 + an "Add participant"
  affordance.
- Filter result empty: "No participants match the current filters" +
  "Clear filters" action.
- Permission denied: action button or row action disabled with tooltip
  explaining the missing permission; bulk actions guard at toolbar.

============================================================
Side panel (View)
============================================================

Slides in from the right. Width: ~480px (enough for vertical sections,
not so wide it eats the table). Sections, top to bottom:
- EP header — name, category, primary/secondary number chips, active
  state.
- Races — list of races the EP is registered in, with status.
- Start groups — list of start groups the EP is assigned to.
- Assigned numbers — primary, secondary, any historical reassignments.
- Seeding — current seed value + history.
- Bottom: links to the full-page detail route + "Edit" + "Close".

============================================================
Output
============================================================

- HTML/JSX + matching styles for E02.
- Screen README that explains:
  - The path-param-driven pinned-context routing (this is a pattern
    contribution to the table substrate).
  - The declarative column-visibility pattern (Team column hidden when
    event has no teams).
  - The click-to-change cell pattern (Number → C04, Category → FU-1).
  - The slide-in side panel for "View".
  - The bulk-action toolbar surfacing in the top-bar utilities row
    when rows are selected.
  - The stats strip with click-to-filter behaviour.
- Note any moments where the table substrate (filter editor, sort
  affordances, pagination control) needs new shared components — the
  substrate is in active design as the JHipster Filterable Data Table
  pattern; this screen is the first to consume it.