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)
|
||||
|
||||
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
|
||||
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å.
|
||||
|
|
|
|||
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.
|
||||
|
||||
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 på 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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue