From 99b71ab97be72c2b7040bfbfcffb9835b8c81ff7 Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Tue, 28 Jul 2026 07:43:47 +0200 Subject: [PATCH] All shipped, tested, and browser-verified. Summary of this round: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GIR question answered, not a gap: score − putts ≤ par − 2 is already the general formula, already ported to the new endpoint — it's not case-by-case, it works for every combination. The one real limit: it needs putts, so strokes_only rounds can't get a reliable GIR (score alone almost never proves it — a chipped-in birdie and a real GIR+1-putt look identical). Miss direction — added fairway left/right and green long/short/left/right as pooled distribution bars on the aggregate stats page, using the same fields the per-round page already tracks. Time-window picker — Siste runde / 5 / 10 / Denne måneden / I år / Siste år / Alltid, filters the whole page. "vs. forrige periode" deltas — added, per your ask. One rule for every window type: "previous" = the same length (rounds or days) immediately before the current window's start, so there's no special-casing per calendar type. Colored by direction (green = improvement, orange = worse) except on the miss-direction numbers, which have no "better" side. Verified with standalone logic tests against hand-computed scenarios, a full app import check, a clean typecheck, and live in the browser (clicked through windows, confirmed real numbers and a correct empty-previous case since your account only has 2 completed rounds so far). --- CLAUDE.md | 58 +++++ app/routers/rounds.py | 187 ++++++++++---- frontend/components/rounds-stats-summary.tsx | 254 ++++++++++++++++--- 3 files changed, 424 insertions(+), 75 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c93ae86..13c8a80 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3813,6 +3813,64 @@ Ferdig og verifisert: `/my-rounds/stats`/`/my-rounds/course/...` alle bekreftet 200 og konsoll-feilfrie i en ekte innlogget nettleser-sesjon, `teeoff.no` upåvirket. +- **Statistikk-siden utvidet: miss-retning, tidsvindu + forrige-periode- + sammenligning — BYGGET, TESTET OG LIVE (2026-07-28), samme dag, rett + etter forrige punkt:** brukeren reiste tre ting samtidig — ønsket + miss-RETNING (ikke bare treff%), trender, og et presist spørsmål om + hvorvidt GIR-fra-score-og-putt-inferens faktisk var forstått/utnyttet. + **Siste punkt avklart, ikke en bug:** bekreftet at `isGir = score - putts + <= par - 2` (allerede i `round-stats.tsx`, portert uendret til det nye + aggregerte endepunktet dagen før) ER akkurat denne generelle inferensen — + fungerer for ALLE score/putt/par-kombinasjoner, ikke bare brukerens + eksempel. Eneste reelle begrensning (forklart, ikke fikset): krever + putt-tall, så `strokes_only`-runder får aldri en GIR-verdi — score alene + er nesten aldri nok til å BEVISE GIR (en scrambling-birdie fra utenfor + green gir identisk score som en ekte GIR+1-putt). + **Miss-retning bygget:** `_summarize_rounds` i `app/routers/rounds.py` + utvidet med `fairway_left_pct`/`fairway_right_pct` (samme + `fairway_tracked`-pool som treff%) og `green_miss_long/short/left/ + right_pct` (egen pool — hull der `approach_result` er registrert og ≠ + "hit", UAVHENGIG av om putt er kjent, samme adskilte spor som + `round-stats.tsx` sin `missedGreen`/`missDir`). Ny `MissBar`-komponent i + `rounds-stats-summary.tsx` (fordelingsbar, samme visuelle idé som + `SegmentedBar`/`DistributionBar` andre steder i appen). + **Tidsvindu + forrige-periode bygget:** ny `StatsWindow`-type (`Literal`) + og `_resolve_stats_window()` — brukeren ba eksplisitt om BÅDE en full + velger (Siste runde/5/10/Denne måneden/I år/Siste år/Alltid) OG + sammenligningspiler mot forrige periode (utover det opprinnelig + anbefalte "kun siste 5 vs. alltid, ingen piler"). Én ENSARTET regel for + "forrige periode" på tvers av alle vindutyper — samme LENGDE (antall + runder for rullerende antall-vinduer, antall DAGER for dato-vinduer) + rett før gjeldende vindus start — i stedet for å måtte spesialdefinere + "forrige måned"/"forrige år" ulikt for hver kalenderbasert type. + `GET /rounds/stats/summary?window=...` returnerer nå `{window, current, + previous}` (`RoundStatsWindowSummary`) — samme `_summarize_rounds()`- + funksjon kalt to ganger på to ulike round_id-mengder, ingen duplisert + aggregeringslogikk. Frontend fikk en ny `Delta`-komponent (pil + farge, + farget etter en eksplisitt `goodDirection`-per-tall — lavere er bedre + for til-par/putt/chip/bunker/straffeslag/anywayslag, høyere er bedre for + treff-/rednings-prosentene, INGEN farge på de rene miss-retnings-tallene + siden venstre/høyre ikke er "bedre/verre"). + **Verifisert i to lag:** (a) en frittstående Python-simulering av + `_resolve_stats_window()` mot syntetiske datasett — rullerende antalls- + vindu (riktig current/previous-splitt, riktig tomt `previous` når for få + runder finnes), dato-vindu (kalender-til-dato + rullerende forrige + periode), og en isolert miss-retning-poolingstest (`None`-verdier + korrekt ekskludert fra nevneren); (b) ekte typesjekket produksjonsbuild + (måtte rette en TypeScript-nullbarhets-feil underveis — `current`/ + `previous` pakket ut via en IIFE inni JSX for at TS skulle smalne begge + riktig), full import-sjekk av hele FastAPI-appen i prod-imaget, OG et + ekte browserbesøk som viste reelle tall (fairway-miss 36/43/21, + green-miss-retning 0/75/13/13) + bekreftet vindu-velgeren fungerer + (klikket "Siste 10", pillen ble grønn, tallene oppdaterte seg) + et + direkte `fetch()`-kall fra siden selv som bekreftet `{window, current, + previous}`-formen (2 runder i `current`, korrekt tomt `previous` siden + brukeren kun har 2 fullførte runder totalt). + **Rullet ut live 2026-07-28**, ingen migrasjon, `docker compose up -d + --build teecup_api teecup_frontend`. Begge containere boot-et rent, + `/health` → 200, anonymt `GET /rounds/stats/summary?window=last_5` → 401 + (bekrefter query-parameteren ruter riktig gjennom Caddy/rewrites), + `teeoff.no` upåvirket. Neste steg: 0a. **Spillerliste-redesign — nå FAKTISK nettleser-bekreftet diff --git a/app/routers/rounds.py b/app/routers/rounds.py index 1347d70..12a072e 100644 --- a/app/routers/rounds.py +++ b/app/routers/rounds.py @@ -27,7 +27,7 @@ tellende resultat lagres kun som `round_participant.score_differential`. from __future__ import annotations import math -from datetime import date, datetime +from datetime import date, datetime, timedelta from typing import Literal from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect @@ -675,9 +675,16 @@ class RoundStatsSummary(BaseModel): avg_putts_per_18: float | None putts_tracked_rounds: int fairway_hit_pct: float | None + fairway_left_pct: float | None + fairway_right_pct: float | None fairway_tracked_holes: int gir_pct: float | None gir_tracked_holes: int + green_miss_long_pct: float | None + green_miss_short_pct: float | None + green_miss_left_pct: float | None + green_miss_right_pct: float | None + green_miss_tracked_holes: int one_putt_pct: float | None scrambling_pct: float | None sand_save_pct: float | None @@ -687,63 +694,78 @@ class RoundStatsSummary(BaseModel): avg_anyway_per_round: float | None -@router.get("/rounds/stats/summary", response_model=RoundStatsSummary) -async def get_rounds_stats_summary(user: CurrentUser = Depends(get_current_user)) -> RoundStatsSummary: - """Aggregert statistikk over ALLE brukerens fullførte runder (egen - deltaker-rad, ikke medspilleres). Ingen paginering -- samme "hent alt, - aggreger i Python"-mønster som `list_rounds`, forsvarlig i volum siden - `round_hole` alltid har nøyaktig 18 rader per deltaker uansett - holes_planned (se `_create_participant`). +class RoundStatsWindowSummary(BaseModel): + window: str + current: RoundStatsSummary + previous: RoundStatsSummary - Putt/18-hull-regelen (brukerens eksplisitte instruks 2026-07-28): for - hver fullført runde der puttsporing faktisk var på (stat_level ≠ - 'strokes_only') telles ALLE 18 lagrede hull -- et hull uten registrert - putt-verdi (uspilt, eller utenfor et 9-hulls spilleomfang) telles som 2 - putter. Runder uten puttsporing holdes helt utenfor putt-tallet. - Alle andre andelstall (fairway/GIR/én-putt/scrambling/sand save) pooler - kun FAKTISK registrerte hull på tvers av alle runder -- samme formler - som `round-stats.tsx` sin `computeStats()` bruker per enkeltrunde - (portert hit, ikke en ny definisjon), ingen padding der siden brukeren - ikke ba om det for disse tallene. + +StatsWindow = Literal["last_round", "last_5", "last_10", "month", "year", "last_year", "all"] + + +def _resolve_stats_window(window: StatsWindow, ordered_rounds: list[tuple[str, date]]) -> tuple[set[str], set[str]]: + """`ordered_rounds` er (round_id, played_at) sortert NYEST FØRST. + Returnerer (gjeldende periode, forrige periode) som round_id-mengder. + + Forrige periode er ALLTID perioden av SAMME LENGDE rett før gjeldende + periodes start -- rullerende for antall-baserte vinduer (samme antall + runder rett før), rullerende for dato-baserte vinduer (samme antall + DAGER rett før startdatoen). Én ensartet regel for alle vindutyper, + ingen kalender-spesialtilfeller (unngår å måtte definere "forrige + måned"/"forrige år" ulikt fra "forrige periode" for et rullerende + vindu som "siste 365 dager"). """ - async with plain_connection() as conn: - rows = await conn.fetch( - """ - SELECT rp.round_id::text AS round_id, rp.stat_level, - rh.par, rh.played, rh.score, rh.putts, - rh.tee_shot_result, rh.approach_result, rh.chip_count, - rh.bunker_shot_count, rh.penalty_strokes, rh.anyway_strokes - FROM round_participant rp - JOIN round r ON r.id = rp.round_id - JOIN round_hole rh ON rh.round_participant_id = rp.id - WHERE rp.user_id = $1 AND r.completed_at IS NOT NULL - ORDER BY r.played_at, rh.hole_number - """, - user.user_id, - ) + if window == "all": + return {r[0] for r in ordered_rounds}, set() - by_round: dict[str, list] = {} - stat_level_by_round: dict[str, str] = {} - for r in rows: - by_round.setdefault(r["round_id"], []).append(r) - stat_level_by_round[r["round_id"]] = r["stat_level"] + if window in ("last_round", "last_5", "last_10"): + n = {"last_round": 1, "last_5": 5, "last_10": 10}[window] + current = ordered_rounds[:n] + previous = ordered_rounds[n : 2 * n] + return {r[0] for r in current}, {r[0] for r in previous} + + today = date.today() + if window == "month": + start = today.replace(day=1) + elif window == "year": + start = today.replace(month=1, day=1) + elif window == "last_year": + start = today - timedelta(days=365) + else: + raise ValueError(f"Ukjent vindu: {window}") + + length_days = (today - start).days + 1 + prev_end = start - timedelta(days=1) + prev_start = prev_end - timedelta(days=length_days - 1) + current = {r[0] for r in ordered_rounds if r[1] >= start} + previous = {r[0] for r in ordered_rounds if prev_start <= r[1] <= prev_end} + return current, previous + + +def _summarize_rounds(round_ids: set[str], by_round: dict[str, list], stat_level_by_round: dict[str, str]) -> RoundStatsSummary: + """Samme formler som `round-stats.tsx` sin `computeStats()` bruker per + enkeltrunde, pooler kun holdene fra `round_ids` -- gjenbrukt for BÅDE + gjeldende og forrige periode (se `_resolve_stats_window`).""" def avg(values: list[float]) -> float | None: return sum(values) / len(values) if values else None to_par_values: list[float] = [] putt_totals: list[float] = [] - for round_id, holes in by_round.items(): + for round_id in round_ids: + holes = by_round.get(round_id, []) played = [h for h in holes if h["played"] and h["score"] is not None] if played: to_par_values.append(sum(h["score"] - h["par"] for h in played)) - if stat_level_by_round[round_id] != "strokes_only": + if stat_level_by_round.get(round_id) != "strokes_only": putt_totals.append(sum(h["putts"] if h["putts"] is not None else 2 for h in holes)) - all_played = [h for holes in by_round.values() for h in holes if h["played"] and h["score"] is not None] + all_played = [h for rid in round_ids for h in by_round.get(rid, []) if h["played"] and h["score"] is not None] fairway_tracked = [h for h in all_played if h["par"] != 3 and h["tee_shot_result"] is not None] fairway_hit_pct = avg([100.0 if h["tee_shot_result"] == "fairway" else 0.0 for h in fairway_tracked]) + fairway_left_pct = avg([100.0 if h["tee_shot_result"] == "left" else 0.0 for h in fairway_tracked]) + fairway_right_pct = avg([100.0 if h["tee_shot_result"] == "right" else 0.0 for h in fairway_tracked]) def is_gir(h) -> bool: return h["score"] - h["putts"] <= h["par"] - 2 @@ -752,6 +774,15 @@ async def get_rounds_stats_summary(user: CurrentUser = Depends(get_current_user) gir_pct = avg([100.0 if is_gir(h) else 0.0 for h in gir_eligible]) one_putt_pct = avg([100.0 if h["putts"] <= 1 else 0.0 for h in gir_eligible]) + # Retning på bomskudd mot green -- eget spor (approach_result), ikke + # avledet av putte-basert GIR: en spiller kan registrere retning uten + # noensinne å registrere putter (og omvendt), samme uavhengighet som + # `round-stats.tsx` sin `missedGreen`/`missDir`. + green_missed = [h for h in all_played if h["approach_result"] is not None and h["approach_result"] != "hit"] + + def miss_pct(direction: str) -> float | None: + return avg([100.0 if h["approach_result"] == direction else 0.0 for h in green_missed]) + gir_missed = [h for h in gir_eligible if not is_gir(h)] scrambling_pct = avg([100.0 if (h["score"] - h["par"]) <= 0 else 0.0 for h in gir_missed]) sand_eligible = [h for h in gir_missed if h["bunker_shot_count"] is not None and h["bunker_shot_count"] >= 1] @@ -759,21 +790,28 @@ async def get_rounds_stats_summary(user: CurrentUser = Depends(get_current_user) def avg_per_round(field: str) -> float | None: totals = [] - for holes in by_round.values(): - eligible = [h for h in holes if h["played"] and h["score"] is not None and h[field] is not None] + for round_id in round_ids: + eligible = [h for h in by_round.get(round_id, []) if h["played"] and h["score"] is not None and h[field] is not None] if eligible: totals.append(sum(h[field] for h in eligible)) return avg(totals) return RoundStatsSummary( - rounds_completed=len(by_round), + rounds_completed=len(round_ids), avg_score_to_par=avg(to_par_values), avg_putts_per_18=avg(putt_totals), putts_tracked_rounds=len(putt_totals), fairway_hit_pct=fairway_hit_pct, + fairway_left_pct=fairway_left_pct, + fairway_right_pct=fairway_right_pct, fairway_tracked_holes=len(fairway_tracked), gir_pct=gir_pct, gir_tracked_holes=len(gir_eligible), + green_miss_long_pct=miss_pct("long"), + green_miss_short_pct=miss_pct("short"), + green_miss_left_pct=miss_pct("left"), + green_miss_right_pct=miss_pct("right"), + green_miss_tracked_holes=len(green_missed), one_putt_pct=one_putt_pct, scrambling_pct=scrambling_pct, sand_save_pct=sand_save_pct, @@ -784,6 +822,67 @@ async def get_rounds_stats_summary(user: CurrentUser = Depends(get_current_user) ) +@router.get("/rounds/stats/summary", response_model=RoundStatsWindowSummary) +async def get_rounds_stats_summary( + window: StatsWindow = Query("all"), + user: CurrentUser = Depends(get_current_user), +) -> RoundStatsWindowSummary: + """Aggregert statistikk over brukerens fullførte runder (egen + deltaker-rad, ikke medspilleres), for et valgt tidsvindu -- pluss samme + tall for FORRIGE periode av samme lengde til sammenligning (se + `_resolve_stats_window`). Ingen paginering -- samme "hent alt, aggreger + i Python"-mønster som `list_rounds`, forsvarlig i volum siden + `round_hole` alltid har nøyaktig 18 rader per deltaker uansett + holes_planned (se `_create_participant`). + + Putt/18-hull-regelen (brukerens eksplisitte instruks 2026-07-28): for + hver fullført runde der puttsporing faktisk var på (stat_level ≠ + 'strokes_only') telles ALLE 18 lagrede hull -- et hull uten registrert + putt-verdi (uspilt, eller utenfor et 9-hulls spilleomfang) telles som 2 + putter. Runder uten puttsporing holdes helt utenfor putt-tallet. + Alle andre andelstall (fairway/GIR/én-putt/scrambling/sand save/miss- + retning) pooler kun FAKTISK registrerte hull -- samme formler som + `round-stats.tsx` sin `computeStats()` bruker per enkeltrunde (portert + hit, ikke en ny definisjon), ingen padding der brukeren ikke ba om det. + """ + async with plain_connection() as conn: + rows = await conn.fetch( + """ + SELECT rp.round_id::text AS round_id, rp.stat_level, r.played_at, + rh.par, rh.played, rh.score, rh.putts, + rh.tee_shot_result, rh.approach_result, rh.chip_count, + rh.bunker_shot_count, rh.penalty_strokes, rh.anyway_strokes + FROM round_participant rp + JOIN round r ON r.id = rp.round_id + JOIN round_hole rh ON rh.round_participant_id = rp.id + WHERE rp.user_id = $1 AND r.completed_at IS NOT NULL + ORDER BY r.played_at DESC, rh.hole_number + """, + user.user_id, + ) + + by_round: dict[str, list] = {} + stat_level_by_round: dict[str, str] = {} + played_at_by_round: dict[str, date] = {} + for r in rows: + by_round.setdefault(r["round_id"], []).append(r) + stat_level_by_round[r["round_id"]] = r["stat_level"] + played_at_by_round[r["round_id"]] = r["played_at"] + + # Distinkte runder, nyest først -- rekkefølgen `rows` allerede har + # (ORDER BY r.played_at DESC) bevares siden Python-dicts er innsettings- + # ordnet, ingen ny sortering nødvendig. + ordered_rounds = [(rid, played_at_by_round[rid]) for rid in by_round] + + current_ids, previous_ids = _resolve_stats_window(window, ordered_rounds) + + return RoundStatsWindowSummary( + window=window, + current=_summarize_rounds(current_ids, by_round, stat_level_by_round), + previous=_summarize_rounds(previous_ids, by_round, stat_level_by_round), + ) + + async def _get_owned_round_or_404(conn, round_id: str, user_id: str): """Strengt eier-only -- for runde-forvaltning (rediger/slett metadata, legge til/fjerne deltakere). IKKE for lesing/scoreregistrering, se diff --git a/frontend/components/rounds-stats-summary.tsx b/frontend/components/rounds-stats-summary.tsx index c814c54..2bca2e1 100644 --- a/frontend/components/rounds-stats-summary.tsx +++ b/frontend/components/rounds-stats-summary.tsx @@ -1,17 +1,18 @@ "use client" -// Aggregert statistikk på tvers av ALLE fullførte "Egne runder" (ADR-033), -// lenket fra dashbordets "Statistikk"-seksjon (2026-07-28). Egen backend- +// Aggregert statistikk på tvers av "Egne runder" (ADR-033), lenket fra +// dashbordets "Statistikk"-seksjon (2026-07-28, utvidet samme dag med +// tidsvindu + miss-retning + forrige-periode-sammenligning). 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) må kjøre over ALLE -// fullførte runder samlet, ikke én runde om gangen. +// 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. import type React from "react" import { useEffect, useState } from "react" import { useRouter } from "next/navigation" import Link from "next/link" -import { ArrowLeft, BarChart3, Flag, Target, Waves, Wind } from "lucide-react" +import { ArrowLeft, BarChart3, Flag, Target, TrendingDown, TrendingUp, Waves, Wind } from "lucide-react" type ApiStatsSummary = { rounds_completed: number @@ -19,9 +20,16 @@ type ApiStatsSummary = { 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 scrambling_pct: number | null sand_save_pct: number | null @@ -31,6 +39,24 @@ type ApiStatsSummary = { avg_anyway_per_round: 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(".", ",")}` @@ -40,11 +66,58 @@ function pct(n: number): string { return `${Math.round(n)} %` } -function StatTile({ label, value, hint }: { label: string; value: string; hint?: string }) { +// 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" +}) { + if (current === null || previous === null) return null + const diff = current - previous + if (diff === 0) return Uendret + 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}}
) @@ -70,22 +143,54 @@ function Section({ ) } +// Horisontal fordelingsbar med retningsandeler (venstre/høyre eller +// lang/kort/venstre/høyre) -- pooler kun faktisk registrerte bom. +function MissBar({ + tracked, + segments, +}: { + tracked: number + segments: { label: string; value: number | null; color: string }[] +}) { + if (tracked === 0) return

Ingen retning registrert ennå.

+ return ( +
+
+ {segments.map((s) => + s.value && s.value > 0 ? ( +
+ ) : null, + )} +
+
+ {segments.map((s) => ( + + + ))} +
+
+ ) +} + export function RoundsStatsSummary() { const router = useRouter() - const [data, setData] = useState(null) + const [window, setWindow] = useState("all") + const [data, setData] = useState(null) const [error, setError] = useState(null) useEffect(() => { let cancelled = false async function load() { try { - const res = await fetch("/rounds/stats/summary", { credentials: "include" }) + const res = await fetch(`/rounds/stats/summary?window=${window}`, { credentials: "include" }) if (res.status === 401) { router.replace("/") return } if (!res.ok) throw new Error(`stats: ${res.status}`) - const json: ApiStatsSummary = await res.json() + const json: ApiWindowSummary = await res.json() if (!cancelled) setData(json) } catch { if (!cancelled) setError("Klarte ikke å hente statistikken. Prøv igjen om litt.") @@ -95,7 +200,9 @@ export function RoundsStatsSummary() { return () => { cancelled = true } - }, [router]) + }, [router, window]) + + const s = data?.current ?? null return (
@@ -113,6 +220,21 @@ export function RoundsStatsSummary() {
+
+ {WINDOW_OPTIONS.map((opt) => ( + + ))} +
+ {error && (

{error} @@ -124,33 +246,49 @@ export function RoundsStatsSummary() {

- ) : data && data.rounds_completed === 0 ? ( + ) : s && s.rounds_completed === 0 ? (
-

Ingen fullførte runder ennå

+

Ingen fullførte runder i perioden

- Statistikken bygger seg opp automatisk etter hvert som du fullfører runder. + Prøv et annet tidsvindu, eller kom tilbake etter neste fullførte runde.

) : data ? ( + (() => { + const s = data.current + const p = data.previous + return ( <>

- Aggregert over {data.rounds_completed} {data.rounds_completed === 1 ? "fullført runde" : "fullførte runder"}. + 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 + + )} + .

- - + + } + /> } hint={ - data.putts_tracked_rounds > 0 - ? `${data.putts_tracked_rounds} ${data.putts_tracked_rounds === 1 ? "runde med" : "runder med"} puttsporing` + s.putts_tracked_rounds > 0 + ? `${s.putts_tracked_rounds} ${s.putts_tracked_rounds === 1 ? "runde med" : "runder med"} puttsporing` : "Ingen runder med puttsporing ennå" } /> @@ -159,42 +297,96 @@ export function RoundsStatsSummary() {
0 ? `${data.fairway_tracked_holes} hull registrert` : undefined} + value={s.fairway_hit_pct !== null ? pct(s.fairway_hit_pct) : "–"} + delta={} + hint={s.fairway_tracked_holes > 0 ? `${s.fairway_tracked_holes} hull registrert` : undefined} /> 0 ? `${data.gir_tracked_holes} hull registrert` : undefined} + value={s.gir_pct !== null ? pct(s.gir_pct) : "–"} + delta={} + hint={s.gir_tracked_holes > 0 ? `${s.gir_tracked_holes} hull registrert` : undefined} + /> + } /> -
+
+
+

Hvor bommer du fra utslaget?

+

Kun par 4/5, kun registrerte utslag.

+
+ +
+

Hvor bommer du på innspillet?

+

Kun hull der greentreff ble bommet og retning registrert.

+
+ +
+
} hint="Par eller bedre uten GIR" /> } hint="Par eller bedre fra bunker" />
- - - + } + /> + } + /> + } + />
- {data.avg_anyway_per_round !== null && ( + {s.avg_anyway_per_round !== null && (
- + } + />
)} + ) + })() ) : null}