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:
Erol Haagenrud 2026-08-16 10:21:48 +02:00
parent 46288c4b5f
commit 9f2711e4b3
6 changed files with 508 additions and 76 deletions

View file

@ -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)
1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003)

View file

@ -11718,3 +11718,37 @@ Neste steg:
**Rullet ut 2026-08-16** -- ingen migrasjon, `docker compose build
teecup_frontend && up -d` etter eksplisitt bekreftelse fra bruker.
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å.

View 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 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>
)
}

View file

@ -11,7 +11,7 @@
// ville vært ren duplisering å bygge den to ganger.
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"
export function golfTermForScore(n: number, par: number): string | null {
@ -406,16 +406,20 @@ type HoleHistory = {
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,
// gjest, eller genuint aldri spilt før).
const [history, setHistory] = useState<HoleHistory | null | undefined>(undefined)
const [expanded, setExpanded] = useState(false)
useEffect(() => {
let cancelled = false
setHistory(undefined)
setExpanded(false)
fetch(url, { credentials: "include" })
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
@ -434,12 +438,10 @@ export function HoleHistoryPanel({ url }: { url: string }) {
const parSign = history.average_score_vs_par >= 0 ? "+" : ""
return (
<div className="flex flex-col gap-2 rounded-2xl border border-border bg-card p-4">
<button
type="button"
onClick={() => setExpanded((e) => !e)}
aria-expanded={expanded}
className="flex items-center justify-between gap-3 text-left"
onClick={onOpenFull}
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"
>
<span className="flex flex-col gap-1">
<span className="text-sm font-bold text-foreground">Din historikk dette hullet</span>
@ -452,29 +454,7 @@ export function HoleHistoryPanel({ url }: { url: string }) {
{history.average_putts !== null && `, ${history.average_putts.toFixed(1)} putter i snitt`}
</span>
</span>
<ChevronDown
aria-hidden="true"
className={cn("size-4 shrink-0 text-muted-foreground transition-transform", expanded && "rotate-180")}
/>
<ChevronRight aria-hidden="true" className="size-4 shrink-0 text-muted-foreground" />
</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>
)
}

View file

@ -21,6 +21,7 @@ import {
Check,
ChevronDown,
ChevronLeft,
ChevronRight,
Copy,
Crosshair,
Flag,
@ -46,11 +47,11 @@ import {
import {
ChoiceRow,
DirectionCross,
HoleHistoryPanel,
NumberPicker,
Stepper,
WizardSection,
} from "@/components/hole-stat-inputs"
import { HoleHistoryDetail } from "@/components/hole-history-detail"
import { ClubPicker } from "@/components/teecup/club-picker"
import { TournamentPresentationPanel } from "@/components/tournament-presentation"
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
// wizardStepsFor()/WizardStep i round-detail.tsx for det speilede
// 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[] {
if (statLevel === "full") return ["strokes", "putts", "puttDistance", "direction", "holeDetails"]
return ["strokes", "putts", "puttDistance"]
if (statLevel === "full") return ["overview", "strokes", "putts", "puttDistance", "direction", "holeDetails"]
return ["overview", "strokes", "putts", "puttDistance"]
}
function HoleStatsSheet({
@ -3292,6 +3314,33 @@ function HoleStatsSheet({
const isLastStep = stepIndex === steps.length - 1
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) --
// 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
@ -3387,8 +3436,35 @@ function HoleStatsSheet({
<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">
{/* ADR-079: personlig historikk, flyttet hit ut av "strokes"-
steget -- se begrunnelsen for auto-hopp ved SheetStep over.
Samme kompakte oppsummering + klikk-for-full-historikk-
mønster som HoleHistoryPanel (hole-stat-inputs.tsx), men
lokal her siden foreldren allerede har hentet dataene (for
auto-hopp-avgjørelsen) -- unngår et redundant andre kall. */}
{step === "overview" && historySummary && (
<button
type="button"
onClick={() => setShowFullHistory(true)}
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"
>
<span className="flex flex-col gap-1">
<span className="text-sm font-bold text-foreground">Din historikk 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>
)}
{step === "strokes" && (
<>
<NumberPicker
label="Slag"
value={strokes}
@ -3401,13 +3477,6 @@ function HoleStatsSheet({
onChange={setStrokes}
readOnly={false}
/>
{/* Spillerens per-hull-historikk (ADR-072) -- skjuler seg
selv når banen er custom/håndlaget eller spilleren ikke
har historikk ennå (se HoleHistoryPanel/app/hole_
history.py). */}
<HoleHistoryPanel url={historyUrl} />
</>
)}
{step === "putts" && (
@ -3503,6 +3572,10 @@ function HoleStatsSheet({
{isLastStep ? (saving ? "Lagrer…" : "Lagre") : "Neste"}
</Button>
</footer>
{showFullHistory && (
<HoleHistoryDetail url={historyUrl} holeNumber={hole.hole_number} onClose={() => setShowFullHistory(false)} />
)}
</div>
)
}

View file

@ -57,6 +57,7 @@ import {
} from "@/components/hole-stat-inputs"
import { ShotMeasurementSheet } from "@/components/shot/shot-measurement-sheet"
import { HoleTargetDistance } from "@/components/hole-target-distance"
import { HoleHistoryDetail } from "@/components/hole-history-detail"
import { bearingDegrees } from "@/lib/geo"
import { enqueueWrite, flushQueue, queueCount } from "@/lib/offline-queue"
@ -2139,13 +2140,6 @@ function ScoringWizard({
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) --
kun for slagspill/Stableford, og kun når serveren faktisk
kan regne ut Net Double Bogey (krever kjent strokes_received). */}
@ -6581,6 +6575,11 @@ function PlayerHoleCards({
) => Promise<{ ok: true } | { ok: false; message: string }>
}) {
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 }> = {}
for (const list of Object.values(holesByParticipant)) {
for (const h of list) {
@ -6839,6 +6838,17 @@ function PlayerHoleCards({
/>
</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
player={player}
holesPlayed={list?.filter((h) => h.played).length ?? 0}
@ -6892,6 +6902,14 @@ function PlayerHoleCards({
)
})}
</ul>
{historyPlayerId && (
<HoleHistoryDetail
url={`/rounds/${roundId}/participants/${historyPlayerId}/holes/${currentHole}/history`}
holeNumber={currentHole}
onClose={() => setHistoryPlayerId(null)}
/>
)}
</div>
)
}