From 9f2711e4b3517b847dacd77d1154d9a08998df32 Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Sun, 16 Aug 2026 10:21:48 +0200 Subject: [PATCH] Personlig hull-historikk ut av slagvinduet + full historikk med grafer (ADR-079) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ARCHITECTURE_DECISIONS.md | 85 ++++++ CHANGELOG.md | 34 +++ frontend/components/hole-history-detail.tsx | 242 ++++++++++++++++++ frontend/components/hole-stat-inputs.tsx | 70 ++--- .../individual-tournament-detail.tsx | 121 +++++++-- frontend/components/round-detail.tsx | 32 ++- 6 files changed, 508 insertions(+), 76 deletions(-) create mode 100644 frontend/components/hole-history-detail.tsx diff --git a/ARCHITECTURE_DECISIONS.md b/ARCHITECTURE_DECISIONS.md index 233a0da..b372f45 100644 --- a/ARCHITECTURE_DECISIONS.md +++ b/ARCHITECTURE_DECISIONS.md @@ -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) diff --git a/CHANGELOG.md b/CHANGELOG.md index db728ae..2fbf163 100644 --- a/CHANGELOG.md +++ b/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å. diff --git a/frontend/components/hole-history-detail.tsx b/frontend/components/hole-history-detail.tsx new file mode 100644 index 0000000..b75b900 --- /dev/null +++ b/frontend/components/hole-history-detail.tsx @@ -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(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 ( +
+
+ +
+ + Hull {holeNumber} + {par !== null ? ` · Par ${par}` : ""} + + Din historikk +
+
+ +
+
+ {history === undefined ? ( +
+ + ) : history === null || history.times_played === 0 ? ( +

+ Ingen historikk å vise på dette hullet ennå. +

+ ) : ( + <> +
+ + + + +
+ +
+ + Snitt til par + + + {signed(history.average_score_vs_par)} + +
+ + {(history.gir_percent !== null || history.fairway_hit_percent !== null) && ( +
+ {history.gir_percent !== null && } + {history.fairway_hit_percent !== null && ( + + )} +
+ )} + + + +
+

Alle gangene

+
    + {history.instances.map((inst, i) => ( +
  • + + {inst.label} + + {inst.played_at ?? "Ukjent dato"} · {inst.source === "personal" ? "Frittstående" : "Turnering"} + + + + {inst.score} slag + {inst.putts !== null ? `, ${inst.putts} putt${inst.putts === 1 ? "" : "er"}` : ""} + +
  • + ))} +
+
+ + )} +
+
+
+ ) +} + +function StatTile({ label, value }: { label: string; value: string }) { + return ( +
+ {label} + {value} +
+ ) +} + +// 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 ( +
+

Score-fordeling

+ {categories.map((c) => { + const width = maxPct > 0 ? (c.percentage / maxPct) * 100 : 0 + return ( +
+
+ {c.label} + + {pct(c.percentage)} · {c.count} + +
+
+
+
+
+ ) + })} +
+ ) +} diff --git a/frontend/components/hole-stat-inputs.tsx b/frontend/components/hole-stat-inputs.tsx index 8ea7364..6e36c47 100644 --- a/frontend/components/hole-stat-inputs.tsx +++ b/frontend/components/hole-stat-inputs.tsx @@ -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(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,47 +438,23 @@ export function HoleHistoryPanel({ url }: { url: string }) { const parSign = history.average_score_vs_par >= 0 ? "+" : "" return ( -
- - {expanded && ( -
    - {history.instances.map((inst, i) => ( -
  • - - {inst.label} - - {inst.played_at ?? "Ukjent dato"} · {inst.source === "personal" ? "Frittstående" : "Turnering"} - - - - {inst.score} slag - {inst.putts !== null ? `, ${inst.putts} putt${inst.putts === 1 ? "" : "er"}` : ""} - -
  • - ))} -
- )} -
+ +
)} + {/* 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å. */} +
+ setHistoryPlayerId(player.id)} + /> +
h.played).length ?? 0} @@ -6892,6 +6902,14 @@ function PlayerHoleCards({ ) })} + + {historyPlayerId && ( + setHistoryPlayerId(null)} + /> + )} ) }