From 629197d22ab0cbabd94e284ce3e441aa9ffb7000 Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Sat, 22 Aug 2026 07:49:05 +0200 Subject: [PATCH] =?UTF-8?q?Fullt=20flerspiller-scorekort-grid=20+=20offlin?= =?UTF-8?q?e-k=C3=B8=20for=20score-f=C3=B8ring=20(ADR-104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Punkt 4 og 1 i scoring-paritet-gap-listen, bygget samtidig (samme fil, begge små/uavhengige endringer). Ny TournamentScorecardGrid erstatter pille-rad + enkelt-spiller-HoleGrid (nå slettet) med et ekte alle-spillere×alle-hull-rutenett, samme struktur som round-detail.tsx sin ScorecardGrid men denne filens egne classify/scoreMarkClasses. updateHoleFull fikk samme offline-kø-/409-håndtering som round-detail.tsx sitt etablerte mønster (ADR-028/057/060), samme delte offline-queue.ts uendret. Ingen backend-endring. Ikke rullet ut mot ekte containere ennå -- venter på brukerbekreftelse. Co-Authored-By: Claude Sonnet 5 --- ARCHITECTURE_DECISIONS.md | 92 +++ CHANGELOG.md | 39 ++ FEATURE_BACKLOG.md | 27 +- .../individual-tournament-detail.tsx | 586 +++++++++++++----- 4 files changed, 559 insertions(+), 185 deletions(-) diff --git a/ARCHITECTURE_DECISIONS.md b/ARCHITECTURE_DECISIONS.md index c814837..f2c776a 100644 --- a/ARCHITECTURE_DECISIONS.md +++ b/ARCHITECTURE_DECISIONS.md @@ -10432,6 +10432,98 @@ Mapbox-token-miljøvariabel underveis) ryddet opp etter verifisering. --- +## ADR-104: Fullt flerspiller-scorekort-grid + offline-kø for score-føring i ScoreTab (2026-08-22) + +**Kontekst:** punkt 4 og 1 i "Scoring-paritet: frittstående runde vs. +org-turnering"-gap-listen (FEATURE_BACKLOG.md), bedt om samtidig av +bruker siden punkt 1 er en liten oppgave og begge berører samme fil +(`ScoreTab` i `individual-tournament-detail.tsx`). Ingen backend- +endring i noen av delene -- ren frontend-runde. + +**Punkt 4 -- flerspiller-scorekort-grid.** + +**Beslutning A -- gjenbruk denne filens EGNE `classify`/ +`scoreMarkClasses`, ikke `round-detail.tsx` sin `ScorecardCell`.** Den +nye `TournamentScorecardGrid`-komponenten speiler `ScorecardGrid` +strukturelt (sticky spillerkolonne, hull-kolonner gruppert Ut(1-9)/ +Inn(10-18) på fysisk hullnummer, Sum-kolonne), men bruker +`individual-tournament-detail.tsx` sitt eget score-tone-språk (allerede +brukt av flight-kortlisten) for visuell konsistens innad i filen, +fremfor å importere `round-detail.tsx` sin lokale celle-komponent. + +**Beslutning B -- droppet ved porting, bekreftet trygt.** Ingen +`sides`/`isTwoSided`/`formatResult`/match-fargelegging/"Stilling"-rad -- +individuelle turneringer har ALDRI et side-begrep (denne grenen i +`ScorecardGrid` er alltid death-path for ren slagspill uansett, bekreftet +ved kodesøk). Ingen "plukket opp"/PU-merkelapp -- `tournament_round_hole` +har ingen tilsvarende kolonne. + +**Beslutning C -- gridet ERSTATTER pille-rad + enkelt-spiller-`HoleGrid`, +ikke lagt til ved siden av.** `HoleGrid`-komponenten (kun brukt fra dette +ene stedet) ble slettet -- ingen gjenværende kallesteder. Cellene tapper +DIREKTE inn i den eksisterende `TournamentScoringWizard`-mounten (samme +instans som hull-navigatoren/flight-kortlisten over bruker) ved å sette +`activeHole` + `wizardPlayerId` -- ingen ny wizard-instans, ingen separat +`sheetHole`-state slik det gamle `HoleGrid` hadde. + +**Beslutning D -- datahenting utvidet, men betinget.** `holesByParticipant`- +hente-effekten hentet tidligere kun flighten + valgt spiller. Gridet +trenger ALLE `visibleParticipants` sine hull (avhenger av "Vis hele +feltet"-bryteren) -- effekten utvidet til å dekke hele det synlige feltet, +men KUN når `showFullScorecard`-seksjonen faktisk er åpen (unngår +unødvendig henting når den er kollapset). `showFullScorecard`-state +flyttet tidligere i komponenten (var deklarert etter det opprinnelige +bruksstedet) slik at hente-effekten kan lese den. + +**Punkt 1 -- offline-kø for score-føring.** + +**Beslutning E -- speiler `round-detail.tsx` sitt +`queueParticipantHoleWrite`/`updateWizardStat`-mønster (ADR-028/057/060) +nøyaktig, samme delte `frontend/lib/offline-queue.ts` uendret.** +`updateHoleFull` fikk samme tre-veis gren: `!navigator.onLine` → kø +umiddelbart; `fetch` kaster (ekte nettverksfeil) → samme kø-fallback; +`res.status === 409` → konfliktvarsel + reload for den deltakeren (manglet +HELT tidligere -- `!res.ok` kastet uansett statuskode). Andre `!res.ok` +kaster fortsatt en generisk feil (uendret fra før -- kun 409 og ekte +nettverksfeil fikk ny håndtering, ingen regresjon på ekte +valideringsfeil). `patch.expected_version` er allerede satt av +`TournamentScoringWizard` sin `save()` (`expected_version: hole.version`) +-- ingen eget versjonsoppslag trengs i kø-hjelperen, ulikt +round-detail.tsx sin variant (som har flere kallesteder uten den +garantien). + +**Beslutning F -- samme tre synk-utløsere, samme banner-språk.** +`window` sitt `"online"`-event, én engangssjekk ved mount +(`queueCount(roundId)`), og en manuell "Synkroniser nå"-knapp -- pluss +identisk offline-/venter-banner-markup (amber, `WifiOff`-ikon) plassert +øverst i `ScoreTab`s JSX (synlig uansett hvilken underseksjon som er +åpen). `TournamentScoringWizard.save()` trengte INGEN endring -- den +kaster kun videre hvis `onSave` selv kaster, og en kølagt skriving kaster +aldri (resolver normalt), så arket lukkes/avanserer allerede riktig ved +en offline-registrering. + +**Status:** `tsc --noEmit` rent, 188/188 pytest uendret (ingen +backend-endring i det hele tatt). Full-stack scratch (egen db-/API-/ +MinIO-container, ekte innlogging, ingen fetch-mocking) med en flight på 3 +spillere (én med delvis utfylt scorekort fra før) bekreftet: gridet viser +alle tre samtidig med riktig Ut/Inn/Sum/golfscore-form, tapp på en +annen spillers celle enn den aktive åpner riktig wizard for akkurat den +spilleren+hullet (kjeden/"Neste"-navigasjonen uendret), et reelt 403 +(selv-only-avvisning, forsøkte lagre for en annen deltaker som vanlig +medlem) bekreftet UENDRET riktig oppførsel fra før. Offline-kø: frakoblet +→ registrerte score → banner + optimistisk grid-oppdatering uten +feilmelding → tilkoblet → automatisk synk (banner forsvant) → bekreftet +direkte mot databasen at BÅDE det online og det kølagte hullet faktisk +ble lagret riktig server-side. Mørk modus + smal mobilskjerm. 409- +konflikthåndteringen ble IKKE eksplisitt live-reprodusert denne runden +(vurdert lavere presserende å simulere enn de to nye atferdene over, +siden selve mekanismen -- `expected_version`/409-semantikk -- allerede +er bevist riktig på nøyaktig dette endepunktet fra tidligere runder, +og round-detail.tsx sitt identiske mønster er langvarig i produksjon). +Alle scratch-ressurser ryddet opp. + +--- + ## Utviklingsplan (rekkefølge) 1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0dba9b..9d6e908 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14514,3 +14514,42 @@ Neste steg: kjøring). `docker compose build teecup_api teecup_frontend && up -d`, begge containere friske, rene logger. `https://teecup.golf/health` 200 etter utrulling. + +162. **Fullt flerspiller-scorekort-grid + offline-kø for score-føring i + ScoreTab (ADR-104), 2026-08-22 -- punkt 4 og 1 i gap-listen, bedt om + samtidig siden punkt 1 er en liten oppgave.** Ren frontend-runde, + ingen backend-endring. + + **Punkt 4:** "Vis hele scorekortet" viste tidligere kun en navne- + pille-rad + ett enkelt-spiller-`HoleGrid` (bytt HVEM som vises). Ny + `TournamentScorecardGrid` speiler `round-detail.tsx` sin + `ScorecardGrid` strukturelt (sticky spillerkolonne, Ut/Inn/Sum), men + bruker denne filens EGNE `classify`/`scoreMarkClasses` for visuell + konsistens med flight-kortlisten. `HoleGrid` (kun ett kallsted) + slettet -- cellene tapper direkte inn i den eksisterende + `TournamentScoringWizard`-mounten (setter `activeHole` + + `wizardPlayerId`), ingen ny wizard-instans. Hente-effekten for + `holesByParticipant` utvidet til å dekke hele det synlige feltet, + men kun når seksjonen faktisk er åpen. + + **Punkt 1:** `updateHoleFull` hadde ingen offline-kø-kobling -- + speiler nå `round-detail.tsx` sitt `queueParticipantHoleWrite`/ + `updateWizardStat`-mønster (ADR-028/057/060) nøyaktig, samme delte + `frontend/lib/offline-queue.ts` uendret. Samme banner-språk, samme + tre synk-utløsere (online-event, mount-sjekk, manuell knapp). 409- + håndtering lagt til for første gang i denne filen (manglet helt før). + + **Verifisert:** `tsc --noEmit` rent, 188/188 pytest uendret (ingen + backend rørt). Full-stack scratch (egen db/API-/MinIO-container, + ekte innlogging, ingen fetch-mocking) med en flight på 3 spillere: + gridet viser alle samtidig med riktig Ut/Inn/Sum/golfscore-form, + tapp på annen spiller enn aktiv åpner riktig wizard for nøyaktig den + spilleren+hullet, et reelt 403 (selv-only-avvisning) bekreftet + uendret riktig. Offline-kø: frakoblet → registrert → banner + + optimistisk oppdatering uten feil → tilkoblet → automatisk synk → + bekreftet direkte mot databasen at BÅDE det online og det kølagte + hullet ble lagret riktig. Mørk modus + smal mobilskjerm. Alle + scratch-ressurser ryddet opp. + + **IKKE rullet ut ennå** -- venter på brukerbekreftelse (ingen + migrasjon -- kun `docker compose build teecup_frontend && up -d`). diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md index 49e18d5..c8cada1 100644 --- a/FEATURE_BACKLOG.md +++ b/FEATURE_BACKLOG.md @@ -4946,14 +4946,11 @@ flyter), kombinert frittstående+turnering hull-historikk (ADR-071). **Gjenstående gap, rangert etter antatt størrelse/verdi (frittstående runde HAR, org-turnering MANGLER, om ikke annet er sagt):** -1. **Offline-kø for score-føring.** `round-detail.tsx` (og - `session-scorecard.tsx` for lag-kamper) bruker `frontend/lib/ - offline-queue.ts` (IndexedDB, ADR-028) ved tapt nett. `ScoreTab`/ - `individual-tournament-detail.tsx` har INGEN referanse til denne - koen i det hele tatt -- mister score ved dårlig dekning på banen. - Antatt **liten** jobb: biblioteket er allerede generisk (bevist ved - at lag-kamper bruker det), ingen backend-endring, kun kabling rundt - PATCH-kallet i ScoreTab. +1. ✅ **Offline-kø for score-føring -- FERDIG BYGGET 2026-08-22 + (ADR-104), se CHANGELOG punkt 162.** `updateHoleFull` i `ScoreTab` + speiler nå `round-detail.tsx` sitt kø-/synk-mønster nøyaktig, samme + delte `frontend/lib/offline-queue.ts` uendret. Bygget samtidig med + punkt 4 (se under). 2. **Sanntidsoppdatering (live push) i den autentiserte scorings-/ leaderboard-visningen.** `round-detail.tsx` åpner en websocket (`/ws/rounds/{roundId}/live`) og oppdaterer automatisk når andre i @@ -4970,14 +4967,12 @@ runde HAR, org-turnering MANGLER, om ikke annet er sagt):** og org-turneringer har ingen egen tilsvarende endepunkt/side i det hele tatt. Antatt **middels/stor** -- ekte datamodell-brobygging (avstemme `stat_level`-forskjeller), ikke bare gjenbruk av UI. -4. **"Vis hele scorekortet" i ScoreTab viser IKKE alle spillere - samtidig.** Frittstående runde sin `ScorecardGrid` er en ekte - alle-spillere×alle-hull-matrise. Org-turneringens likt navngitte - toggle gjenbruker bevisst kun det eksisterende ett-spiller-`HoleGrid` - (kommentar i koden bekrefter dette var en villet avgrensning den - gang, ikke en forglemmelse) -- bytter kun HVEM som vises, viser aldri - alle på én gang. Antatt **middels** -- kjedet scoring (ADR-100) henter - allerede alle flightmedlemmers hull parallelt, så data finnes. +4. ✅ **Fullt flerspiller-scorekort-grid -- FERDIG BYGGET 2026-08-22 + (ADR-104), se CHANGELOG punkt 162.** Ny `TournamentScorecardGrid` + (ekte alle-spillere×alle-hull-matrise, samme struktur som + round-detail.tsx sin ScorecardGrid) erstattet den gamle pille-rad + + enkelt-spiller-`HoleGrid`-visningen (nå slettet). Bygget samtidig med + punkt 1 (se over). 5. **"Plukket opp"-hurtigknapp finnes ikke for turneringer, selv der Stableford er scoringsmetoden.** `round_hole.picked_up` (migrasjon 038) + knapp finnes i frittstående runder. `tournament_round_hole` diff --git a/frontend/components/individual-tournament-detail.tsx b/frontend/components/individual-tournament-detail.tsx index b3effaf..459bce6 100644 --- a/frontend/components/individual-tournament-detail.tsx +++ b/frontend/components/individual-tournament-detail.tsx @@ -35,12 +35,14 @@ import { MapPin, Medal, Plus, + RefreshCw, Settings2, Trash2, Trophy, Upload, UserPlus, Users, + WifiOff, X, } from "lucide-react" import { Button } from "@/components/ui/button" @@ -74,6 +76,7 @@ import { StrokePlayLeaderboard, type LeaderboardRow as StrokePlayRow } from "@/c import { TournamentStatusPicker, type TournamentStatus } from "@/components/tournament-status-badge" import { CourseTemplatePicker } from "@/components/course-template-editor" import { TournamentRoundMessages } from "@/components/tournament-round-messages" +import { enqueueWrite, flushQueue, queueCount } from "@/lib/offline-queue" import { cn } from "@/lib/utils" // --- Types (matcher app/routers/individual_tournaments.py/tournaments.py) -- @@ -3044,11 +3047,24 @@ function ScoreTab({ // "hent det som mangler"-mønster som round-detail.tsx sin // holesByParticipant/loadHoles. const [holesByParticipant, setHolesByParticipant] = useState>({}) + // "Vis hele scorekortet" (ADR-099, steg 2; flerspiller-rutenett ADR-104 + // punkt 4) -- kollapset som standard, samme fiks som round-detail.tsx sin + // PlayerHoleCards/ScorecardGrid-splitt. Deklarert her (ikke lenger sammen + // med wizardPlayerId lenger ned) siden hente-effekten rett under må vite + // om seksjonen faktisk er åpen før den henter HELE feltets hull. + const [showFullScorecard, setShowFullScorecard] = useState(false) useEffect(() => { if (!roundId) return let cancelled = false + // Flighten hentes alltid (hull-først-flyten trenger den uansett). + // Hele feltet hentes KUN når "Vis hele scorekortet" faktisk er åpen -- + // ADR-104 punkt 4 sitt rutenett trenger alle visibleParticipants sine + // hull (avhenger av "Vis hele feltet"-bryteren), ikke bare flighten. const ids = new Set(flightParticipants.map((rp) => rp.id)) if (participantId) ids.add(participantId) + if (showFullScorecard) { + for (const rp of visibleParticipants) ids.add(rp.id) + } for (const id of ids) { if (holesByParticipant[id]) continue getJson(`${base}/rounds/${roundId}/participants/${id}/holes`).then((data) => { @@ -3060,9 +3076,106 @@ function ScoreTab({ cancelled = true } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [base, roundId, roundParticipants, participantId, holesByParticipant]) + }, [base, roundId, roundParticipants, participantId, holesByParticipant, showFullScorecard, visibleParticipants]) - const holes = participantId ? (holesByParticipant[participantId] ?? null) : null + // Offline-kø for score-føring (ADR-104, punkt 1 av parity-gap-listen) -- + // speiler round-detail.tsx sitt queueParticipantHoleWrite/ + // updateWizardStat-mønster (ADR-028/057/060) nøyaktig, samme delte + // frontend/lib/offline-queue.ts (ingen endring i selve biblioteket). + const [isOnline, setIsOnline] = useState(true) + const [pendingCount, setPendingCount] = useState(0) + const [syncing, setSyncing] = useState(false) + const [pendingHoles, setPendingHoles] = useState>(new Set()) + const [conflictNotice, setConflictNotice] = useState(null) + + // patch.expected_version er allerede satt av TournamentScoringWizard sin + // save() (expected_version: hole.version) -- ingen egen versjonsoppslag + // trengs her, ulikt round-detail.tsx sin variant (som har flere + // kallesteder uten den garantien). + async function queueHoleWrite(participantIdParam: string, holeNumber: number, patch: HolePatch) { + const url = `${base}/rounds/${roundId}/participants/${participantIdParam}/holes/${holeNumber}` + await enqueueWrite({ url, method: "PATCH", body: patch, matchId: roundId }) + setPendingHoles((prev) => new Set(prev).add(`${participantIdParam}:${holeNumber}`)) + setPendingCount((c) => c + 1) + setHolesByParticipant((prev) => ({ + ...prev, + [participantIdParam]: (prev[participantIdParam] ?? []).map((h) => + h.hole_number === holeNumber ? { ...h, ...patch } : h, + ), + })) + } + + async function flushPending() { + if (syncing) return + setSyncing(true) + try { + const outcomes = await flushQueue(roundId) + if (outcomes.length === 0) return + const syncedParticipants = new Set() + for (const o of outcomes) { + const match = o.entry.url.match(/\/participants\/([^/]+)\/holes\/(\d+)$/) + if (!match) continue + setPendingHoles((prev) => { + const next = new Set(prev) + next.delete(`${match[1]}:${match[2]}`) + return next + }) + syncedParticipants.add(match[1]) + } + const failed = outcomes.filter((o) => !o.ok) + if (failed.length > 0) { + // ADR-057: en avvist kø-synk (f.eks. en versjonskonflikt) er + // forbigående -- IKKE onError, som ville tatt over hele siden. + setConflictNotice( + `${failed.length} lagret ${failed.length === 1 ? "endring" : "endringer"} kunne ikke synkroniseres: ${failed[0].message}`, + ) + } + setPendingCount(await queueCount(roundId)) + for (const id of syncedParticipants) { + void getJson(`${base}/rounds/${roundId}/participants/${id}/holes`).then((data) => { + setHolesByParticipant((prev) => ({ ...prev, [id]: data ?? [] })) + }) + } + setRefreshTick((n) => n + 1) + } finally { + setSyncing(false) + } + } + + useEffect(() => { + setIsOnline(navigator.onLine) + function handleOnline() { + setIsOnline(true) + void flushPending() + } + function handleOffline() { + setIsOnline(false) + } + window.addEventListener("online", handleOnline) + window.addEventListener("offline", handleOffline) + return () => { + window.removeEventListener("online", handleOnline) + window.removeEventListener("offline", handleOffline) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [roundId]) + + // Køede skrivinger fra en tidligere økt (f.eks. siden ble lukket mens + // offline) -- samme engangs-sjekk-ved-mount-mønster som round-detail.tsx. + useEffect(() => { + let cancelled = false + queueCount(roundId) + .then((count) => { + if (cancelled) return + setPendingCount(count) + if (count > 0 && navigator.onLine) void flushPending() + }) + .catch(() => {}) + return () => { + cancelled = true + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [roundId]) // Full statistikkdybde (migrasjon 075, ADR-071) -- ÉN batched save() ved // siste steg (ikke PATCH per felt, se ADR-078). Skriver alltid inn i @@ -3071,13 +3184,34 @@ function ScoreTab({ // siden flere spilleres ark nå kan være i bruk etter hverandre uten // remount). async function updateHoleFull(participantIdParam: string, holeNumber: number, patch: HolePatch) { - const res = await fetch(`${base}/rounds/${roundId}/participants/${participantIdParam}/holes/${holeNumber}`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - credentials: "include", - body: JSON.stringify(patch), - }) + if (!navigator.onLine) { + await queueHoleWrite(participantIdParam, holeNumber, patch) + return + } + let res: Response + try { + res = await fetch(`${base}/rounds/${roundId}/participants/${participantIdParam}/holes/${holeNumber}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify(patch), + }) + } catch { + // Ekte nettverksfeil (ikke bare et avvist svar) -- køordne i stedet + // for å tape registreringen, se ADR-028. + await queueHoleWrite(participantIdParam, holeNumber, patch) + return + } if (!res.ok) { + if (res.status === 409) { + // En annen enhet skrev til akkurat dette hullet først (ADR-057) -- + // vis det tydelig og hent inn den andres versjon. + setConflictNotice("Noen andre har endret dette hullet i mellomtiden. Viser siste versjon.") + void getJson(`${base}/rounds/${roundId}/participants/${participantIdParam}/holes`).then((data) => { + setHolesByParticipant((prev) => ({ ...prev, [participantIdParam]: data ?? [] })) + }) + return + } throw new Error("Klarte ikke å lagre.") } const updated: ApiHole = await res.json() @@ -3130,10 +3264,6 @@ function ScoreTab({ }, [activeHole, chain, holesByParticipant, flightHoleNumbers]) const [wizardPlayerId, setWizardPlayerId] = useState(null) - // "Vis hele scorekortet" (ADR-099, steg 2) -- kollapset som standard, - // samme fiks som round-detail.tsx sin PlayerHoleCards/ScorecardGrid- - // splitt. - const [showFullScorecard, setShowFullScorecard] = useState(false) // Rundespesifikk kommentartråd (ADR-102) -- kollapset som standard, // samme mønster som "Vis hele scorekortet". const [showComments, setShowComments] = useState(false) @@ -3213,6 +3343,47 @@ function ScoreTab({ ))} + {/* Offline scoreføring (ADR-104, punkt 1) -- synlig uansett hvilken + underseksjon som er åpen, offline-tilstanden er global for hele + fanen. Samme banner-mønster som round-detail.tsx (ADR-028). */} + {!isOnline && ( +
+
+ )} + {pendingCount > 0 && ( +
+ + {pendingCount} {pendingCount === 1 ? "endring venter" : "endringer venter"} på synkronisering. + + {isOnline && ( + + )} +
+ )} + {conflictNotice && ( +
+ {conflictNotice} + +
+ )} + {roundParticipants.length === 0 ? (

Ingen deltakere i denne runden ennå -- legg til under «Oppsett». @@ -3373,11 +3544,15 @@ function ScoreTab({ /> )} - {/* Fullt scorekort, spiller-først (steg 1) -- sekundært nå, bak en - bryter, samme fiks som round-detail.tsx sitt "Vis hele - scorekortet" (kommentar der: et bredt grid tar for stor plass - i bredden på mobil som standardvisning). Ingen ny kode for - selve gridet, kun ny plassering. */} + {/* Fullt flerspiller-scorekort-rutenett (ADR-104, punkt 4 av + parity-gap-listen) -- ekte alle-spillere×alle-hull-rutenett, + speiler round-detail.tsx sin ScorecardGrid strukturelt (sticky + spillerkolonne, Ut/Inn/Sum), men bruker denne filens EGNE + classify/scoreMarkClasses-fargespråk (ikke ScorecardGrid sin + lokale ScorecardCell) for visuell konsistens med flight- + kortlisten/HoleGrid over. Erstatter den tidligere pille-rad + + enkelt-spiller-HoleGrid-visningen (samme kollapsbare + container/plassering som før). */}

- ))} -
- {myGroupId && ( - ) - })} - - - - {totalGross !== null && totalPar !== null && ( -
- - {played.length} av {numbers.length} hull spilt - - - {totalGross} slag ({totalGross - totalPar >= 0 ? "+" : ""} - {totalGross - totalPar} til par) - -
- )} - - {sheetHole !== null && byNumber.get(sheetHole) && ( - {}} - hasNextPlayer={false} - nextPlayerName={null} - onSave={(patch) => onUpdateFull(sheetHole, patch)} - onAdvance={() => setSheetHole(null)} - onClose={() => setSheetHole(null)} - /> - )} - - ) -} - // ========================================================================= // Leaderboard-fane // =========================================================================