From 64bcc7ad2e8c5728923a0306bd9670657829408e Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Wed, 29 Jul 2026 22:20:50 +0200 Subject: [PATCH] =?UTF-8?q?"Utslag=20brukt"-statistikk=20for=20scramble/gr?= =?UTF-8?q?eensome=20finnes=20n=C3=A5=20ogs=C3=A5=20for=20org-scopede=20tu?= =?UTF-8?q?rneringer=20=E2=80=94=20samme=20funksjon=20som=20frittst=C3=A5e?= =?UTF-8?q?nde=20runder=20fikk=202026-07-25,=20bygget=20som=20en=20rask,?= =?UTF-8?q?=20avgrenset=20oppf=C3=B8lger.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ny kolonne hole_score.selected_participant_id (migrasjon 039), validert til å tilhøre riktig side og kun gyldig for delt-ball-hull (ikke individuell ball). Ny "Hvem sitt utslag ble brukt?"-knapperad i scorekortet for foursome/greensome/scramble-matcher. Ny "Utslag brukt"-oppsummering i "Vis full oversikt", med opptelling per spiller og "registrert på N av M spilte hull". Verifisert med en full org-turnering bygget fra bunnen (org/bane/lag/spillere/foursome-match), 51/51 API-sjekker, og en ekte nettleser-gjennomgang der jeg klikket et faktisk valg og bekreftet det landet riktig i databasen. Migrasjon kjørt, begge containere redeployet, teeoff.no upåvirket. Dermed er "Scramble-grensesnitt" (ARCHITECTURE_DECISIONS.md sitt åpne spørsmål 4) helt avsluttet — begge domener dekket. --- 039_hole_score_selected_participant.sql | 27 ++++ ARCHITECTURE_DECISIONS.md | 26 ++-- CLAUDE.md | 73 +++++++++++ FEATURE_BACKLOG.md | 29 +++-- app/routers/scoring.py | 47 ++++++- frontend/components/session-scorecard.tsx | 151 +++++++++++++++++++++- 6 files changed, 321 insertions(+), 32 deletions(-) create mode 100644 039_hole_score_selected_participant.sql diff --git a/039_hole_score_selected_participant.sql b/039_hole_score_selected_participant.sql new file mode 100644 index 0000000..77aa9f8 --- /dev/null +++ b/039_hole_score_selected_participant.sql @@ -0,0 +1,27 @@ +-- ===================================================================== +-- TeeCup — scramble/greensome i ORG-SCOPEDE turneringer: hvilken +-- spillers utslag ble valgt (migrasjon 039). Samme funksjon som +-- migrasjon 036 ga frittstående runder 2026-07-25 -- bevisst holdt +-- utenfor DEN runden (se dens egen kommentar), tatt fatt på nå som +-- rask, avgrenset oppfølger (FEATURE_BACKLOG.md "Scramble: statistikk +-- over utslag brukt per spiller"). +-- +-- Kun meningsfullt på en DELT-BALL-hull-rad (hole_score. +-- match_participant_id IS NULL) -- håndhevet i app-laget +-- (submit_hole_score), ikke en CHECK, samme mønster som migrasjon 036. +-- Nullable -- helt valgfritt å registrere. +-- +-- Composite FK til match_participant (organization_id, id), samme +-- mønster som hole_score.match_participant_id sin egen FK over -- +-- RLS håndheves via organization_id på hole_score selv, uendret. +-- ON DELETE SET NULL (ikke CASCADE): fjernes en deltaker fra matchen +-- (mulig FØR lås, se matches.py DELETE-endepunkt) skal ikke slette +-- allerede registrerte hull-scorer, kun nullstille selve valget. +-- ===================================================================== +\set ON_ERROR_STOP on + +ALTER TABLE hole_score ADD COLUMN selected_participant_id uuid; + +ALTER TABLE hole_score ADD CONSTRAINT hole_score_selected_participant_fk + FOREIGN KEY (organization_id, selected_participant_id) + REFERENCES match_participant(organization_id, id) ON DELETE SET NULL; diff --git a/ARCHITECTURE_DECISIONS.md b/ARCHITECTURE_DECISIONS.md index 46846a1..d5bbb93 100644 --- a/ARCHITECTURE_DECISIONS.md +++ b/ARCHITECTURE_DECISIONS.md @@ -3614,18 +3614,20 @@ Disse må avklares før eller under de relevante fasene: mellom flere workers/containere. Ved skalering trengs Redis. *(Skalering)* 3. **Prising:** Per organisasjon (abonnement) eller per turnering? Påvirker ikke isolasjonsmodellen, men påvirker fakturerings-/kvotemodell. -4. **Scramble-grensesnitt:** Arkitekturen skal ta høyde for formatet; eksakt - UI-løsning spesifiseres senere. **Utvidet 2026-07-25, DELVIS AVKLART - OG BYGGET 2026-07-28:** brukeren ba om statistikk over hvor mange - utslag hver spiller har hatt (dvs. hvor mange ganger spillerens drive - ble valgt). Bygget for FRITTSTÅENDE RUNDER (ADR-039 sitt delt-ball- - format, `round_hole.selected_participant_id`, migrasjon 036) — se - FEATURE_BACKLOG.md ("Scramble: statistikk over utslag brukt per - spiller") for full detalj. **Fortsatt åpent:** samme funksjon for - org-scopede turnering-scramble (`hole_score`/`match_hole_result`) — - bevisst utenfor omfang i 2026-07-28-runden, samme underliggende - problem som punkt 5 under. Sannsynligvis samme problemstilling for - greensome i begge domener. +4. **Scramble-grensesnitt — ✅ HELT FERDIG 2026-07-29.** Arkitekturen skal + ta høyde for formatet; eksakt UI-løsning spesifiseres senere. + **Utvidet 2026-07-25, DELVIS AVKLART OG BYGGET 2026-07-28:** brukeren + ba om statistikk over hvor mange utslag hver spiller har hatt (dvs. + hvor mange ganger spillerens drive ble valgt). Bygget for FRITTSTÅENDE + RUNDER (ADR-039 sitt delt-ball-format, `round_hole.selected_ + participant_id`, migrasjon 036) — se FEATURE_BACKLOG.md ("Scramble: + statistikk over utslag brukt per spiller") for full detalj. + **Fullført 2026-07-29:** samme funksjon bygget for org-scopede + turnering-scramble/-greensome også (`hole_score.selected_ + participant_id`, migrasjon 039) — samme mønster, se CLAUDE.md-status + 2026-07-29 for full detalj (validering, delt `submitStroke`-utvidelse, + ny `SelectedDriverSummary` i `session-scorecard.tsx`). Begge domener + dekket, ingen kjent gjenstående forskjell mellom scramble og greensome. 5. **Individuell-vs-delt-ball i `hole_score`:** Håndheves i app-laget, ikke av databasen (CHECK når ikke opp til `session.format`). Motoren/API-et må passe på at f.eks. et foursome ikke får per-spiller-scorer. diff --git a/CLAUDE.md b/CLAUDE.md index 25e154f..f97fd29 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5439,6 +5439,79 @@ Ferdig og verifisert: compose up -d --build teecup_api teecup_frontend`. Begge containere boot-et rent, `/health`/`/dashboard` → 200, `teeoff.no` upåvirket. +- **Scramble/greensome: "utslag brukt"-statistikk NÅ OGSÅ for org-scopede + turneringer, BYGGET, GRUNDIG SCRATCH-/BROWSERVERIFISERT OG LIVE + (2026-07-29), samme dag:** direkte oppfølging av brukerens eget forslag + fra "hva nå?"-runden rett etter Stableford-arbeidet -- funksjonen ble + bevisst holdt utenfor migrasjon 036 (frittstående runder, 2026-07-25), + se dens egen kommentar; dette er den avgrensede oppfølgeren. + **Design, ny migrasjon `039_hole_score_selected_participant.sql`:** + `hole_score.selected_participant_id uuid` (nullable), composite FK + `(organization_id, selected_participant_id) REFERENCES match_ + participant(organization_id, id) ON DELETE SET NULL` -- samme mønster + som `hole_score.match_participant_id` sin egen FK, men SET NULL (ikke + CASCADE): fjernes en deltaker fra matchen (mulig FØR lås) skal ikke + slette allerede registrerte hull-scorer, kun nullstille selve valget. + Kun meningsfullt på en DELT-BALL-hull-rad (`match_participant_id IS + NULL`) -- håndhevet i app-laget (`submit_hole_score`), ikke en CHECK, + samme mønster som migrasjon 036. + **Backend (`app/routers/scoring.py`):** `HoleScoreCreate`/`HoleScoreOut` + fikk `selected_participant_id`. Validering: avvist (400 + `WRONG_PARTICIPANT_MODE`) for individuell ball (`match_participant_id` + satt) -- gir ingen mening der. For delt ball: validert å tilhøre + matchen OG samme `team_side` som selve scoren (400 `VALIDATION_FAILED` + ellers), speiler `round.py` sin identiske sjekk for frittstående runder. + Begge INSERT/UPSERT-setningene i `submit_hole_score` oppdatert (delt- + ball-grenen skriver/oppdaterer feltet, individuell-ball-grenen inkluderer + det aldri i det hele tatt -- alltid NULL der). `get_scorecard` sin + `stroke_entries`-SELECT utvidet -- feltet flyter automatisk gjennom + siden den allerede delte `HoleScoreOut`-modellen gjenbrukes. + **Frontend (`session-scorecard.tsx`):** ny "Hvem sitt utslag ble + brukt?"-knapperad (samme mønster som round-detail.tsx sin + `SideScoreWizard` fikk 2025-07-25) under `StrokePicker` for delt-ball- + enheter -- vises KUN når et slagtall allerede er registrert for hullet + (ulikt frittstående runders `round_hole`, som alltid forhåndsopprettes + med nullbar score, krever `hole_score`-raden faktisk å EKSISTERE først, + siden `gross_strokes` er `NOT NULL` i skjemaet -- en reell strukturell + forskjell fra migrasjon 036s mønster, løst ved å gjenbruke samme + `submitStroke`-funksjon med et nytt valgfritt fjerde argument som + enten beholder gjeldende valg (utelatt) eller setter et nytt (inkl. + eksplisitt `null` for å fjerne). Ny `SelectedDriverSummary`-komponent + (samme presentasjon som round-stats.tsx sin tilsvarende, tilpasset + denne sidens `teams`/`match.participants`/`scorecard.stroke_entries`- + datform) lagt inn i den eksisterende "Vis full oversikt"-seksjonen. + **Scratch-verifisert grundig** (isolert `teecup_app_scratch`-rolle + + isolert scratch-MinIO + engangs API-container, alle 39 migrasjoner + kjørt friskt): en FULL org-turnering-scaffold bygget fra bunnen via API + for FØRSTE gang i en test for akkurat denne funksjonen (org/egendefinert + bane/18 hull/tee/turnering/to lag/fire spillere/roster/foursome-økt+ + match/fire deltakere/lås) -- 51/51 sjekker: skriving uten valg, skriving + med valg (gross_strokes bevart), feil-side-valg avvist (400 + VALIDATION_FAILED), individuell-ball+valg avvist (400 + WRONG_PARTICIPANT_MODE), persistens bekreftet via `GET .../scorecard`, + korrekt opptelling (2 vs. 1 utslag), eksplisitt `null` fjerner valget. + `test_isolation.sql` 12/12 uendret (additiv migrasjon). Ekte typesjekket + produksjonsbuild kjørt og bekreftet. + **Deretter en FULL, ekte nettleser-gjennomgang** (Chrome DevTools MCP, + isolert scratch-backend, inkl. et ekte 2FA-oppsett for en fersk + organisasjonseier siden ADR-021 krever det): naviger til foursome- + matchens scorekort, bekreftet at allerede-registrerte valg (satt via + API) vises korrekt som trykte knapper for RIKTIG spiller på RIKTIG + hull -- OG et ekte klikk i UI-et som byttet valgt spiller for et hull, + bekreftet persistert direkte i databasen etterpå (ikke bare at UI-et + så riktig ut). "Vis full oversikt" sin nye "Utslag brukt"-seksjon + bekreftet med nøyaktig riktig opptelling for begge lag (2/1 og 1/0, + inkl. "Registrert på N av M spilte hull"-teksten). Ingen konsollfeil. + **Rullet ut live 2026-07-29**, bruker bekreftet eksplisitt (plan vist + FØR migrasjonen): migrasjon 039 kjørt mot ekte `teecup_db` (kolonne + + FK-constraint bekreftet, `test_isolation.sql` fortsatt 12/12), deretter + `docker compose up -d --build teecup_api teecup_frontend`. Begge + containere boot-et rent, `/health`/`/dashboard` → 200, `teeoff.no` + upåvirket. **"Utslag brukt"-statistikken finnes dermed nå for BEGGE + domener** (frittstående runder siden 2026-07-25, org-scopede + turneringer siden i dag) -- se ARCHITECTURE_DECISIONS.md sitt åpne + spørsmål 4 (Scramble-grensesnitt), som dermed er helt avsluttet. + Neste steg: 0a. **Spillerliste-redesign — nå FAKTISK nettleser-bekreftet (2026-07-27, full 22-skjerms gjennomgang):** rendrer korrekt, ingen diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md index 2572324..815c704 100644 --- a/FEATURE_BACKLOG.md +++ b/FEATURE_BACKLOG.md @@ -1508,7 +1508,7 @@ blant minst to fullførte runder). Full detalj i CLAUDE.md sin statuslogg --- -## Scramble: statistikk over utslag brukt per spiller — ✅ BYGGET OG LIVE 2026-07-28 (frittstående runder) +## Scramble: statistikk over utslag brukt per spiller — ✅ HELT FERDIG (frittstående runder 2026-07-28, org-scopede turneringer 2026-07-29) Brukeren ba om at det i scramble-turneringer skal føres statistikk over hvor mange utslag hver spiller har hatt (dvs. hvor mange ganger den @@ -1567,15 +1567,24 @@ punktene denne dagen): PATCH med valgt spiller lykkes, feil side avvist (`selected_participant_id` pekte riktig), OG bekreftet "Utslag brukt"- oppsummeringen viste "Gjest Partner: 1 utslag" korrekt på statistikksiden, ingen konsollfeil. -**Bevisst utenfor omfang:** org-scopede turnering-scramble/greensome -(`hole_score`/`match_hole_result`, delt mellom lag i en Ryder Cup- -tourney) har samme strukturelle hull, men ble IKKE bygget i denne -runden -- appens hovedfokus er frittstående runder (ADR-033/035/036), og -å bygge samme funksjon i to atskilte datamodeller i samme økt som tre -andre store leveranser ble vurdert som unødvendig risiko. Notert her, -ikke glemt, hvis/når turnering-scramble tas fatt på som egen sak. -**Rullet ut live 2026-07-28**, se CLAUDE.md-status for felles -utrullingsdetalj med de tre andre punktene samme dag. +**Org-scopede turneringer — ✅ BYGGET, SCRATCH-/BROWSERVERIFISERT OG LIVE +2026-07-29,** rask, avgrenset oppfølger rett etter Stableford-runden +samme uke: `hole_score.selected_participant_id` (migrasjon 039), samme +composite-FK-mønster som `match_participant_id` sin egen FK men `ON +DELETE SET NULL` (fjernes en deltaker skal ikke slette allerede +registrerte scorer). Validering i `submit_hole_score` (app/routers/ +scoring.py): avvist for individuell ball, validert til samme `team_side` +som scoren for delt ball. Ny "Hvem sitt utslag ble brukt?"-knapperad i +`session-scorecard.tsx` (vises kun når et slagtall allerede er +registrert, siden `hole_score.gross_strokes` er `NOT NULL` -- en reell +strukturell forskjell fra `round_hole`, som alltid forhåndsoppretter en +nullbar rad). Ny `SelectedDriverSummary` i "Vis full oversikt"- +seksjonen. 51/51 scratch-sjekker (full org-turnering-scaffold bygget fra +bunnen) + full nettleser-gjennomgang (ekte klikk, persistens bekreftet +direkte i databasen, riktig opptelling). Se CLAUDE.md-status 2026-07-29 +for full detalj. **Begge domener (frittstående runder og org-scopede +turneringer) dekker nå samme funksjon** -- ingen kjent gjenstående +forskjell mellom scramble og greensome i noen av domenene. --- diff --git a/app/routers/scoring.py b/app/routers/scoring.py index 10a304f..0e224b5 100644 --- a/app/routers/scoring.py +++ b/app/routers/scoring.py @@ -341,6 +341,10 @@ class HoleScoreCreate(BaseModel): match_participant_id: str | None = None hole_number: int = Field(ge=1, le=18) gross_strokes: int = Field(ge=1, le=20) + # Scramble/greensome: hvem sitt utslag ble valgt (migrasjon 039, samme + # funksjon som frittstående runder fikk 2026-07-25). KUN meningsfullt for + # delt-ball-hull (match_participant_id er None) -- helt valgfritt. + selected_participant_id: str | None = None class HoleScoreOut(BaseModel): @@ -348,6 +352,7 @@ class HoleScoreOut(BaseModel): team_side: str match_participant_id: str | None gross_strokes: int + selected_participant_id: str | None = None @router.post( @@ -416,6 +421,29 @@ async def submit_hole_score( 400, "MISMATCHED_SIDE", "match_participant_id tilhører ikke angitt side." ) + # Scramble/greensome: hvem sitt utslag ble valgt (migrasjon 039) -- + # gir kun mening for delt-ball-hull, og må tilhøre samme side som + # selve scoren registreres for (samme sjekk som round.py sin + # tilsvarende validering for frittstående runder). + if body.selected_participant_id is not None: + if body.match_participant_id is not None: + raise app_error( + 400, + "WRONG_PARTICIPANT_MODE", + "selected_participant_id gjelder kun delt-ball-hull, ikke individuell ball.", + ) + selected = await conn.fetchrow( + "SELECT team_side::text AS team_side FROM match_participant WHERE id = $1 AND match_id = $2", + body.selected_participant_id, + match_id, + ) + if selected is None: + raise app_error(404, "NOT_FOUND", "Den valgte spilleren finnes ikke i denne matchen.") + if selected["team_side"] != body.team_side: + raise app_error( + 400, "VALIDATION_FAILED", "Valgt spiller hører ikke til denne siden." + ) + if not await user_is_match_participant( conn, organization_id, match_id, user.user_id, team_side=body.team_side ): @@ -434,7 +462,8 @@ async def submit_hole_score( ON CONFLICT (match_participant_id, hole_number) WHERE match_participant_id IS NOT NULL DO UPDATE SET gross_strokes = EXCLUDED.gross_strokes, updated_at = now() RETURNING hole_number, team_side::text AS team_side, - match_participant_id::text AS match_participant_id, gross_strokes + match_participant_id::text AS match_participant_id, gross_strokes, + selected_participant_id::text AS selected_participant_id """, organization_id, match_id, @@ -447,18 +476,23 @@ async def submit_hole_score( row = await conn.fetchrow( """ INSERT INTO hole_score - (organization_id, match_id, team_side, match_participant_id, hole_number, gross_strokes) - VALUES ($1, $2, $3, NULL, $4, $5) + (organization_id, match_id, team_side, match_participant_id, hole_number, + gross_strokes, selected_participant_id) + VALUES ($1, $2, $3, NULL, $4, $5, $6) ON CONFLICT (match_id, team_side, hole_number) WHERE match_participant_id IS NULL - DO UPDATE SET gross_strokes = EXCLUDED.gross_strokes, updated_at = now() + DO UPDATE SET gross_strokes = EXCLUDED.gross_strokes, + selected_participant_id = EXCLUDED.selected_participant_id, + updated_at = now() RETURNING hole_number, team_side::text AS team_side, - match_participant_id::text AS match_participant_id, gross_strokes + match_participant_id::text AS match_participant_id, gross_strokes, + selected_participant_id::text AS selected_participant_id """, organization_id, match_id, body.team_side, body.hole_number, body.gross_strokes, + body.selected_participant_id, ) await recompute_and_cache_match_state(conn, match_id) @@ -588,7 +622,8 @@ async def fetch_scorecard(conn, match_id: str) -> Scorecard: rows = await conn.fetch( """ SELECT hole_number, team_side::text AS team_side, - match_participant_id::text AS match_participant_id, gross_strokes + match_participant_id::text AS match_participant_id, gross_strokes, + selected_participant_id::text AS selected_participant_id FROM hole_score WHERE match_id = $1 ORDER BY hole_number """, match_id, diff --git a/frontend/components/session-scorecard.tsx b/frontend/components/session-scorecard.tsx index 6c9a34d..80e2b19 100644 --- a/frontend/components/session-scorecard.tsx +++ b/frontend/components/session-scorecard.tsx @@ -70,6 +70,10 @@ type ApiHoleScore = { team_side: "a" | "b" match_participant_id: string | null gross_strokes: number + // Scramble/greensome: hvem sitt utslag ble valgt (migrasjon 039, samme + // funksjon som frittstående runder fikk 2026-07-25). Kun satt for + // delt-ball-rader (match_participant_id null). + selected_participant_id: string | null } type ApiHoleResult = { hole_number: number; winning_side: "a" | "b" | null } @@ -205,13 +209,19 @@ export function SessionScorecard({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [matchId]) - async function queueStroke(holeNumber: number, unit: Unit, grossStrokes: number) { + async function queueStroke( + holeNumber: number, + unit: Unit, + grossStrokes: number, + selectedParticipantId: string | null, + ) { const url = `/orgs/${organizationId}/matches/${matchId}/hole-scores` const body = { team_side: unit.side, match_participant_id: unit.matchParticipantId, hole_number: holeNumber, gross_strokes: grossStrokes, + selected_participant_id: selectedParticipantId, } await enqueueWrite({ url, method: "POST", body, matchId }) setPendingStrokes((prev) => new Map(prev).set(strokeKey(holeNumber, unit), grossStrokes)) @@ -320,10 +330,30 @@ export function SessionScorecard({ const decided = scorecard?.points_side_a !== null && scorecard?.points_side_b !== null - async function submitStroke(holeNumber: number, unit: Unit, grossStrokes: number) { + // Scramble/greensome: hvem sitt utslag som ble valgt for et allerede + // registrert hull (migrasjon 039) -- lest fra scorekortet, ikke lagret + // lokalt separat. + function selectedDriverFor(holeNumber: number, unit: Unit): string | null { + const entry = (scorecard?.stroke_entries ?? []).find( + (e) => e.hole_number === holeNumber && e.team_side === unit.side && e.match_participant_id === unit.matchParticipantId, + ) + return entry?.selected_participant_id ?? null + } + + // `selectedParticipantId` utelatt = behold gjeldende valg uendret (brukt + // av selve slagregistreringen); eksplisitt sendt (inkl. `null`) = et nytt + // utslags-valg (brukt av "hvem sitt utslag ble brukt"-knappene under). + async function submitStroke( + holeNumber: number, + unit: Unit, + grossStrokes: number, + selectedParticipantId?: string | null, + ) { + const resolvedSelection = + selectedParticipantId !== undefined ? selectedParticipantId : selectedDriverFor(holeNumber, unit) setError(null) if (!navigator.onLine) { - await queueStroke(holeNumber, unit, grossStrokes) + await queueStroke(holeNumber, unit, grossStrokes, resolvedSelection) return } let res: Response @@ -337,12 +367,13 @@ export function SessionScorecard({ match_participant_id: unit.matchParticipantId, hole_number: holeNumber, gross_strokes: grossStrokes, + selected_participant_id: resolvedSelection, }), }) } catch { // Ekte nettverksfeil (ikke bare et avvist svar) -- køordne i stedet // for å vise en feil, se ADR-028. - await queueStroke(holeNumber, unit, grossStrokes) + await queueStroke(holeNumber, unit, grossStrokes, resolvedSelection) return } if (!res.ok) { @@ -646,6 +677,13 @@ export function SessionScorecard({ scoringMode={session.scoring_mode} holeConfig={session.hole_config} /> + {!isIndividual && session.scoring_mode === "stroke" && match && ( + + )} )} @@ -749,6 +787,43 @@ export function SessionScorecard({ isPending={isPending} onSelect={(n) => void submitStroke(currentHole, unit, n)} /> + {/* Scramble/greensome: hvem sitt utslag ble valgt + (migrasjon 039, samme funksjon som frittstående + runder fikk 2026-07-25). Krever et registrert + slagtall å knytte valget til -- hole_score-raden + finnes ikke før da (ulikt round_hole, som alltid + forhåndsopprettes). Helt valgfritt, egen handling. */} + {!isIndividual && value !== null && match && ( +
+ + Hvem sitt utslag ble brukt?{" "} + (valgfritt) + +
+ {match.participants + .filter((p) => p.team_side === unit.side) + .map((p) => { + const active = selectedDriverFor(currentHole, unit) === p.id + return ( + + ) + })} +
+
+ )} ) })} @@ -1027,6 +1102,74 @@ function IdentityBlock({ ) } +// Scramble/greensome: "utslag brukt" per spiller (migrasjon 039, samme +// funksjon som frittstående runder fikk 2026-07-25, se round-stats.tsx sin +// tilsvarende SelectedDriverSummary). Ren klientside-telling fra allerede +// hentet scorecard-data -- vises kun de hullene som faktisk har et +// registrert valg, ingen antakelse om at ALLE hull må ha ett. +function SelectedDriverSummary({ + teams, + participants, + strokeEntries, +}: { + teams: [ApiTeam, ApiTeam] + participants: ApiParticipant[] + strokeEntries: ApiHoleScore[] +}) { + const rows = (["a", "b"] as const).map((side, idx) => { + const sideEntries = strokeEntries.filter((e) => e.team_side === side && e.match_participant_id === null) + const withSelection = sideEntries.filter((e) => e.selected_participant_id !== null) + const counts = new Map() + for (const e of withSelection) { + const pid = e.selected_participant_id as string + counts.set(pid, (counts.get(pid) ?? 0) + 1) + } + const sideParticipants = participants.filter((p) => p.team_side === side) + return { + team: teams[idx], + playedCount: sideEntries.length, + registeredCount: withSelection.length, + entries: sideParticipants + .map((p) => ({ name: p.player_name, count: counts.get(p.id) ?? 0 })) + .sort((a, b) => b.count - a.count), + } + }) + + if (rows.every((r) => r.registeredCount === 0)) { + return ( +
+ Ingen har registrert hvem sitt utslag som ble brukt på noe hull ennå -- valgfritt felt i + scoreregistreringen. +
+ ) + } + + return ( +
+ Utslag brukt + {rows.map(({ team, playedCount, registeredCount, entries }) => ( +
+ + +
    + {entries.map((e) => ( +
  • + {e.name} + {e.count} utslag +
  • + ))} +
+ + Registrert på {registeredCount} av {playedCount} spilte hull. + +
+ ))} +
+ ) +} + function TournamentMatchGrid({ teams, units,