From dcc2a9275c8b138c83ccdd1eb4debd24a5c36621 Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Sat, 22 Aug 2026 05:26:46 +0200 Subject: [PATCH] Hull-for-hull-utvidelse i den autentiserte leaderboard-fanen (ADR-101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Klikk på en spiller i slagspill/netto/Stableford-leaderboardet ekspanderer nå raden til Ut/Inn-tabeller med hull-for-hull-score, portert fra det tilsvarende mønsteret på den offentlige "Følg live"-siden. Nytt lite, isolert backend-endepunkt (tournament_participant_id-nøkkede hull), ingen migrasjon. StrokePlayLeaderboard sin LeaderboardRow-type er uendret -- nytt valgfritt prop-sett på komponenten selv. Co-Authored-By: Claude Sonnet 5 --- ARCHITECTURE_DECISIONS.md | 79 +++++++++ CHANGELOG.md | 33 ++++ app/routers/individual_tournaments.py | 79 ++++++++- .../individual-tournament-detail.tsx | 157 ++++++++++++++++++ .../components/stroke-play-leaderboard.tsx | 150 ++++++++++++----- tests/test_individual_leaderboard.py | 49 ++++++ 6 files changed, 506 insertions(+), 41 deletions(-) diff --git a/ARCHITECTURE_DECISIONS.md b/ARCHITECTURE_DECISIONS.md index 25fe2d7..eaf980b 100644 --- a/ARCHITECTURE_DECISIONS.md +++ b/ARCHITECTURE_DECISIONS.md @@ -10149,6 +10149,85 @@ mot ekte containere ennå. --- +## ADR-101: Hull-for-hull-utvidelse i den autentiserte leaderboard-fanen (2026-08-22) + +**Kontekst:** bruker, midt i forrige runde: "Leaderboard: Det må være +mulig å klikke på en enkeltspiller og få ekspandert raden, slik at man +ser scoren på hvert enkelt hull vedkommende har spilt (og summen av +runden(e) og første og siste ni)." `LeaderboardTab` hadde utvidelse +KUN for eclectic -- slagspill/netto/Stableford (`isStrokePlay`, vist +via `StrokePlayLeaderboard`, den klart vanligste gruppen) hadde INGEN +utvidelse. Mønsteret fantes allerede, ferdig bygget og verifisert, for +den OFFENTLIGE siden (`public-individual-live.tsx` sin `LeaderboardRow` ++ `HoleNine`, bygget samme uke) -- en reell asymmetri: tilskuersiden +hadde en funksjon den autentiserte org-siden manglet. Denne runden +PORTERER mønsteret (ingen ny V0-runde -- gjenbruker et allerede +designet/godkjent visuelt mønster), tilpasset den autentiserte +flerrunde-visningen. + +**Beslutning A -- omfang: slagspill/netto/Stableford, ikke Copenhagen/ +BBB/eclectic.** Eclectic har allerede sitt eget hull-for-hull-konsept +(beste-per-hull). Copenhagen (poeng) og BBB (observasjon om HELE +feltet) passer ikke inn i "hvert hull + sum + ut/inn"-rammen uten en +egen vurdering -- bevisst utenfor omfanget her. + +**Beslutning B -- `stroke-play-leaderboard.tsx` sin `LeaderboardRow`- +TYPE ("eksakt kontrakt -- ikke endre") er UENDRET.** Nytt, valgfritt +prop-sett på selve `StrokePlayLeaderboard`-KOMPONENTEN i stedet +(`expandedId`/`onToggleExpand`/`renderExpanded`) -- forelderen +(`LeaderboardTab`) eier state og henting, komponenten er ren +presentasjon, samme "eier-logikk-i-forelder"-prinsipp som +`TournamentScoringWizard`/`ScoreTab` (ADR-100). Ingen effekt når +propsettet ikke sendes. + +**Beslutning C -- kun FELT-rader (ikke ledere) er utvidbare.** +`StrokePlayLeaderboard` er en autoscrollende "kiosk-ticker" +(sticky-posisjonerte ledere, `leaderTops` beregnet via ResizeObserver) +-- en utvidet LEDER-rad ville i teorien håndteres riktig av den +eksisterende ResizeObserver-kaskaden, men risikoen for et sjeldent +geometri-avvik ble vurdert unødvendig når lederen(e) uansett allerede +er fremhevet og synlig øverst uten å måtte utvides. Rene felt-rader +har ingen slik sticky-avhengighet. + +**Beslutning D -- nytt lite backend-endepunkt, IKKE gjenbruk av det +eksisterende round_participant_id-nøkkede.** `RoundCellOut` (per-runde- +cellen i leaderboard-responsen) += `tournament_round_id` (rent +tillegg, populert fra en spørring som allerede har `tr` i scope). Nytt +endepunkt `GET .../individual-leaderboard/{tournament_participant_id} +/rounds/{round_id}/holes` -- speiler `registration.py` sitt offentlige +`tournament_participant_id`-nøkkede endepunkt nøyaktig (samme +oppløsning til `tournament_round_participant.id` internt), men under +et EGET stiprefiks: samme stiform som den eksisterende +`.../participants/{round_participant_id}/holes` ville kollidert +(FastAPI kan ikke skille to param med ulik betydning på samme +posisjon). Leaderboardet viser ALLE spilte runder stablet ved +utvidelse (ikke ett hull-sett per valgt runde som den offentlige +siden) -- `StrokePlayLeaderboard` viser allerede én kolonne per runde, +ingen ekstra rundevelger trengs. + +**Beslutning E -- netto-par-bevisst golfscore-form, ikke offentlig +sidens enklere brutto-only variant.** Nye `LeaderboardHoleNine`/ +`LeaderboardRoundHoles` bruker filens EGNE `classify`/ +`scoreMarkClasses` (samme `strokes_received`-bevisste logikk som +`HoleGrid`), ikke `public-individual-live.tsx` sin enklere `toneOf` -- +konsistent med resten av den autentiserte scoreførings-/scorekort- +visningen. + +**Status:** 165/165 pytest (164 + én ny dedikert test for det nye +endepunktet: riktig hull-data, 404 for ukjent runde). `tsc --noEmit` +rent. Full-stack scratch-verifisering (egen scratch-db, egen API- +container, egen MinIO, ekte innlogging, ingen fetch-mocking) med en +flerrunde-turnering (2 runder, én spiller begge, én kun runde 1) +bekreftet: utvidelse viser riktig Ut/Inn-seksjon (kun én, ingen +"Runde N"-overskrift siden spilleren bare spilte én runde), korrekt +Par-/Slag-sum per ni (36/63 verifisert mot faktisk seed-data), korrekt +golfscore-form (fylt oransje ved 3 over par, matcher "2+ fra +par"-regelen), chevron-rotasjon, riktig i mørk OG lys modus, smal +mobilskjerm. Alle scratch-ressurser ryddet opp. IKKE bygget/rullet ut +mot ekte containere ennå. + +--- + ## Utviklingsplan (rekkefølge) 1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003) diff --git a/CHANGELOG.md b/CHANGELOG.md index fef19b3..1873595 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14343,3 +14343,36 @@ Neste steg: backend-endring). `docker compose build teecup_api teecup_frontend && up -d`, begge containere friske, rene logger. `https://teecup.golf/health` 200 etter utrulling. + +158. **Hull-for-hull-utvidelse i den autentiserte leaderboard-fanen + (ADR-101), 2026-08-22.** Bruker, mens forrige runde pågikk: klikk + på en spiller skal ekspandere raden og vise scoren på hvert hull, + pluss sum per runde og for ut/inn. `LeaderboardTab` hadde utvidelse + kun for eclectic -- slagspill/netto/Stableford (StrokePlayLeaderboard, + den vanligste gruppen) hadde ingen. Mønsteret fantes allerede, + ferdig bygget, for den OFFENTLIGE siden (`public-individual- + live.tsx` sin LeaderboardRow/HoleNine) -- portert derfra (ingen ny + V0-runde), ikke bygget fra bunnen. + + **Backend, to små tillegg, ingen migrasjon:** `RoundCellOut` += + `tournament_round_id`. Nytt endepunkt + `.../individual-leaderboard/{tournament_participant_id}/rounds/ + {round_id}/holes`, speiler registration.py sitt offentlige + endepunkt (eget stiprefiks -- unngår kollisjon med det eksisterende + round_participant_id-nøkkede). + + **Frontend:** `StrokePlayLeaderboard` sin `LeaderboardRow`-type + ("eksakt kontrakt") UENDRET -- nytt valgfritt prop-sett på selve + komponenten i stedet (`expandedId`/`onToggleExpand`/ + `renderExpanded`), forelderen eier state/henting. Kun felt-rader + (ikke ledere) er utvidbare -- unngår risiko i den sticky + leder-geometrien. Nye `LeaderboardHoleNine`/`LeaderboardRoundHoles` + bruker filens egne netto-par-bevisste `classify`/`scoreMarkClasses` + (samme som HoleGrid), ikke den offentlige sidens enklere variant. + + **Verifisert:** 165/165 pytest (+1 ny), `tsc --noEmit` rent. + Full-stack scratch (egen db/API-container/MinIO, ekte innlogging, + ingen fetch-mocking) med en flerrunde-turnering (2 runder, én + spiller begge, én kun runde 1) -- riktig Ut/Inn-seksjon(er), riktig + Par-/Slag-sum, riktig golfscore-form, mørk og lys modus, smal + mobilskjerm. Alle scratch-ressurser ryddet opp. diff --git a/app/routers/individual_tournaments.py b/app/routers/individual_tournaments.py index 7ff4636..21e9987 100644 --- a/app/routers/individual_tournaments.py +++ b/app/routers/individual_tournaments.py @@ -1690,6 +1690,11 @@ class RoundCellOut(BaseModel): round_number: int label: str tone: str | None # "under" | "even" | "over" -- matches stroke-play-leaderboard.tsx sin RoundCell + # Hull-for-hull-utvidelse i leaderboardet (2026-08-22) -- lar frontend + # hente denne rundens hull direkte via det nye + # individual-leaderboard/{tp}/rounds/{round_id}/holes-endepunktet uten + # en ekstra oppslags-runde. + tournament_round_id: str class EclecticHoleCellOut(BaseModel): @@ -1904,6 +1909,70 @@ async def individual_leaderboard( return entries +@router.get( + # Eget stiprefiks (2026-08-22) -- IKKE + # .../rounds/{round_id}/participants/{x}/holes (samme stiform som den + # eksisterende round_participant_id-nøkkede list_round_participant_holes + # over, FastAPI kan ikke skille dem). Autentisert speilbilde av + # registration.py sitt offentlige, tournament_participant_id-nøkkede + # endepunkt -- bygget for leaderboardets hull-for-hull-utvidelse + # (ADR-101), som kun kjenner tournament_participant_id (leaderboard- + # radens egen nøkkel), ikke det interne tournament_round_participant_id. + "/orgs/{organization_id}/tournaments/{tournament_id}" + "/individual-leaderboard/{tournament_participant_id}/rounds/{round_id}/holes", + response_model=list[RoundHoleOut], +) +async def leaderboard_participant_round_holes( + tournament_id: str, + tournament_participant_id: str, + round_id: str, + organization_id: str = Depends(get_authorized_org), +) -> list[RoundHoleOut]: + async with org_connection(organization_id) as conn: + round_row = await conn.fetchrow( + "SELECT course_id::text AS course_id FROM tournament_round WHERE id = $1 AND tournament_id = $2", + round_id, + tournament_id, + ) + if round_row is None: + raise app_error(404, "NOT_FOUND", "Runden finnes ikke.") + participant_row = await conn.fetchrow( + "SELECT id::text AS id, playing_handicap FROM tournament_round_participant " + "WHERE tournament_round_id = $1 AND tournament_participant_id = $2", + round_id, + tournament_participant_id, + ) + if participant_row is None: + raise app_error(404, "NOT_FOUND", "Deltakeren har ikke spilt denne runden.") + holes = await conn.fetch( + """ + SELECT h.hole_number, h.par, h.stroke_index, trh.gross_strokes, + trh.putts, trh.club_off_tee, trh.tee_shot_result, trh.approach_result, + trh.chip_count, trh.bunker_shot_count, trh.penalty_strokes, + trh.first_putt_distance_bucket, trh.anyway_strokes, + COALESCE(trh.version, 1) AS version + FROM hole h + LEFT JOIN tournament_round_hole trh + ON trh.tournament_round_participant_id = $1 AND trh.hole_number = h.hole_number + WHERE h.course_id = $2 + ORDER BY h.hole_number + """, + participant_row["id"], + round_row["course_id"], + ) + allocation = ( + allocate_over_played_holes( + participant_row["playing_handicap"], [h["stroke_index"] for h in holes], [h["hole_number"] for h in holes] + ) + if participant_row["playing_handicap"] is not None + else [None] * len(holes) + ) + return [ + RoundHoleOut(strokes_received=a, **dict(h)) + for h, a in zip(holes, allocation) + ] + + class CutResult(BaseModel): cut_after_round: int cut_size: int @@ -2266,7 +2335,7 @@ async def _attach_stroke_play_columns( # er ferdigspilt). round_rows = await conn.fetch( """ - SELECT tr.sequence, + SELECT tr.sequence, tr.id::text AS tournament_round_id, trs.tournament_participant_id::text AS tournament_participant_id, trs.holes_played, trs.gross_total, trs.net_total, trs.stableford_points, COALESCE(pp.par_played, 0)::int AS par_played @@ -2327,7 +2396,12 @@ async def _attach_stroke_play_columns( points = row["stableford_points"] or 0 expected = 2 * row["holes_played"] rounds_out.append( - RoundCellOut(round_number=seq, label=f"{points} p", tone=_to_par_tone(points - expected)) + RoundCellOut( + round_number=seq, + label=f"{points} p", + tone=_to_par_tone(points - expected), + tournament_round_id=row["tournament_round_id"], + ) ) else: val = row[value_col] @@ -2338,6 +2412,7 @@ async def _attach_stroke_play_columns( round_number=seq, label=str(val), tone=_to_par_tone(val - row["par_played"]), + tournament_round_id=row["tournament_round_id"], ) ) e.rounds = rounds_out diff --git a/frontend/components/individual-tournament-detail.tsx b/frontend/components/individual-tournament-detail.tsx index 422496c..dfc3a7b 100644 --- a/frontend/components/individual-tournament-detail.tsx +++ b/frontend/components/individual-tournament-detail.tsx @@ -236,6 +236,8 @@ type ApiRoundCell = { round_number: number label: string tone: "under" | "even" | "over" | null + // Hull-for-hull-utvidelse i leaderboardet (ADR-101). + tournament_round_id: string } type ApiEclecticHoleCell = { @@ -4925,6 +4927,18 @@ function LeaderboardTab({ base, tournament }: { base: string; tournament: ApiTou // resten av appen, ikke alle utvidet samtidig). const [expandedEclecticId, setExpandedEclecticId] = useState(null) + // Hull-for-hull-utvidelse for slagspill/netto/stableford (ADR-101, + // 2026-08-22) -- portert fra public-individual-live.tsx sin + // LeaderboardRow, tilpasset den autentiserte flerrunde-visningen (ALLE + // spilte runder vises stablet ved utvidelse, ikke ett hull-sett per + // valgt runde -- StrokePlayLeaderboard viser allerede én kolonne per + // runde, ingen ekstra rundevelger trengs her). `expandedStrokeRowKey` + // nøkkeltes på `player_name`, samme identitet StrokePlayLeaderboard + // allerede bruker som sin egen React-key (raden bærer ingen egen id). + const [expandedStrokeRowKey, setExpandedStrokeRowKey] = useState(null) + const [holesByRound, setHolesByRound] = useState>({}) + const [loadingRoundKey, setLoadingRoundKey] = useState(null) + useEffect(() => { let cancelled = false async function load() { @@ -5045,6 +5059,62 @@ function LeaderboardTab({ base, tournament }: { base: string; tournament: ApiTou } } + // StrokePlayRow bærer ingen id (kun playerName, samme identitet + // komponenten selv bruker som React-key) -- dette oppslaget lar + // toggleStrokeExpand/renderStrokeExpanded finne tilbake til den ekte + // ApiLeaderboardEntry-en (med tournament_participant_id og rundenes + // tournament_round_id) for en gitt rad. + const entryByPlayerName = new Map(entries.map((e) => [e.player_name, e])) + + async function toggleStrokeExpand(row: StrokePlayRow) { + const next = expandedStrokeRowKey === row.playerName ? null : row.playerName + setExpandedStrokeRowKey(next) + if (next === null) return + const entry = entryByPlayerName.get(row.playerName) + if (!entry) return + for (const r of entry.rounds) { + const key = `${entry.tournament_participant_id}:${r.tournament_round_id}` + if (holesByRound[key] !== undefined) continue + setLoadingRoundKey(key) + const data = await getJson( + `${base}/individual-leaderboard/${entry.tournament_participant_id}/rounds/${r.tournament_round_id}/holes`, + ) + setHolesByRound((prev) => ({ ...prev, [key]: data ?? [] })) + setLoadingRoundKey(null) + } + } + + function renderStrokeExpanded(row: StrokePlayRow) { + const entry = entryByPlayerName.get(row.playerName) + if (!entry || entry.rounds.length === 0) { + return ( +

Ingen hull registrert ennå.

+ ) + } + return ( +
+ {entry.rounds.map((r) => { + const key = `${entry.tournament_participant_id}:${r.tournament_round_id}` + const holes = holesByRound[key] + return ( +
+ {entry.rounds.length > 1 && ( + + Runde {r.round_number} + + )} + {loadingRoundKey === key || holes === undefined ? ( +

Laster hull …

+ ) : ( + + )} +
+ ) + })} +
+ ) + } + const isEclectic = method !== null && ECLECTIC_SCORING_METHODS.has(method) function renderRow(entry: ApiLeaderboardEntry, rank: number) { @@ -5143,6 +5213,9 @@ function LeaderboardTab({ base, tournament }: { base: string; tournament: ApiTou ) : (
    @@ -5155,6 +5228,90 @@ function LeaderboardTab({ base, tournament }: { base: string; tournament: ApiTou ) } +// Hull-for-hull-utvidelse for slagspill/netto/stableford (ADR-101, +// 2026-08-22) -- Ut/Inn-tabeller portert fra public-individual-live.tsx +// sin HoleNine, men netto-par-bevisst (bruker filens EGNE classify/ +// scoreMarkClasses via strokes_received, samme som HoleGrid over) i +// stedet for den offentlige sidens enklere brutto-only inndeling -- +// konsistent med resten av den autentiserte scoreføringen/scorekortet. +function LeaderboardHoleNine({ holes, label }: { holes: ApiHole[]; label: string }) { + const parSum = holes.reduce((s, h) => s + h.par, 0) + const played = holes.filter((h) => h.gross_strokes !== null) + const strokeSum = played.length > 0 ? played.reduce((s, h) => s + (h.gross_strokes as number), 0) : null + + return ( +
    +
    +
    {label}
    + {holes.map((h) => ( +
    + {h.hole_number} +
    + ))} +
    Sum
    +
    +
    +
    Par
    + {holes.map((h) => ( +
    + {h.par} +
    + ))} +
    {parSum}
    +
    +
    +
    Slag
    + {holes.map((h) => { + const gross = h.gross_strokes + const net = gross !== null && h.strokes_received !== null && h.strokes_received !== undefined ? gross - h.strokes_received : gross + const diff = net !== null ? net - h.par : null + const kind = diff !== null ? classify(diff) : null + return gross !== null ? ( + + {gross} + + ) : ( + + ) + })} +
    {strokeSum ?? "–"}
    +
    +
    + ) +} + +function LeaderboardRoundHoles({ holes }: { holes: ApiHole[] }) { + if (holes.length === 0) { + return

    Ingen hull registrert for denne runden ennå.

    + } + const sorted = [...holes].sort((a, b) => a.hole_number - b.hole_number) + const front = sorted.filter((h) => h.hole_number <= 9) + const back = sorted.filter((h) => h.hole_number > 9) + return ( +
    + {front.length > 0 && } + {back.length > 0 && } +
    + ) +} + // Eclectic (ADR-067-tillegget "Del C") -- "per-hull beste-kilde"-visning: // hvilket hull, hva slaget/poenget ble, og HVILKEN runde det kom fra. // Beviser for spilleren at "drømmerunden" faktisk er satt sammen av det diff --git a/frontend/components/stroke-play-leaderboard.tsx b/frontend/components/stroke-play-leaderboard.tsx index 724ebb7..ce31532 100644 --- a/frontend/components/stroke-play-leaderboard.tsx +++ b/frontend/components/stroke-play-leaderboard.tsx @@ -1,7 +1,7 @@ "use client" import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react" -import { Clock, Flag, Pause, Play, Trophy } from "lucide-react" +import { ChevronDown, Clock, Flag, Pause, Play, Trophy } from "lucide-react" import { cn } from "@/lib/utils" // --- Types (exact contract — do not change) -------------------------------- @@ -119,9 +119,26 @@ const cellPad = "px-3 py-4 sm:px-4 xl:px-6 xl:py-6" export function StrokePlayLeaderboard({ rows = MOCK_ROWS, caption = "Resultattavle", + expandedId = null, + onToggleExpand, + renderExpanded, }: { rows?: LeaderboardRow[] caption?: string + // Hull-for-hull-utvidelse (2026-08-22, ADR-101) -- IKKE en del av + // LeaderboardRow-kontrakten over ("eksakt kontrakt -- ikke endre"), et + // helt separat, valgfritt prop-sett på selve komponenten. Forelderen + // (LeaderboardTab) eier state + henting, akkurat som + // TournamentScoringWizard/ScoreTab -- denne komponenten er ren + // presentasjon. `playerName` er nøkkelen (samme identitet komponenten + // allerede bruker som React-key), siden LeaderboardRow ikke bærer noen + // egen id. Kun FELT-rader (ikke ledere) er utvidbare -- en utvidet + // ledertrad ville krevd at leaderTops-beregningen over tok høyde for + // utvidelsens ekstra høyde i den STICKY posisjoneringen, en unødvendig + // risiko når lederen(e) uansett allerede er fremhevet og synlig øverst. + expandedId?: string | null + onToggleExpand?: (row: LeaderboardRow) => void + renderExpanded?: (row: LeaderboardRow) => React.ReactNode }) { const reducedMotion = usePrefersReducedMotion() const [playing, setPlaying] = useState(true) @@ -358,7 +375,15 @@ export function StrokePlayLeaderboard({ return field.map((row, i) => ( {i === firstCutIndex && } - + onToggleExpand(row) : undefined} + renderExpanded={renderExpanded} + totalCols={totalCols} + /> )) })()} @@ -481,10 +506,18 @@ function FieldRow({ row, roundNumbers, striped, + expanded, + onToggleExpand, + renderExpanded, + totalCols, }: { row: LeaderboardRow roundNumbers: number[] striped: boolean + expanded: boolean + onToggleExpand?: () => void + renderExpanded?: (row: LeaderboardRow) => React.ReactNode + totalCols: number }) { const status = row.totalLabel != null && isStatus(row.totalLabel) // Hasn't played a single hole yet anywhere in the tournament (backend @@ -495,48 +528,87 @@ function FieldRow({ const notStarted = !status && row.totalLabel == null const bg = striped ? "bg-clubhouse-field" : "bg-clubhouse-card" const border = "border-b border-clubhouse-border" + const canExpand = Boolean(onToggleExpand && renderExpanded) + const panelId = `stroke-play-holes-${row.playerName}` return ( - - - {row.position} - - - - {row.playerName} - - - - {status ? ( - <> - - - - - {row.totalLabel} + <> + { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault() + onToggleExpand?.() + } + }, + tabIndex: 0, + role: "button", + "aria-expanded": expanded, + "aria-controls": panelId, + } + : {})} + > + + {row.position} + + + + + {row.playerName} + {canExpand && ( + + + + {status ? ( + <> + + + + + {row.totalLabel} + + + {roundNumbers.map((n) => { + const cell = row.rounds.find((c) => c.roundNumber === n) + return + })} + + ) : ( + <> + + + {notStarted ? ( + + ) : ( + + )} + {roundNumbers.map((n) => { + const cell = row.rounds.find((c) => c.roundNumber === n) + return + })} + + )} + + {canExpand && expanded && ( + + + {renderExpanded?.(row)} - {roundNumbers.map((n) => { - const cell = row.rounds.find((c) => c.roundNumber === n) - return - })} - - ) : ( - <> - - - {notStarted ? ( - - ) : ( - - )} - {roundNumbers.map((n) => { - const cell = row.rounds.find((c) => c.roundNumber === n) - return - })} - + )} - + ) } diff --git a/tests/test_individual_leaderboard.py b/tests/test_individual_leaderboard.py index bbda3ee..156ffb0 100644 --- a/tests/test_individual_leaderboard.py +++ b/tests/test_individual_leaderboard.py @@ -6,10 +6,14 @@ har et oppgitt klokkeslett/starthull, vise det i stedet. Kaller den ekte router-funksjonen direkte (samme mønster som test_order_of_merit.py). """ +from fastapi import HTTPException +import pytest + from app.auth import CurrentUser from app.routers.individual_tournaments import ( HoleUpdate as TournamentHoleUpdate, individual_leaderboard, + leaderboard_participant_round_holes, update_hole as tournament_update_hole, ) @@ -119,3 +123,48 @@ async def test_all_not_started_has_no_leader(pool): assert entries[0].total_label is None assert entries[0].position is None assert entries[0].is_leader is False + + +async def test_leaderboard_round_id_and_holes_endpoint(pool): + """ADR-101: RoundCellOut.tournament_round_id skal peke til riktig runde, + og det nye tournament_participant_id-nøkkede hull-endepunktet (bygget for + leaderboardets hull-for-hull-utvidelse) skal returnere de samme hullene + som den eksisterende round_participant_id-nøkkede varianten -- kun + oppslagsveien er ny.""" + org_id = await create_org() + owner_id = await create_user() + await add_membership(org_id, owner_id, role="owner") + + tournament_id = await create_tournament(org_id, name="Hull-for-hull-turnering") + course_id = await create_course(org_id, name="Testbanen") + for n in range(1, 19): + await create_org_hole(org_id, course_id, hole_number=n, par=4, stroke_index=n) + tee_id = await create_tee(org_id, course_id) + async with app_db.org_connection(org_id) as conn: + await conn.execute( + "UPDATE tournament SET format_type = 'individual', scoring_method = 'stroke_gross' WHERE id = $1", + tournament_id, + ) + + round_id = await create_tournament_round(org_id, tournament_id, course_id, sequence=1) + player_id = await create_player(org_id, display_name="Spiller Ett") + tp_id = await create_tournament_participant(org_id, tournament_id, player_id) + rp_id = await create_tournament_round_participant(org_id, round_id, tp_id, tee_id) + await _score_all_holes(tournament_id, round_id, rp_id, org_id, owner_id, score=5) + + entries = await individual_leaderboard(tournament_id, organization_id=org_id) + entry = next(e for e in entries if e.tournament_participant_id == tp_id) + assert len(entry.rounds) == 1 + assert entry.rounds[0].tournament_round_id == round_id + + holes = await leaderboard_participant_round_holes( + tournament_id, tp_id, round_id, organization_id=org_id, + ) + assert len(holes) == 18 + assert all(h.gross_strokes == 5 for h in holes) + + with pytest.raises(HTTPException) as exc_info: + await leaderboard_participant_round_holes( + tournament_id, tp_id, "00000000-0000-0000-0000-000000000000", organization_id=org_id, + ) + assert exc_info.value.status_code == 404