Personlig hull-historikk ut av slagvinduet + full historikk med grafer (ADR-079)
Historikk-panelet flyttet ut av selve slagvinduet/-arket i begge flyter: inn i PlayerHoleCards-kortet (frittstående runder), som nytt "overview"- steg med auto-hopp når tomt (org-turneringer). Klikk åpner nå en ny delt full-historikk-skjerm (hole-history-detail.tsx) med aggregerte tall og en score-fordeling som fargede stolper -- samme CategoryBar-mønster som round-stats.tsx, ingen ny graf-avhengighet. Ingen backend-endring. tsc + 45/45 vitest, scratch-verifisert i begge flyter inkl. kombinert historikk på tvers av runde-/turneringstyper og auto-hopp for hull uten historikk (lys+mørk). Rullet ut. Med dette er alle tre forbedringene i score-registreringen (ADR-077/078/079) fullført. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
46288c4b5f
commit
9f2711e4b3
6 changed files with 508 additions and 76 deletions
|
|
@ -7732,6 +7732,91 @@ Containeren startet rent.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## ADR-079: Personlig hull-historikk ut av slagvinduet + full historikk med grafer ("Steg 3" av tre forbedringer i score-registreringen) (2026-08-16)
|
||||||
|
|
||||||
|
Foranledning: bruker observerte at personlig hull-historikk ble vist INNI
|
||||||
|
selve slagvinduet/-arket (forstyrret registreringen), og at klikk kun ga
|
||||||
|
en kort oppsummering -- ikke full historikk. Ba om at den flyttes til
|
||||||
|
"vinduet før, f.eks. før 'Avslutt før...'", og at klikk skal vise FULL
|
||||||
|
historikk inkl. grafer. Bekreftet (AskUserQuestion) at "grafer" betyr det
|
||||||
|
ETABLERTE stolpe-mønsteret appen allerede bruker (`CategoryBar`/
|
||||||
|
`DeviationBar` i `round-stats.tsx`), ikke et nytt graf-bibliotek -- og at
|
||||||
|
begge scoringsflyter skal dekkes.
|
||||||
|
|
||||||
|
**Ingen backend-endring** -- `app/hole_history.py` sin `HoleHistoryOut`
|
||||||
|
(bygget i ADR-072) returnerte allerede alt som trengs (aggregert
|
||||||
|
oppsummering + full per-instans-liste); rent frontend-arbeid.
|
||||||
|
|
||||||
|
**Flytting, begge flyter:**
|
||||||
|
- Frittstående runder: `HoleHistoryPanel` fjernet fra `ScoringWizard` sitt
|
||||||
|
`"strokes"`-steg, lagt til i `PlayerHoleCards` sitt spillerkort (RETT
|
||||||
|
FØR `ParticipantCompletionActions`, altså nøyaktig "vinduet før 'Avslutt
|
||||||
|
for...'" brukeren pekte på).
|
||||||
|
- Org-turneringer: fjernet fra `HoleStatsSheet` sitt `"strokes"`-steg,
|
||||||
|
lagt til som et NYTT FØRSTE steg (`"overview"`) i steg-flyten ADR-078
|
||||||
|
bygde -- ingen ekstern "kort-liste"-skjerm finnes der slik runde-siden
|
||||||
|
har (`HoleGrid` er et tett rutenett). For å unngå en ekstra obligatorisk
|
||||||
|
trykk-runde for HVER hull-registrering (18 hull × N spillere) hopper
|
||||||
|
dette steget automatisk videre av seg selv når spilleren ikke har noen
|
||||||
|
historikk å vise -- kun synlig når det faktisk er noe der, verifisert
|
||||||
|
eksplisitt (hull uten historikk hoppet rett til "Slag", hull MED
|
||||||
|
historikk viste steget og ventet på trykk).
|
||||||
|
|
||||||
|
**Klikk-oppførsel endret**: `HoleHistoryPanel` ekspanderte tidligere en
|
||||||
|
enkel liste inline ved klikk -- åpner nå i stedet den nye fulle
|
||||||
|
historikk-skjermen (`onOpenFull`-prop). Chevron endret fra ned (ekspander)
|
||||||
|
til høyre (naviger videre), samme visuelle konvensjon som resten av appen
|
||||||
|
bruker for "gå til en annen skjerm" vs. "utvid her".
|
||||||
|
|
||||||
|
**Ny delt komponent `components/hole-history-detail.tsx`
|
||||||
|
(`HoleHistoryDetail`)** -- fullskjerm (`fixed inset-0 z-50`, samme
|
||||||
|
konvensjon som `ScoringWizard`/`HoleStatsSheet`), samme `historyUrl`-
|
||||||
|
parametermønster som panelet selv brukte. Viser: fire stat-fliser (ganger
|
||||||
|
spilt/snitt slag/beste/dårligste), snitt-til-par som en fremhevet stor
|
||||||
|
DeviationBar-lignende tall-boks, GIR%/fairway%, en SCORE-FORDELING som
|
||||||
|
fargede `CategoryBar`-stolper (EKSAKT samme 6-kategori-inndeling og
|
||||||
|
`--chart-1..6`-fargelogikk som `round-stats.tsx`, egen lokal kopi -- samme
|
||||||
|
"ikke delt kode på tvers av filer"-mønster som resten av dette området),
|
||||||
|
og den detaljerte per-instans-listen under (dato/kilde/score/putter).
|
||||||
|
|
||||||
|
**Bevisst avvik fra planen: INGEN V0-runde for denne skjermen.** Planen
|
||||||
|
forutså en V0-prompt for denne, men i motsetning til ADR-078 sin
|
||||||
|
holeDetails-skjerm (som IKKE hadde noe visuelt forbilde) ba brukeren
|
||||||
|
EKSPLISITT om å gjenbruke et NAVNGITT, allerede eksisterende mønster
|
||||||
|
(`CategoryBar`/`DeviationBar`) -- selve designbeslutningen var dermed
|
||||||
|
allerede tatt og bekreftet av brukeren, ikke noe som gjensto å utforske
|
||||||
|
via V0. Bygget direkte, med samme presisjon som om mønsteret var hentet
|
||||||
|
fra en V0-eksport.
|
||||||
|
|
||||||
|
**Verifisert:** `tsc --noEmit` rent + 45/45 vitest. Egen scratch-database
|
||||||
|
+ scratch `teecup_api` (port 18107) + lokal `next dev` (port 13107): to
|
||||||
|
frittstående runder på SAMME bane-nøkkel (hull 1 scoret Par i den ene,
|
||||||
|
Birdie i den andre) + en org-turnering på en bane med MATCHENDE
|
||||||
|
`external_course_ref`-bro (`app/hole_history.py` sin teeoff-bro-streng),
|
||||||
|
for å bevise kombinert historikk på tvers av BEGGE kontekster i
|
||||||
|
`"overview"`-steget. Bekreftet: historikk-panelet vises korrekt plassert
|
||||||
|
FØR "Avslutt for {navn}" i `PlayerHoleCards` (runde-siden), IKKE lenger
|
||||||
|
synlig inni selve slagvinduet. Klikk åpnet full historikk-skjerm med
|
||||||
|
korrekt aggregerte tall (snitt 3,5 slag, −0,50 til par, GIR 100%) OG en
|
||||||
|
korrekt score-fordelingsgraf (Birdie 50%/Par 50%, riktige farger). Samme
|
||||||
|
skjerm bekreftet identisk fra org-turnering-siden sitt `"overview"`-steg,
|
||||||
|
med kombinert historikk fra begge runde-typer. Hull UTEN historikk
|
||||||
|
bekreftet hoppet automatisk forbi `"overview"`-steget til "Slag" uten
|
||||||
|
synlig flimmer. Lys+mørk bekreftet på hele historikk-skjermen (fliser,
|
||||||
|
stolpediagram, liste). Scratch-stacken fullstendig revet ned -- ekte
|
||||||
|
`teecup_db`/`teecup_api`/`teecup_frontend` urørt.
|
||||||
|
|
||||||
|
Med dette er alle tre forbedringene i score-registreringen brukeren ba om
|
||||||
|
etter Order of Merit fullført: tilbake-navigering (ADR-077), egen skjerm
|
||||||
|
for hull-detaljer (ADR-078), personlig historikk relokert + full historikk
|
||||||
|
med grafer (dette dokumentet).
|
||||||
|
|
||||||
|
**Ingen migrasjon** -- ren frontend-endring.
|
||||||
|
|
||||||
|
**Rullet ut**: ikke ennå -- venter på bekreftelse fra bruker.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Utviklingsplan (rekkefølge)
|
## Utviklingsplan (rekkefølge)
|
||||||
|
|
||||||
1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003)
|
1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003)
|
||||||
|
|
|
||||||
34
CHANGELOG.md
34
CHANGELOG.md
|
|
@ -11718,3 +11718,37 @@ Neste steg:
|
||||||
**Rullet ut 2026-08-16** -- ingen migrasjon, `docker compose build
|
**Rullet ut 2026-08-16** -- ingen migrasjon, `docker compose build
|
||||||
teecup_frontend && up -d` etter eksplisitt bekreftelse fra bruker.
|
teecup_frontend && up -d` etter eksplisitt bekreftelse fra bruker.
|
||||||
Containeren startet rent.
|
Containeren startet rent.
|
||||||
|
|
||||||
|
95. **Personlig hull-historikk ut av slagvinduet + full historikk med
|
||||||
|
grafer (ADR-079) — 2026-08-16.** Tredje og siste av tre forbedringer i
|
||||||
|
score-registreringen. Ingen backend-endring -- `app/hole_history.py`
|
||||||
|
(ADR-072) returnerte allerede alt som trengs.
|
||||||
|
|
||||||
|
Flyttet historikk-panelet ut av selve slagvinduet/-arket i begge
|
||||||
|
flyter: frittstående runder -- inn i `PlayerHoleCards`-kortet, rett
|
||||||
|
før "Avslutt for {navn}". Org-turneringer -- som et nytt FØRSTE steg
|
||||||
|
(`"overview"`) i ADR-078 sin steg-flyt, med automatisk hopp forbi når
|
||||||
|
det ikke er noe å vise (unngår en ekstra obligatorisk trykk-runde for
|
||||||
|
hver hull-registrering). Klikk åpner nå en ny delt fullskjerm-
|
||||||
|
komponent (`hole-history-detail.tsx`) i stedet for å ekspandere en
|
||||||
|
liste inline -- stat-fliser, en score-fordeling som fargede stolper
|
||||||
|
(samme `CategoryBar`-mønster/farger som `round-stats.tsx`, bekreftet
|
||||||
|
med bruker som ØNSKET graf-form), og full instansliste under.
|
||||||
|
|
||||||
|
Bevisst avvik fra planen: ingen V0-runde her (i motsetning til
|
||||||
|
ADR-078) -- brukeren hadde allerede navngitt og bekreftet det
|
||||||
|
eksakte mønsteret å gjenbruke, ingenting å utforske via V0.
|
||||||
|
|
||||||
|
**Verifisert:** `tsc --noEmit` rent + 45/45 vitest. Scratch-database +
|
||||||
|
scratch `teecup_api` (port 18107): to frittstående runder + en
|
||||||
|
org-turnering på matchende bane-nøkkel, kombinert historikk bekreftet
|
||||||
|
i BEGGE flyter, aggregerte tall og score-fordelingsgraf bekreftet
|
||||||
|
håndregnet-korrekt, hull uten historikk bekreftet hoppet automatisk
|
||||||
|
forbi "overview"-steget. Lys+mørk bekreftet. Scratch-stacken
|
||||||
|
fullstendig revet ned.
|
||||||
|
|
||||||
|
Med dette er alle tre forbedringene i score-registreringen fullført.
|
||||||
|
|
||||||
|
**Ingen migrasjon.**
|
||||||
|
|
||||||
|
**Rullet ut**: ikke ennå.
|
||||||
|
|
|
||||||
242
frontend/components/hole-history-detail.tsx
Normal file
242
frontend/components/hole-history-detail.tsx
Normal file
|
|
@ -0,0 +1,242 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// Full historikk på ett hull, inkl. grafer (ADR-079, 2026-08-16) -- åpnes
|
||||||
|
// ved klikk fra HoleHistoryPanel sin kompakte oppsummering (hole-stat-
|
||||||
|
// inputs.tsx), som selv sitter i "skjermen før" slagvinduet/-arket (se
|
||||||
|
// round-detail.tsx sin PlayerHoleCards og individual-tournament-detail.tsx
|
||||||
|
// sin HoleStatsSheet-"Oversikt"-steg). INGEN backend-endring -- samme
|
||||||
|
// GET .../holes/{n}/history-endepunkt som panelet selv allerede brukte
|
||||||
|
// (app/hole_history.py), som allerede returnerer alt som trengs her.
|
||||||
|
//
|
||||||
|
// Grafene gjenbruker EKSAKT samme mønster/fargelogikk som round-stats.tsx
|
||||||
|
// sin CategoryBar/DeviationBar (bekreftet med bruker: "grafer" betyr dette
|
||||||
|
// etablerte stolpe-mønsteret, ikke et nytt graf-bibliotek) -- egne lokale
|
||||||
|
// kopier her, samme "ikke delt kode på tvers av filer"-mønster som resten
|
||||||
|
// av dette området (se DESIGN_SYSTEM.md).
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { X } from "lucide-react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
type HoleHistoryInstance = {
|
||||||
|
source: "personal" | "tournament"
|
||||||
|
played_at: string | null
|
||||||
|
label: string
|
||||||
|
score: number
|
||||||
|
par: number
|
||||||
|
putts: number | null
|
||||||
|
tee_shot_result: string | null
|
||||||
|
approach_result: string | null
|
||||||
|
chip_count: number | null
|
||||||
|
bunker_shot_count: number | null
|
||||||
|
penalty_strokes: number | null
|
||||||
|
first_putt_distance_bucket: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type HoleHistory = {
|
||||||
|
times_played: number
|
||||||
|
times_personal: number
|
||||||
|
times_tournament: number
|
||||||
|
average_score: number
|
||||||
|
average_score_vs_par: number
|
||||||
|
gir_percent: number | null
|
||||||
|
fairway_hit_percent: number | null
|
||||||
|
average_putts: number | null
|
||||||
|
best_score: number
|
||||||
|
worst_score: number
|
||||||
|
instances: HoleHistoryInstance[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Samme fargeskala/kategori-inndeling som round-stats.tsx (chart-1..6,
|
||||||
|
// grønn->rød golf-resultat-skala).
|
||||||
|
const C = {
|
||||||
|
eagle: "var(--chart-1)",
|
||||||
|
birdie: "var(--chart-2)",
|
||||||
|
par: "var(--chart-3)",
|
||||||
|
bogey: "var(--chart-4)",
|
||||||
|
dobbel: "var(--chart-5)",
|
||||||
|
verre: "var(--chart-6)",
|
||||||
|
}
|
||||||
|
|
||||||
|
function signed(n: number): string {
|
||||||
|
const sign = n > 0 ? "+" : n < 0 ? "−" : "±"
|
||||||
|
return `${sign}${Math.abs(n).toFixed(2).replace(".", ",")}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function pct(n: number): string {
|
||||||
|
return `${Math.round(n)} %`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HoleHistoryDetail({
|
||||||
|
url,
|
||||||
|
holeNumber,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
url: string
|
||||||
|
holeNumber: number
|
||||||
|
onClose: () => void
|
||||||
|
}) {
|
||||||
|
const [history, setHistory] = useState<HoleHistory | null | undefined>(undefined)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
fetch(url, { credentials: "include" })
|
||||||
|
.then((res) => (res.ok ? res.json() : null))
|
||||||
|
.then((data) => {
|
||||||
|
if (!cancelled) setHistory(data)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled) setHistory(null)
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [url])
|
||||||
|
|
||||||
|
const par = history?.instances[0]?.par ?? null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex flex-col bg-background">
|
||||||
|
<header className="flex min-h-14 shrink-0 items-center gap-2 border-b border-border px-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
aria-label="Lukk"
|
||||||
|
className="flex size-11 shrink-0 items-center justify-center rounded-xl text-foreground transition-colors hover:bg-accent"
|
||||||
|
>
|
||||||
|
<X aria-hidden="true" className="size-5" />
|
||||||
|
</button>
|
||||||
|
<div className="flex min-w-0 flex-1 flex-col items-center text-center">
|
||||||
|
<span className="truncate text-base font-extrabold text-foreground">
|
||||||
|
Hull {holeNumber}
|
||||||
|
{par !== null ? ` · Par ${par}` : ""}
|
||||||
|
</span>
|
||||||
|
<span className="truncate text-sm font-semibold text-muted-foreground">Din historikk</span>
|
||||||
|
</div>
|
||||||
|
<div className="size-11 shrink-0" aria-hidden="true" />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="flex min-h-0 flex-1 flex-col overflow-y-auto p-5 sm:p-6">
|
||||||
|
<div className="mx-auto flex w-full max-w-sm flex-col gap-6">
|
||||||
|
{history === undefined ? (
|
||||||
|
<div className="flex flex-1 items-center justify-center py-16">
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="size-10 animate-spin rounded-full border-4 border-primary/20 border-t-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : history === null || history.times_played === 0 ? (
|
||||||
|
<p className="rounded-3xl border border-border bg-card p-6 text-center text-base font-semibold text-muted-foreground">
|
||||||
|
Ingen historikk å vise på dette hullet ennå.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<StatTile label="Ganger spilt" value={String(history.times_played)} />
|
||||||
|
<StatTile label="Snitt slag" value={history.average_score.toFixed(1)} />
|
||||||
|
<StatTile label="Beste" value={String(history.best_score)} />
|
||||||
|
<StatTile label="Dårligste" value={String(history.worst_score)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center gap-1 rounded-2xl border border-border bg-muted/40 p-4">
|
||||||
|
<span className="text-sm font-bold uppercase tracking-wide text-muted-foreground">
|
||||||
|
Snitt til par
|
||||||
|
</span>
|
||||||
|
<span className="text-5xl font-extrabold tabular-nums text-brand-orange">
|
||||||
|
{signed(history.average_score_vs_par)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(history.gir_percent !== null || history.fairway_hit_percent !== null) && (
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
{history.gir_percent !== null && <StatTile label="GIR" value={pct(history.gir_percent)} />}
|
||||||
|
{history.fairway_hit_percent !== null && (
|
||||||
|
<StatTile label="Fairway truffet" value={pct(history.fairway_hit_percent)} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ScoreDistribution instances={history.instances} />
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h3 className="text-xs font-bold uppercase tracking-wide text-muted-foreground">Alle gangene</h3>
|
||||||
|
<ul className="flex flex-col divide-y divide-border overflow-hidden rounded-2xl border border-border">
|
||||||
|
{history.instances.map((inst, i) => (
|
||||||
|
<li key={i} className="flex items-center justify-between gap-3 bg-card px-4 py-3">
|
||||||
|
<span className="flex min-w-0 flex-col">
|
||||||
|
<span className="truncate text-sm font-semibold text-foreground">{inst.label}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{inst.played_at ?? "Ukjent dato"} · {inst.source === "personal" ? "Frittstående" : "Turnering"}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className="shrink-0 text-sm font-extrabold tabular-nums text-foreground">
|
||||||
|
{inst.score} slag
|
||||||
|
{inst.putts !== null ? `, ${inst.putts} putt${inst.putts === 1 ? "" : "er"}` : ""}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatTile({ label, value }: { label: string; value: string }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-1 rounded-2xl border border-border bg-card p-4">
|
||||||
|
<span className="text-xs font-bold uppercase tracking-wide text-muted-foreground">{label}</span>
|
||||||
|
<span className="text-2xl font-extrabold tabular-nums text-foreground">{value}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Samme stolpe-mønster som round-stats.tsx sin CategoryBar -- fargede,
|
||||||
|
// prosent-bredde rektangler med store tall og etiketter.
|
||||||
|
function ScoreDistribution({ instances }: { instances: HoleHistoryInstance[] }) {
|
||||||
|
const diff = (inst: HoleHistoryInstance) => inst.score - inst.par
|
||||||
|
const categories = [
|
||||||
|
{ label: "Eagle+", test: (d: number) => d <= -2, color: C.eagle },
|
||||||
|
{ label: "Birdie", test: (d: number) => d === -1, color: C.birdie },
|
||||||
|
{ label: "Par", test: (d: number) => d === 0, color: C.par },
|
||||||
|
{ label: "Bogey", test: (d: number) => d === 1, color: C.bogey },
|
||||||
|
{ label: "Dobbel bogey", test: (d: number) => d === 2, color: C.dobbel },
|
||||||
|
{ label: "Verre", test: (d: number) => d >= 3, color: C.verre },
|
||||||
|
]
|
||||||
|
.map((c) => {
|
||||||
|
const count = instances.filter((inst) => c.test(diff(inst))).length
|
||||||
|
const percentage = instances.length > 0 ? (count / instances.length) * 100 : 0
|
||||||
|
return { ...c, count, percentage }
|
||||||
|
})
|
||||||
|
.filter((c) => c.count > 0)
|
||||||
|
|
||||||
|
const maxPct = Math.max(...categories.map((c) => c.percentage), 1)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<h3 className="text-xs font-bold uppercase tracking-wide text-muted-foreground">Score-fordeling</h3>
|
||||||
|
{categories.map((c) => {
|
||||||
|
const width = maxPct > 0 ? (c.percentage / maxPct) * 100 : 0
|
||||||
|
return (
|
||||||
|
<div key={c.label} className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-baseline justify-between gap-2">
|
||||||
|
<span className="text-base font-semibold text-foreground">{c.label}</span>
|
||||||
|
<span className="text-sm font-bold tabular-nums text-muted-foreground">
|
||||||
|
{pct(c.percentage)} <span className="text-foreground">· {c.count}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-6 overflow-hidden rounded-lg bg-muted">
|
||||||
|
<div
|
||||||
|
className={cn("h-full rounded-lg")}
|
||||||
|
style={{ width: `${Math.max(width, 6)}%`, backgroundColor: c.color }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
// ville vært ren duplisering å bygge den to ganger.
|
// ville vært ren duplisering å bygge den to ganger.
|
||||||
|
|
||||||
import { useEffect, useState, type ReactNode } from "react"
|
import { useEffect, useState, type ReactNode } from "react"
|
||||||
import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, ChevronDown, Flag, Minus, Plus, Target } from "lucide-react"
|
import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, ChevronRight, Flag, Minus, Plus, Target } from "lucide-react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
export function golfTermForScore(n: number, par: number): string | null {
|
export function golfTermForScore(n: number, par: number): string | null {
|
||||||
|
|
@ -406,16 +406,20 @@ type HoleHistory = {
|
||||||
instances: HoleHistoryInstance[]
|
instances: HoleHistoryInstance[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HoleHistoryPanel({ url }: { url: string }) {
|
// ADR-079 (2026-08-16): panelet åpner nå den FULLE historikk-skjermen
|
||||||
|
// (components/hole-history-detail.tsx) ved klikk i stedet for å ekspandere
|
||||||
|
// en enkel liste inline -- flyttet HIT ut av selve slagvinduet/-arket til
|
||||||
|
// "skjermen før" (PlayerHoleCards for frittstående runder, "Oversikt"-
|
||||||
|
// steget for org-turneringer), se round-detail.tsx/individual-tournament-
|
||||||
|
// detail.tsx for de to bruksstedene.
|
||||||
|
export function HoleHistoryPanel({ url, onOpenFull }: { url: string; onOpenFull: () => void }) {
|
||||||
// undefined = laster ennå, null = ingen historikk å vise (custom bane,
|
// undefined = laster ennå, null = ingen historikk å vise (custom bane,
|
||||||
// gjest, eller genuint aldri spilt før).
|
// gjest, eller genuint aldri spilt før).
|
||||||
const [history, setHistory] = useState<HoleHistory | null | undefined>(undefined)
|
const [history, setHistory] = useState<HoleHistory | null | undefined>(undefined)
|
||||||
const [expanded, setExpanded] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
setHistory(undefined)
|
setHistory(undefined)
|
||||||
setExpanded(false)
|
|
||||||
fetch(url, { credentials: "include" })
|
fetch(url, { credentials: "include" })
|
||||||
.then((res) => (res.ok ? res.json() : null))
|
.then((res) => (res.ok ? res.json() : null))
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
|
@ -434,47 +438,23 @@ export function HoleHistoryPanel({ url }: { url: string }) {
|
||||||
const parSign = history.average_score_vs_par >= 0 ? "+" : ""
|
const parSign = history.average_score_vs_par >= 0 ? "+" : ""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 rounded-2xl border border-border bg-card p-4">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
onClick={onOpenFull}
|
||||||
onClick={() => setExpanded((e) => !e)}
|
className="flex items-center justify-between gap-3 rounded-2xl border border-border bg-card p-4 text-left transition-colors hover:bg-accent/50"
|
||||||
aria-expanded={expanded}
|
>
|
||||||
className="flex items-center justify-between gap-3 text-left"
|
<span className="flex flex-col gap-1">
|
||||||
>
|
<span className="text-sm font-bold text-foreground">Din historikk på dette hullet</span>
|
||||||
<span className="flex flex-col gap-1">
|
<span className="text-xs leading-relaxed text-muted-foreground text-pretty">
|
||||||
<span className="text-sm font-bold text-foreground">Din historikk på dette hullet</span>
|
Spilt {history.times_played} {history.times_played === 1 ? "gang" : "ganger"} før
|
||||||
<span className="text-xs leading-relaxed text-muted-foreground text-pretty">
|
{" "}({history.times_personal} frittstående, {history.times_tournament} turnering) -- snitt{" "}
|
||||||
Spilt {history.times_played} {history.times_played === 1 ? "gang" : "ganger"} før
|
{history.average_score.toFixed(1)} slag ({parSign}
|
||||||
{" "}({history.times_personal} frittstående, {history.times_tournament} turnering) -- snitt{" "}
|
{history.average_score_vs_par.toFixed(1)} til par)
|
||||||
{history.average_score.toFixed(1)} slag ({parSign}
|
{history.gir_percent !== null && `, GIR ${Math.round(history.gir_percent)}%`}
|
||||||
{history.average_score_vs_par.toFixed(1)} til par)
|
{history.average_putts !== null && `, ${history.average_putts.toFixed(1)} putter i snitt`}
|
||||||
{history.gir_percent !== null && `, GIR ${Math.round(history.gir_percent)}%`}
|
|
||||||
{history.average_putts !== null && `, ${history.average_putts.toFixed(1)} putter i snitt`}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown
|
</span>
|
||||||
aria-hidden="true"
|
<ChevronRight aria-hidden="true" className="size-4 shrink-0 text-muted-foreground" />
|
||||||
className={cn("size-4 shrink-0 text-muted-foreground transition-transform", expanded && "rotate-180")}
|
</button>
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
{expanded && (
|
|
||||||
<ul className="flex flex-col divide-y divide-border border-t border-border pt-2">
|
|
||||||
{history.instances.map((inst, i) => (
|
|
||||||
<li key={i} className="flex items-center justify-between gap-3 py-2 text-sm">
|
|
||||||
<span className="flex min-w-0 flex-col">
|
|
||||||
<span className="truncate font-semibold text-foreground">{inst.label}</span>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{inst.played_at ?? "Ukjent dato"} · {inst.source === "personal" ? "Frittstående" : "Turnering"}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span className="shrink-0 font-extrabold tabular-nums text-foreground">
|
|
||||||
{inst.score} slag
|
|
||||||
{inst.putts !== null ? `, ${inst.putts} putt${inst.putts === 1 ? "" : "er"}` : ""}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import {
|
||||||
Check,
|
Check,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
Copy,
|
Copy,
|
||||||
Crosshair,
|
Crosshair,
|
||||||
Flag,
|
Flag,
|
||||||
|
|
@ -46,11 +47,11 @@ import {
|
||||||
import {
|
import {
|
||||||
ChoiceRow,
|
ChoiceRow,
|
||||||
DirectionCross,
|
DirectionCross,
|
||||||
HoleHistoryPanel,
|
|
||||||
NumberPicker,
|
NumberPicker,
|
||||||
Stepper,
|
Stepper,
|
||||||
WizardSection,
|
WizardSection,
|
||||||
} from "@/components/hole-stat-inputs"
|
} from "@/components/hole-stat-inputs"
|
||||||
|
import { HoleHistoryDetail } from "@/components/hole-history-detail"
|
||||||
import { ClubPicker } from "@/components/teecup/club-picker"
|
import { ClubPicker } from "@/components/teecup/club-picker"
|
||||||
import { TournamentPresentationPanel } from "@/components/tournament-presentation"
|
import { TournamentPresentationPanel } from "@/components/tournament-presentation"
|
||||||
import { StrokePlayLeaderboard, type LeaderboardRow as StrokePlayRow } from "@/components/stroke-play-leaderboard"
|
import { StrokePlayLeaderboard, type LeaderboardRow as StrokePlayRow } from "@/components/stroke-play-leaderboard"
|
||||||
|
|
@ -3246,11 +3247,32 @@ type HolePatch = {
|
||||||
// arkitektur mekanikk som ikke var en del av forespørselen. Se
|
// arkitektur mekanikk som ikke var en del av forespørselen. Se
|
||||||
// wizardStepsFor()/WizardStep i round-detail.tsx for det speilede
|
// wizardStepsFor()/WizardStep i round-detail.tsx for det speilede
|
||||||
// mønsteret på runde-siden.
|
// mønsteret på runde-siden.
|
||||||
type SheetStep = "strokes" | "putts" | "puttDistance" | "direction" | "holeDetails"
|
// ADR-079 (2026-08-16): "overview" er et nytt FØRSTE steg -- personlig
|
||||||
|
// hull-historikk flyttet hit ut av selve "strokes"-steget, samme
|
||||||
|
// begrunnelse som round-siden (se PlayerHoleCards i round-detail.tsx).
|
||||||
|
// Turnering-siden har ingen ekstern "skjerm før" å legge den kompakte
|
||||||
|
// oppsummeringen i slik runde-siden har (HoleGrid er et tett rutenett,
|
||||||
|
// ikke en kortliste) -- blir derfor et eget steg i stedet. For å unngå å
|
||||||
|
// legge en ekstra obligatorisk trykk-runde til for HVER hull-registrering
|
||||||
|
// (18 hull × N spillere), hopper steget automatisk videre av seg selv når
|
||||||
|
// spilleren ikke har noen historikk å vise her (samme selv-skjulende
|
||||||
|
// oppførsel som HoleHistoryPanel selv har) -- kun synlig når det faktisk
|
||||||
|
// er noe å vise.
|
||||||
|
type HoleHistorySummary = {
|
||||||
|
times_played: number
|
||||||
|
times_personal: number
|
||||||
|
times_tournament: number
|
||||||
|
average_score: number
|
||||||
|
average_score_vs_par: number
|
||||||
|
gir_percent: number | null
|
||||||
|
average_putts: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type SheetStep = "overview" | "strokes" | "putts" | "puttDistance" | "direction" | "holeDetails"
|
||||||
|
|
||||||
function sheetStepsFor(statLevel: "strokes_and_putts" | "full"): SheetStep[] {
|
function sheetStepsFor(statLevel: "strokes_and_putts" | "full"): SheetStep[] {
|
||||||
if (statLevel === "full") return ["strokes", "putts", "puttDistance", "direction", "holeDetails"]
|
if (statLevel === "full") return ["overview", "strokes", "putts", "puttDistance", "direction", "holeDetails"]
|
||||||
return ["strokes", "putts", "puttDistance"]
|
return ["overview", "strokes", "putts", "puttDistance"]
|
||||||
}
|
}
|
||||||
|
|
||||||
function HoleStatsSheet({
|
function HoleStatsSheet({
|
||||||
|
|
@ -3292,6 +3314,33 @@ function HoleStatsSheet({
|
||||||
const isLastStep = stepIndex === steps.length - 1
|
const isLastStep = stepIndex === steps.length - 1
|
||||||
const canProceed = step === "strokes" ? strokes !== null : true
|
const canProceed = step === "strokes" ? strokes !== null : true
|
||||||
|
|
||||||
|
// "overview"-steget (ADR-079): hentet én gang ved åpning, ikke re-hentet
|
||||||
|
// per steg-bytte. undefined = laster ennå, null = ingen historikk.
|
||||||
|
const [historySummary, setHistorySummary] = useState<HoleHistorySummary | null | undefined>(undefined)
|
||||||
|
const [showFullHistory, setShowFullHistory] = useState(false)
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
fetch(historyUrl, { credentials: "include" })
|
||||||
|
.then((res) => (res.ok ? res.json() : null))
|
||||||
|
.then((data) => {
|
||||||
|
if (!cancelled) setHistorySummary(data)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!cancelled) setHistorySummary(null)
|
||||||
|
})
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
// Hopp automatisk forbi "overview" når det ikke er noe å vise -- unngår
|
||||||
|
// et ekstra obligatorisk trykk for HVER hull-registrering (se
|
||||||
|
// begrunnelsen på SheetStep over).
|
||||||
|
useEffect(() => {
|
||||||
|
if (step === "overview" && historySummary === null) forward()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [step, historySummary])
|
||||||
|
|
||||||
// Auto-hopp, samme mønster som ScoringWizard (round-detail.tsx) --
|
// Auto-hopp, samme mønster som ScoringWizard (round-detail.tsx) --
|
||||||
// hopper videre av seg selv når steget sitt eget felt får en verdi.
|
// hopper videre av seg selv når steget sitt eget felt får en verdi.
|
||||||
const autoField = step === "strokes" ? "strokes" : step === "putts" ? "putts" : step === "puttDistance" ? "puttBucket" : null
|
const autoField = step === "strokes" ? "strokes" : step === "putts" ? "putts" : step === "puttDistance" ? "puttBucket" : null
|
||||||
|
|
@ -3387,27 +3436,47 @@ function HoleStatsSheet({
|
||||||
|
|
||||||
<main className="flex min-h-0 flex-1 flex-col overflow-y-auto p-5 sm:p-6">
|
<main className="flex min-h-0 flex-1 flex-col overflow-y-auto p-5 sm:p-6">
|
||||||
<div className="mx-auto flex w-full max-w-sm flex-col gap-6">
|
<div className="mx-auto flex w-full max-w-sm flex-col gap-6">
|
||||||
{step === "strokes" && (
|
{/* ADR-079: personlig historikk, flyttet hit ut av "strokes"-
|
||||||
<>
|
steget -- se begrunnelsen for auto-hopp ved SheetStep over.
|
||||||
<NumberPicker
|
Samme kompakte oppsummering + klikk-for-full-historikk-
|
||||||
label="Slag"
|
mønster som HoleHistoryPanel (hole-stat-inputs.tsx), men
|
||||||
value={strokes}
|
lokal her siden foreldren allerede har hentet dataene (for
|
||||||
directValues={[1, 2, 3, 4, 5, 6, 7, 8, 9]}
|
auto-hopp-avgjørelsen) -- unngår et redundant andre kall. */}
|
||||||
expandValues={[10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]}
|
{step === "overview" && historySummary && (
|
||||||
expandLabel="10+"
|
<button
|
||||||
parValue={hole.par}
|
type="button"
|
||||||
showGolfTerms
|
onClick={() => setShowFullHistory(true)}
|
||||||
strokesReceived={strokesReceived}
|
className="flex items-center justify-between gap-3 rounded-2xl border border-border bg-card p-4 text-left transition-colors hover:bg-accent/50"
|
||||||
onChange={setStrokes}
|
>
|
||||||
readOnly={false}
|
<span className="flex flex-col gap-1">
|
||||||
/>
|
<span className="text-sm font-bold text-foreground">Din historikk på dette hullet</span>
|
||||||
|
<span className="text-xs leading-relaxed text-muted-foreground text-pretty">
|
||||||
|
Spilt {historySummary.times_played} {historySummary.times_played === 1 ? "gang" : "ganger"} før
|
||||||
|
{" "}({historySummary.times_personal} frittstående, {historySummary.times_tournament} turnering) --
|
||||||
|
{" "}snitt {historySummary.average_score.toFixed(1)} slag (
|
||||||
|
{historySummary.average_score_vs_par >= 0 ? "+" : ""}
|
||||||
|
{historySummary.average_score_vs_par.toFixed(1)} til par)
|
||||||
|
{historySummary.gir_percent !== null && `, GIR ${Math.round(historySummary.gir_percent)}%`}
|
||||||
|
{historySummary.average_putts !== null && `, ${historySummary.average_putts.toFixed(1)} putter i snitt`}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<ChevronRight aria-hidden="true" className="size-4 shrink-0 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Spillerens per-hull-historikk (ADR-072) -- skjuler seg
|
{step === "strokes" && (
|
||||||
selv når banen er custom/håndlaget eller spilleren ikke
|
<NumberPicker
|
||||||
har historikk ennå (se HoleHistoryPanel/app/hole_
|
label="Slag"
|
||||||
history.py). */}
|
value={strokes}
|
||||||
<HoleHistoryPanel url={historyUrl} />
|
directValues={[1, 2, 3, 4, 5, 6, 7, 8, 9]}
|
||||||
</>
|
expandValues={[10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]}
|
||||||
|
expandLabel="10+"
|
||||||
|
parValue={hole.par}
|
||||||
|
showGolfTerms
|
||||||
|
strokesReceived={strokesReceived}
|
||||||
|
onChange={setStrokes}
|
||||||
|
readOnly={false}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{step === "putts" && (
|
{step === "putts" && (
|
||||||
|
|
@ -3503,6 +3572,10 @@ function HoleStatsSheet({
|
||||||
{isLastStep ? (saving ? "Lagrer…" : "Lagre") : "Neste"}
|
{isLastStep ? (saving ? "Lagrer…" : "Lagre") : "Neste"}
|
||||||
</Button>
|
</Button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
{showFullHistory && (
|
||||||
|
<HoleHistoryDetail url={historyUrl} holeNumber={hole.hole_number} onClose={() => setShowFullHistory(false)} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ import {
|
||||||
} from "@/components/hole-stat-inputs"
|
} from "@/components/hole-stat-inputs"
|
||||||
import { ShotMeasurementSheet } from "@/components/shot/shot-measurement-sheet"
|
import { ShotMeasurementSheet } from "@/components/shot/shot-measurement-sheet"
|
||||||
import { HoleTargetDistance } from "@/components/hole-target-distance"
|
import { HoleTargetDistance } from "@/components/hole-target-distance"
|
||||||
|
import { HoleHistoryDetail } from "@/components/hole-history-detail"
|
||||||
import { bearingDegrees } from "@/lib/geo"
|
import { bearingDegrees } from "@/lib/geo"
|
||||||
import { enqueueWrite, flushQueue, queueCount } from "@/lib/offline-queue"
|
import { enqueueWrite, flushQueue, queueCount } from "@/lib/offline-queue"
|
||||||
|
|
||||||
|
|
@ -2139,13 +2140,6 @@ function ScoringWizard({
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Spillerens per-hull-historikk (ADR-072) -- skjuler seg selv
|
|
||||||
når banen er custom/håndlaget eller spilleren ikke har noen
|
|
||||||
historikk ennå (se HoleHistoryPanel/app/hole_history.py). */}
|
|
||||||
<HoleHistoryPanel
|
|
||||||
url={`/rounds/${roundId}/participants/${player.id}/holes/${hole.holeNumber}/history`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* "Plukket opp" (migrasjon 038, Stableford, 2026-07-29) --
|
{/* "Plukket opp" (migrasjon 038, Stableford, 2026-07-29) --
|
||||||
kun for slagspill/Stableford, og kun når serveren faktisk
|
kun for slagspill/Stableford, og kun når serveren faktisk
|
||||||
kan regne ut Net Double Bogey (krever kjent strokes_received). */}
|
kan regne ut Net Double Bogey (krever kjent strokes_received). */}
|
||||||
|
|
@ -6581,6 +6575,11 @@ function PlayerHoleCards({
|
||||||
) => Promise<{ ok: true } | { ok: false; message: string }>
|
) => Promise<{ ok: true } | { ok: false; message: string }>
|
||||||
}) {
|
}) {
|
||||||
const [flagSheetParticipantId, setFlagSheetParticipantId] = useState<string | null>(null)
|
const [flagSheetParticipantId, setFlagSheetParticipantId] = useState<string | null>(null)
|
||||||
|
// ADR-079 (2026-08-16): hvilken spillers fulle hull-historikk som vises
|
||||||
|
// akkurat nå, om noen -- historikk-oppsummeringen (HoleHistoryPanel)
|
||||||
|
// sitter her i kortet (skjermen FØR slagvinduet), åpner denne fullskjerm-
|
||||||
|
// visningen ved klikk i stedet for å ekspandere inline.
|
||||||
|
const [historyPlayerId, setHistoryPlayerId] = useState<string | null>(null)
|
||||||
const holeMeta: Record<number, { par: number; index: number }> = {}
|
const holeMeta: Record<number, { par: number; index: number }> = {}
|
||||||
for (const list of Object.values(holesByParticipant)) {
|
for (const list of Object.values(holesByParticipant)) {
|
||||||
for (const h of list) {
|
for (const h of list) {
|
||||||
|
|
@ -6839,6 +6838,17 @@ function PlayerHoleCards({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* Personlig hull-historikk (ADR-072, flyttet hit ut av
|
||||||
|
slagvinduet ADR-079, 2026-08-16) -- skjermen FØR
|
||||||
|
registrering/"Avslutt for {navn}", ikke inni selve
|
||||||
|
slagvinduet. Skjuler seg selv når banen er custom eller
|
||||||
|
spilleren ikke har historikk ennå. */}
|
||||||
|
<div className="px-4 pb-2">
|
||||||
|
<HoleHistoryPanel
|
||||||
|
url={`/rounds/${roundId}/participants/${player.id}/holes/${currentHole}/history`}
|
||||||
|
onOpenFull={() => setHistoryPlayerId(player.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<ParticipantCompletionActions
|
<ParticipantCompletionActions
|
||||||
player={player}
|
player={player}
|
||||||
holesPlayed={list?.filter((h) => h.played).length ?? 0}
|
holesPlayed={list?.filter((h) => h.played).length ?? 0}
|
||||||
|
|
@ -6892,6 +6902,14 @@ function PlayerHoleCards({
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
{historyPlayerId && (
|
||||||
|
<HoleHistoryDetail
|
||||||
|
url={`/rounds/${roundId}/participants/${historyPlayerId}/holes/${currentHole}/history`}
|
||||||
|
holeNumber={currentHole}
|
||||||
|
onClose={() => setHistoryPlayerId(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue