Fixes #289 * docs: add batch qualifying results entry design spec Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add batch qualifying results entry implementation plan Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: ignore .worktrees directory * feat: add qualifying results text parser with tests * fix: handle no-space after dot separator, tighten T-prefix to uppercase only * feat: add batch-add-results server intent with duplicate filtering Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add create-participant intent for inline participant creation * fix: trim sportsSeasonId validation in create-participant intent * feat: fill 0-QP rows for unplaced participants on qualifying event finalize Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * perf: use bulk insert for 0-QP rows on qualifying event finalize * feat: add BatchResultEntry component for paste-and-parse batch result import Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: disable add participant button while create request is in flight * feat: render BatchResultEntry on qualifying event pages Add BatchResultEntry component import and render it on event pages when the event is a qualifying event, not a final_standings/playoff_game type, and not yet complete. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address final review issues (sportsSeasonId authority, participant validation, CRLF, unused prop) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
6 KiB
Batch Qualifying Results Entry
Date: 2026-04-12
Status: Approved
Overview
Replace the one-at-a-time "Add Result" form on qualifying events with a paste-and-parse batch entry flow. Admins paste a ranked results list, resolve any unmatched names, confirm, and save. On finalize, all remaining season participants automatically receive 0-QP rows so simulations can correctly project remaining events.
The existing single-entry form stays as a fallback for individual corrections after batch import.
Data Flow
- Paste & Parse (client-side) — Admin pastes a ranked text list into a textarea. A pure utility function parses it into
{ placement, rawName }pairs. No server call. - Preview & Resolve (client-side) — Each parsed name is matched against the participants already in loader data (case-insensitive exact match). Unresolved names show a dropdown to map to an existing participant or an inline form to create a new one.
- Batch Save (server) — "Confirm & Save All" posts all rows to a new
batch-add-resultsintent, which callscreateEventResultsBulk. Already-existing results for the event are skipped (idempotent). - Finalize (server, modified
completeintent) — Existing flow (mark complete →processQualifyingEvent) runs unchanged. Afterward, every season participant without an eventResult for this event gets aqualifyingPointsAwarded = 0row. These rows signal "competed, earned nothing" for simulations without affecting QP totals.
Parser (app/lib/parse-results-text.ts)
Pure function, no React or DB dependencies.
interface ParsedLine {
placement: number;
rawName: string;
}
function parseResultsText(text: string): ParsedLine[]
Supported formats:
| Input | Placement |
|---|---|
1. Gerwyn Price |
1 |
1, Gerwyn Price |
1 |
1 Gerwyn Price |
1 |
T3. Luke Littler |
3 (tied) |
T3, Luke Littler |
3 (tied) |
3. Player A then 3. Player B |
3 for both (tied) |
| blank lines | ignored |
Tie detection: T prefix OR repeated rank numbers on consecutive lines. Both surfaces as a T badge in the preview UI — no change to QP calculation logic (ties already handled correctly in processQualifyingEvent).
BatchResultEntry Component (app/components/BatchResultEntry.tsx)
Props:
{
participants: { id: string; name: string }[];
eventId: string;
sportsSeasonId: string;
existingResultParticipantIds: Set<string>;
}
State stages: idle → preview → saving
idle: Textarea with example format placeholder + "Parse Results" button.preview: Table of rows (see below) + "Back" link + "Confirm & Save All" button (disabled until all rows resolved).saving: Spinner on button. On success: inline "✓ X results imported" message +revalidate()to refresh loader data.
Preview table row states:
| State | Visual | Behavior |
|---|---|---|
| Matched | Green badge | Read-only — name matched case-insensitively |
| Unresolved — map | Amber | Dropdown of unplaced participants; selecting resolves the row |
| Unresolved — add new | Red | "Add new participant…" option expands inline form with name pre-filled; on save, new participant is auto-selected |
Placement in page: Rendered above the existing "Add Result" card. Only shown for qualifying events (event.isQualifyingEvent) that are not yet complete.
Server Changes (admin.sports-seasons.$id.events.$eventId.server.ts)
New intent: batch-add-results
- Receives hidden field
resultsas a JSON string:{ participantId: string, placement: number }[] - Loads existing eventResults for the event; filters out any participantId already present (idempotent)
- Calls existing
createEventResultsBulkwith remaining rows - Returns
{ success: "X results saved." }
New intent: create-participant
- Receives
name(string) andsportsSeasonId(string) - Calls
createParticipantfromapp/models/participant.ts - Links new participant to the sports season via the existing join table (verify exact table/column during implementation by reading
findParticipantsBySportsSeasonId) - Returns
{ participantId, participantName }so the component can auto-select the new row
Modified intent: complete (qualifying events)
Existing flow unchanged. This step only runs when event.isQualifyingEvent is true (already the existing gate). After processQualifyingEvent:
- Load all participants for the sports season
- Load all existing eventResults for this event
- For each participant without an eventResult, call
createEventResultwithqualifyingPointsAwarded: 0and no placement - These 0-QP rows do not affect
recalculateParticipantQPtotals (that function only sums entries whereqp > 0) but mark participation for simulation logic
Testing
Unit tests — parser (app/lib/__tests__/parse-results-text.test.ts)
- Dot, comma, and space separators parse correctly
Tprefix is detected as a tie- Repeated rank numbers on consecutive lines both get the same placement
- Blank lines and extra whitespace are ignored
- Mixed formats in one paste work correctly
Unit tests — server actions
batch-add-results: skips existing result participantIds, saves new rows, returns correct countcreate-participant: creates participant and season link, returns id and name- Modified
complete: after processing, all season participants have an eventResult row; 0-QP rows do not change QP totals
No E2E tests
Admin flows are not covered by Cypress; this feature does not touch user-facing pages.
Files Touched
| File | Change |
|---|---|
app/lib/parse-results-text.ts |
New — parser utility |
app/lib/__tests__/parse-results-text.test.ts |
New — parser unit tests |
app/components/BatchResultEntry.tsx |
New — batch paste component |
app/routes/admin.sports-seasons.$id.events.$eventId.tsx |
Add BatchResultEntry above single-entry form |
app/routes/admin.sports-seasons.$id.events.$eventId.server.ts |
Add batch-add-results, create-participant intents; modify complete |