"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" import { ArrowDown, ArrowLeft, ArrowRight, ArrowUp, BarChart3, Flag, TrendingDown, TrendingUp, Waves, Wind } from "lucide-react" type ApiRoundSeriesPoint = { played_at: string score_to_par: number putts_per_18: number | null fairway_hit_pct: number | null gir_pct: number | null one_putt_pct: number | null chip_per_round: number | null bunker_per_round: number | null penalty_per_round: number | null anyway_per_round: number | null scrambling_cumulative_pct: number | null sand_save_cumulative_pct: number | null } type ApiStatsSummary = { rounds_completed: number avg_score_to_par: number | null 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 round_series: ApiRoundSeriesPoint[] } 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)} %` } // Enhetsløs versjon av `pct` -- til bruk som `formatSlope` i BarTrendChart // der `slopeSuffix` allerede bærer enheten ("pp per runde"); `pct` sin // egen "%" ville da kollidert med det. function roundedNoUnit(n: number): string { return String(Math.round(n)) } function oneDecimal(n: number): string { return n.toFixed(1).replace(".", ",") } // 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" }) { // 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 if (diff === 0) { return Uendret vs. forrige periode } 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 ( ) } function StatTile({ label, value, hint, delta, }: { label: string value: string hint?: string delta?: React.ReactNode }) { return (
{label} {value} {delta} {hint && {hint}}
) } function Card({ title, subtitle, children }: { title: string; subtitle?: string; children: React.ReactNode }) { return (

{title}

{subtitle &&

{subtitle}

}
{children}
) } function Section({ title, icon: Icon, children, }: { title: string icon: typeof BarChart3 children: React.ReactNode }) { return (

{children}
) } // --- 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 = (
`${s.label} ${Math.round(s.value)} prosent`).join(", ")}`} >
{centerValue} {centerLabel}
) if (!showLegend) return ring return (
{ring}
) } // --- 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 (
) } // 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

Ingen greentreff-data registrert ennå.

} return (

{trackedHoles} hull registrert

) } function ApproachDirectionCompass({ trackedHoles, long, short, left, right, }: { trackedHoles: number long: number | null short: number | null left: number | null right: number | null }) { if (trackedHoles === 0) { return

Ingen innspill med retning registrert ennå.

} return (