teecup/frontend/components/rounds-stats-summary.tsx

830 lines
35 KiB
TypeScript
Raw Normal View History

"use client"
// Aggregert statistikk på tvers av "Egne runder" (ADR-033), lenket fra
// dashbordets "Statistikk"-seksjon. Egen backend-aggregering
// (GET /rounds/stats/summary) -- ikke bare klientside-utledning av
// round-stats.tsx sin logikk, siden putt/18-hull-regelen (padding av
// uspilte hull til 2 putter, KUN for dette tallet) og "forrige periode"-
// vinduet må regnes over ALLE fullførte runder samlet, ikke én om gangen.
//
// Visuell versjon 2 (2026-07-28): brukeren delte et referansebilde av en
// konkurrent-app sin statistikkskjerm (donuter med sentertall, kompass-
// diagram for miss-retning) og ba om noe tilsvarende -- håndkodet direkte
// (ingen V0-credits igjen), verifisert med ekte skjermbilder via Chrome
// DevTools MCP i stedet for kun typesjekk, se CLAUDE.md. Inspirert av
// referansen, ikke kopiert: egne farger/ikonografi, ingen flyt-diagram for
// scrambling/sand save (ville antydet en forgrening dataene ikke har -- det
// er to uavhengige prosenter, ikke en beslutningstre), ingen trendlinje
// ennå (for lite rundehistorikk til å være meningsfullt, se CLAUDE.md).
import type React from "react"
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats Tre brukertilbakemeldinger etter gjennomgang av statistikksidene: 1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått). 2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP, beregnet fra faktiske runder) i stedet for det manuelt satte tallet i profilen -- historikk-sparklinen filtreres til samme kilde (source= "computed"), så den ikke lenger blander manuelle og beregnede punkter. 3. /my-rounds/stats: - Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst, seende brukere så bare et ikon og et tall uten kontekst. - Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet andel, Innspill er en retningsbeskrivelse -- samme prinsipp som round-stats.tsx sin eksisterende splitt). Utslag flyttet først. Redning og "Til par: med vs. uten" flyttet ned under Annet. - Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde, eldste til venstre) -- tidligere bevisst utsatt pga. for lite rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend: RoundStatsSummary fikk et nytt round_series-felt (kronologisk snitt-til-par per runde i valgt vindu). Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl. det nye round_series-feltet via en scratch-DB-kjøring). Full scratch- stack med seks syntetiske runder (varierende dato/resultat) browser- verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, BarChart3, Flag, TrendingDown, TrendingUp, Waves, Wind } from "lucide-react"
type ApiRoundSeriesPoint = { played_at: string; score_to_par: number }
type ApiStatsSummary = {
rounds_completed: number
avg_score_to_par: number | null
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats Tre brukertilbakemeldinger etter gjennomgang av statistikksidene: 1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått). 2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP, beregnet fra faktiske runder) i stedet for det manuelt satte tallet i profilen -- historikk-sparklinen filtreres til samme kilde (source= "computed"), så den ikke lenger blander manuelle og beregnede punkter. 3. /my-rounds/stats: - Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst, seende brukere så bare et ikon og et tall uten kontekst. - Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet andel, Innspill er en retningsbeskrivelse -- samme prinsipp som round-stats.tsx sin eksisterende splitt). Utslag flyttet først. Redning og "Til par: med vs. uten" flyttet ned under Annet. - Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde, eldste til venstre) -- tidligere bevisst utsatt pga. for lite rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend: RoundStatsSummary fikk et nytt round_series-felt (kronologisk snitt-til-par per runde i valgt vindu). Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl. det nye round_series-feltet via en scratch-DB-kjøring). Full scratch- stack med seks syntetiske runder (varierende dato/resultat) browser- verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
round_series: ApiRoundSeriesPoint[]
avg_putts_per_18: number | null
putts_tracked_rounds: number
fairway_hit_pct: number | null
fairway_left_pct: number | null
fairway_right_pct: number | null
fairway_tracked_holes: number
gir_pct: number | null
gir_tracked_holes: number
green_miss_long_pct: number | null
green_miss_short_pct: number | null
green_miss_left_pct: number | null
green_miss_right_pct: number | null
green_miss_tracked_holes: number
one_putt_pct: number | null
putt_dist_one_pct: number | null
putt_dist_two_pct: number | null
putt_dist_three_plus_pct: number | null
scrambling_pct: number | null
sand_save_pct: number | null
avg_chip_per_round: number | null
avg_bunker_per_round: number | null
avg_penalty_per_round: number | null
avg_anyway_per_round: number | null
avg_to_par_with_gir: number | null
avg_to_par_without_gir: number | null
avg_to_par_fairway_hit: number | null
avg_to_par_fairway_miss: number | null
avg_to_par_with_bunker: number | null
avg_to_par_without_bunker: number | null
avg_to_par_with_anyway: number | null
avg_to_par_without_anyway: number | null
}
type ApiWindowSummary = {
window: string
current: ApiStatsSummary
previous: ApiStatsSummary
}
type WindowKey = "last_round" | "last_5" | "last_10" | "month" | "year" | "last_year" | "all"
const WINDOW_OPTIONS: { key: WindowKey; label: string }[] = [
{ key: "last_round", label: "Siste runde" },
{ key: "last_5", label: "Siste 5" },
{ key: "last_10", label: "Siste 10" },
{ key: "month", label: "Denne måneden" },
{ key: "year", label: "I år" },
{ key: "last_year", label: "Siste år" },
{ key: "all", label: "Alltid" },
]
function signed(n: number): string {
const sign = n > 0 ? "+" : n < 0 ? "" : "±"
return `${sign}${Math.abs(n).toFixed(1).replace(".", ",")}`
}
function pct(n: number): string {
return `${Math.round(n)} %`
}
// Prosentpoeng-differanse mellom to andeler (ikke "N % endring", men "N pp").
function signedPoints(n: number): string {
const rounded = Math.round(n)
const sign = rounded > 0 ? "+" : rounded < 0 ? "" : "±"
return `${sign}${Math.abs(rounded)} pp`
}
// "up" = høyere tall er bedre (fairwaytreff, GIR, ...), "down" = lavere
// tall er bedre (snitt til par, putt/runde, ...). Utelatt = ren beskrivende
// retning (miss-retning venstre/høyre osv.) -- ingen "bedre/verre" der.
function Delta({
current,
previous,
format,
goodDirection,
}: {
current: number | null
previous: number | null
format: (diff: number) => string
goodDirection?: "up" | "down"
}) {
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats Tre brukertilbakemeldinger etter gjennomgang av statistikksidene: 1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått). 2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP, beregnet fra faktiske runder) i stedet for det manuelt satte tallet i profilen -- historikk-sparklinen filtreres til samme kilde (source= "computed"), så den ikke lenger blander manuelle og beregnede punkter. 3. /my-rounds/stats: - Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst, seende brukere så bare et ikon og et tall uten kontekst. - Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet andel, Innspill er en retningsbeskrivelse -- samme prinsipp som round-stats.tsx sin eksisterende splitt). Utslag flyttet først. Redning og "Til par: med vs. uten" flyttet ned under Annet. - Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde, eldste til venstre) -- tidligere bevisst utsatt pga. for lite rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend: RoundStatsSummary fikk et nytt round_series-felt (kronologisk snitt-til-par per runde i valgt vindu). Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl. det nye round_series-feltet via en scratch-DB-kjøring). Full scratch- stack med seks syntetiske runder (varierende dato/resultat) browser- verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
// Teksten "vs. forrige periode" var tidligere KUN skjermleser-tekst --
// seende brukere så bare et ikon + et tall + "pp", uten noen synlig
// forklaring på hva tallet ble sammenlignet MOT. Nå alltid synlig, slik
// at delta-en er selvforklarende alene, uten å måtte lese en fjern
// overskrift lenger oppe på siden (2026-08-12, brukertilbakemelding).
if (current === null || previous === null) return null
const diff = current - previous
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats Tre brukertilbakemeldinger etter gjennomgang av statistikksidene: 1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått). 2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP, beregnet fra faktiske runder) i stedet for det manuelt satte tallet i profilen -- historikk-sparklinen filtreres til samme kilde (source= "computed"), så den ikke lenger blander manuelle og beregnede punkter. 3. /my-rounds/stats: - Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst, seende brukere så bare et ikon og et tall uten kontekst. - Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet andel, Innspill er en retningsbeskrivelse -- samme prinsipp som round-stats.tsx sin eksisterende splitt). Utslag flyttet først. Redning og "Til par: med vs. uten" flyttet ned under Annet. - Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde, eldste til venstre) -- tidligere bevisst utsatt pga. for lite rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend: RoundStatsSummary fikk et nytt round_series-felt (kronologisk snitt-til-par per runde i valgt vindu). Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl. det nye round_series-feltet via en scratch-DB-kjøring). Full scratch- stack med seks syntetiske runder (varierende dato/resultat) browser- verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
if (diff === 0) {
return <span className="text-xs font-semibold text-muted-foreground">Uendret vs. forrige periode</span>
}
const improved = goodDirection === "up" ? diff > 0 : goodDirection === "down" ? diff < 0 : null
const Icon = diff > 0 ? TrendingUp : TrendingDown
const colorClass = improved === true ? "text-primary" : improved === false ? "text-brand-orange" : "text-muted-foreground"
return (
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats Tre brukertilbakemeldinger etter gjennomgang av statistikksidene: 1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått). 2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP, beregnet fra faktiske runder) i stedet for det manuelt satte tallet i profilen -- historikk-sparklinen filtreres til samme kilde (source= "computed"), så den ikke lenger blander manuelle og beregnede punkter. 3. /my-rounds/stats: - Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst, seende brukere så bare et ikon og et tall uten kontekst. - Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet andel, Innspill er en retningsbeskrivelse -- samme prinsipp som round-stats.tsx sin eksisterende splitt). Utslag flyttet først. Redning og "Til par: med vs. uten" flyttet ned under Annet. - Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde, eldste til venstre) -- tidligere bevisst utsatt pga. for lite rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend: RoundStatsSummary fikk et nytt round_series-felt (kronologisk snitt-til-par per runde i valgt vindu). Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl. det nye round_series-feltet via en scratch-DB-kjøring). Full scratch- stack med seks syntetiske runder (varierende dato/resultat) browser- verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
<span className={`inline-flex flex-wrap items-center gap-1 text-xs font-bold ${colorClass}`}>
<Icon aria-hidden="true" className="size-3.5" />
<span className="tabular-nums">{format(diff)}</span>
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats Tre brukertilbakemeldinger etter gjennomgang av statistikksidene: 1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått). 2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP, beregnet fra faktiske runder) i stedet for det manuelt satte tallet i profilen -- historikk-sparklinen filtreres til samme kilde (source= "computed"), så den ikke lenger blander manuelle og beregnede punkter. 3. /my-rounds/stats: - Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst, seende brukere så bare et ikon og et tall uten kontekst. - Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet andel, Innspill er en retningsbeskrivelse -- samme prinsipp som round-stats.tsx sin eksisterende splitt). Utslag flyttet først. Redning og "Til par: med vs. uten" flyttet ned under Annet. - Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde, eldste til venstre) -- tidligere bevisst utsatt pga. for lite rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend: RoundStatsSummary fikk et nytt round_series-felt (kronologisk snitt-til-par per runde i valgt vindu). Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl. det nye round_series-feltet via en scratch-DB-kjøring). Full scratch- stack med seks syntetiske runder (varierende dato/resultat) browser- verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
<span className="font-semibold text-muted-foreground">vs. forrige periode</span>
</span>
)
}
function StatTile({
label,
value,
hint,
delta,
}: {
label: string
value: string
hint?: string
delta?: React.ReactNode
}) {
return (
2026-07-29 12:44:51 +02:00
<div className="flex flex-col gap-1 rounded-2xl border border-border bg-card p-4 shadow-md shadow-black/8">
<span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">{label}</span>
<span className="text-2xl font-extrabold tabular-nums text-foreground sm:text-3xl">{value}</span>
{delta}
{hint && <span className="text-xs text-muted-foreground">{hint}</span>}
</div>
)
}
function Card({ title, subtitle, children }: { title: string; subtitle?: string; children: React.ReactNode }) {
return (
2026-07-29 12:44:51 +02:00
<section className="flex flex-col gap-4 rounded-2xl border border-border bg-card p-4 shadow-md shadow-black/8 sm:p-5">
<div>
<h2 className="text-lg font-extrabold tracking-tight text-foreground">{title}</h2>
{subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
</div>
{children}
</section>
)
}
function Section({
title,
icon: Icon,
children,
}: {
title: string
icon: typeof BarChart3
children: React.ReactNode
}) {
return (
<section className="flex flex-col gap-3">
<h2 className="flex items-center gap-2 text-lg font-extrabold tracking-tight text-foreground">
<Icon aria-hidden="true" className="size-5 text-primary" />
{title}
</h2>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3">{children}</div>
</section>
)
}
// --- Donut: sentertall + inntil 4 fargede segmenter (conic-gradient) -------
// Gjenbrukt for BÅDE puttfordelingen (3 segmenter) og de enkle rednings-
// gaugene (1 segment fylt + resten "spor"). Prosent er ALLTID skrevet ut i
// en forklaringsliste under -- fargen bærer aldri betydningen alene.
function Donut({
segments,
centerValue,
centerLabel,
size = 132,
thickness = 18,
showLegend = true,
}: {
segments: { label: string; value: number; color: string }[]
centerValue: string
centerLabel: string
size?: number
thickness?: number
showLegend?: boolean
}) {
let cursor = 0
const stops: string[] = []
for (const seg of segments) {
const start = cursor
const end = cursor + seg.value
stops.push(`${seg.color} ${start}% ${end}%`)
cursor = end
}
if (cursor < 100) stops.push(`var(--muted) ${cursor}% 100%`)
const gradient = `conic-gradient(${stops.join(", ")})`
const ring = (
<div
className="relative shrink-0 rounded-full"
style={{ width: size, height: size, background: gradient }}
role="img"
aria-label={`${centerLabel}: ${centerValue}. ${segments.map((s) => `${s.label} ${Math.round(s.value)} prosent`).join(", ")}`}
>
<div
className="absolute rounded-full bg-card"
style={{
width: size - thickness * 2,
height: size - thickness * 2,
top: thickness,
left: thickness,
}}
/>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-xl font-extrabold tabular-nums text-foreground">{centerValue}</span>
<span className="text-[11px] font-semibold text-muted-foreground">{centerLabel}</span>
</div>
</div>
)
if (!showLegend) return ring
return (
<div className="flex flex-col items-center gap-3 sm:flex-row sm:items-center">
{ring}
<ul className="flex flex-1 flex-col gap-1.5">
{segments.map((s) => (
<li key={s.label} className="flex items-center gap-2 text-sm text-muted-foreground">
<span aria-hidden="true" className="size-2.5 shrink-0 rounded-full" style={{ backgroundColor: s.color }} />
<span className="flex-1">{s.label}</span>
<span className="font-bold tabular-nums text-foreground">{Math.round(s.value)} %</span>
</li>
))}
</ul>
</div>
)
}
// --- Kompass-diagram for greentreff-miss-retning ----------------------------
// Greentreff i midten, langt over, kort under, venstre til venstre, høyre
// til høyre -- speiler hvor bommen faktisk gikk relativt til green, i
// stedet for en flat fordelingsbar. Alle fire retningsceller viser tallet
// sitt som tekst uansett -- retning er beskrivende, ikke god/dårlig, så
// ingen av dem fargekodes grønn/oransje (kun sentercellen, som ER et
// treff/bom-tall, får grønn fremheving).
function CompassCell({ icon: Icon, label, value }: { icon: typeof ArrowUp; label: string; value: number | null }) {
return (
<div className="flex min-h-16 flex-col items-center justify-center gap-0.5 rounded-2xl border border-border bg-card p-2">
<Icon aria-hidden="true" className="size-4 text-muted-foreground" />
<span className="text-lg font-extrabold tabular-nums text-foreground">{value !== null ? `${Math.round(value)} %` : ""}</span>
<span className="text-[11px] font-semibold text-muted-foreground">{label}</span>
</div>
)
}
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats Tre brukertilbakemeldinger etter gjennomgang av statistikksidene: 1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått). 2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP, beregnet fra faktiske runder) i stedet for det manuelt satte tallet i profilen -- historikk-sparklinen filtreres til samme kilde (source= "computed"), så den ikke lenger blander manuelle og beregnede punkter. 3. /my-rounds/stats: - Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst, seende brukere så bare et ikon og et tall uten kontekst. - Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet andel, Innspill er en retningsbeskrivelse -- samme prinsipp som round-stats.tsx sin eksisterende splitt). Utslag flyttet først. Redning og "Til par: med vs. uten" flyttet ned under Annet. - Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde, eldste til venstre) -- tidligere bevisst utsatt pga. for lite rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend: RoundStatsSummary fikk et nytt round_series-felt (kronologisk snitt-til-par per runde i valgt vindu). Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl. det nye round_series-feltet via en scratch-DB-kjøring). Full scratch- stack med seks syntetiske runder (varierende dato/resultat) browser- verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
// Greentreff (GIR) og Innspill var tidligere ÉN kombinert komponent
// (GirCompass). Splittet i to 2026-08-12 på brukerens eksplisitte
// instruks: GIR er en beregnet andel (traff/bommet), Innspill er en
// retningsbeskrivelse -- to ulike typer informasjon, samme prinsipp som
// round-stats.tsx sin split mellom "Greentreff (GIR)" og "Bom-retning på
// green" (se den filen).
function GirGauge({ hitPct, trackedHoles }: { hitPct: number | null; trackedHoles: number }) {
if (hitPct === null) {
return <p className="text-sm text-muted-foreground">Ingen greentreff-data registrert ennå.</p>
}
return (
<div className="flex flex-col items-center gap-2">
<Donut
segments={[
{ label: "Traff green", value: hitPct, color: "var(--primary)" },
{ label: "Bommet green", value: 100 - hitPct, color: "var(--brand-orange)" },
]}
centerValue={`${Math.round(hitPct)} %`}
centerLabel="GIR"
/>
<p className="text-xs text-muted-foreground">{trackedHoles} hull registrert</p>
</div>
)
}
function ApproachDirectionCompass({
trackedHoles,
long,
short,
left,
right,
}: {
trackedHoles: number
long: number | null
short: number | null
left: number | null
right: number | null
}) {
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats Tre brukertilbakemeldinger etter gjennomgang av statistikksidene: 1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått). 2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP, beregnet fra faktiske runder) i stedet for det manuelt satte tallet i profilen -- historikk-sparklinen filtreres til samme kilde (source= "computed"), så den ikke lenger blander manuelle og beregnede punkter. 3. /my-rounds/stats: - Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst, seende brukere så bare et ikon og et tall uten kontekst. - Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet andel, Innspill er en retningsbeskrivelse -- samme prinsipp som round-stats.tsx sin eksisterende splitt). Utslag flyttet først. Redning og "Til par: med vs. uten" flyttet ned under Annet. - Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde, eldste til venstre) -- tidligere bevisst utsatt pga. for lite rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend: RoundStatsSummary fikk et nytt round_series-felt (kronologisk snitt-til-par per runde i valgt vindu). Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl. det nye round_series-feltet via en scratch-DB-kjøring). Full scratch- stack med seks syntetiske runder (varierende dato/resultat) browser- verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
if (trackedHoles === 0) {
return <p className="text-sm text-muted-foreground">Ingen innspill med retning registrert ennå.</p>
}
return (
<div className="flex flex-col items-center gap-3">
<div className="grid w-full max-w-[280px] grid-cols-3 grid-rows-3 gap-2.5">
<div aria-hidden="true" />
<CompassCell icon={ArrowUp} label="Langt" value={long} />
<div aria-hidden="true" />
<CompassCell icon={ArrowLeft} label="Venstre" value={left} />
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats Tre brukertilbakemeldinger etter gjennomgang av statistikksidene: 1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått). 2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP, beregnet fra faktiske runder) i stedet for det manuelt satte tallet i profilen -- historikk-sparklinen filtreres til samme kilde (source= "computed"), så den ikke lenger blander manuelle og beregnede punkter. 3. /my-rounds/stats: - Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst, seende brukere så bare et ikon og et tall uten kontekst. - Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet andel, Innspill er en retningsbeskrivelse -- samme prinsipp som round-stats.tsx sin eksisterende splitt). Utslag flyttet først. Redning og "Til par: med vs. uten" flyttet ned under Annet. - Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde, eldste til venstre) -- tidligere bevisst utsatt pga. for lite rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend: RoundStatsSummary fikk et nytt round_series-felt (kronologisk snitt-til-par per runde i valgt vindu). Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl. det nye round_series-feltet via en scratch-DB-kjøring). Full scratch- stack med seks syntetiske runder (varierende dato/resultat) browser- verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
<div className="flex items-center justify-center">
<div className="relative flex size-14 items-center justify-center rounded-full bg-primary/15">
<div className="flex size-10 items-center justify-center rounded-full bg-primary/30">
<Flag aria-hidden="true" className="size-5 text-primary" fill="currentColor" />
</div>
</div>
</div>
<CompassCell icon={ArrowRight} label="Høyre" value={right} />
<div aria-hidden="true" />
<CompassCell icon={ArrowDown} label="Kort" value={short} />
<div aria-hidden="true" />
</div>
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats Tre brukertilbakemeldinger etter gjennomgang av statistikksidene: 1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått). 2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP, beregnet fra faktiske runder) i stedet for det manuelt satte tallet i profilen -- historikk-sparklinen filtreres til samme kilde (source= "computed"), så den ikke lenger blander manuelle og beregnede punkter. 3. /my-rounds/stats: - Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst, seende brukere så bare et ikon og et tall uten kontekst. - Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet andel, Innspill er en retningsbeskrivelse -- samme prinsipp som round-stats.tsx sin eksisterende splitt). Utslag flyttet først. Redning og "Til par: med vs. uten" flyttet ned under Annet. - Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde, eldste til venstre) -- tidligere bevisst utsatt pga. for lite rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend: RoundStatsSummary fikk et nytt round_series-felt (kronologisk snitt-til-par per runde i valgt vindu). Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl. det nye round_series-feltet via en scratch-DB-kjøring). Full scratch- stack med seks syntetiske runder (varierende dato/resultat) browser- verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
<p className="text-center text-xs text-muted-foreground">{trackedHoles} innspill registrert med retning.</p>
</div>
)
}
// --- Fairway: enklere venstre/fairway/høyre-bar (ingen langt/kort-akse
// fra utslaget, så ikke et fullt kompass) --------------------------------
function FairwayBar({
tracked,
left,
hit,
right,
}: {
tracked: number
left: number | null
hit: number | null
right: number | null
}) {
if (tracked === 0) return <p className="text-sm text-muted-foreground">Ingen utslag registrert ennå.</p>
const segments = [
{ label: "Venstre", value: left, color: "var(--brand-orange)" },
{ label: "Fairway", value: hit, color: "var(--primary)" },
{ label: "Høyre", value: right, color: "var(--brand-orange)" },
]
return (
<div className="flex flex-col gap-2">
<div className="flex h-4 w-full overflow-hidden rounded-full bg-muted">
{segments.map((s) =>
s.value && s.value > 0 ? (
<div key={s.label} style={{ width: `${s.value}%`, backgroundColor: s.color }} title={`${s.label}: ${Math.round(s.value)} %`} />
) : null,
)}
</div>
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted-foreground">
{segments.map((s) => (
<span key={s.label} className="inline-flex items-center gap-1.5">
<span aria-hidden="true" className="size-2.5 rounded-full" style={{ backgroundColor: s.color }} />
{s.label}: <span className="font-semibold tabular-nums text-foreground">{s.value !== null ? pct(s.value) : ""}</span>
</span>
))}
</div>
</div>
)
}
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats Tre brukertilbakemeldinger etter gjennomgang av statistikksidene: 1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått). 2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP, beregnet fra faktiske runder) i stedet for det manuelt satte tallet i profilen -- historikk-sparklinen filtreres til samme kilde (source= "computed"), så den ikke lenger blander manuelle og beregnede punkter. 3. /my-rounds/stats: - Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst, seende brukere så bare et ikon og et tall uten kontekst. - Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet andel, Innspill er en retningsbeskrivelse -- samme prinsipp som round-stats.tsx sin eksisterende splitt). Utslag flyttet først. Redning og "Til par: med vs. uten" flyttet ned under Annet. - Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde, eldste til venstre) -- tidligere bevisst utsatt pga. for lite rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend: RoundStatsSummary fikk et nytt round_series-felt (kronologisk snitt-til-par per runde i valgt vindu). Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl. det nye round_series-feltet via en scratch-DB-kjøring). Full scratch- stack med seks syntetiske runder (varierende dato/resultat) browser- verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
// --- Utvikling over tid: stolpegraf + trendlinje -----------------------
// Brukerinstruks 2026-08-12: stolpe per runde (eldste til venstre) +
// trendlinje over. Tidligere bevisst UTSATT (se CHANGELOG 2026-07-28: "for
// lite rundehistorikk til å vise noe meningsfullt") -- bygges nå som
// brukeren har nok fullførte runder til at en trend faktisk sier noe.
//
// Fargevalg følger samme diverging-par som `Delta` allerede bruker på
// resten av siden (primary = under/på par, brand-orange = over par) --
// ikke en ny palett, samme etablerte semantikk. Trendlinjen er et
// glidende snitt (ikke rå lineær regresjon), tegnet i en nøytral,
// dempet farge og stiplet, slik at den tydelig leses som UTLEDET, ikke
// som en tredje rådataserie.
const TREND_BAR_WIDTH = 20
const TREND_BAR_GAP = 10
const TREND_CHART_HEIGHT = 160
const TREND_HALF_HEIGHT = TREND_CHART_HEIGHT / 2 - 14 // plass til dato-etiketter under
function movingAverage(values: number[], window: number): number[] {
return values.map((_, i) => {
const start = Math.max(0, i - window + 1)
const slice = values.slice(start, i + 1)
return slice.reduce((sum, v) => sum + v, 0) / slice.length
})
}
function formatShortDate(iso: string): string {
const d = new Date(iso)
return `${d.getDate()}.${d.getMonth() + 1}`
}
// Stolpe med avrundet ende BORTE fra 0-linjen, skarp kant VED 0-linjen --
// samme prinsipp uansett om verdien er over eller under par (dataviz-
// skillets "4px avrundet dataende, skarp ved grunnlinjen").
function barPath(x: number, valueY: number, baselineY: number, width: number, radius: number): string {
const top = Math.min(valueY, baselineY)
const bottom = Math.max(valueY, baselineY)
const h = bottom - top
if (h < radius) {
// For kort til å avrunde pent -- vanlig rektangel.
return `M ${x} ${top} H ${x + width} V ${bottom} H ${x} Z`
}
const roundedAtTop = valueY < baselineY
if (roundedAtTop) {
return `M ${x} ${top + radius} A ${radius} ${radius} 0 0 1 ${x + radius} ${top} H ${x + width - radius} A ${radius} ${radius} 0 0 1 ${x + width} ${top + radius} V ${bottom} H ${x} Z`
}
return `M ${x} ${top} H ${x + width} V ${bottom - radius} A ${radius} ${radius} 0 0 1 ${x + width - radius} ${bottom} H ${x + radius} A ${radius} ${radius} 0 0 1 ${x} ${bottom - radius} Z`
}
function ScoreTrendChart({ series }: { series: ApiRoundSeriesPoint[] }) {
if (series.length < 2) return null
const values = series.map((p) => p.score_to_par)
const trend = movingAverage(values, 3)
const maxAbs = Math.max(...values.map((v) => Math.abs(v)), 1)
const scale = TREND_HALF_HEIGHT / maxAbs
const baselineY = TREND_CHART_HEIGHT - 20
const width = series.length * (TREND_BAR_WIDTH + TREND_BAR_GAP) + TREND_BAR_GAP
const barCenterX = (i: number) => TREND_BAR_GAP + i * (TREND_BAR_WIDTH + TREND_BAR_GAP) + TREND_BAR_WIDTH / 2
const valueToY = (v: number) => baselineY - v * scale
const trendPoints = trend.map((v, i) => `${barCenterX(i)},${valueToY(v)}`).join(" ")
return (
<Card title="Utvikling" subtitle="Snitt til par per runde, eldste runde til venstre.">
<div className="overflow-x-auto">
<svg
role="img"
aria-label={`Snitt til par for ${series.length} runder, fra ${formatShortDate(series[0].played_at)} til ${formatShortDate(series[series.length - 1].played_at)}.`}
viewBox={`0 0 ${width} ${TREND_CHART_HEIGHT}`}
width={width}
height={TREND_CHART_HEIGHT}
className="block"
>
{/* 0-linjen (par) -- hårfin, ubrutt, tilbaketrukket, som resten av rutenettet skal være. */}
<line x1={0} y1={baselineY} x2={width} y2={baselineY} stroke="var(--border)" strokeWidth={1} />
{series.map((point, i) => {
const cx = barCenterX(i)
const overPar = point.score_to_par > 0
const isLast = i === series.length - 1
return (
<g key={point.played_at + i}>
<path
d={barPath(cx - TREND_BAR_WIDTH / 2, valueToY(point.score_to_par), baselineY, TREND_BAR_WIDTH, 4)}
fill={overPar ? "var(--brand-orange)" : "var(--primary)"}
>
<title>
{formatShortDate(point.played_at)}: {signed(point.score_to_par)}
</title>
</path>
{/* Direkte etikett kun på siste (nyeste) stolpe -- dataviz-prinsipp
"merk endepunktet, ikke hvert punkt". Resten leses via 0-linjen,
stolpenes relative høyde, og trendlinjens retning. */}
{isLast && (
<text
x={cx}
y={valueToY(point.score_to_par) + (overPar ? -6 : 14)}
textAnchor="middle"
className="fill-foreground text-[11px] font-bold tabular-nums"
>
{signed(point.score_to_par)}
</text>
)}
{(i === 0 || i === series.length - 1 || i === Math.floor(series.length / 2)) && (
<text x={cx} y={TREND_CHART_HEIGHT - 4} textAnchor="middle" className="fill-muted-foreground text-[10px] font-semibold">
{formatShortDate(point.played_at)}
</text>
)}
</g>
)
})}
<polyline points={trendPoints} fill="none" stroke="var(--muted-foreground)" strokeWidth={2} strokeDasharray="5 4" strokeLinecap="round" />
</svg>
</div>
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs font-semibold text-muted-foreground">
<span className="inline-flex items-center gap-1.5">
<span aria-hidden="true" className="h-0.5 w-4 rounded-full" style={{ backgroundColor: "var(--muted-foreground)", opacity: 0.7 }} />
Trendlinje (glidende snitt, siste 3 runder)
</span>
</div>
</Card>
)
}
// --- Til par: med vs. uten en gitt hendelse på hullet -----------------------
// Brukerens eksplisitte instruks 2026-07-28: se snitt til par NÅR noe
// inntraff mot NÅR det ikke gjorde det (greentreff, fairwaytreff, bunker,
// anywayslag), i stedet for bare en isolert prosentandel. Fargen følger
// hvilken av de to som FAKTISK var lavest denne perioden (ikke en fast
// antakelse om hvilken side som "skal" være best), siden det er selve
// sammenligningen som er poenget.
function CompareToPar({
title,
aLabel,
aValue,
bLabel,
bValue,
}: {
title: string
aLabel: string
aValue: number | null
bLabel: string
bValue: number | null
}) {
if (aValue === null && bValue === null) {
return (
<div>
<h3 className="text-sm font-bold text-foreground">{title}</h3>
<p className="text-sm text-muted-foreground">Ingen data registrert ennå.</p>
</div>
)
}
const aBetter = aValue !== null && bValue !== null && aValue < bValue
const bBetter = aValue !== null && bValue !== null && bValue < aValue
return (
<div className="flex flex-col gap-2">
<h3 className="text-sm font-bold text-foreground">{title}</h3>
<div className="grid grid-cols-2 gap-3">
<div className={`rounded-xl border p-3 ${aBetter ? "border-primary bg-primary/10" : "border-border bg-card"}`}>
<span className="text-xs font-semibold text-muted-foreground">{aLabel}</span>
<div className="text-xl font-extrabold tabular-nums text-foreground">{aValue !== null ? signed(aValue) : ""}</div>
</div>
<div className={`rounded-xl border p-3 ${bBetter ? "border-primary bg-primary/10" : "border-border bg-card"}`}>
<span className="text-xs font-semibold text-muted-foreground">{bLabel}</span>
<div className="text-xl font-extrabold tabular-nums text-foreground">{bValue !== null ? signed(bValue) : ""}</div>
</div>
</div>
</div>
)
}
export function RoundsStatsSummary() {
const router = useRouter()
const [window, setWindow] = useState<WindowKey>("all")
const [data, setData] = useState<ApiWindowSummary | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let cancelled = false
async function load() {
try {
const res = await fetch(`/rounds/stats/summary?window=${window}`, { credentials: "include" })
if (res.status === 401) {
router.replace("/logg-inn")
return
}
if (!res.ok) throw new Error(`stats: ${res.status}`)
const json: ApiWindowSummary = await res.json()
if (!cancelled) setData(json)
} catch {
if (!cancelled) setError("Klarte ikke å hente statistikken. Prøv igjen om litt.")
}
}
void load()
return () => {
cancelled = true
}
}, [router, window])
const s = data?.current ?? null
return (
<div className="flex min-h-[100dvh] flex-col bg-background">
<header className="sticky top-0 z-10 border-b border-border bg-background/95 backdrop-blur">
<div className="mx-auto flex min-h-14 max-w-xl items-center gap-2 px-4 py-2">
<Link
href="/dashboard"
aria-label="Til dashbord"
className="flex size-11 shrink-0 items-center justify-center rounded-xl text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<ArrowLeft aria-hidden="true" className="size-6" />
</Link>
<h1 className="text-lg font-extrabold tracking-tight text-foreground">Statistikk</h1>
</div>
</header>
<main className="mx-auto flex w-full max-w-xl flex-1 flex-col gap-6 px-4 py-6 pb-16">
<div className="flex gap-2 overflow-x-auto pb-1">
{WINDOW_OPTIONS.map((opt) => (
<button
key={opt.key}
type="button"
onClick={() => setWindow(opt.key)}
className={`min-h-11 shrink-0 rounded-full px-4 text-sm font-bold transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
window === opt.key ? "bg-primary text-primary-foreground" : "bg-muted text-foreground hover:bg-accent"
}`}
>
{opt.label}
</button>
))}
</div>
{error && (
<p role="alert" className="text-base font-medium text-destructive">
{error}
</p>
)}
{data === null && !error ? (
<div role="status" aria-live="polite" className="flex flex-col items-center justify-center gap-4 py-20 text-center">
<div aria-hidden="true" className="size-9 animate-spin rounded-full border-4 border-primary/20 border-t-primary" />
<span className="text-base font-medium text-muted-foreground">Laster statistikk</span>
</div>
) : s && s.rounds_completed === 0 ? (
<div className="flex flex-col items-center gap-5 rounded-3xl border border-dashed border-border bg-card/50 px-6 py-16 text-center">
2026-08-02 16:34:54 +02:00
<div className="flex size-16 items-center justify-center rounded-2xl bg-muted">
<BarChart3 aria-hidden="true" className="size-8 text-muted-foreground" />
</div>
<div className="flex max-w-md flex-col gap-2">
<h2 className="text-xl font-bold text-foreground">Ingen fullførte runder i perioden</h2>
<p className="text-lg leading-relaxed text-muted-foreground text-pretty">
Prøv et annet tidsvindu, eller kom tilbake etter neste fullførte runde.
</p>
</div>
</div>
) : data ? (
(() => {
const s = data.current
const p = data.previous
return (
<>
<p className="text-base leading-relaxed text-muted-foreground text-pretty">
Aggregert over {s.rounds_completed} {s.rounds_completed === 1 ? "fullført runde" : "fullførte runder"}
{p.rounds_completed > 0 && (
<>
{" "}
· sammenlignet med {p.rounds_completed} {p.rounds_completed === 1 ? "runde" : "runder"} i perioden før
</>
)}
.
</p>
<Section title="Oversikt" icon={BarChart3}>
<StatTile label="Runder fullført" value={String(s.rounds_completed)} />
<StatTile
label="Snitt til par"
value={s.avg_score_to_par !== null ? signed(s.avg_score_to_par) : ""}
delta={<Delta current={s.avg_score_to_par} previous={p.avg_score_to_par} format={signed} goodDirection="down" />}
/>
<StatTile
label="Putt / 18 hull"
value={s.avg_putts_per_18 !== null ? s.avg_putts_per_18.toFixed(1).replace(".", ",") : ""}
delta={<Delta current={s.avg_putts_per_18} previous={p.avg_putts_per_18} format={signed} goodDirection="down" />}
hint={
s.putts_tracked_rounds > 0
? `${s.putts_tracked_rounds} ${s.putts_tracked_rounds === 1 ? "runde med" : "runder med"} puttsporing`
: "Ingen runder med puttsporing ennå"
}
/>
</Section>
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats Tre brukertilbakemeldinger etter gjennomgang av statistikksidene: 1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått). 2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP, beregnet fra faktiske runder) i stedet for det manuelt satte tallet i profilen -- historikk-sparklinen filtreres til samme kilde (source= "computed"), så den ikke lenger blander manuelle og beregnede punkter. 3. /my-rounds/stats: - Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst, seende brukere så bare et ikon og et tall uten kontekst. - Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet andel, Innspill er en retningsbeskrivelse -- samme prinsipp som round-stats.tsx sin eksisterende splitt). Utslag flyttet først. Redning og "Til par: med vs. uten" flyttet ned under Annet. - Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde, eldste til venstre) -- tidligere bevisst utsatt pga. for lite rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend: RoundStatsSummary fikk et nytt round_series-felt (kronologisk snitt-til-par per runde i valgt vindu). Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl. det nye round_series-feltet via en scratch-DB-kjøring). Full scratch- stack med seks syntetiske runder (varierende dato/resultat) browser- verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
<ScoreTrendChart series={s.round_series} />
{/* Rekkefølge 2026-08-12 (brukerinstruks): Utslag først, deretter
Greentreff (en beregnet andel), Innspill (en retnings-
beskrivelse -- egen kortype av samme grunn som round-stats.tsx
skiller "Greentreff (GIR)" fra "Bom-retning på green"). Redning
og "Til par: med vs. uten" flyttet ned under Annet, se der. */}
<Card title="Utslag" subtitle="Kun par 4/5, kun registrerte utslag.">
<FairwayBar tracked={s.fairway_tracked_holes} left={s.fairway_left_pct} hit={s.fairway_hit_pct} right={s.fairway_right_pct} />
<div className="flex items-center justify-between border-t border-border pt-3">
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats Tre brukertilbakemeldinger etter gjennomgang av statistikksidene: 1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått). 2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP, beregnet fra faktiske runder) i stedet for det manuelt satte tallet i profilen -- historikk-sparklinen filtreres til samme kilde (source= "computed"), så den ikke lenger blander manuelle og beregnede punkter. 3. /my-rounds/stats: - Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst, seende brukere så bare et ikon og et tall uten kontekst. - Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet andel, Innspill er en retningsbeskrivelse -- samme prinsipp som round-stats.tsx sin eksisterende splitt). Utslag flyttet først. Redning og "Til par: med vs. uten" flyttet ned under Annet. - Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde, eldste til venstre) -- tidligere bevisst utsatt pga. for lite rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend: RoundStatsSummary fikk et nytt round_series-felt (kronologisk snitt-til-par per runde i valgt vindu). Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl. det nye round_series-feltet via en scratch-DB-kjøring). Full scratch- stack med seks syntetiske runder (varierende dato/resultat) browser- verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
<span className="text-sm font-semibold text-muted-foreground">Fairwaytreff</span>
<Delta current={s.fairway_hit_pct} previous={p.fairway_hit_pct} format={signedPoints} goodDirection="up" />
</div>
</Card>
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats Tre brukertilbakemeldinger etter gjennomgang av statistikksidene: 1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått). 2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP, beregnet fra faktiske runder) i stedet for det manuelt satte tallet i profilen -- historikk-sparklinen filtreres til samme kilde (source= "computed"), så den ikke lenger blander manuelle og beregnede punkter. 3. /my-rounds/stats: - Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst, seende brukere så bare et ikon og et tall uten kontekst. - Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet andel, Innspill er en retningsbeskrivelse -- samme prinsipp som round-stats.tsx sin eksisterende splitt). Utslag flyttet først. Redning og "Til par: med vs. uten" flyttet ned under Annet. - Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde, eldste til venstre) -- tidligere bevisst utsatt pga. for lite rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend: RoundStatsSummary fikk et nytt round_series-felt (kronologisk snitt-til-par per runde i valgt vindu). Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl. det nye round_series-feltet via en scratch-DB-kjøring). Full scratch- stack med seks syntetiske runder (varierende dato/resultat) browser- verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
<Card title="Greentreff (GIR)" subtitle="Andel innspill som traff green i regulert antall slag.">
<GirGauge hitPct={s.gir_pct} trackedHoles={s.gir_tracked_holes} />
<div className="flex items-center justify-between border-t border-border pt-3">
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats Tre brukertilbakemeldinger etter gjennomgang av statistikksidene: 1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått). 2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP, beregnet fra faktiske runder) i stedet for det manuelt satte tallet i profilen -- historikk-sparklinen filtreres til samme kilde (source= "computed"), så den ikke lenger blander manuelle og beregnede punkter. 3. /my-rounds/stats: - Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst, seende brukere så bare et ikon og et tall uten kontekst. - Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet andel, Innspill er en retningsbeskrivelse -- samme prinsipp som round-stats.tsx sin eksisterende splitt). Utslag flyttet først. Redning og "Til par: med vs. uten" flyttet ned under Annet. - Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde, eldste til venstre) -- tidligere bevisst utsatt pga. for lite rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend: RoundStatsSummary fikk et nytt round_series-felt (kronologisk snitt-til-par per runde i valgt vindu). Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl. det nye round_series-feltet via en scratch-DB-kjøring). Full scratch- stack med seks syntetiske runder (varierende dato/resultat) browser- verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
<span className="text-sm font-semibold text-muted-foreground">Greentreff</span>
<Delta current={s.gir_pct} previous={p.gir_pct} format={signedPoints} goodDirection="up" />
</div>
</Card>
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats Tre brukertilbakemeldinger etter gjennomgang av statistikksidene: 1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått). 2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP, beregnet fra faktiske runder) i stedet for det manuelt satte tallet i profilen -- historikk-sparklinen filtreres til samme kilde (source= "computed"), så den ikke lenger blander manuelle og beregnede punkter. 3. /my-rounds/stats: - Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst, seende brukere så bare et ikon og et tall uten kontekst. - Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet andel, Innspill er en retningsbeskrivelse -- samme prinsipp som round-stats.tsx sin eksisterende splitt). Utslag flyttet først. Redning og "Til par: med vs. uten" flyttet ned under Annet. - Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde, eldste til venstre) -- tidligere bevisst utsatt pga. for lite rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend: RoundStatsSummary fikk et nytt round_series-felt (kronologisk snitt-til-par per runde i valgt vindu). Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl. det nye round_series-feltet via en scratch-DB-kjøring). Full scratch- stack med seks syntetiske runder (varierende dato/resultat) browser- verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
<Card title="Innspill" subtitle="Retningen du bommer i, når du bommer greenen.">
<ApproachDirectionCompass
trackedHoles={s.green_miss_tracked_holes}
long={s.green_miss_long_pct}
short={s.green_miss_short_pct}
left={s.green_miss_left_pct}
right={s.green_miss_right_pct}
/>
</Card>
<Card title="Putting">
<Donut
centerValue={s.avg_putts_per_18 !== null ? s.avg_putts_per_18.toFixed(1).replace(".", ",") : ""}
centerLabel="putt / 18 hull"
segments={
s.putt_dist_one_pct !== null
? [
{ label: "1-putt", value: s.putt_dist_one_pct ?? 0, color: "var(--primary)" },
{ label: "2-putt", value: s.putt_dist_two_pct ?? 0, color: "var(--info)" },
{ label: "3-putt+", value: s.putt_dist_three_plus_pct ?? 0, color: "var(--brand-orange)" },
]
: []
}
/>
{s.putt_dist_one_pct === null && <p className="text-sm text-muted-foreground">Ingen puttfordeling registrert ennå.</p>}
<div className="flex items-center justify-between border-t border-border pt-3">
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats Tre brukertilbakemeldinger etter gjennomgang av statistikksidene: 1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått). 2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP, beregnet fra faktiske runder) i stedet for det manuelt satte tallet i profilen -- historikk-sparklinen filtreres til samme kilde (source= "computed"), så den ikke lenger blander manuelle og beregnede punkter. 3. /my-rounds/stats: - Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst, seende brukere så bare et ikon og et tall uten kontekst. - Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet andel, Innspill er en retningsbeskrivelse -- samme prinsipp som round-stats.tsx sin eksisterende splitt). Utslag flyttet først. Redning og "Til par: med vs. uten" flyttet ned under Annet. - Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde, eldste til venstre) -- tidligere bevisst utsatt pga. for lite rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend: RoundStatsSummary fikk et nytt round_series-felt (kronologisk snitt-til-par per runde i valgt vindu). Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl. det nye round_series-feltet via en scratch-DB-kjøring). Full scratch- stack med seks syntetiske runder (varierende dato/resultat) browser- verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
<span className="text-sm font-semibold text-muted-foreground">Én-putt</span>
<Delta current={s.one_putt_pct} previous={p.one_putt_pct} format={signedPoints} goodDirection="up" />
</div>
</Card>
Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats Tre brukertilbakemeldinger etter gjennomgang av statistikksidene: 1. round-stats.tsx: trekkspillseksjonene starter nå ekspandert (reverserer en tidligere beslutning fra 2026-07-25 om å vise dem sammenslått). 2. Dashbordets "HCP nå" viser nå computed_handicap_index ("spilt til"-HCP, beregnet fra faktiske runder) i stedet for det manuelt satte tallet i profilen -- historikk-sparklinen filtreres til samme kilde (source= "computed"), så den ikke lenger blander manuelle og beregnede punkter. 3. /my-rounds/stats: - Alle "vs. forrige periode"-deltaer (pil+tall+pp) er nå synlig selvforklarende -- teksten fantes tidligere kun som skjermleser-tekst, seende brukere så bare et ikon og et tall uten kontekst. - Greentreff (GIR) og Innspill splittet i to kort (GIR er en beregnet andel, Innspill er en retningsbeskrivelse -- samme prinsipp som round-stats.tsx sin eksisterende splitt). Utslag flyttet først. Redning og "Til par: med vs. uten" flyttet ned under Annet. - Ny stolpegraf med glidende snitt-trendlinje (snitt til par per runde, eldste til venstre) -- tidligere bevisst utsatt pga. for lite rundehistorikk (se CHANGELOG 2026-07-28), bygget nå. Backend: RoundStatsSummary fikk et nytt round_series-felt (kronologisk snitt-til-par per runde i valgt vindu). Verifisert: tsc --noEmit rent, 45/45 vitest, 31/31 backend-tester (inkl. det nye round_series-feltet via en scratch-DB-kjøring). Full scratch- stack med seks syntetiske runder (varierende dato/resultat) browser- verifisert lys+mørk: HCP-bytte bekreftet (computed 5,3 vist i stedet for manuelt satt 15,0), ny kortrekkefølge, splittet GIR/Innspill, synlige delta-bildetekster, graf med trendlinje. Ingen konsollfeil. teecup_db urørt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-12 09:12:49 +02:00
<Section title="Chip, bunker og straffeslag" icon={Waves}>
<StatTile
label="Chip / runde"
value={s.avg_chip_per_round !== null ? s.avg_chip_per_round.toFixed(1).replace(".", ",") : ""}
delta={<Delta current={s.avg_chip_per_round} previous={p.avg_chip_per_round} format={signed} goodDirection="down" />}
/>
<StatTile
label="Bunkerslag / runde"
value={s.avg_bunker_per_round !== null ? s.avg_bunker_per_round.toFixed(1).replace(".", ",") : ""}
delta={<Delta current={s.avg_bunker_per_round} previous={p.avg_bunker_per_round} format={signed} goodDirection="down" />}
/>
<StatTile
label="Straffeslag / runde"
value={s.avg_penalty_per_round !== null ? s.avg_penalty_per_round.toFixed(1).replace(".", ",") : ""}
delta={<Delta current={s.avg_penalty_per_round} previous={p.avg_penalty_per_round} format={signed} goodDirection="down" />}
/>
</Section>
{s.avg_anyway_per_round !== null && (
<Section title="Annet" icon={Wind}>
<StatTile
label="Anywayslag / runde"
value={s.avg_anyway_per_round.toFixed(1).replace(".", ",")}
delta={<Delta current={s.avg_anyway_per_round} previous={p.avg_anyway_per_round} format={signed} goodDirection="down" />}
/>
</Section>
)}
<Card title="Redning" subtitle="Par eller bedre etter en bom.">
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col items-center gap-2">
<Donut
size={104}
thickness={14}
showLegend={false}
centerValue={s.scrambling_pct !== null ? `${Math.round(s.scrambling_pct)}%` : ""}
centerLabel=""
segments={s.scrambling_pct !== null ? [{ label: "Scrambling", value: s.scrambling_pct, color: "var(--primary)" }] : []}
/>
<span className="text-sm font-bold text-foreground">Scrambling</span>
<Delta current={s.scrambling_pct} previous={p.scrambling_pct} format={signedPoints} goodDirection="up" />
</div>
<div className="flex flex-col items-center gap-2">
<Donut
size={104}
thickness={14}
showLegend={false}
centerValue={s.sand_save_pct !== null ? `${Math.round(s.sand_save_pct)}%` : ""}
centerLabel=""
segments={s.sand_save_pct !== null ? [{ label: "Sand save", value: s.sand_save_pct, color: "var(--primary)" }] : []}
/>
<span className="text-sm font-bold text-foreground">Sand save</span>
<Delta current={s.sand_save_pct} previous={p.sand_save_pct} format={signedPoints} goodDirection="up" />
</div>
</div>
</Card>
<Card title="Til par: med vs. uten" subtitle="Snitt til par på enkelthull, splittet på om noe faktisk inntraff.">
<CompareToPar
title="Greentreff (GIR)"
aLabel="Med GIR"
aValue={s.avg_to_par_with_gir}
bLabel="Uten GIR"
bValue={s.avg_to_par_without_gir}
/>
<CompareToPar
title="Fairwaytreff"
aLabel="Traff fairway"
aValue={s.avg_to_par_fairway_hit}
bLabel="Bom fairway"
bValue={s.avg_to_par_fairway_miss}
/>
<CompareToPar
title="Bunker"
2026-08-07 22:02:56 +02:00
aLabel="Ikke i bunker"
aValue={s.avg_to_par_without_bunker}
bLabel="I bunker"
bValue={s.avg_to_par_with_bunker}
/>
<CompareToPar
title="Anywayslag"
2026-08-07 22:02:56 +02:00
aLabel="Uten anywayslag"
aValue={s.avg_to_par_without_anyway}
bLabel="Med anywayslag"
bValue={s.avg_to_par_with_anyway}
/>
</Card>
</>
)
})()
) : null}
</main>
</div>
)
}