[C05] Import Mapping Flow (shared)

Summary

Cross-cutting shared middle steps of the async import workflow. Used by both E06 EP import and E08 result import. Captures column-mapping dialog, cell-mapping dialog, and progress + row-results component.

The use-case-specific upload screens (E06/E08 bookend 1) write a localStorage caller key before redirecting into C05; C05 reads the key on completion and redirects back to the matching summary screen (E06/E08 bookend 2).

Actor & Context

Actor: event organiser; staff with import permission. Frequency: per import job — every CSV/XLSX upload that needs mapping resolution. Precondition: a job has been created via POST /api/imports; localStorage caller key written by the calling page. Entry point: redirect from E06 upload screen, E08 upload screen, or a future caller; resume from My imports link or job-list entry.

Main Flow

  1. Read job state via GET /api/imports/{uuid}.

  2. If state is COLUMN_MAPPING — render column-mapping dialog: unmatched columns picker against target fields returned by the server. User submits with PUT /api/imports/{uuid}/column-mappings.

  3. If state is CELL_MAPPING — render cell-mapping dialog: unmatched-FK-values picker (dropdown of valid options). User submits with PUT /api/imports/{uuid}/cell-mappings.

  4. While state is PROCESSING — render progress component with polling loop.

  5. When state is COMPLETED or FAILED — render row-results component (paginated row outcomes) briefly, then read the caller key from localStorage and redirect to the matching use-case summary screen.

Alternative Flows

  • AF-1 — Pause. User leaves the mapping screen; state is server-side; resume via My imports returns to the same point.

  • AF-2 — Cancel. DELETE /api/imports/{uuid} aborts; SPA reflects CANCELLED state.

  • AF-3 — Invalid state transition (409). Surface a clear message and a recovery path: refresh status, retry mapping, abort.

  • AF-4 — No caller key. Fall back to a generic landing (My imports list); do not assume a caller.

  • AF-5 — Fatal job error. Render the failure with reason + a Try again action that returns to the caller’s upload screen.

Acceptance Criteria

  • Use-case page authored.

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

  • :design-url: populated.

  • Cross-references to E06 + E08 use cases.

  • Caller-tracking convention documented (key name, write-time, read-time, fallback).

  • State-transition error (409) UX is concrete and actionable.

API Surface

Call Purpose

GET /api/imports/{uuid}

Fetch current job state + pending mappings.

PUT /api/imports/{uuid}/column-mappings

Submit column mapping corrections (202 Accepted).

PUT /api/imports/{uuid}/cell-mappings

Submit cell mapping corrections (202 Accepted).

GET /api/imports/{uuid}/results

Paginated row-by-row results.

DELETE /api/imports/{uuid}

Cancel the job.

Out of Scope

  • Use-case-specific upload screens — those are E06 / E08 bookends.

  • Use-case-specific summary screens — those are E06 / E08 bookends.

  • Backend changes to async-import — already shipped on develop and deployed to dev.

  • Mapping template authoring — the UI to create / edit / preview reusable mappings. See FU-1 / FU-3 below; not part of the current UI phase.

  • Mapping template apply path — pulled INTO scope by Participant Identity Correlation. See Design Decisions — Per-RegSystem template auto-apply below.

Future Use Cases

The following are forward-looking features identified during a prototype design pass on 2026-04-29. They are not part of the current UI phase and must not be designed or implemented as part of C05’s first delivery. Captured here so the design intent isn’t lost.

FU-1 — Save mapping as template

After resolving column-mapping (and optionally cell-mapping), the operator can save the resulting mapping as a named, reusable template. The persisted template captures the column-mapping array minus file-specific bits.

Scoping:

  • Per tenant.

  • Tagged with eventTypeKey (e.g. road-cycle, mtb-stage, road-running).

  • Tagged with importerKey (participants, results, entrants, …).

Implication: a road-cycle participants template never suggests itself for a result-import upload. Tagging keeps suggestions narrow and operationally meaningful.

FU-2 — Auto-detect template on new upload

When a new file is uploaded for an importer that has saved templates in scope, the system computes a column-shape similarity between the upload and each candidate template. If ≥80% match, surface a dismissible banner above the mapping rows: "Saved template matches this file — <name> · N% match · last used <date>" with two actions:

  • Apply template — overwrite the current auto-suggested mapping with the template’s mapping.

  • Preview template — see FU-3.

  • Dismiss — operator continues with auto-suggest only.

Hard rule: never auto-apply silently. The banner is opt-in. Auto-suggest still runs on first load; the template offer is a second, dismissible signal.

FU-3 — Preview template

Diff view triggered from the FU-2 banner. Shows what would change if the template were applied — which rows of the current mapping would be overwritten, and what each row’s target field would become. Operator can apply or back out without committing.

Cross-cutting

  • Templates apply equally to E06 EP import and E08 result import — the FU work is on C05 itself, callers consume it transparently.

  • Backend API surface is undefined — implies new endpoints (GET /api/import-templates, POST /api/import-templates, POST /api/imports/{uuid}/apply-template) and a template table scoped by tenant + event-type + importer.

  • Implementation prerequisite: the v1 column-mapping screen (current phase) must structure its mapping output in a shape that can be persisted as-is — i.e. don’t conflate file-specific data into the mapping payload.

Prior Art

A v1 prototype design for this screen, generated independently of this .adoc spec, has been archived at prototypes/C05-import-mapping-v1-prototype/ (workspace root). It diverges from the canonical 2-phase mapping flow specified here and is not the active design. It is kept for two reasons:

  • Historical reference — what was tried before the design-first workflow was followed.

  • Source for the Mapping templates concept that has been promoted into this spec as Future Use Cases FU-1 / FU-2 / FU-3 (see above). The prototype’s TemplateBanner and TemplateBox components in import-mapping.jsx are a useful reference for the future-FU work.

The active C05 design must follow the spec on this page, not the prototype.

Design Anchors

Design Decisions

  • Stage bar — read-only, forward-only (2026-04-29). Render a 4-pill stage indicator (Columns · Cells · Processing · Done) showing the current stage and marking prior stages complete. Stages are not clickable. Rationale: backend enforces a strict state machine; PUT /column-mappings is 409 once past COLUMN_MAPPING, so a clickable affordance would advertise an action that fails. Going back is deferred — a future caller can DELETE /api/imports/{uuid} and re-upload.

  • Column-mapping rows — three visual states (2026-04-29). Each unmapped-column row renders in one of three states:

    • Auto-matched — quiet/grey treatment, target field shown, small auto tag, expandable to override.

    • User-set — neutral/strong treatment (no tag), target field shown.

    • Needs mapping — accent colour + required indicator, dropdown prominent.

      Rationale: the auto tag makes a "did I review every server choice?" pass cheap (scan for tags). Changing an auto-matched row drops the tag and promotes it to user-set.

  • Cell-mapping picker — single filterable dropdown for all FK types (2026-04-29). Use one PrimeNG-style filterable dropdown component where the filter input auto-appears once the candidate list exceeds ~10 options. Renders as a plain dropdown for small lists (categories: ~20), as a filter+list for medium lists (schools: ~50), and remains usable for larger reference tables. Rationale: one component scales the full range of expected FK sizes; consistent with registration-portal visual language; can be swapped to server-side search later without changing call sites. Practical sizing target for v1: client-side filtering, lists up to a few hundred entries.

  • "About this screen" information panel (2026-04-29). Persistent collapsible right-rail panel with a short purpose paragraph + bulleted operational details for the current stage (column-mapping vs cell-mapping vs processing vs results). Default expanded, collapse state persisted per screen in localStorage. Stage-aware content trains operators in-flow. Cross-cutting pattern — applied symmetrically in E06 and C04; candidate for promotion to UI Design Principles.

  • Stage-4 ownership — C05 owns PROCESSING; bookend owns post-flight summary; immediate redirect on terminal state (2026-04-29). C05 retains the operator through PROCESSING (progress component, polling loop). On COMPLETED / FAILED / CANCELLED, C05 reads localStorage.importCaller:{uuid} and redirects immediately to the caller’s summary bookend — no intermediate "Done" card; the bookend renders any "N rows processed" header itself. The summary bookend (E06/E08) does NOT poll and shows no progress UI — it loads on arrival, calls GET /api/imports/{uuid}/results once, and renders a pure reconciliation view. Rationale: clean ownership split + the bookend already knows how to visualise its own results; an intermediate pause is distracting.

  • Progress component — future-ready for pre/post-processing sub-stages (2026-04-29). The current backend exposes a single PROCESSING state with a row-by-row counter. The progress component must be structured so a future API can expose sub-stages (e.g. Validating → 15% → 47% → 84% → Summarising → Done) without restructuring the UI. Implementation hint: render a thin sub-stage label above the progress bar, hidden when the API returns no sub-stage; the bar binds to a generic 0–100 percent. No backend change required for v1, but the visual scaffolding lands now.

  • Caller-key — per-uuid scoping (2026-04-29). Caller writes localStorage.setItem('importCaller:' + uuid, 'e06' \| 'e08' \| …) after POST /api/imports returns the uuid, before navigating to C05. C05 reads localStorage.getItem('importCaller:' + uuidFromRoute) and removes the entry once it has used the value to redirect. Rationale: a flat importCaller key collides across tabs running concurrent imports — the second tab’s write clobbers the first, and both redirect-backs land the wrong place. Per-uuid keys make tabs independent. Refresh mid-flow is unaffected (localStorage survives). Missing key on redirect-back falls through to AF-4 (My imports landing).

  • Target-field filter by sourceSystem.is_self (2026-05-04). Column-mapping stage filters available target fields based on the source system selected on the upload screen:

    • When sourceSystem.is_self == trueourEventParticipantId and ourPersonId ARE mappable; sourceSystemPersonId is NOT (hidden).

    • When sourceSystem.is_self == falsesourceSystemPersonId IS mappable; ourEventParticipantId and ourPersonId are NOT (hidden).

    • sourceSystemParticipantId (lands in EP.registrationId) is mappable in BOTH modes.

      Rationale: prevents the operator from mapping a UID column to our Person.id PK target when the file is from an external system. Defence-in-depth — the backend enforces the same constraint, so a UI bypass cannot bypass safety. Closes the bug class that caused the 2026-04-22 wp_users incident. Ref: Participant Identity Correlation. If a file from a non-self system contains an unmappable PK column (e.g. an EPID column), the operator simply leaves it unmapped; the backend silently ignores the column with a one-line warning in the import summary (NOT a hard error).

  • Per-RegSystem mapping-template auto-apply (apply-only; authoring stays future) (2026-05-04). When the operator selects sourceSystem=X on the upload screen, C05 column-mapping stage:

    • If exactly ONE ColumnMappingTemplate exists for (sourceSystem=X, importerKey=<participants\|results>, eventTypeKey=<…​>) — auto-applies the template’s column mappings (rows render as auto — same visual treatment as server-suggested matches; operator can override).

    • If MULTIPLE templates match — surfaces a non-blocking banner above the mapping rows: "N saved templates match this source system: <list> — pick one to apply, or continue with auto-suggest only."

    • If NO templates match — falls through to the default auto-suggest behaviour.

      Authoring (FU-1) and preview (FU-3) remain future work. Apply path is pulled forward because the Participant Identity Correlation design seeds templates for known external systems (WPCA Legacy, Entry Ninja) at migration time.

  • Upload-bookend identity flags replace Mode radio (2026-05-04). The v3 prompt’s Mode: Overwrite | Append radio is dropped. The Participant Identity Correlation match strategy is uniformly "match-then-create-on-miss" — there is no insert-only mode. Replaced with three bookend-screen controls written by the caller (E06 / E08), not by C05 itself, but listed here because they materially shape C05’s downstream stages:

    • sourceSystem dropdown — required, single-select. Lists the tenant’s RegistrationSystem rows. Drives target-field filtering on the column-mapping stage (see Target-field filter by sourceSystem.is_self above) and template auto-apply (see Per-RegSystem mapping-template auto-apply above).

    • trustPKs checkbox — default false. Always visible; disabled with tooltip when the selected sourceSystem.is_self == false (the impossible (is_self=false, trustPKs=true) combination is rejected at validation per the journal’s matrix). Tooltip copy: "Trust file PKs is only available for our own registration systems." Per the project-wide rule on disclosure of policy (see project memory feedback_explain_disabled_ui.md), the disabled state must be self-explanatory at hover.

    • updatePII checkbox — default false. Controls whether matched-EP fields get refreshed from the row per the PersonIdentityUpdatePolicy rules. Always enabled.

      Backend equivalents land on the import job DTO at POST /api/imports; C05 reads them via GET /api/imports/{uuid} and reflects them in stage-aware UI (e.g. the Verifying stage only renders when is_self && trustPKs).

  • Five-pill stage bar — Verifying between Cells and Processing (2026-05-04). The stage bar is now: Columns · Cells · Verifying · Processing · Done. The Verifying pill maps to the FINGERPRINT_CHECK / FINGERPRINT_WARN / FINGERPRINT_ABORT sub-states added to the async-import state machine by the Participant Identity Correlation design. Rationale: making the state machine visible matches the operator’s mental model — pretending the sub-state isn’t there hides a real interactive step. When the bookend’s flags don’t trigger fingerprinting (is_self=false or trustPKs=false), the Verifying pill renders in a skipped state (greyed out, with a small "skipped — file is from an external system" tooltip per the disabled-explanation rule) and the flow transitions Cells → Processing without operator interaction. When it does run, three states are possible:

    • FINGERPRINT_CHECK (in flight) — pill spins; centred panel reads "Verifying file matches our records…​" with a thin progress indicator.

    • FINGERPRINT_WARN (1–2/10 inconsistent) — pill yellow; centred card shows the sample summary (N rows checked, M inconsistent), a per-row consistency table (showing the field-level mismatches), and two buttons: Continue anyway (sends acknowledgeFingerprintWarning=true on the resume call) and Abort import (DELETE the job and return to upload). No stage progression without explicit click.

    • FINGERPRINT_ABORT (≥3/10 inconsistent) — pill red; same sample card but only an Abort import button. Terminal failure; "Try again" returns to the caller’s upload screen.

Notes

Async-import backend is on develop and deployed to dev; testable end-to-end without backend changes. Strict state-machine — 409 on invalid transitions; UX must surface this gracefully.

Caller-key convention: localStorage.setItem('importCaller:' + uuid, 'e06' | 'e08' | …) written by the caller after POST /api/imports returns the uuid; C05 reads importCaller:{uuid} and removes it after redirect-back. See Caller-key — per-uuid scoping under Design Decisions.

Design pass complete 2026-05-04. Bundle archived under design-journal/claude-design/2026-05-04-P8bHfTGZ/. The bundle contains six screen states across project/E06-import-bookend/ (upload.jsx, summary.jsx) and project/C05-import-mapping/ (stage-columns.jsx, stage-cells.jsx, stage-verifying.jsx, stage-processing.jsx, import-shared.jsx). Folder-level READMEs encode the full state-machine, target-field filter, template auto-apply, fingerprint sub-states, and caller-key contract — implementer can drive directly off them. Next step: implementation under Feature #540, USs #550 / #551 / #552 / #553.

Appendix A: Claude Design Prompts

Prompts persisted for audit trail. Most recent first. The v4 prompt is the active hand-off prompt; earlier versions are retained for lessons-learned reference.

v4 — 2026-05-04 — derived from this .adoc (bundled with E06; folds in participant-identity-correlation decisions)

Status: drafted; ready for hand-off. Source: this .adoc at :status: in-design, with the 2026-05-04 Design Decisions (target-field filter, per-RegSystem template auto-apply, upload-bookend identity flags, five-pill stage bar) folded in. Bundling: covers E06 upload → C05 columns → C05 cells → C05 verifying → C05 processing → E06 summary as one cohesive flow; the same prompt is also persisted in E06's appendix. Differences from v3: drops the Mode radio; adds sourceSystem dropdown + trustPKs + updatePII upload-screen controls; adds target-field filtering on column-mapping; adds template auto-apply / multi-match banner; adds a fifth Verifying stage pill with three sub-states (in-flight, warn, abort); enforces the project rule that disabled / hidden / limited affordances are explained inline.

I'm designing a multi-step screen flow 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 existing portal screens you've designed in this project (C01, C03,
C06, E01, E02, E05) — JHipster 8 / Angular 16 / PrimeNG / ng-bootstrap
stack; match fonts, spacing, palette, density.

Design the **import flow**: an event organiser uploads a participants
CSV/XLSX, walks through a strict mapping flow, watches an async job run,
and lands on a reconciliation summary. The flow spans two use-case
screens that must feel like one journey:

  E06 upload  →  C05 columns  →  C05 cells  →  C05 verifying  →  C05 processing  →  E06 summary
   bookend         shared           shared        shared           shared            bookend

The middle four steps (C05) are SHARED — the same screens are reused
later by E08 (result import) with no behaviour change, just a different
caller key. Design accordingly: nothing in C05 may be EP-specific.

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.

============================================================
[E06] Upload screen
============================================================

Single full-screen page with a right-rail "About this screen" panel
(see "Information panel pattern" below).

Main area:
- Page title: "Import participants — <event name>".
- File picker (CSV/XLSX) with drag-and-drop zone.
- **Source system** dropdown (REQUIRED, single-select). Options come
  from the tenant's RegistrationSystem rows. Each option's data
  carries a hidden `is_self: boolean` flag that drives downstream UI
  state. Helper text below: "Where does this file come from? Pick the
  system that produced it."
- **Trust file PKs** checkbox. Default OFF. Always visible.
  - When the selected source system has `is_self == true` → ENABLED.
  - When `is_self == false` → DISABLED with tooltip on hover:
    "Trust file PKs is only available for our own registration
    systems."
  - Helper text below the checkbox: "Use the file's EventParticipant
    and Person IDs as authoritative when matching rows."
- **Update existing PII** checkbox. Default OFF. Always enabled.
  Helper text below: "When a row matches an existing person, refresh
  their name / DOB / gender / contact from the file. Blank values in
  the file never overwrite populated fields."
- "Recent imports" list below the inputs — date, filename, source
  system, row count, status. Clicking a row opens a read-only summary
  of that import (the same E06 summary screen, but for that uuid).
- Primary action: "Upload" (disabled until a file is picked AND a
  source system is selected; tooltip on the disabled button explains
  whichever requirement is missing).

On Upload click:
- POST /api/imports (multipart: file + sourceSystemId + trustPKs +
  updatePII).
- On 201 response, take the returned `uuid` and write
  `localStorage.setItem('importCaller:' + uuid, 'e06')`.
- Navigate to C05 at `/imports/{uuid}`.

============================================================
[C05] Mapping flow — shared 5-stage screen
============================================================

ONE full-screen page that progresses through 5 stages driven by the
backend state machine: COLUMN_MAPPING → CELL_MAPPING → FINGERPRINT_*
→ PROCESSING → COMPLETED/FAILED/CANCELLED.

Persistent UI on every stage:
- **Stage bar** — 5 pills at the top:
    Columns · Cells · Verifying · Processing · Done
  Current stage highlighted; prior stages marked complete; pills are
  READ-ONLY (not clickable). Forward-only flow; going back requires
  cancel + re-upload.
  - The **Verifying** pill renders SKIPPED (greyed out, dotted border
    or similar quiet treatment) when the operator's upload had
    `sourceSystem.is_self == false` OR `trustPKs == false`, because
    the backend skips FINGERPRINT_CHECK in those modes. Tooltip on
    the skipped pill: "Skipped — file is from an external system" or
    "Skipped — Trust file PKs was not enabled" depending on which
    condition skipped it. (Disabled-with-explanation rule.)
- **Right-rail "About this screen" information panel** — collapsible,
  default expanded, collapse state persisted in localStorage per screen.
  Content is STAGE-AWARE: different short purpose paragraph + bulleted
  operational details for each of the 5 stages.
- **Top-right "Cancel import" button** — DELETE /api/imports/{uuid},
  then redirect to caller's summary bookend (E06) with cancelled state.

Stage 1 — COLUMN_MAPPING
  Two-column layout:
  - Left: source CSV columns (header + first 3 sample rows for context).
  - Right: each source column's row showing what it maps to in the
    canonical schema. Three visual row states:
      • AUTO-MATCHED — quiet/grey treatment, target field shown,
        small `auto` tag, expandable to override.
      • USER-SET — neutral/strong treatment (no tag), target field shown.
      • NEEDS MAPPING — accent colour + required indicator, dropdown
        prominent.

  TARGET-FIELD FILTERING by sourceSystem.is_self:
  - When `is_self == true` — `ourEventParticipantId`, `ourPersonId`,
    and `sourceSystemParticipantId` are mappable; `sourceSystemPersonId`
    is NOT in the dropdown.
  - When `is_self == false` — `sourceSystemPersonId` and
    `sourceSystemParticipantId` are mappable; `ourEventParticipantId`
    and `ourPersonId` are NOT in the dropdown.
  - When a column's auto-suggested target was filtered out by this
    rule (e.g. file has an `EPID` column but source system is
    external), the row renders in a fourth visual state: NOT MAPPABLE
    HERE — quiet/grey treatment, target field shows "—" with helper
    icon, hover tooltip: "This column can't be mapped — your source
    system is external. The column will be ignored when the import
    runs." (Disabled-with-explanation rule.) The row is non-blocking;
    operator continues without resolving it.

  TEMPLATE AUTO-APPLY:
  - On stage entry, the SPA calls a (server-resolved) preview that
    indicates how many ColumnMappingTemplate rows match the
    `(sourceSystem, importerKey, eventTypeKey)` tuple.
  - Zero matches → no banner, default auto-suggest behaviour.
  - Exactly ONE match → mapping rows render with the template's
    mappings already applied (visually indistinguishable from
    server-suggested AUTO-MATCHED; same `auto` tag).
  - MULTIPLE matches → non-blocking banner above the mapping rows:
      "N saved templates match this source system: <name1> ·
      <name2> · <name3> — pick one to apply, or continue with
      auto-suggest only."
    Each name is a clickable chip; clicking applies that template
    (rows update to AUTO-MATCHED with the template's targets).
    Banner has a `Dismiss` button to revert to default auto-suggest.

  - Primary action bottom-right: "Continue" — disabled while any row
    is NEEDS MAPPING (NOT MAPPABLE HERE rows do not block).
    Calls PUT /api/imports/{uuid}/column-mappings.

Stage 2 — CELL_MAPPING
  List of unmatched FK values, grouped by FK type (e.g. "Category" group
  with the 3 unrecognised category strings beneath it). Each unmatched
  value gets a single filterable dropdown — ONE component reused across
  all FK types. Filter input auto-appears when the candidate list
  exceeds ~10 options; smaller lists render as a plain dropdown.
  Practical sizing: client-side filtering, hundreds of entries max.
  - Primary action: "Continue" — disabled until every unmatched FK is
    resolved. Calls PUT /api/imports/{uuid}/cell-mappings. (Renamed
    from "Start import" because the next stage might be Verifying,
    not processing.)

Stage 3 — Verifying (FINGERPRINT_CHECK / FINGERPRINT_WARN /
FINGERPRINT_ABORT)
  ONLY runs when `sourceSystem.is_self == true AND trustPKs == true`.
  Otherwise this stage is SKIPPED (pill greyed; flow auto-progresses
  to Processing).

  When running:
  - State `FINGERPRINT_CHECK` (in flight, transient ~1-3s):
    Centred panel reads "Verifying file matches our records..." with
    a thin indeterminate progress indicator. Polls
    GET /api/imports/{uuid}.

  - State `FINGERPRINT_WARN` (1-2 of 10 sample rows inconsistent):
    Stage pill turns YELLOW. Centred card:
    - Title: "We found some unexpected differences"
    - Body: "We checked <N> sample rows from your file against the
      records that match by ID. <M> rows had differences strong
      enough to flag (mismatched name, DOB, or gender)."
    - Sample table: per-row breakdown showing the matched DB record's
      values vs the file row's values, with mismatched fields
      highlighted.
    - Two buttons:
      • PRIMARY-LIKE-WARNING: "Continue anyway" — sends
        `acknowledgeFingerprintWarning=true` on the resume call and
        progresses to Processing.
      • SECONDARY: "Abort import" — DELETE /api/imports/{uuid},
        return to upload screen.

  - State `FINGERPRINT_ABORT` (≥3 of 10 inconsistent — terminal):
    Stage pill turns RED. Centred card:
    - Title: "This file doesn't look like ours"
    - Body: "<M> of <N> sample rows didn't match the records they
      claim to update. The import has been stopped to protect your
      data."
    - Same sample table.
    - Single button: "Abort import" — same DELETE + redirect.
    - No "Continue anyway" — abort is non-overrideable.

Stage 4 — PROCESSING
  Centred progress component:
  - Optional thin sub-stage label above the bar (hidden when the API
    returns no sub-stage). Designed for future API: `Validating →
    Summarising → Done`. v1 backend exposes a single PROCESSING state
    with a row-by-row counter, so the sub-stage label is hidden today
    — but the visual scaffolding is in place.
  - 0–100% progress bar.
  - "<rows processed> of <total> rows" counter.
  - Elapsed time.
  - "Cancel" button (DELETE /api/imports/{uuid}).
  - Polling cadence ~1s.

Stage 5 — Terminal (COMPLETED / FAILED / CANCELLED)
  IMMEDIATE redirect — no intermediate "Done" card.
  - Read `localStorage.getItem('importCaller:' + uuidFromRoute)`.
  - Remove that localStorage entry.
  - Navigate to the matching bookend's summary screen.
  - Fallback if caller key missing: navigate to `My imports` list.

State-error UX:
  - 409 on PUT /column-mappings or /cell-mappings — toast "This step
    has already been completed" + auto-refresh state via
    GET /api/imports/{uuid}.
  - Fatal job error (non-fingerprint) — render the failure with
    reason + a "Try again" action that returns to the caller's
    upload screen.

============================================================
[E06] Summary screen — pure reconciliation view
============================================================

Operator arrives here from C05's terminal-state redirect. The job is
already in a terminal state — this screen does NOT poll, does NOT show
a progress bar, and never reverts to "in-flight" rendering.

On mount:
- Call GET /api/imports/{uuid}/results once.
- Render the reconciliation view.

Layout:
- Page header — "Import complete" (or "Import cancelled" / "Import
  failed"), filename, finished-at timestamp, source system name,
  "N rows processed" counter.
- Right-rail "About this screen" panel (same pattern as C05; copy
  describes what the summary represents and what to do next).
- Body: per-row reconciliation, in tabbed sections:
  - Created / updated counts.
  - Unresolved-person count + drill-down list.
  - FK-mismatch count + drill-down list (e.g. category not found).
  - Merge candidates created (when present): list with link to merge
    review queue. Surfaces external-UID-vs-SAID collisions.
  - Ignored columns (when present): list of columns that were
    auto-filtered out by the source-system rule (e.g. "EPID — column
    ignored because the source system is external"). One-line
    warning, not a hard error.
- Bottom: "Open event participants" (links to E02) + "Import another
  file" (returns to E06 upload).

Failure-mode rendering:
- CANCELLED → header reads "Import cancelled" + the same per-row
  breakdown for whatever was processed before cancel.
- FAILED → header reads "Import failed" + reason + "Try again"
  returning to the upload screen.
- Fingerprint-aborted job (FAILED with fingerprint-abort reason) —
  same FAILED layout, but the sample-mismatch table from C05 is
  preserved at the top of the body so the operator has a reference
  when they investigate the file offline.

============================================================
Information panel pattern (cross-cutting)
============================================================

Persistent collapsible right-rail panel ("About this screen") on every
full-screen step in this flow. Default expanded. Collapse state
persisted per-screen-key in localStorage. Content per screen:
- Short purpose paragraph (1–2 sentences explaining what THIS step is
  for).
- Bulleted operational details (the things an operator might be
  unsure about — what `is_self` means, what `Trust file PKs` enables,
  what `auto` means, what happens on cancel, what fingerprint
  verification is doing, etc).

Goal: train operators in-flow. They shouldn't need external docs to
complete an import.

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

For each of the 6 screen states (E06 upload, C05 columns, C05 cells,
C05 verifying [render all three sub-states: in-flight, warn, abort],
C05 processing, E06 summary):
- HTML/JSX + matching styles.
- Screen README that explains what it does and which API endpoints
  it consumes.
- Cross-screen README that explains:
  - The caller-key contract (per-uuid scoping, who writes, who reads,
    who removes, fallback).
  - The stage-ownership split (C05 owns columns/cells/verifying/
    processing; bookend owns upload + summary).
  - When the Verifying stage is skipped vs run (the
    `is_self && trustPKs` predicate) and how the SKIPPED pill state
    is rendered + tooltipped.
  - How target-field filtering changes when the operator picks
    different source systems on the upload screen.
  - How the template auto-apply + multi-match banner relate to the
    server's stored ColumnMappingTemplate rows.
  - How to reuse C05 unchanged when E08 (result import) becomes the
    caller — the only difference is the literal value written to the
    caller key (`'e06'` vs `'e08'`).

v3 — 2026-04-29 — superseded by v4 (kept for reference)

Status: superseded. Reason: drafted before the 2026-05-04 participant-identity-correlation design pass. v3’s Mode: Overwrite | Append upload control and 4-stage stage bar (no Verifying) are not correct for the current spec. v3 is otherwise structurally aligned and useful for understanding the lineage of the v4 design.

Source: this .adoc at :status: in-design. Bundling: this prompt covers E06 upload → C05 columns → C05 cells → C05 processing → E06 summary as one cohesive flow; the same prompt is also persisted in E06's appendix.

I'm designing a multi-step screen flow 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 existing portal screens you've designed in this project (C01, C03,
E01, E05) — JHipster 8 / Angular 16 / PrimeNG / ng-bootstrap stack;
match fonts, spacing, palette, density.

Design the **import flow**: an event organiser uploads a participants
CSV/XLSX, walks through a strict 2-phase mapping flow, watches an async
job run, and lands on a reconciliation summary. The flow spans two
use-case screens that must feel like one journey:

  E06 upload  →  C05 columns  →  C05 cells  →  C05 processing  →  E06 summary
   bookend         shared           shared        shared            bookend

The middle three steps (C05) are SHARED — the same screens are reused
later by E08 (result import) with no behaviour change, just different
caller key. Design accordingly: nothing in C05 may be EP-specific.

============================================================
[E06] Upload screen
============================================================

Single full-screen page with a right-rail "About this screen" panel
(see "Information panel pattern" below).

Main area:
- Page title: "Import participants — <event name>".
- File picker (CSV/XLSX) with drag-and-drop zone.
- One option: **Mode** — radio: `Overwrite` (default) | `Append`.
- "Recent imports" list below the picker — date, filename, row count,
  status. Clicking a row opens a read-only summary of that import (the
  same E06 summary screen, but for that uuid).
- Primary action: "Upload" (disabled until file picked).

On Upload click:
- POST /api/imports (multipart: file + mode).
- On 201 response, take the returned `uuid` and write
  `localStorage.setItem('importCaller:' + uuid, 'e06')`.
- Navigate to C05 at `/imports/{uuid}`.

============================================================
[C05] Mapping flow — shared 4-stage screen
============================================================

ONE full-screen page that progresses through 4 stages driven by the
backend state machine: COLUMN_MAPPING → CELL_MAPPING → PROCESSING →
COMPLETED/FAILED/CANCELLED.

Persistent UI on every stage:
- **Stage bar** — 4 pills at the top: Columns · Cells · Processing · Done.
  Current stage highlighted; prior stages marked complete; pills are
  READ-ONLY (not clickable). Forward-only flow; going back requires
  cancel + re-upload.
- **Right-rail "About this screen" information panel** — collapsible,
  default expanded, collapse state persisted in localStorage per screen.
  Content is STAGE-AWARE: different short purpose paragraph + bulleted
  operational details for each of the 4 stages.
- **Top-right** "Cancel import" button — DELETE /api/imports/{uuid},
  then redirect to caller's summary bookend (E06) with cancelled state.

Stage 1 — COLUMN_MAPPING
  Two-column layout:
  - Left: source CSV columns (header + first 3 sample rows for context).
  - Right: each source column's row showing what it maps to in the
    canonical schema. Three visual row states:
      • AUTO-MATCHED — quiet/grey treatment, target field shown,
        small `auto` tag, expandable to override.
      • USER-SET — neutral/strong treatment (no tag), target field shown.
      • NEEDS MAPPING — accent colour + required indicator, dropdown
        prominent.
    Operator's job is to scan auto-matched rows (the `auto` tag is the
    review affordance) and resolve any NEEDS MAPPING rows.
  - Primary action bottom-right: "Continue" — disabled while any row is
    NEEDS MAPPING. Calls PUT /api/imports/{uuid}/column-mappings.

Stage 2 — CELL_MAPPING
  List of unmatched FK values, grouped by FK type (e.g. "Category" group
  with the 3 unrecognised category strings beneath it). Each unmatched
  value gets a single filterable dropdown — ONE component reused across
  all FK types. Filter input auto-appears when the candidate list
  exceeds ~10 options; smaller lists render as a plain dropdown.
  Practical sizing: client-side filtering, hundreds of entries max.
  - Primary action: "Start import" — disabled until every unmatched FK
    is resolved. Calls PUT /api/imports/{uuid}/cell-mappings.

Stage 3 — PROCESSING
  Centred progress component:
  - Optional thin sub-stage label above the bar (hidden when the API
    returns no sub-stage). Designed for future API: `Validating → 15%
    → 47% → 84% → Summarising → Done`. v1 backend exposes only one
    PROCESSING state with a row-by-row counter, so the sub-stage label
    is hidden today — but the visual scaffolding is in place.
  - 0–100% progress bar.
  - "<rows processed> of <total> rows" counter.
  - Elapsed time.
  - "Cancel" button (DELETE /api/imports/{uuid}).
  - Polling cadence ~1s.

Stage 4 — Terminal (COMPLETED / FAILED / CANCELLED)
  IMMEDIATE redirect — no intermediate "Done" card.
  - Read `localStorage.getItem('importCaller:' + uuidFromRoute)`.
  - Remove that localStorage entry.
  - Navigate to the matching bookend's summary screen.
  - Fallback if caller key missing: navigate to `My imports` list.

State-error UX:
  - 409 on PUT /column-mappings or /cell-mappings — toast "This step has
    already been completed" + auto-refresh state via GET /api/imports/{uuid}.
  - Fatal job error — render the failure with reason + a "Try again"
    action that returns to the caller's upload screen.

============================================================
[E06] Summary screen — pure reconciliation view
============================================================

Operator arrives here from C05's terminal-state redirect. The job is
already in a terminal state — this screen does NOT poll, does NOT show
a progress bar, and never reverts to "in-flight" rendering.

On mount:
- Call GET /api/imports/{uuid}/results once.
- Render the reconciliation view.

Layout:
- Page header — "Import complete" (or "Import cancelled" / "Import
  failed"), filename, finished-at timestamp, "N rows processed" counter.
- Right-rail "About this screen" panel (same pattern as C05; copy
  describes what the summary represents and what to do next).
- Body: per-row reconciliation, in tabbed sections:
  - Created / updated counts.
  - Unresolved-person count + drill-down list.
  - FK-mismatch count + drill-down list (e.g. category not found).
- Bottom: "Open event participants" (links to E02) + "Import another
  file" (returns to E06 upload).

Failure-mode rendering:
- CANCELLED → header reads "Import cancelled" + the same per-row
  breakdown for whatever was processed before cancel.
- FAILED → header reads "Import failed" + reason + "Try again" returning
  to the upload screen.

============================================================
Information panel pattern (cross-cutting)
============================================================

Persistent collapsible right-rail panel ("About this screen") on every
full-screen step in this flow. Default expanded. Collapse state
persisted per-screen-key in localStorage. Content per screen:
- Short purpose paragraph (1–2 sentences explaining what THIS step is for).
- Bulleted operational details (the things an operator might be unsure
  about — what mode does, what `auto` means, what happens on cancel,
  etc).

Goal: train operators in-flow. They shouldn't need external docs to
complete an import.

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

For each of the 5 screens (E06 upload, C05 columns, C05 cells, C05
processing, E06 summary):
- HTML/JSX + matching styles.
- Screen README that explains what it does and which API endpoints
  it consumes.
- Cross-screen README that explains:
  - The caller-key contract (per-uuid scoping, who writes, who reads,
    who removes, fallback).
  - The stage-ownership split (C05 owns processing; bookend owns summary).
  - How to reuse C05 unchanged when E08 (result import) becomes the
    caller — the only difference is the literal value written to the
    caller key (`'e06'` vs `'e08'`).

v2 — 2026-04-29 — discarded (summary only)

Status: discarded. Reason: 4-stage wizard correctly addressed the 2-phase API but invented details (e.g. "Save mapping as template") not in the spec. Full text not preserved — the journal owner deliberately retained only the structural summary below to avoid anchoring v3 to v2’s structure during the design-first re-derivation. Full prompt content was lost with the chat transcript.

[Long prompt with 4 stages: Upload → Column mapping → Cell mapping → Processing+results;
 attempted to address the 2-phase issue but still didn't reference the canonical `.adoc`,
 invented some details (e.g. "Save mapping as template") that aren't in the spec, and the
 user said "Even the updated prompt is incorrect. Lets take a step back and review how we
 are supposed to design this."]

v1 — 2026-04-29 — discarded

Status: discarded. Reason: collapsed the 2-phase API into a single screen with header matching + data validation in one go. User flagged: "the screen tries to perform header matching and data validation in one go; however, the APIs and underlying process is a distinct 2 step process. Do I misunderstand, or is the UI design wrong?"

I'm designing a screen for the EMS admin portal — a Spring Boot + Angular SPA
admin tool that lets event operators (race directors) manage participants,
events, members, and operational data.

Context: a tenant operator uploads a CSV (or pastes spreadsheet rows) of
participants or race results. The CSV's column headers don't match our
canonical field names — e.g. "First Name" vs our `firstName`, "Email Address"
vs our `email`. C05 is the shared mapping screen used by every import in
the system: participant import, result import, future entrant imports.

Design a screen that:
- Shows the CSV's detected columns on the left (header row + first 3 sample
  rows of data).
- Shows our canonical fields on the right (about 15 fields for participants:
  firstName, lastName, idNumber, idType, email, phone, dateOfBirth, gender,
  category, raceNumber, t-shirtSize, emergencyContact, etc.). Required fields
  are marked.
- Lets the user drag-link or dropdown-select which CSV column maps to which
  canonical field. Auto-suggests obvious matches (case-insensitive name
  similarity).
- Shows real-time validation: missing required mappings, duplicate mappings,
  type mismatches (e.g. mapping "First Name" column to a `dateOfBirth` field).
- Has a "preview parse" pane below the mapping showing what the first 5 rows
  would look like after the mapping is applied. Errors highlighted inline.
- Has a "Save mapping as template" option — operators import the same shape
  of file weekly and shouldn't have to re-map every time. Templates are
  named and per-event-type.
- Bottom toolbar: Cancel, Save Template, Start Import (disabled until
  required mappings are filled and preview is error-free).

Style consistency: this lives inside a Spring Boot gateway portal modeled
on the JHipster 8 / Angular 16 / PrimeNG / ng-bootstrap stack used in
registration-portal. Existing screens you've designed in this project
(C01 user/tenant switcher, C03 navigation shell, E01 event overview, E05
events control centre) should set the visual language — same fonts,
spacing, color palette, density.

Output: HTML/JSX + matching styles, a screen README explaining the data
contract (input CSV shape, output mapping JSON shape), and a 3-line
summary of how this screen is reused across importers.