diff --git a/CHANGELOG.md b/CHANGELOG.md index ce33e06..40872c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12488,4 +12488,73 @@ Neste steg: manuelt for at scratch-dataen skulle vise seg riktig -- verdt å huske for fremtidig scratch-seeding av individuelle turneringer. + **Rullet ut 2026-08-18** -- bruker bekreftet ("Ja, bygg og rull ut"). + `docker compose build teecup_frontend && docker compose up -d + teecup_frontend`, rene logger ("Ready in 0ms"). `curl -I + https://teecup.golf/logg-inn` -> 200 etter utrulling. + +113. **Nøytral "ikke startet"-rad + tee-tid/starthull i leaderboardet — + 2026-08-18.** Bruker: "De som ikke har begynt å spille ennå må være + nøytrale på et vis, i leaderboardet. Det bør kanskje komme frem + teetime og starthull på de, dersom det er angitt?" Rot funnet i + `_attach_stroke_play_columns` (`individual_tournaments.py`): en + deltaker som ikke hadde spilt ETT eneste hull ennå (`total_par_played + == 0`) fikk likevel `total_label = "E"` (brutto/netto) eller `"0 p"` + (stableford) -- til forveksling likt en ekte, jevn/poengløs SCORE, + ikke "ingen data ennå". Samme deltaker fikk også en tallfestet + plassering (nederst i feltet, siden NO_SCORE-nøkkelen sorterer sist) + -- så de så ut som en reelt rangert, dårlig presterende spiller i + stedet for en som rett og slett ikke har startet. + + **Backend (`app/routers/individual_tournaments.py`):** ny + `LeaderboardEntry.next_tee_time`/`next_start_hole` (kun individuelle + turneringer har en `tournament_round`-tabell å hente disse fra -- + ETT klokkeslett/starthull PER RUNDE, delt av hele feltet, ikke per + deltaker -- ingen per-deltaker utslettidspunkt finnes noe sted i + denne turneringsformen). Populeres når `thru_label == "-"` (samme + betingelse som already brukes for "ikke startet gjeldende runde"): + referanserunden er `today_sequence` (runden som er i gang) hvis + turneringen er underveis, ellers runde 1 (ingen har startet noe + ennå). `next_start_hole` vises alltid når referanserunden finnes + (kolonnen har alltid en verdi, default 1); `next_tee_time` vises kun + når runden faktisk har et satt `scheduled_at` (nullable -- ikke alle + turneringer setter det). `total_label` er nå `None` (ikke "E"/"0 p") + når deltakeren ikke har spilt noe -- **kun** når `total_par_played + == 0`, IKKE når de bare venter på neste runde etter å ha fullført en + tidligere (den deltakeren beholder sin ekte total, kun THRU viser + "-", eksisterende oppførsel uendret). `position`/`is_leader` settes + til `None`/`False` for hele NO_SCORE-gruppen i stedet for en + tallfestet sisteplass -- inkl. kant-tilfellet der INGEN i feltet har + spilt noe ennå (da ville rank 1 ellers blitt feilaktig kåret til + "leder"). + + **Frontend (`stroke-play-leaderboard.tsx`, liten, veloverveid + håndkodet utvidelse av en eksisterende V0-komponent -- IKKE en ny + UI-flate, se [[feedback_frontend_via_v0]]):** `totalLabel` er nå + `string | null`; når `null` (og raden ikke allerede er en WD/DQ- + statusrad) rendres en ny `TeeInfoTd` i TOTALT-kolonnen i stedet for + en `ValueTd` -- en nøytral pille (klokke-ikon, `bg-muted`) med + `"{tee-tid} · Hull {starthull}"`, eller bare det ene hvis kun det er + satt, eller en ren dash hvis ingen av delene er satt. Raden får + samme dempede `opacity-70`-behandling som WD/DQ-rader (bevisst + gjenbruk -- samme "dette er ikke et konkurransedyktig resultat ennå"- + signal). Ny "Ikke startet"-forklaring lagt til i toppens Legend + (klokke-ikon), siden pillen introduserer en ny visuell betydning + Legend ikke dekket fra før. + + **Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55. Backend: + to nye pytest-tester i `tests/test_individual_leaderboard.py` (én + med en reell deltaker som HAR spilt runde 1 ved siden av én som + ikke har startet noe -- bekrefter begge veier samtidig -- og én for + alle-ikke-startet-kant-tilfellet), kjørt via + `scripts/run_backend_tests.sh` (egen scratch-database, rører aldri + ekte `teecup_db`) -- 117/117 grønt (opp fra 115). Frontend: en + midlertidig lokal `/leaderboardpreviewtmp`-side (kun i en `next dev`- + prosess, ALDRI committet) rendret `StrokePlayLeaderboard` med + `MOCK_ROWS` utvidet med to nye rader (én med både tee-tid og + starthull, én med KUN starthull) -- skjermbilde + a11y-snapshot + bekreftet riktig pille-tekst, riktig dempet radstil, og at + posisjonen viser "-" i stedet for et tall. Midlertidig side slettet + igjen etter verifisering. + **Rullet ut:** venter på bekreftelse. diff --git a/app/routers/individual_tournaments.py b/app/routers/individual_tournaments.py index 511d4be..f3e03d8 100644 --- a/app/routers/individual_tournaments.py +++ b/app/routers/individual_tournaments.py @@ -1230,6 +1230,13 @@ class LeaderboardEntry(BaseModel): thru_label: str | None = None total_label: str | None = None rounds: list[RoundCellOut] = [] + # Nøytral visning for spillere som ikke har startet gjeldende/neste runde + # ennå (2026-08-18). next_start_hole settes alltid når det finnes en + # referanse-runde (start_hole har alltid en verdi -- default 1). + # next_tee_time settes kun når runden faktisk har et oppgitt klokkeslett + # (scheduled_at er nullable -- ikke alle turneringer setter det). + next_tee_time: str | None = None + next_start_hole: int | None = None # Eclectic (ADR-067-tillegget "Del C", migrasjon 072) -- beste resultat # per hull på tvers av ALLE turneringens runder (samme bane, håndhevet # ved rundeopprettelse). Alltid None/tom liste for øvrige scoring_ @@ -1356,7 +1363,7 @@ async def _attach_stroke_play_columns( """ rounds_meta = await conn.fetch( """ - SELECT sequence, hole_config::text AS hole_config + SELECT sequence, hole_config::text AS hole_config, scheduled_at::text AS scheduled_at, start_hole FROM tournament_round WHERE tournament_id = $1 ORDER BY sequence @@ -1366,6 +1373,8 @@ async def _attach_stroke_play_columns( hole_count_by_seq = { r["sequence"]: _HOLE_COUNT_BY_CONFIG[r["hole_config"]] for r in rounds_meta } + round_meta_by_seq = {r["sequence"]: r for r in rounds_meta} + first_sequence = rounds_meta[0]["sequence"] if rounds_meta else None # Én rad per (runde, deltaker) som faktisk er PÅBEGYNT (holes_played > 0). # par_played = summen av par for KUN de hullene deltakeren faktisk har @@ -1462,18 +1471,36 @@ async def _attach_stroke_play_columns( e.thru_label = "-" e.today_label = None + # Ikke-startet-info (2026-08-18): så lenge deltakeren ikke har + # startet den gjeldende/neste runden ("-" over), vis tee-tid/ + # starthull for DEN runden -- referanserunden er selve gjeldende + # runden (today_sequence) hvis turneringen er i gang, ellers + # runde 1 (ingen har startet noe ennå). + if e.thru_label == "-": + reference_sequence = today_sequence if today_sequence is not None else first_sequence + meta = round_meta_by_seq.get(reference_sequence) if reference_sequence is not None else None + if meta is not None: + e.next_tee_time = meta["scheduled_at"] + e.next_start_hole = meta["start_hole"] + # TOTAL -- gjenbruker den allerede korrekt summerte gross_total/ # net_total/stableford_total (uendret av denne funksjonen), trekker - # kun fra summert par-for-spilte-hull for til-par-formatet. + # kun fra summert par-for-spilte-hull for til-par-formatet. Ingen + # runde spilt ennå (total_holes_played == 0) -> total_label forblir + # None (nøytral -- IKKE "E"/"0 p", som ville sett ut som en ekte, + # jevn/poengløs score i stedet for "ikke startet"). + started_any = total_holes_played > 0 if method == "stableford": - e.total_label = f"{e.stableford_total or 0} p" + e.total_label = f"{e.stableford_total or 0} p" if started_any else None rank_key_by_id[e.tournament_participant_id] = ( - NO_SCORE if e.stableford_total is None or total_holes_played == 0 + NO_SCORE if e.stableford_total is None or not started_any else -(e.stableford_total - 2 * total_holes_played) ) else: total_val = e.net_total if method == "stroke_net" else e.gross_total - e.total_label = _to_par_label(total_val - total_par_played) if total_val is not None else "E" + e.total_label = ( + _to_par_label(total_val - total_par_played) if (total_val is not None and started_any) else None + ) rank_key_by_id[e.tournament_participant_id] = ( NO_SCORE if total_val is None or total_par_played == 0 else total_val - total_par_played ) @@ -1485,6 +1512,9 @@ async def _attach_stroke_play_columns( # POS -- vanlig konkurranse-rangering (uavgjort deler plass, neste # rangering hopper over like mange plasser som antall uavgjorte). + # Deltakere som ikke har spilt ETT eneste hull ennå (NO_SCORE) får INGEN + # posisjon -- de er ikke "sist rangert", de er ennå ikke en del av + # rangeringen (nøytral, se next_tee_time/next_start_hole over). i, n = 0, len(entries) while i < n: j = i @@ -1493,9 +1523,10 @@ async def _attach_stroke_play_columns( j += 1 rank = i + 1 tied = (j - i) > 1 + not_started_group = key_i == NO_SCORE for k in range(i, j): - entries[k].position = f"T{rank}" if tied else str(rank) - entries[k].is_leader = rank == 1 + entries[k].position = None if not_started_group else (f"T{rank}" if tied else str(rank)) + entries[k].is_leader = rank == 1 and not not_started_group i = j diff --git a/frontend/components/individual-tournament-detail.tsx b/frontend/components/individual-tournament-detail.tsx index 822fc72..943f4d7 100644 --- a/frontend/components/individual-tournament-detail.tsx +++ b/frontend/components/individual-tournament-detail.tsx @@ -216,6 +216,11 @@ type ApiLeaderboardEntry = { today_label: string | null thru_label: string | null total_label: string | null + // Nøytral "ikke startet"-info (2026-08-18) -- kun satt når deltakeren + // ikke har startet gjeldende/neste runde (thru_label "-"), se + // stroke-play-leaderboard.tsx. + next_tee_time: string | null + next_start_hole: number | null rounds: ApiRoundCell[] // Eclectic (ADR-067-tillegget "Del C", 2026-08-14) -- kun populert for // scoring_method eclectic_gross/eclectic_net/eclectic_stableford. @@ -3979,9 +3984,15 @@ function LeaderboardTab({ base, tournament }: { base: string; tournament: ApiTou todayLabel: entry.today_label, todayIsUnderPar: null, thruLabel: entry.thru_label ?? "-", - totalLabel: entry.total_label ?? "E", + // total_label er null når deltakeren ikke har spilt ett eneste hull + // ennå -- nøytral tilstand, IKKE til forveksling med "E" (jevnt med + // par, en ekte score). stroke-play-leaderboard.tsx viser tee-tid/ + // starthull i stedet når totalLabel er null. + totalLabel: entry.total_label, totalIsUnderPar: false, isLeader: entry.is_leader, + nextTeeTime: entry.next_tee_time, + nextStartHole: entry.next_start_hole, rounds: entry.rounds.map((r) => ({ roundNumber: r.round_number, label: r.label, tone: r.tone })), } } diff --git a/frontend/components/stroke-play-leaderboard.tsx b/frontend/components/stroke-play-leaderboard.tsx index 5075b2a..636fbf9 100644 --- a/frontend/components/stroke-play-leaderboard.tsx +++ b/frontend/components/stroke-play-leaderboard.tsx @@ -1,7 +1,7 @@ "use client" import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react" -import { Flag, Pause, Play, Trophy } from "lucide-react" +import { Clock, Flag, Pause, Play, Trophy } from "lucide-react" import { cn } from "@/lib/utils" // --- Types (exact contract — do not change) -------------------------------- @@ -18,9 +18,14 @@ export type LeaderboardRow = { todayLabel: string | null // "-2", "+4", "E", "38 p" — null = today's round not started yet todayIsUnderPar: boolean | null thruLabel: string // "F" (finished), "14" (holes played), "-" (not started) - totalLabel: string // "-13", "+7", "146 p" + totalLabel: string | null // "-13", "+7", "146 p" — null = hasn't played a single hole yet (neutral, not "E") totalIsUnderPar: boolean isLeader: boolean // true for every row sharing the current lead position (ties possible) + // When totalLabel is null, the row is rendered neutrally and shows this + // instead of a score: the tee time / start hole of the round they're + // about to play, if the organizer set one. Either can be null/undefined. + nextTeeTime?: string | null // ISO datetime, or null if no tee time was set + nextStartHole?: number | null rounds: RoundCell[] // one entry per round played so far (1-4 entries, not always 4) } @@ -54,6 +59,8 @@ export const MOCK_ROWS: LeaderboardRow[] = [ { position: "14", playerName: "Maja Lund", todayLabel: "+2", todayIsUnderPar: false, thruLabel: "F", totalLabel: "+9", totalIsUnderPar: false, isLeader: false, rounds: [rc(1, 79), rc(2, 74)] }, { position: "15", playerName: "Frida Ness", todayLabel: null, todayIsUnderPar: null, thruLabel: "-", totalLabel: "+11", totalIsUnderPar: false, isLeader: false, rounds: [rc(1, 83)] }, { position: "–", playerName: "Kaia Strand", todayLabel: null, todayIsUnderPar: null, thruLabel: "-", totalLabel: "WD", totalIsUnderPar: false, isLeader: false, rounds: [rc(1, 76)] }, + { position: "-", playerName: "Julie Berget", todayLabel: null, todayIsUnderPar: null, thruLabel: "-", totalLabel: null, totalIsUnderPar: false, isLeader: false, nextTeeTime: "2026-08-19T11:10:00", nextStartHole: 1, rounds: [] }, + { position: "-", playerName: "Selma Vang", todayLabel: null, todayIsUnderPar: null, thruLabel: "-", totalLabel: null, totalIsUnderPar: false, isLeader: false, nextTeeTime: null, nextStartHole: 10, rounds: [] }, ] // --- Tone helpers ---------------------------------------------------------- @@ -371,6 +378,10 @@ function Legend() { Par +