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>
136 lines
6 KiB
Markdown
136 lines
6 KiB
Markdown
# 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
|
|
|
|
1. **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.
|
|
2. **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.
|
|
3. **Batch Save (server)** — "Confirm & Save All" posts all rows to a new `batch-add-results` intent, which calls `createEventResultsBulk`. Already-existing results for the event are skipped (idempotent).
|
|
4. **Finalize (server, modified `complete` intent)** — Existing flow (mark complete → `processQualifyingEvent`) runs unchanged. Afterward, every season participant without an eventResult for this event gets a `qualifyingPointsAwarded = 0` row. 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.
|
|
|
|
```typescript
|
|
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:**
|
|
```typescript
|
|
{
|
|
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 `results` as a JSON string: `{ participantId: string, placement: number }[]`
|
|
- Loads existing eventResults for the event; filters out any participantId already present (idempotent)
|
|
- Calls existing `createEventResultsBulk` with remaining rows
|
|
- Returns `{ success: "X results saved." }`
|
|
|
|
### New intent: `create-participant`
|
|
|
|
- Receives `name` (string) and `sportsSeasonId` (string)
|
|
- Calls `createParticipant` from `app/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`:
|
|
|
|
1. Load all participants for the sports season
|
|
2. Load all existing eventResults for this event
|
|
3. For each participant without an eventResult, call `createEventResult` with `qualifyingPointsAwarded: 0` and no placement
|
|
4. These 0-QP rows do not affect `recalculateParticipantQP` totals (that function only sums entries where `qp > 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
|
|
- `T` prefix 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 count
|
|
- `create-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` |
|