From c967ed11dac92e76d7076ffddea1a339521846a2 Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Wed, 12 Aug 2026 09:12:49 +0200 Subject: [PATCH] Statistikk: ekspandert trekkspill, spilt-til-HCP, omstrukturert /my-rounds/stats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/routers/rounds.py | 31 +- frontend/components/dashboard.tsx | 10 +- frontend/components/round-stats.tsx | 10 +- frontend/components/rounds-stats-summary.tsx | 298 +++++++++++++++---- 4 files changed, 273 insertions(+), 76 deletions(-) diff --git a/app/routers/rounds.py b/app/routers/rounds.py index dae3a7e..63c4c93 100644 --- a/app/routers/rounds.py +++ b/app/routers/rounds.py @@ -1657,9 +1657,21 @@ async def list_rounds(user: CurrentUser = Depends(get_current_user)) -> list[Rou return [await _load_round_out(conn, r["id"], user.user_id) for r in ids] +class RoundStatsSeriesPoint(BaseModel): + played_at: date + score_to_par: float + + class RoundStatsSummary(BaseModel): rounds_completed: int avg_score_to_par: float | None + # Kronologisk (eldste først) snitt-til-par per fullført runde i + # perioden -- til stolpegraf-med-trendlinje på frontend (bruker- + # instruks 2026-08-12). Kun for `current`-vinduet (se + # get_rounds_stats_summary); `previous` trenger ikke egen serie siden + # grafen bare viser inneværende periodes utvikling, ikke en + # sammenligning mot forrige periode slik de andre tallene på siden gjør. + round_series: list[RoundStatsSeriesPoint] avg_putts_per_18: float | None putts_tracked_rounds: int fairway_hit_pct: float | None @@ -1746,7 +1758,12 @@ def _resolve_stats_window(window: StatsWindow, ordered_rounds: list[tuple[str, d return current, previous -def _summarize_rounds(round_ids: set[str], by_round: dict[str, list], stat_level_by_round: dict[str, str]) -> RoundStatsSummary: +def _summarize_rounds( + round_ids: set[str], + by_round: dict[str, list], + stat_level_by_round: dict[str, str], + played_at_by_round: dict[str, date], +) -> 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`).""" @@ -1756,13 +1773,18 @@ def _summarize_rounds(round_ids: set[str], by_round: dict[str, list], stat_level to_par_values: list[float] = [] putt_totals: list[float] = [] + round_series: list[RoundStatsSeriesPoint] = [] 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)) + round_to_par = float(sum(h["score"] - h["par"] for h in played)) + to_par_values.append(round_to_par) + round_series.append(RoundStatsSeriesPoint(played_at=played_at_by_round[round_id], score_to_par=round_to_par)) 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)) + # Eldste først -- stolpegrafen på frontend viser utvikling venstre->høyre. + round_series.sort(key=lambda pt: pt.played_at) all_played = [h for rid in round_ids for h in by_round.get(rid, []) if h["played"] and h["score"] is not None] @@ -1834,6 +1856,7 @@ def _summarize_rounds(round_ids: set[str], by_round: dict[str, list], stat_level return RoundStatsSummary( rounds_completed=len(round_ids), avg_score_to_par=avg(to_par_values), + round_series=round_series, avg_putts_per_18=avg(putt_totals), putts_tracked_rounds=len(putt_totals), fairway_hit_pct=fairway_hit_pct, @@ -1924,8 +1947,8 @@ async def get_rounds_stats_summary( 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), + current=_summarize_rounds(current_ids, by_round, stat_level_by_round, played_at_by_round), + previous=_summarize_rounds(previous_ids, by_round, stat_level_by_round, played_at_by_round), ) diff --git a/frontend/components/dashboard.tsx b/frontend/components/dashboard.tsx index 37439a1..d1a2b9f 100644 --- a/frontend/components/dashboard.tsx +++ b/frontend/components/dashboard.tsx @@ -124,6 +124,7 @@ type Me = { preferred_locale: string profile_complete: boolean handicap_index: number | null + computed_handicap_index: number | null organizations: MyOrg[] my_tournaments: MyTournament[] } @@ -160,7 +161,7 @@ type ApiRound = { my_match_lead: number | null } -type ApiHandicapPoint = { handicap_index: number; recorded_at: string } +type ApiHandicapPoint = { handicap_index: number; recorded_at: string; source: "manual" | "computed" } // ADR-036 fase 1 -- kun det dashbordets kompakte kort trenger (ikke hele // GET /friends-responsen, som også bærer utgående forespørsler/kategorier). @@ -453,9 +454,12 @@ export function Dashboard() { .map((r) => r.my_score_to_par as number) const avgToPar = avgToParSample.length > 0 ? formatSigned(avgToParSample.reduce((s, v) => s + v, 0) / avgToParSample.length, 1) : "—" - const hcpValues = hcpHistory.map((h) => h.handicap_index) + // Dashbordet skal vise "spilt til"-HCP (beregnet fra faktiske runder), + // ikke det manuelt satte tallet i profilen -- historikk-grafen filtreres + // til samme kilde, ellers ville sparklinen blande to ulike signaler. + const hcpValues = hcpHistory.filter((h) => h.source === "computed").map((h) => h.handicap_index) const hcpTrend = hcpValues.length >= 2 ? hcpValues[hcpValues.length - 1] - hcpValues[hcpValues.length - 2] : null - const hcpNow = me.handicap_index !== null ? me.handicap_index.toFixed(1).replace(".", ",") : "—" + const hcpNow = me.computed_handicap_index !== null ? me.computed_handicap_index.toFixed(1).replace(".", ",") : "—" const coursesMap = new Map() for (const r of rows) { diff --git a/frontend/components/round-stats.tsx b/frontend/components/round-stats.tsx index 064c8ab..5b01ad1 100644 --- a/frontend/components/round-stats.tsx +++ b/frontend/components/round-stats.tsx @@ -57,10 +57,12 @@ const C = { function StatCard({ title, icon: Icon, - // Alle seksjonene starter kollapset (etterspurt av bruker 2026-07-25: - // "trekkspillet skal vises sammenslått") -- må trykkes på hver enkelt - // for å se innholdet, i stedet for at hele siden lastes åpen. - defaultOpen = false, + // Alle seksjonene starter ekspandert (2026-08-12, snur en tidligere + // beslutning fra 2026-07-25 om at trekkspillet skulle vises sammenslått) + // -- brukeren skal se alt med det samme, i stedet for å måtte åpne hver + // seksjon manuelt. Trekkspillet finnes fortsatt, for å kunne lukke ned + // seksjoner man ikke bryr seg om akkurat nå. + defaultOpen = true, children, }: { title: string diff --git a/frontend/components/rounds-stats-summary.tsx b/frontend/components/rounds-stats-summary.tsx index fcfd7ac..ea00eb1 100644 --- a/frontend/components/rounds-stats-summary.tsx +++ b/frontend/components/rounds-stats-summary.tsx @@ -21,11 +21,14 @@ 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, Target, TrendingDown, TrendingUp, Waves, Wind } from "lucide-react" +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 + round_series: ApiRoundSeriesPoint[] avg_putts_per_18: number | null putts_tracked_rounds: number fairway_hit_pct: number | null @@ -107,17 +110,24 @@ function Delta({ 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 + 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 ( - + ) } @@ -265,25 +275,47 @@ function CompassCell({ icon: Icon, label, value }: { icon: typeof ArrowUp; label ) } -function GirCompass({ - hitPct, +// 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, - missTrackedHoles, long, short, left, right, }: { - hitPct: number | null trackedHoles: number - missTrackedHoles: number long: number | null short: number | null left: number | null right: number | null }) { - if (hitPct === null) { - return

Ingen greentreff-data registrert ennå.

+ if (trackedHoles === 0) { + return

Ingen innspill med retning registrert ennå.

} return (
@@ -292,20 +324,19 @@ function GirCompass({