Fullt flerspiller-scorekort-grid + offline-kø for score-føring (ADR-104)
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 <noreply@anthropic.com>
This commit is contained in:
parent
8c3cdfff2e
commit
629197d22a
4 changed files with 559 additions and 185 deletions
|
|
@ -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)
|
||||
|
|
|
|||
39
CHANGELOG.md
39
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`).
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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<Record<string, ApiHole[]>>({})
|
||||
// "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<ApiHole[]>(`${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<Set<string>>(new Set())
|
||||
const [conflictNotice, setConflictNotice] = useState<string | null>(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<string>()
|
||||
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<ApiHole[]>(`${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<ApiHole[]>(`${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<string | null>(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({
|
|||
))}
|
||||
</div>
|
||||
|
||||
{/* 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 && (
|
||||
<div className="flex items-center gap-2 rounded-2xl border border-amber-300 bg-amber-50 px-4 py-3 text-sm font-medium text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200">
|
||||
<WifiOff aria-hidden="true" className="size-4 shrink-0" />
|
||||
<span>Du er offline. Endringer lagres lokalt og sendes automatisk når du er tilbake på nett.</span>
|
||||
</div>
|
||||
)}
|
||||
{pendingCount > 0 && (
|
||||
<div className="flex items-center justify-between gap-2 rounded-2xl border border-border bg-card px-4 py-3 text-sm font-medium text-foreground">
|
||||
<span>
|
||||
{pendingCount} {pendingCount === 1 ? "endring venter" : "endringer venter"} på synkronisering.
|
||||
</span>
|
||||
{isOnline && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void flushPending()}
|
||||
disabled={syncing}
|
||||
className="inline-flex shrink-0 items-center gap-1 font-semibold text-primary disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw aria-hidden="true" className={cn("size-3.5", syncing && "animate-spin")} />
|
||||
Synkroniser nå
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{conflictNotice && (
|
||||
<div className="flex items-center justify-between gap-2 rounded-2xl border border-border bg-card px-4 py-3 text-sm font-medium text-foreground">
|
||||
<span>{conflictNotice}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setConflictNotice(null)}
|
||||
className="shrink-0 text-muted-foreground hover:text-foreground"
|
||||
aria-label="Lukk"
|
||||
>
|
||||
<X aria-hidden="true" className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{roundParticipants.length === 0 ? (
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
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). */}
|
||||
<div className="overflow-hidden rounded-2xl border border-border bg-card">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -3390,25 +3565,6 @@ function ScoreTab({
|
|||
</button>
|
||||
{showFullScorecard && (
|
||||
<div className="flex flex-col gap-3 border-t border-border p-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{visibleParticipants.map((rp) => (
|
||||
<button
|
||||
key={rp.id}
|
||||
type="button"
|
||||
onClick={() => setParticipantId(rp.id)}
|
||||
aria-pressed={rp.id === participantId}
|
||||
className={cn(
|
||||
"min-h-11 rounded-full border px-4 text-sm font-bold transition-colors",
|
||||
rp.id === participantId
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
: "border-border bg-card text-foreground hover:bg-accent/50",
|
||||
)}
|
||||
>
|
||||
{rp.player_name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{myGroupId && (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -3419,18 +3575,18 @@ function ScoreTab({
|
|||
</button>
|
||||
)}
|
||||
|
||||
{round && holes && (
|
||||
<HoleGrid
|
||||
base={base}
|
||||
roundId={roundId}
|
||||
participantId={participantId}
|
||||
holes={holes}
|
||||
holeConfig={round.hole_config}
|
||||
statLevel={roundParticipants.find((rp) => rp.id === participantId)?.stat_level ?? "strokes_only"}
|
||||
playerName={roundParticipants.find((rp) => rp.id === participantId)?.player_name ?? ""}
|
||||
strokesReceived={(n) => holes.find((h) => h.hole_number === n)?.strokes_received ?? null}
|
||||
ownBagClubs={[]}
|
||||
onUpdateFull={(holeNumber, patch) => updateHoleFull(participantId, holeNumber, patch)}
|
||||
{round && (
|
||||
<TournamentScorecardGrid
|
||||
participants={visibleParticipants}
|
||||
holesByParticipant={holesByParticipant}
|
||||
holeOrder={flightHoleNumbers}
|
||||
activeHole={activeHole}
|
||||
hcpFor={hcpFor}
|
||||
onSelectHole={(n) => setActiveHole(n)}
|
||||
onOpenEntry={(rpId, n) => {
|
||||
setActiveHole(n)
|
||||
setWizardPlayerId(rpId)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -4385,6 +4541,219 @@ function scoreMarkClasses(kind: ScoreClass): string {
|
|||
}
|
||||
}
|
||||
|
||||
// Fullt flerspiller-scorekort-rutenett (ADR-104, punkt 4 av "Scoring-
|
||||
// paritet: frittstående runde vs. org-turnering"-gap-listen) -- speiler
|
||||
// round-detail.tsx sin ScorecardGrid strukturelt (sticky spillerkolonne,
|
||||
// hull-kolonner gruppert Ut(1-9)/Inn(10-18) på FYSISK hullnummer, Sum-
|
||||
// kolonne), men MED denne filens egne classify/scoreMarkClasses (ikke
|
||||
// ScorecardGrid sin lokale ScorecardCell) -- visuell konsistens med
|
||||
// flight-kortlisten over. Ingen sides/isTwoSided/formatResult-gren --
|
||||
// individuelle turneringer har ALDRI et side-begrep (bekreftet: den
|
||||
// grenen i ScorecardGrid er alltid death-path for ren slagspill uansett).
|
||||
// Ingen "plukket opp"/PU-merkelapp -- tournament_round_hole har ingen
|
||||
// tilsvarende kolonne.
|
||||
function TournamentScorecardGrid({
|
||||
participants,
|
||||
holesByParticipant,
|
||||
holeOrder,
|
||||
activeHole,
|
||||
hcpFor,
|
||||
onSelectHole,
|
||||
onOpenEntry,
|
||||
}: {
|
||||
participants: ApiRoundParticipant[]
|
||||
holesByParticipant: Record<string, ApiHole[]>
|
||||
holeOrder: number[]
|
||||
activeHole: number | null
|
||||
hcpFor: (rp: ApiRoundParticipant) => number | null
|
||||
onSelectHole: (holeNumber: number) => void
|
||||
onOpenEntry: (participantId: string, holeNumber: number) => void
|
||||
}) {
|
||||
const holeMeta: Record<number, { par: number }> = {}
|
||||
for (const list of Object.values(holesByParticipant)) {
|
||||
for (const h of list) {
|
||||
if (!(h.hole_number in holeMeta)) holeMeta[h.hole_number] = { par: h.par }
|
||||
}
|
||||
}
|
||||
|
||||
// Ut/Inn er alltid gruppert på FYSISK hullnummer (tradisjonell
|
||||
// scorekort-konvensjon), ikke spillerekkefølge -- samme regel som
|
||||
// round-detail.tsx sin ScorecardGrid. Kun meningsfullt å splitte for en
|
||||
// 18-hulls runde.
|
||||
const showSplit = holeOrder.length === 18
|
||||
const front = holeOrder.filter((n) => n <= 9)
|
||||
const back = holeOrder.filter((n) => n >= 10)
|
||||
|
||||
function sumFor(participantId: string, numbers: number[]): { strokes: number; par: number } | null {
|
||||
const list = holesByParticipant[participantId]
|
||||
if (!list || numbers.length === 0) return null
|
||||
const played = numbers
|
||||
.map((n) => list.find((h) => h.hole_number === n))
|
||||
.filter((h): h is ApiHole => Boolean(h) && h!.gross_strokes !== null)
|
||||
if (played.length === 0) return null
|
||||
return {
|
||||
strokes: played.reduce((sum, h) => sum + (h.gross_strokes as number), 0),
|
||||
par: played.reduce((sum, h) => sum + h.par, 0),
|
||||
}
|
||||
}
|
||||
|
||||
const parRowTotal = holeOrder.reduce((sum, n) => sum + (holeMeta[n]?.par ?? 0), 0)
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-2xl border border-border">
|
||||
<table className="border-separate border-spacing-0">
|
||||
<colgroup>
|
||||
<col style={{ width: 132 }} />
|
||||
{holeOrder.map((n) => (
|
||||
<col key={n} style={{ width: 48 }} />
|
||||
))}
|
||||
{showSplit && <col style={{ width: 56 }} />}
|
||||
{showSplit && <col style={{ width: 56 }} />}
|
||||
<col style={{ width: 64 }} />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="sticky left-0 z-10 border-b border-border bg-card p-2 text-left text-xs font-bold text-muted-foreground">
|
||||
Spiller
|
||||
</th>
|
||||
{holeOrder.map((n) => (
|
||||
<th key={n} className="border-b border-border p-0 text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelectHole(n)}
|
||||
aria-current={n === activeHole ? "true" : undefined}
|
||||
aria-label={`Hull ${n}`}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-center text-sm font-bold tabular-nums transition-colors",
|
||||
n === activeHole
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "bg-card text-foreground hover:bg-accent/50",
|
||||
)}
|
||||
>
|
||||
{n}
|
||||
</button>
|
||||
</th>
|
||||
))}
|
||||
{showSplit && (
|
||||
<th className="border-b border-border p-2 text-center text-xs font-bold text-muted-foreground">Ut</th>
|
||||
)}
|
||||
{showSplit && (
|
||||
<th className="border-b border-border p-2 text-center text-xs font-bold text-muted-foreground">Inn</th>
|
||||
)}
|
||||
<th className="border-b border-border p-2 text-center text-xs font-bold text-muted-foreground">Sum</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="sticky left-0 z-10 border-b border-border bg-card p-2 text-xs font-semibold text-muted-foreground">
|
||||
Par
|
||||
</td>
|
||||
{holeOrder.map((n) => (
|
||||
<td
|
||||
key={n}
|
||||
className="border-b border-border p-1 text-center text-xs font-semibold tabular-nums text-muted-foreground"
|
||||
>
|
||||
{holeMeta[n]?.par ?? "–"}
|
||||
</td>
|
||||
))}
|
||||
{showSplit && (
|
||||
<td className="border-b border-border p-1 text-center text-xs font-semibold tabular-nums text-muted-foreground">
|
||||
{front.reduce((s, n) => s + (holeMeta[n]?.par ?? 0), 0)}
|
||||
</td>
|
||||
)}
|
||||
{showSplit && (
|
||||
<td className="border-b border-border p-1 text-center text-xs font-semibold tabular-nums text-muted-foreground">
|
||||
{back.reduce((s, n) => s + (holeMeta[n]?.par ?? 0), 0)}
|
||||
</td>
|
||||
)}
|
||||
<td className="border-b border-border p-1 text-center text-xs font-semibold tabular-nums text-muted-foreground">
|
||||
{parRowTotal}
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{participants.map((rp) => {
|
||||
const frontSum = sumFor(rp.id, front)
|
||||
const backSum = sumFor(rp.id, back)
|
||||
const totalSum = sumFor(rp.id, holeOrder)
|
||||
const hcp = hcpFor(rp)
|
||||
return (
|
||||
<tr key={rp.id}>
|
||||
<td className="sticky left-0 z-10 border-b border-border bg-card p-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => activeHole !== null && onOpenEntry(rp.id, activeHole)}
|
||||
disabled={activeHole === null}
|
||||
className="flex min-h-11 w-full flex-col items-start justify-center gap-0.5 px-2 py-1.5 text-left transition-colors hover:bg-accent/50 disabled:pointer-events-none"
|
||||
>
|
||||
<span className="flex items-center gap-1.5 truncate text-sm font-bold text-foreground">
|
||||
{rp.player_name}
|
||||
{rp.is_self && (
|
||||
<span className="rounded-full bg-primary/15 px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wide text-primary">
|
||||
Deg
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-[10px] font-semibold tabular-nums text-muted-foreground">
|
||||
HCP {hcp !== null ? hcp.toFixed(1).replace(".", ",") : "–"}
|
||||
</span>
|
||||
</button>
|
||||
</td>
|
||||
{holeOrder.map((n) => {
|
||||
const hole = holesByParticipant[rp.id]?.find((h) => h.hole_number === n)
|
||||
const gross = hole?.gross_strokes ?? null
|
||||
const net =
|
||||
gross !== null && hole?.strokes_received !== null && hole?.strokes_received !== undefined
|
||||
? gross - hole.strokes_received
|
||||
: gross
|
||||
const diff = net !== null && hole ? net - hole.par : null
|
||||
const kind = diff !== null ? classify(diff) : null
|
||||
return (
|
||||
<td key={n} className="border-b border-border p-1 text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenEntry(rp.id, n)}
|
||||
aria-label={`${rp.player_name}, hull ${n}, ${gross !== null ? `${gross} slag` : "ikke registrert"}`}
|
||||
className={cn(
|
||||
"mx-auto flex size-9 items-center justify-center text-sm font-extrabold tabular-nums transition-colors",
|
||||
kind
|
||||
? scoreMarkClasses(kind)
|
||||
: "rounded-[4px] border-2 border-dashed border-border text-muted-foreground hover:bg-accent/50",
|
||||
)}
|
||||
>
|
||||
{gross ?? "–"}
|
||||
</button>
|
||||
</td>
|
||||
)
|
||||
})}
|
||||
{showSplit && (
|
||||
<td className="border-b border-border p-1 text-center text-sm font-bold tabular-nums text-foreground">
|
||||
{frontSum !== null ? frontSum.strokes : "–"}
|
||||
</td>
|
||||
)}
|
||||
{showSplit && (
|
||||
<td className="border-b border-border p-1 text-center text-sm font-bold tabular-nums text-foreground">
|
||||
{backSum !== null ? backSum.strokes : "–"}
|
||||
</td>
|
||||
)}
|
||||
<td className="border-b border-border p-1 text-center">
|
||||
<span className="block text-sm font-extrabold tabular-nums text-foreground">
|
||||
{totalSum !== null ? totalSum.strokes : "–"}
|
||||
</span>
|
||||
{totalSum !== null && (
|
||||
<span className="block text-[10px] font-semibold tabular-nums text-muted-foreground">
|
||||
{totalSum.strokes - totalSum.par >= 0 ? "+" : ""}
|
||||
{totalSum.strokes - totalSum.par}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// --- Full statistikkregistrering per hull (migrasjon 075, ADR-071) --------
|
||||
// Enkelt-skjerm (IKKE en flerstegs-veiviser som ScoringWizard i round-
|
||||
// detail.tsx -- HoleGrid åpner ETT hull om gangen for ÉN allerede valgt
|
||||
|
|
@ -4857,127 +5226,6 @@ function TournamentScoringWizard({
|
|||
)
|
||||
}
|
||||
|
||||
function HoleGrid({
|
||||
base,
|
||||
roundId,
|
||||
participantId,
|
||||
holes,
|
||||
holeConfig,
|
||||
statLevel,
|
||||
playerName,
|
||||
strokesReceived,
|
||||
ownBagClubs,
|
||||
onUpdateFull,
|
||||
}: {
|
||||
base: string
|
||||
roundId: string
|
||||
participantId: string
|
||||
holes: ApiHole[]
|
||||
holeConfig: ApiRound["hole_config"]
|
||||
statLevel: "strokes_only" | "strokes_and_putts" | "full"
|
||||
playerName: string
|
||||
strokesReceived: (holeNumber: number) => number | null
|
||||
ownBagClubs: string[]
|
||||
onUpdateFull: (holeNumber: number, patch: HolePatch) => Promise<void>
|
||||
}) {
|
||||
// Full statistikkdybde (migrasjon 075, ADR-071), utvidet ADR-099 (2026-
|
||||
// 08-22): ÉN wizard for alle tre statLevel-nivåer nå (strokes_only fikk
|
||||
// tidligere kun et bart inline-tallfelt her -- fjernet, samme picker-
|
||||
// opplevelse som resten av appen for alle nivåer, se TournamentScoringWizard).
|
||||
const [sheetHole, setSheetHole] = useState<number | null>(null)
|
||||
const numbers = holeNumbersFor(holeConfig)
|
||||
const byNumber = new Map(holes.map((h) => [h.hole_number, h]))
|
||||
|
||||
const played = holes.filter((h) => h.gross_strokes !== null)
|
||||
const totalGross = played.length > 0 ? played.reduce((sum, h) => sum + (h.gross_strokes as number), 0) : null
|
||||
const totalPar = played.length > 0 ? played.reduce((sum, h) => sum + h.par, 0) : null
|
||||
|
||||
function openEditor(holeNumber: number) {
|
||||
setSheetHole(holeNumber)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 shadow-md shadow-black/8">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="inline-grid min-w-full gap-y-1" style={{ gridTemplateColumns: `repeat(${numbers.length}, minmax(2.5rem, 1fr))` }}>
|
||||
{numbers.map((n) => (
|
||||
<span key={`num-${n}`} className="text-center text-[11px] font-bold text-muted-foreground">
|
||||
{n}
|
||||
</span>
|
||||
))}
|
||||
{numbers.map((n) => (
|
||||
<span key={`par-${n}`} className="text-center text-[10px] font-semibold tabular-nums text-muted-foreground">
|
||||
Par {byNumber.get(n)?.par ?? "–"}
|
||||
</span>
|
||||
))}
|
||||
{numbers.map((n) => {
|
||||
const hole = byNumber.get(n)
|
||||
const gross = hole?.gross_strokes ?? null
|
||||
const net = gross !== null && hole?.strokes_received !== null && hole?.strokes_received !== undefined
|
||||
? gross - hole.strokes_received
|
||||
: gross
|
||||
const diff = net !== null && hole ? net - hole.par : null
|
||||
const kind = diff !== null ? classify(diff) : null
|
||||
|
||||
return (
|
||||
<button
|
||||
key={`cell-${n}`}
|
||||
type="button"
|
||||
onClick={() => openEditor(n)}
|
||||
aria-label={
|
||||
gross !== null
|
||||
? `Hull ${n}, ${gross} slag, trykk for å endre`
|
||||
: `Hull ${n}, ikke registrert, trykk for å registrere`
|
||||
}
|
||||
className={cn(
|
||||
"mx-auto flex size-9 items-center justify-center text-sm font-extrabold tabular-nums transition-colors",
|
||||
kind
|
||||
? scoreMarkClasses(kind)
|
||||
: "rounded-[4px] border-2 border-dashed border-border text-muted-foreground hover:bg-accent/50",
|
||||
)}
|
||||
>
|
||||
{gross ?? "–"}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{totalGross !== null && totalPar !== null && (
|
||||
<div className="flex items-center justify-between border-t border-border pt-3 text-sm">
|
||||
<span className="font-semibold text-muted-foreground">
|
||||
{played.length} av {numbers.length} hull spilt
|
||||
</span>
|
||||
<span className="font-extrabold tabular-nums text-foreground">
|
||||
{totalGross} slag ({totalGross - totalPar >= 0 ? "+" : ""}
|
||||
{totalGross - totalPar} til par)
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sheetHole !== null && byNumber.get(sheetHole) && (
|
||||
<TournamentScoringWizard
|
||||
hole={byNumber.get(sheetHole)!}
|
||||
playerName={playerName}
|
||||
statLevel={statLevel}
|
||||
strokesReceived={strokesReceived(sheetHole)}
|
||||
ownBagClubs={ownBagClubs}
|
||||
historyUrl={`${base}/rounds/${roundId}/participants/${participantId}/holes/${sheetHole}/history`}
|
||||
targetPointsBaseUrl={`${base}/rounds/${roundId}`}
|
||||
players={[{ id: participantId, playerName, registered: (byNumber.get(sheetHole)?.gross_strokes ?? null) !== null }]}
|
||||
activePlayerId={participantId}
|
||||
onSwitchPlayer={() => {}}
|
||||
hasNextPlayer={false}
|
||||
nextPlayerName={null}
|
||||
onSave={(patch) => onUpdateFull(sheetHole, patch)}
|
||||
onAdvance={() => setSheetHole(null)}
|
||||
onClose={() => setSheetHole(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Leaderboard-fane
|
||||
// =========================================================================
|
||||
|
|
|
|||
Loading…
Reference in a new issue