Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
"use client"
|
|
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
// Hull-for-hull-registrering for en frittstående runde (ADR-033).
|
|
|
|
|
|
// Presentasjon fra V0, datalag skrevet om fra mock til ekte fetch/PATCH.
|
|
|
|
|
|
//
|
|
|
|
|
|
// VIKTIG kontraktsdetalj (bekreftet i scratch): hull-PATCH-endepunktet er
|
|
|
|
|
|
// IKKE et ekte delvis-PATCH -- det skriver ALLE felt ved hvert kall. Derfor
|
|
|
|
|
|
// slår updateStat() alltid sammen med gjeldende hull-data FØR den sender,
|
|
|
|
|
|
// aldri kun det ene feltet som ble endret.
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
|
|
|
|
|
|
import type React from "react"
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
import { useCallback, useEffect, useRef, useState } from "react"
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
import Link from "next/link"
|
2026-08-02 16:34:54 +02:00
|
|
|
|
import { useRouter, useSearchParams } from "next/navigation"
|
|
|
|
|
|
import { RoundPageShell } from "@/components/round-page-shell"
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
import {
|
2026-07-24 11:21:39 +02:00
|
|
|
|
ArrowDown,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
ArrowLeft,
|
2026-07-24 11:21:39 +02:00
|
|
|
|
ArrowRight,
|
|
|
|
|
|
ArrowUp,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
Check,
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
ChevronDown,
|
2026-07-24 11:21:39 +02:00
|
|
|
|
Clock,
|
|
|
|
|
|
Trash2,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
ChevronLeft,
|
|
|
|
|
|
ChevronRight,
|
|
|
|
|
|
Flag,
|
|
|
|
|
|
Minus,
|
|
|
|
|
|
Plus,
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
RefreshCw,
|
2026-07-24 11:21:39 +02:00
|
|
|
|
Search,
|
|
|
|
|
|
Settings2,
|
|
|
|
|
|
Target,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
Trophy,
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
Users,
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
WifiOff,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
X,
|
|
|
|
|
|
} from "lucide-react"
|
2026-07-26 15:48:07 +02:00
|
|
|
|
import { Badge } from "@/components/ui/badge"
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
import { Button } from "@/components/ui/button"
|
2026-07-26 15:48:07 +02:00
|
|
|
|
import { RoundLeaderboardMini } from "@/components/round-leaderboard"
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
import { Input } from "@/components/ui/input"
|
|
|
|
|
|
import { Label } from "@/components/ui/label"
|
|
|
|
|
|
import { cn } from "@/lib/utils"
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
import { enqueueWrite, flushQueue, queueCount } from "@/lib/offline-queue"
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
// --- Types -----------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
type Gender = "male" | "female" | "other"
|
|
|
|
|
|
type ApiGender = "m" | "f" | "x"
|
|
|
|
|
|
type TeeShot = "left" | "fairway" | "right"
|
|
|
|
|
|
type Approach = "left" | "short" | "hit" | "long" | "right"
|
2026-07-24 06:27:35 +02:00
|
|
|
|
type StatLevel = "strokes_only" | "strokes_and_putts" | "full"
|
|
|
|
|
|
type PuttBucket = "<1m" | "<2m" | "<3m" | "<5m" | "<8m" | "8m+"
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
|
|
|
|
|
|
type Player = {
|
|
|
|
|
|
id: string
|
2026-07-27 10:32:51 +02:00
|
|
|
|
// Alltid den faktiske display_name (2026-07-27: fjernet "Deg"-erstatning
|
|
|
|
|
|
// her -- se playerLabel() -- brøt "roster-kontekst → fullt navn" og ga en
|
|
|
|
|
|
// "Deg Deg"-duplikat der en egen Badge også viste "Deg" ved siden av).
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
name: string
|
2026-07-26 15:48:07 +02:00
|
|
|
|
userId: string | null
|
|
|
|
|
|
guestEmail: string | null
|
|
|
|
|
|
isOwner: boolean
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
gender: Gender
|
|
|
|
|
|
hcp: number | null
|
2026-07-26 15:48:07 +02:00
|
|
|
|
// Tildelte slag for RUNDEN (course handicap, ikke selve HCP-indeksen) --
|
|
|
|
|
|
// null når HCP ikke spores for spilleren (2026-07-26).
|
|
|
|
|
|
courseHandicap: number | null
|
2026-07-26 09:18:52 +02:00
|
|
|
|
teeName: string
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
isSelf: boolean
|
|
|
|
|
|
countsForHandicap: boolean
|
|
|
|
|
|
scoreDifferential: number | null
|
2026-07-24 06:27:35 +02:00
|
|
|
|
statLevel: StatLevel
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
// ADR-038 -- manuell opt-out fra faktisk HCP, uavhengig av
|
|
|
|
|
|
// countsForHandicap (den automatiske kvalifiseringen).
|
|
|
|
|
|
excludeFromHandicap: boolean
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
// ADR-039 -- hvilken side (match/fourball/foursome/greensome/scramble),
|
|
|
|
|
|
// og den format-relative match-play-enheten (ikke samme som courseHandicap).
|
|
|
|
|
|
roundSideId: string | null
|
|
|
|
|
|
playingHandicap: number | null
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
// Money Ball (2026-07-30) -- se ApiParticipant.lineup_order.
|
|
|
|
|
|
lineupOrder: number | null
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type HoleStat = {
|
|
|
|
|
|
played: boolean
|
|
|
|
|
|
strokes: number | null
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
// "Plukket opp" (migrasjon 038, Stableford) -- serveren regner selv ut
|
|
|
|
|
|
// og skriver Net Double Bogey som `strokes` når dette er sant; feltet
|
|
|
|
|
|
// her er KUN til visning ("Plukket opp" i stedet for tallet).
|
|
|
|
|
|
pickedUp: boolean
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
putts: number | null
|
|
|
|
|
|
club: string
|
|
|
|
|
|
teeShot: TeeShot | null
|
|
|
|
|
|
approach: Approach | null
|
|
|
|
|
|
chip: number
|
|
|
|
|
|
bunker: number
|
|
|
|
|
|
penalty: number
|
2026-07-24 06:27:35 +02:00
|
|
|
|
firstPuttBucket: PuttBucket | null
|
|
|
|
|
|
anywayStrokes: number | null
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type Hole = {
|
|
|
|
|
|
holeNumber: number
|
|
|
|
|
|
par: number
|
|
|
|
|
|
index: number
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function emptyStat(): HoleStat {
|
|
|
|
|
|
return {
|
|
|
|
|
|
played: false,
|
|
|
|
|
|
strokes: null,
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
pickedUp: false,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
putts: null,
|
|
|
|
|
|
club: "",
|
|
|
|
|
|
teeShot: null,
|
|
|
|
|
|
approach: null,
|
|
|
|
|
|
chip: 0,
|
|
|
|
|
|
bunker: 0,
|
|
|
|
|
|
penalty: 0,
|
2026-07-24 06:27:35 +02:00
|
|
|
|
firstPuttBucket: null,
|
|
|
|
|
|
anywayStrokes: null,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function statKey(playerId: string, holeNumber: number) {
|
|
|
|
|
|
return `${playerId}:${holeNumber}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-26 19:54:18 +02:00
|
|
|
|
// Til-par-formatering, golfvis (ekte minustegn) -- samme konvensjon som
|
|
|
|
|
|
// round-scorecard.tsx/round-leaderboard.tsx (2026-07-26, brukt for
|
|
|
|
|
|
// akkumulert score-så-langt per spiller i den nye scoringsflyten).
|
|
|
|
|
|
function formatToPar(value: number): string {
|
|
|
|
|
|
if (value === 0) return "E"
|
|
|
|
|
|
return value > 0 ? `+${value}` : `−${Math.abs(value)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function computeRunningToPar(holes: ApiHole[] | undefined): { toPar: number; holesPlayed: number } | null {
|
|
|
|
|
|
if (!holes) return null
|
|
|
|
|
|
const played = holes.filter((h) => h.played && h.score !== null)
|
|
|
|
|
|
if (played.length === 0) return null
|
|
|
|
|
|
const score = played.reduce((sum, h) => sum + (h.score as number), 0)
|
|
|
|
|
|
const par = played.reduce((sum, h) => sum + h.par, 0)
|
|
|
|
|
|
return { toPar: score - par, holesPlayed: played.length }
|
2026-07-26 15:48:07 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
function apiGenderToUi(g: ApiGender): Gender {
|
|
|
|
|
|
return g === "m" ? "male" : g === "f" ? "female" : "other"
|
|
|
|
|
|
}
|
|
|
|
|
|
function uiGenderToApi(g: Gender): ApiGender {
|
|
|
|
|
|
return g === "male" ? "m" : g === "female" ? "f" : "x"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- API-typer ---------------------------------------------------------------
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
|
|
|
|
|
|
type ApiParticipant = {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
user_id: string | null
|
|
|
|
|
|
guest_name: string | null
|
2026-07-26 15:48:07 +02:00
|
|
|
|
// Valgfritt kontaktfelt, KUN meningsfullt for en gjest (2026-07-26).
|
|
|
|
|
|
guest_email: string | null
|
2026-07-26 08:22:41 +02:00
|
|
|
|
// Alltid utfylt av API-et -- gjestens navn, eller en levende oppslått
|
|
|
|
|
|
// visningsnavn for en lenket bruker (eier eller medspiller).
|
|
|
|
|
|
display_name: string
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
is_owner: boolean
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
gender: ApiGender
|
2026-07-26 09:18:52 +02:00
|
|
|
|
// Per-deltaker utslagssted (2026-07-26) -- kan avvike fra en annen
|
|
|
|
|
|
// deltakers, redigerbart av eieren kun for DENNE runden.
|
|
|
|
|
|
tee_name_snapshot: string
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
handicap_index_snapshot: number | null
|
|
|
|
|
|
course_handicap_snapshot: number | null
|
|
|
|
|
|
counts_for_handicap: boolean
|
|
|
|
|
|
score_differential: number | null
|
2026-07-24 06:27:35 +02:00
|
|
|
|
stat_level: StatLevel
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
exclude_from_handicap: boolean
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
// ADR-039 -- hvilken side deltakeren er på (match/fourball/foursome/
|
|
|
|
|
|
// greensome/scramble), null for slagspill/skins (ingen sider).
|
|
|
|
|
|
round_side_id: string | null
|
|
|
|
|
|
// ADR-039 -- format-avledet, RELATIV match-play-enhet, ikke samme som
|
|
|
|
|
|
// course_handicap_snapshot (den absolutte WHS-verdien).
|
|
|
|
|
|
playing_handicap: number | null
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
// Money Ball (2026-07-30) -- fast plass i rotasjonsrekkefølgen (0-3),
|
|
|
|
|
|
// null inntil eieren tildeler den. KUN meningsfullt for play_format="money_ball".
|
|
|
|
|
|
lineup_order: number | null
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type ApiSide = {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
label: string | null
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-26 08:22:41 +02:00
|
|
|
|
type PersonMatch = {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
first_name: string
|
|
|
|
|
|
last_name: string
|
|
|
|
|
|
avatar_url: string | null
|
|
|
|
|
|
home_club: string | null
|
2026-07-26 09:18:52 +02:00
|
|
|
|
handicap_index: number | null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type ApiTeeOption = {
|
|
|
|
|
|
name: string
|
|
|
|
|
|
genders: ApiGender[]
|
2026-07-26 08:22:41 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
type ApiRound = {
|
|
|
|
|
|
id: string
|
2026-07-25 06:29:09 +02:00
|
|
|
|
name: string | null
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
course_name_snapshot: string
|
|
|
|
|
|
tee_name_snapshot: string
|
|
|
|
|
|
played_at: string
|
|
|
|
|
|
start_hole: number
|
|
|
|
|
|
holes_planned: number
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
play_format: string
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
skins_scoring: string | null
|
|
|
|
|
|
skins_tie_handling: string | null
|
2026-07-24 11:21:39 +02:00
|
|
|
|
started_at: string | null
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
completed_at: string | null
|
|
|
|
|
|
participants: ApiParticipant[]
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
sides: ApiSide[]
|
|
|
|
|
|
setup_complete: boolean
|
|
|
|
|
|
setup_message: string | null
|
2026-07-28 19:25:37 +02:00
|
|
|
|
// ADR-036 Beslutning B (fase 2, 2026-07-28) -- styrer KUN tredjeparts
|
|
|
|
|
|
// innsyn (venner/offentligheten), aldri en faktisk lagt-til medspiller.
|
|
|
|
|
|
visibility_mode: string
|
|
|
|
|
|
visible_categories: string[]
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
// Flere flighter (migrasjon 035) -- null for en vanlig enkeltflight-runde.
|
|
|
|
|
|
flight_group_id: string | null
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
// Samme faste sett/rekkefølge som friends.py sin Category og friends.tsx
|
|
|
|
|
|
// sin CATEGORY_OPTIONS -- egen, lokal kopi (samme "liten duplisering
|
|
|
|
|
|
// fremfor cross-file-import"-mønster som FORMAT_LABELS ellers i appen).
|
|
|
|
|
|
const VISIBILITY_CATEGORY_OPTIONS = [
|
|
|
|
|
|
{ code: "spouse", label: "Make" },
|
|
|
|
|
|
{ code: "close_family", label: "Nær familie" },
|
|
|
|
|
|
{ code: "extended_family", label: "Storfamilie" },
|
|
|
|
|
|
{ code: "close_friends", label: "Nære venner" },
|
|
|
|
|
|
{ code: "golf_friends", label: "Golfvenner" },
|
|
|
|
|
|
{ code: "colleagues", label: "Kollegaer" },
|
|
|
|
|
|
{ code: "business", label: "Forretningsforbindelser" },
|
|
|
|
|
|
{ code: "classmates", label: "Studiekamerater" },
|
|
|
|
|
|
{ code: "acquaintances", label: "Perifere bekjente" },
|
|
|
|
|
|
{ code: "other", label: "Ymse" },
|
|
|
|
|
|
] as const
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
// ADR-039 -- speiler backend sine konstanter i app/routers/rounds.py
|
|
|
|
|
|
// (_TWO_SIDED_FORMATS/_SIDE_PLAYER_COUNT/SIDE_IS_UNIT) klientside, kun for
|
|
|
|
|
|
// å vite hvilken oppsett-UI som skal vises. Selve håndhevelsen skjer alltid
|
|
|
|
|
|
// server-side -- disse styrer bare presentasjon.
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
// Åtte nye formater (2026-07-30). Chapman er to-sidet+delt-ball (som
|
|
|
|
|
|
// foursome). High-low-high er to-sidet+individuell-ball (som fourball).
|
|
|
|
|
|
// Copenhagen/BBB/Flag/Shamble/Money Ball er ALDRI to-sidet (flatt felt,
|
|
|
|
|
|
// se ADR/plan) -- faller derfor gjennom til vanlig individuell-ball
|
|
|
|
|
|
// scorekort/veiviser (samme som stroke/stableford/skins), med egne
|
|
|
|
|
|
// tilleggsvisninger i FormatResultPanel.
|
|
|
|
|
|
const TWO_SIDED_FORMATS = new Set([
|
|
|
|
|
|
"match", "fourball", "foursome", "greensome", "scramble_2", "scramble_4", "chapman", "high_low_high",
|
|
|
|
|
|
])
|
|
|
|
|
|
const SHARED_BALL_FORMATS = new Set(["foursome", "greensome", "scramble_2", "scramble_4", "chapman"])
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
const SIDE_PLAYER_COUNT: Record<string, number> = {
|
|
|
|
|
|
match: 1,
|
|
|
|
|
|
fourball: 2,
|
|
|
|
|
|
foursome: 2,
|
|
|
|
|
|
greensome: 2,
|
|
|
|
|
|
scramble_2: 2,
|
|
|
|
|
|
scramble_4: 4,
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
chapman: 2,
|
|
|
|
|
|
high_low_high: 2,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
const FORMAT_LABELS: Record<string, string> = {
|
|
|
|
|
|
stroke: "Slagspill",
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
stableford: "Stableford",
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
match: "Match",
|
|
|
|
|
|
skins: "Skins",
|
|
|
|
|
|
fourball: "Fourball",
|
|
|
|
|
|
foursome: "Foursome",
|
|
|
|
|
|
greensome: "Greensome",
|
|
|
|
|
|
scramble_2: "Scramble (2)",
|
|
|
|
|
|
scramble_4: "Scramble (4)",
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
chapman: "Chapman",
|
|
|
|
|
|
copenhagen: "Københavner",
|
|
|
|
|
|
bbb: "Bingo Bango Bongo",
|
|
|
|
|
|
flag: "Flaggturnering",
|
|
|
|
|
|
shamble: "Shamble",
|
|
|
|
|
|
money_ball: "Money Ball",
|
|
|
|
|
|
high_low_high: "High-low-high",
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
// Flatt felt, egne tilleggsvisninger i FormatResultPanel -- ingen sider,
|
|
|
|
|
|
// bruker vanlig individuell-ball scorekort/veiviser for selve registreringen.
|
|
|
|
|
|
const FLAT_FIELD_FORMATS = new Set(["copenhagen", "bbb", "flag", "shamble", "money_ball"])
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
|
2026-07-24 11:21:39 +02:00
|
|
|
|
// Utslagstid vises i klokkeslett-format, og tidsbruk beregnes som
|
|
|
|
|
|
// completed_at - started_at (begge ekte tidsstempler) -- ingen egen
|
|
|
|
|
|
// lagret varighet, kun utledet ved visning.
|
|
|
|
|
|
function formatDuration(startedAt: string, completedAt: string): string | null {
|
|
|
|
|
|
const start = new Date(startedAt).getTime()
|
|
|
|
|
|
const end = new Date(completedAt).getTime()
|
|
|
|
|
|
if (Number.isNaN(start) || Number.isNaN(end) || end <= start) return null
|
|
|
|
|
|
const totalMinutes = Math.round((end - start) / 60000)
|
|
|
|
|
|
const hours = Math.floor(totalMinutes / 60)
|
|
|
|
|
|
const minutes = totalMinutes % 60
|
|
|
|
|
|
if (hours === 0) return `${minutes} min`
|
|
|
|
|
|
return `${hours}t ${minutes}min`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
type ApiHole = {
|
|
|
|
|
|
hole_number: number
|
|
|
|
|
|
par: number
|
|
|
|
|
|
stroke_index: number
|
|
|
|
|
|
played: boolean
|
|
|
|
|
|
score: number | null
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
picked_up: boolean
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
putts: number | null
|
|
|
|
|
|
club_off_tee: string | null
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
tee_shot_result: TeeShot | null
|
|
|
|
|
|
approach_result: Approach | null
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
chip_count: number | null
|
|
|
|
|
|
bunker_shot_count: number | null
|
|
|
|
|
|
penalty_strokes: number | null
|
2026-07-24 06:27:35 +02:00
|
|
|
|
first_putt_distance_bucket: PuttBucket | null
|
|
|
|
|
|
anyway_strokes: number | null
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
strokes_received: number | null
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
// ADR-039 Beslutning C -- delt-ball-formater (foursome/greensome/scramble)
|
|
|
|
|
|
// har ÉN score PER SIDE per hull, ikke per spiller -- ingen av de andre
|
|
|
|
|
|
// detalj-feltene gir entydig mening for en delt ball.
|
|
|
|
|
|
type ApiSideHole = {
|
|
|
|
|
|
hole_number: number
|
|
|
|
|
|
par: number
|
|
|
|
|
|
stroke_index: number
|
|
|
|
|
|
played: boolean
|
|
|
|
|
|
score: number | null
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
// Scramble/greensome: hvilken av sidens partnere sitt utslag ble valgt
|
|
|
|
|
|
// (migrasjon 036) -- helt valgfritt, null = ikke registrert.
|
|
|
|
|
|
selected_participant_id: string | null
|
2026-07-29 15:46:49 +02:00
|
|
|
|
// Sidens kombinerte playing_handicap sin allokering for DETTE hullet --
|
|
|
|
|
|
// fantes allerede i API-svaret (RoundSideHoleOut), bare aldri lest her
|
|
|
|
|
|
// før nå (2026-07-29, "vis slag mottatt før hullet er fylt ut").
|
|
|
|
|
|
strokes_received: number | null
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
function apiHoleToStat(h: ApiHole): HoleStat {
|
|
|
|
|
|
return {
|
|
|
|
|
|
played: h.played,
|
|
|
|
|
|
strokes: h.score,
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
pickedUp: h.picked_up,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
putts: h.putts,
|
|
|
|
|
|
club: h.club_off_tee ?? "",
|
|
|
|
|
|
teeShot: h.tee_shot_result,
|
|
|
|
|
|
approach: h.approach_result,
|
|
|
|
|
|
chip: h.chip_count ?? 0,
|
|
|
|
|
|
bunker: h.bunker_shot_count ?? 0,
|
|
|
|
|
|
penalty: h.penalty_strokes ?? 0,
|
2026-07-24 06:27:35 +02:00
|
|
|
|
firstPuttBucket: h.first_putt_distance_bucket,
|
|
|
|
|
|
anywayStrokes: h.anyway_strokes,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
}
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
function statToPatchBody(s: HoleStat) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
played: s.played,
|
|
|
|
|
|
score: s.strokes,
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
picked_up: s.pickedUp,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
putts: s.putts,
|
|
|
|
|
|
club_off_tee: s.club.trim() === "" ? null : s.club,
|
|
|
|
|
|
tee_shot_result: s.teeShot,
|
|
|
|
|
|
approach_result: s.approach,
|
|
|
|
|
|
chip_count: s.chip,
|
|
|
|
|
|
bunker_shot_count: s.bunker,
|
|
|
|
|
|
penalty_strokes: s.penalty,
|
2026-07-24 06:27:35 +02:00
|
|
|
|
first_putt_distance_bucket: s.firstPuttBucket,
|
|
|
|
|
|
anyway_strokes: s.anywayStrokes,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-27 10:32:51 +02:00
|
|
|
|
// Alltid det faktiske visningsnavnet -- ALDRI "Deg" (2026-07-27, compliance-
|
|
|
|
|
|
// pass mot teecup-scorekort-og-entry-spec.md). "Deg" var tidligere en
|
|
|
|
|
|
// viewer-relativ erstatning for selve navnet, men det produserte en ekte
|
|
|
|
|
|
// "Deg Deg"-duplikat der navnet OGSÅ sto ved siden av en egen "Deg"-badge
|
|
|
|
|
|
// (spillerkort, scoringslisten) -- og brøt spesifikasjonens eksplisitte
|
|
|
|
|
|
// "roster-kontekst → fullt navn"-regel i skjermer uten noen badge i det
|
|
|
|
|
|
// hele tatt (veiviserens header/kontekst-rad, "Neste: {navn}", fullført-
|
|
|
|
|
|
// banneret). `isSelf` (se players-konstruksjonen under) er fortsatt
|
|
|
|
|
|
// viewer-relativ og brukes alene til å vise "Deg"-badgen der det trengs.
|
|
|
|
|
|
function playerLabel(p: ApiParticipant): string {
|
|
|
|
|
|
return p.display_name
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
// --- Component -------------------------------------------------------------
|
|
|
|
|
|
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
export function RoundDetail({ roundId }: { roundId: string }) {
|
2026-07-24 11:21:39 +02:00
|
|
|
|
const router = useRouter()
|
2026-08-02 16:34:54 +02:00
|
|
|
|
// Delt header (ADR-040) sin "Administrer"-dialog lenker hit med
|
|
|
|
|
|
// ?tab=manage for å nå "Spillere og runde" fra Scorekort-/Leaderboard-
|
|
|
|
|
|
// siden -- lest KUN ved mount (bevisst ikke reaktiv på senere endringer,
|
|
|
|
|
|
// matcher den eksisterende lokale pill-vekslingen som fortsatt fungerer
|
|
|
|
|
|
// uendret for navigering INNAD på siden).
|
|
|
|
|
|
const searchParams = useSearchParams()
|
|
|
|
|
|
const initialTab = searchParams.get("tab") === "manage" ? "manage" : "score"
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
const [round, setRound] = useState<ApiRound | null>(null)
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
const [activePlayerId, setActivePlayerId] = useState<string | null>(null)
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
const [holesByParticipant, setHolesByParticipant] = useState<Record<string, ApiHole[]>>({})
|
2026-07-24 06:27:35 +02:00
|
|
|
|
// Eierens egen kølle-bag (personlig profil) -- brukt til å tilby et
|
|
|
|
|
|
// knapp-utvalg for "Kølle brukt ved utslaget" i stedet for fritekst, kun
|
|
|
|
|
|
// for eieren selv (gjester har ingen profil å hente dette fra).
|
|
|
|
|
|
const [ownBagClubs, setOwnBagClubs] = useState<string[]>([])
|
2026-07-26 08:22:41 +02:00
|
|
|
|
// Den innloggede brukerens egen id (ADR-036 fase 3-utvidelsen,
|
|
|
|
|
|
// 2026-07-26) -- runden kan nå åpnes av en lenket medspiller, ikke bare
|
|
|
|
|
|
// eieren, så "hvem er 'Deg'" og "hvem har lov til å forvalte runden"
|
|
|
|
|
|
// avhenger av DENNE, ikke av `is_owner` alene.
|
|
|
|
|
|
const [viewerId, setViewerId] = useState<string | null>(null)
|
2026-07-24 06:27:35 +02:00
|
|
|
|
// `null` betyr "ikke satt ennå" -- MÅ være null, ikke f.eks. 1, siden 1
|
|
|
|
|
|
// er en gyldig, truthy hullverdi og ville gjort `prev || start_hole`
|
|
|
|
|
|
// lenger ned til en no-op (funnet 2026-07-24: runden åpnet alltid på
|
|
|
|
|
|
// hull 1 uansett faktisk starthull).
|
|
|
|
|
|
const [currentHole, setCurrentHole] = useState<number | null>(null)
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
// ADR-039 -- bumpes hver gang noe som kan ha endret matchstatus/skins-
|
|
|
|
|
|
// tavlen skjer (hull-registrering, websocket-signal), slik at
|
2026-07-29 15:46:49 +02:00
|
|
|
|
// formatResult-hentingen under vet den skal hente på nytt.
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
const [formatResultRefreshTick, setFormatResultRefreshTick] = useState(0)
|
2026-07-29 15:46:49 +02:00
|
|
|
|
// Løftet OPP hit fra FormatResultPanel (2026-07-29) -- ScorecardGrid/
|
|
|
|
|
|
// SideScorecardGrid trenger NÅ den samme per-hull-oppløsningen (hvem vant
|
|
|
|
|
|
// hvilket hull) for å farge scorecellene og vise en løpende Stilling-rad,
|
|
|
|
|
|
// ikke bare selve statusbanneret. Én henting, delt av begge.
|
|
|
|
|
|
const [formatResult, setFormatResult] = useState<ApiFormatResult | null>(null)
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
const [showAddGuest, setShowAddGuest] = useState(false)
|
Ja, den gir mye mening — og jeg sjekket to av påstandene direkte mot koden i stedet for å ta dem på tro:
Bekreftet, reelt: "Deg Deg"-duplikaten er ekte. playerLabel() erstatter selve NAVNET med teksten "Deg" for din egen rad, OG kortet legger på en egen "Deg"-badge ved siden av — du ser den bokstavelig to ganger, akkurat som i skjermbildet du delte tidligere. Samme sted i koden (spillerkortet OG den nye scoringslisten).
Også bekreftet: score-sirkelen i den nye listen er rounded-full + grønn UANSETT om resultatet er under, over eller på par — den følger ikke golfscore-språket i det hele tatt (sirkel skal bety under par, firkant over par). Det forklarer noe jeg ikke helt klarte å sette fingeren på selv: fargen grønn er kraftig overbrukt — aktiv hull-fane, valgt tall, denne sirkelen, "Deg"-badgen, rangeringsmerket for lederen, primærknapper — alt er grønt, uansett om det faktisk betyr "aktiv/positivt" eller bare er standard-stilen. Det er en skarpere diagnose enn noe jeg selv har formulert denne økten, og den forklarer trolig mye av "fortsatt rotete"-følelsen din utover selve tettheten.
Resten (tetthetsprinsippet, "verktøy ikke nettside", scorekort-som-grid) er en sammenhengende, godt begrunnet retning — men det er en STØRRE strukturell endring (bytte fra dagens "faner + kompakt liste" til et fullt scorekort-grid som primærvisning, med veiviseren åpnet fra en hvilken som helst celle).
Jeg foreslår å dele dette i to:
Compliance-passet (seksjon 3) nå — konkrete, allerede bekreftede feil, lav risiko, rask gevinst.
Grid-redesignet som egen, større runde — fortjener et bevisst ja fra deg først, siden det er en helt annen informasjonsarkitektur enn det jeg bygde i går.
2026-07-27 09:03:15 +02:00
|
|
|
|
// Administrasjon (leaderboard-forhåndsvisning/Rediger/Fullfør/Slett/
|
|
|
|
|
|
// spillerliste) og selve scoringen er nå to ADSKILTE faner (2026-07-26,
|
|
|
|
|
|
// etterspurt av bruker etter å ha sammenlignet direkte med en
|
|
|
|
|
|
// konkurrentapp -- forrige runde flyttet kun ScoreSoFar, men lot alt det
|
|
|
|
|
|
// ANDRE administrative innholdet stå igjen øverst på samme side som
|
|
|
|
|
|
// scoringen, som fortsatt gjorde siden "bråkete" før man i det hele tatt
|
|
|
|
|
|
// når frem til oppgaven). "Score" er default -- den ENESTE fanen de
|
|
|
|
|
|
// fleste trenger under selve spillingen.
|
2026-08-02 16:34:54 +02:00
|
|
|
|
const [pageTab, setPageTab] = useState<"score" | "manage">(initialTab)
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
const [completing, setCompleting] = useState(false)
|
2026-07-24 11:21:39 +02:00
|
|
|
|
const [deleting, setDeleting] = useState(false)
|
|
|
|
|
|
const [showEditRound, setShowEditRound] = useState(false)
|
2026-07-26 15:48:07 +02:00
|
|
|
|
// Hvilken spillers "Rediger"-panel som er åpent, om noen (2026-07-26 --
|
|
|
|
|
|
// panelet ligger nå INNI hvert spillerkort i den vertikale listen, kan
|
|
|
|
|
|
// derfor gjelde en ANNEN rad enn den aktive/valgte, ulikt tidligere).
|
|
|
|
|
|
const [editingPlayerId, setEditingPlayerId] = useState<string | null>(null)
|
2026-07-26 19:54:18 +02:00
|
|
|
|
// Hvilken spillers scoringsveiviser (fullskjerm) som er åpen for AKTIVT
|
|
|
|
|
|
// HULL, om noen (2026-07-26 -- erstatter den tidligere "vis alt inline for
|
|
|
|
|
|
// aktiv spiller"-flyten, inspirert av en konkurrentapps samlebåndsflyt).
|
|
|
|
|
|
const [wizardPlayerId, setWizardPlayerId] = useState<string | null>(null)
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
// ADR-039 -- delt-ball-formater (foursome/greensome/scramble) scorer PER
|
|
|
|
|
|
// SIDE, ikke per spiller -- egen, enklere veiviser-tilstand for dette.
|
|
|
|
|
|
const [wizardSideId, setWizardSideId] = useState<string | null>(null)
|
|
|
|
|
|
const [holesBySide, setHolesBySide] = useState<Record<string, ApiSideHole[]>>({})
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
|
|
|
|
|
|
// --- Offline scoreregistrering (ADR-028-utvidelse, 2026-07-28) ------------
|
|
|
|
|
|
// Samme kø/synk-mønster som turnering-scorekortet (session-scorecard.tsx)
|
|
|
|
|
|
// -- portert hit siden frittstående runder er der man oftest står alene
|
|
|
|
|
|
// ute på banen uten dekning, og ADR-028s kø tidligere kun var koblet til
|
|
|
|
|
|
// turnering-scorekortet. Enklere her: PATCH-endepunktene erstatter HELE
|
|
|
|
|
|
// hull-raden per kall (ikke en append-historie), så en køet skriving kan
|
|
|
|
|
|
// speiles direkte inn i holesByParticipant/holesBySide -- ingen egen
|
|
|
|
|
|
// verdi-overlay nødvendig, kun et sett med nøkler for selve "lagret
|
|
|
|
|
|
// lokalt"-indikatoren i veiviseren.
|
|
|
|
|
|
const [isOnline, setIsOnline] = useState(true)
|
|
|
|
|
|
const [pendingCount, setPendingCount] = useState(0)
|
|
|
|
|
|
const [syncing, setSyncing] = useState(false)
|
|
|
|
|
|
const [pendingParticipantHoles, setPendingParticipantHoles] = useState<Set<string>>(new Set())
|
|
|
|
|
|
const [pendingSideHoles, setPendingSideHoles] = useState<Set<string>>(new Set())
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
// Hull-panelet gjenbrukes ved bytte av hull -- "Forrige"/"Neste hull" ligger
|
|
|
|
|
|
// NEDERST i panelet, så uten dette ville brukeren blitt stående scrollet
|
|
|
|
|
|
// helt ned (der knappene er) mens det NYE hullets Slag-felt (øverst i
|
|
|
|
|
|
// panelet) er utenfor skjermen (rapportert av bruker 2026-07-25).
|
|
|
|
|
|
const holePanelRef = useRef<HTMLElement>(null)
|
|
|
|
|
|
function scrollToHolePanel() {
|
|
|
|
|
|
holePanelRef.current?.scrollIntoView({ behavior: "smooth", block: "start" })
|
|
|
|
|
|
}
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
|
|
|
|
|
|
const loadRound = useCallback(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(`/rounds/${roundId}`, { credentials: "include" })
|
|
|
|
|
|
if (res.status === 403 || res.status === 404) {
|
|
|
|
|
|
setError("Denne runden finnes ikke, eller du har ikke tilgang til den.")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!res.ok) throw new Error(`round: ${res.status}`)
|
|
|
|
|
|
const data: ApiRound = await res.json()
|
|
|
|
|
|
setRound(data)
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
setActivePlayerId((prev) => prev ?? data.participants.find((p) => p.is_owner)?.id ?? data.participants[0]?.id ?? null)
|
2026-07-24 06:27:35 +02:00
|
|
|
|
setCurrentHole((prev) => prev ?? data.start_hole)
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
// ADR-039 -- sider/deltaker-tildeling kan ha endret seg (SidesPanel
|
|
|
|
|
|
// kaller denne som onSidesChanged), som igjen kan gjøre FormatResultPanel
|
|
|
|
|
|
// sitt tidligere "ikke klart"-svar utdatert. Bump ved HVER runde-refetch
|
|
|
|
|
|
// (billig, og runde-refetch skjer uansett ikke ofte nok til at det er en
|
|
|
|
|
|
// reell kostnad) i stedet for å spore hver enkelt kallsted som kan
|
|
|
|
|
|
// påvirke handicap-beregningen.
|
|
|
|
|
|
setFormatResultRefreshTick((n) => n + 1)
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
} catch {
|
|
|
|
|
|
setError("Klarte ikke å hente runden. Prøv igjen om litt.")
|
|
|
|
|
|
}
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
}, [roundId])
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
void loadRound()
|
|
|
|
|
|
}, [loadRound])
|
|
|
|
|
|
|
2026-07-29 15:46:49 +02:00
|
|
|
|
// Løpende matchstatus/skins-tavle (ADR-039) -- avledet ved LESING (aldri
|
|
|
|
|
|
// lagret), delt av FormatResultPanel OG ScorecardGrid/SideScorecardGrid
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
// (per-hull-fargelegging + Stilling-rad, 2026-07-29). Slagspill/Stableford
|
|
|
|
|
|
// (migrasjon 038) trenger ingen egen henting -- ingen sider/matchstatus.
|
2026-07-29 15:46:49 +02:00
|
|
|
|
useEffect(() => {
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
if (!round || round.play_format === "stroke" || round.play_format === "stableford") {
|
2026-07-29 15:46:49 +02:00
|
|
|
|
setFormatResult(null)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
fetch(`/rounds/${roundId}/format-result`, { credentials: "include" })
|
|
|
|
|
|
.then((res) => (res.ok ? res.json() : null))
|
|
|
|
|
|
.then((data: ApiFormatResult | null) => {
|
|
|
|
|
|
if (!cancelled) setFormatResult(data)
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {})
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [roundId, round?.play_format, formatResultRefreshTick])
|
|
|
|
|
|
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
// Bingo Bango Bongo (2026-07-30) -- rå bingo/bango/bongo-VALG per hull
|
|
|
|
|
|
// (ikke poengene, som ligger i formatResult over). Egen henting siden
|
|
|
|
|
|
// dette er en separat tabell (round_bbb_hole), ikke del av
|
|
|
|
|
|
// RoundFormatResultOut sin generiske form.
|
|
|
|
|
|
const [bbbHoles, setBbbHoles] = useState<Record<number, ApiBBBHole>>({})
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!round || round.play_format !== "bbb") {
|
|
|
|
|
|
setBbbHoles({})
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
fetch(`/rounds/${roundId}/bbb`, { credentials: "include" })
|
|
|
|
|
|
.then((res) => (res.ok ? res.json() : []))
|
|
|
|
|
|
.then((data: ApiBBBHole[]) => {
|
|
|
|
|
|
if (!cancelled) setBbbHoles(Object.fromEntries(data.map((h) => [h.hole_number, h])))
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {})
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [roundId, round?.play_format, formatResultRefreshTick])
|
|
|
|
|
|
|
|
|
|
|
|
async function patchBbbHole(holeNumber: number, body: Record<string, string | null>) {
|
|
|
|
|
|
const res = await fetch(`/rounds/${roundId}/bbb/${holeNumber}`, {
|
|
|
|
|
|
method: "PATCH",
|
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
|
credentials: "include",
|
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!res.ok) return
|
|
|
|
|
|
const updated: ApiBBBHole = await res.json()
|
|
|
|
|
|
setBbbHoles((prev) => ({ ...prev, [updated.hole_number]: updated }))
|
|
|
|
|
|
setFormatResultRefreshTick((n) => n + 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 06:27:35 +02:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
fetch("/auth/me", { credentials: "include" })
|
|
|
|
|
|
.then((res) => (res.ok ? res.json() : null))
|
2026-07-26 08:22:41 +02:00
|
|
|
|
.then((data: { id: string; bag_clubs: string[] } | null) => {
|
|
|
|
|
|
if (!cancelled && data) {
|
|
|
|
|
|
setOwnBagClubs(data.bag_clubs)
|
|
|
|
|
|
setViewerId(data.id)
|
|
|
|
|
|
}
|
2026-07-24 06:27:35 +02:00
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {})
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
const loadHoles = useCallback(
|
|
|
|
|
|
async (participantId: string) => {
|
|
|
|
|
|
const res = await fetch(`/rounds/${roundId}/participants/${participantId}/holes`, { credentials: "include" })
|
|
|
|
|
|
if (!res.ok) return
|
|
|
|
|
|
const data: ApiHole[] = await res.json()
|
|
|
|
|
|
setHolesByParticipant((prev) => ({ ...prev, [participantId]: data }))
|
|
|
|
|
|
},
|
|
|
|
|
|
[roundId],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-26 19:54:18 +02:00
|
|
|
|
// Laster hull for ALLE deltakere (2026-07-26, ikke bare aktiv spiller) --
|
|
|
|
|
|
// den nye scoringslisten viser akkumulert score-så-langt for HVER spiller
|
|
|
|
|
|
// samtidig, ikke kun den ene som tilfeldigvis er "aktiv".
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
useEffect(() => {
|
2026-07-26 19:54:18 +02:00
|
|
|
|
if (!round) return
|
|
|
|
|
|
for (const p of round.participants) {
|
|
|
|
|
|
if (!holesByParticipant[p.id]) {
|
|
|
|
|
|
void loadHoles(p.id)
|
|
|
|
|
|
}
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}
|
2026-07-26 19:54:18 +02:00
|
|
|
|
}, [round, holesByParticipant, loadHoles])
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
// ADR-039 -- delt-ball-formater (foursome/greensome/scramble) scorer per
|
|
|
|
|
|
// SIDE, ikke per deltaker (se holesBySide over).
|
|
|
|
|
|
const loadSideHoles = useCallback(
|
|
|
|
|
|
async (sideId: string) => {
|
|
|
|
|
|
const res = await fetch(`/rounds/${roundId}/sides/${sideId}/holes`, { credentials: "include" })
|
|
|
|
|
|
if (!res.ok) return
|
|
|
|
|
|
const data: ApiSideHole[] = await res.json()
|
|
|
|
|
|
setHolesBySide((prev) => ({ ...prev, [sideId]: data }))
|
|
|
|
|
|
},
|
|
|
|
|
|
[roundId],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!round) return
|
|
|
|
|
|
for (const s of round.sides) {
|
|
|
|
|
|
if (!holesBySide[s.id]) {
|
|
|
|
|
|
void loadSideHoles(s.id)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [round, holesBySide, loadSideHoles])
|
|
|
|
|
|
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
// --- Offline scoreregistrering, forts. -------------------------------
|
|
|
|
|
|
// Køer en PATCH i IndexedDB og speiler den samme kroppen optimistisk inn
|
|
|
|
|
|
// i lokal state -- body sine felt matcher ApiHole/ApiSideHole sine
|
|
|
|
|
|
// navn 1:1 (se statToPatchBody), så en enkel spread er nok, ingen egen
|
|
|
|
|
|
// oversettelse. Serveren får siste ord når synk faktisk skjer (bl.a.
|
|
|
|
|
|
// strokes_received kan derfor være utdatert helt til da).
|
|
|
|
|
|
async function queueParticipantHoleWrite(
|
|
|
|
|
|
participantId: string,
|
|
|
|
|
|
holeNumber: number,
|
|
|
|
|
|
body: ReturnType<typeof statToPatchBody>,
|
|
|
|
|
|
) {
|
|
|
|
|
|
const url = `/rounds/${roundId}/participants/${participantId}/holes/${holeNumber}`
|
|
|
|
|
|
await enqueueWrite({ url, method: "PATCH", body, matchId: roundId })
|
|
|
|
|
|
setPendingParticipantHoles((prev) => new Set(prev).add(`${participantId}:${holeNumber}`))
|
|
|
|
|
|
setPendingCount((c) => c + 1)
|
|
|
|
|
|
setHolesByParticipant((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[participantId]: (prev[participantId] ?? []).map((h) => (h.hole_number === holeNumber ? { ...h, ...body } : h)),
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
async function queueSideHoleWrite(
|
|
|
|
|
|
sideId: string,
|
|
|
|
|
|
holeNumber: number,
|
|
|
|
|
|
body: { played: boolean; score: number | null; selected_participant_id: string | null },
|
|
|
|
|
|
) {
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
const url = `/rounds/${roundId}/sides/${sideId}/holes/${holeNumber}`
|
|
|
|
|
|
await enqueueWrite({ url, method: "PATCH", body, matchId: roundId })
|
|
|
|
|
|
setPendingSideHoles((prev) => new Set(prev).add(`${sideId}:${holeNumber}`))
|
|
|
|
|
|
setPendingCount((c) => c + 1)
|
|
|
|
|
|
setHolesBySide((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[sideId]: (prev[sideId] ?? []).map((h) => (h.hole_number === holeNumber ? { ...h, ...body } : h)),
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Sender køede skrivinger og henter deltakerens/sidens hull på nytt fra
|
|
|
|
|
|
// serveren for hver som faktisk fikk noe synkronisert -- reconciles bl.a.
|
|
|
|
|
|
// strokes_received, som den optimistiske speilingen over ikke kan regne
|
|
|
|
|
|
// ut selv. Matcher URL-mønsteret for å vite hvilken type/id det gjaldt
|
|
|
|
|
|
// (samme kø brukes til begge endepunktstypene).
|
|
|
|
|
|
async function flushPending() {
|
|
|
|
|
|
if (syncing) return
|
|
|
|
|
|
setSyncing(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const outcomes = await flushQueue(roundId)
|
|
|
|
|
|
if (outcomes.length === 0) return
|
|
|
|
|
|
const syncedParticipants = new Set<string>()
|
|
|
|
|
|
const syncedSides = new Set<string>()
|
|
|
|
|
|
for (const o of outcomes) {
|
|
|
|
|
|
const participantMatch = o.entry.url.match(/\/participants\/([^/]+)\/holes\/(\d+)$/)
|
|
|
|
|
|
if (participantMatch) {
|
|
|
|
|
|
setPendingParticipantHoles((prev) => {
|
|
|
|
|
|
const next = new Set(prev)
|
|
|
|
|
|
next.delete(`${participantMatch[1]}:${participantMatch[2]}`)
|
|
|
|
|
|
return next
|
|
|
|
|
|
})
|
|
|
|
|
|
syncedParticipants.add(participantMatch[1])
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
const sideMatch = o.entry.url.match(/\/sides\/([^/]+)\/holes\/(\d+)$/)
|
|
|
|
|
|
if (sideMatch) {
|
|
|
|
|
|
setPendingSideHoles((prev) => {
|
|
|
|
|
|
const next = new Set(prev)
|
|
|
|
|
|
next.delete(`${sideMatch[1]}:${sideMatch[2]}`)
|
|
|
|
|
|
return next
|
|
|
|
|
|
})
|
|
|
|
|
|
syncedSides.add(sideMatch[1])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
const failed = outcomes.filter((o) => !o.ok)
|
|
|
|
|
|
if (failed.length > 0) {
|
|
|
|
|
|
setError(
|
|
|
|
|
|
`${failed.length} lagret ${failed.length === 1 ? "endring" : "endringer"} kunne ikke synkroniseres: ${failed[0].message}`,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
setPendingCount(await queueCount(roundId))
|
|
|
|
|
|
for (const id of syncedParticipants) void loadHoles(id)
|
|
|
|
|
|
for (const id of syncedSides) void loadSideHoles(id)
|
|
|
|
|
|
setFormatResultRefreshTick((n) => n + 1)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setSyncing(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setIsOnline(navigator.onLine)
|
|
|
|
|
|
function handleOnline() {
|
|
|
|
|
|
setIsOnline(true)
|
|
|
|
|
|
void flushPending()
|
|
|
|
|
|
}
|
|
|
|
|
|
function handleOffline() {
|
|
|
|
|
|
setIsOnline(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
window.addEventListener("online", handleOnline)
|
|
|
|
|
|
window.addEventListener("offline", handleOffline)
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
window.removeEventListener("online", handleOnline)
|
|
|
|
|
|
window.removeEventListener("offline", handleOffline)
|
|
|
|
|
|
}
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
|
}, [roundId])
|
|
|
|
|
|
|
|
|
|
|
|
// Sjekk for allerede køede skrivinger fra en tidligere økt (f.eks. siden
|
|
|
|
|
|
// ble lukket mens offline) -- samme engangs-sjekk som session-scorecard.tsx.
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
queueCount(roundId)
|
|
|
|
|
|
.then((count) => {
|
|
|
|
|
|
if (cancelled) return
|
|
|
|
|
|
setPendingCount(count)
|
|
|
|
|
|
if (count > 0 && navigator.onLine) void flushPending()
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {})
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
}
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
|
}, [roundId])
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
// Alltid gjeldende deltaker-/side-id-er tilgjengelig INNI WebSocket-
|
|
|
|
|
|
// handleren under, uten å måtte koble socket-en til/fra ved hver ny
|
|
|
|
|
|
// runde-lasting (en vanlig closure over `round` i effektens
|
|
|
|
|
|
// dependency-array ville krevd det).
|
2026-07-26 19:54:18 +02:00
|
|
|
|
const participantIdsRef = useRef<string[]>([])
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
const sideIdsRef = useRef<string[]>([])
|
2026-07-26 08:49:37 +02:00
|
|
|
|
useEffect(() => {
|
2026-07-26 19:54:18 +02:00
|
|
|
|
participantIdsRef.current = round?.participants.map((p) => p.id) ?? []
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
sideIdsRef.current = round?.sides.map((s) => s.id) ?? []
|
2026-07-26 19:54:18 +02:00
|
|
|
|
}, [round])
|
2026-07-26 08:49:37 +02:00
|
|
|
|
|
|
|
|
|
|
// Sanntid (2026-07-26, oppfølging av ADR-027): et rent "noe endret seg"-
|
|
|
|
|
|
// signal -- IKKE selve dataen. Klienten reagerer med de vanlige REST-
|
2026-07-26 19:54:18 +02:00
|
|
|
|
// kallene den uansett gjør ved førstegangslasting. Henter nå ALLE
|
|
|
|
|
|
// deltakeres hull på nytt (ikke bare én aktiv) siden scoringslisten
|
|
|
|
|
|
// viser alles akkumulerte score-så-langt samtidig.
|
2026-07-26 08:49:37 +02:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
|
|
|
|
|
|
const socket = new WebSocket(`${protocol}//${window.location.host}/ws/rounds/${roundId}/live`)
|
|
|
|
|
|
socket.onmessage = () => {
|
|
|
|
|
|
void loadRound()
|
2026-07-26 19:54:18 +02:00
|
|
|
|
for (const id of participantIdsRef.current) {
|
|
|
|
|
|
void loadHoles(id)
|
|
|
|
|
|
}
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
for (const id of sideIdsRef.current) {
|
|
|
|
|
|
void loadSideHoles(id)
|
|
|
|
|
|
}
|
|
|
|
|
|
setFormatResultRefreshTick((n) => n + 1)
|
2026-07-26 08:49:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
return () => socket.close()
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
}, [roundId, loadRound, loadHoles, loadSideHoles])
|
2026-07-26 08:49:37 +02:00
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
const completed = round?.completed_at != null
|
|
|
|
|
|
const readOnly = completed
|
|
|
|
|
|
|
|
|
|
|
|
const players: Player[] =
|
|
|
|
|
|
round?.participants.map((p) => ({
|
|
|
|
|
|
id: p.id,
|
2026-07-27 10:32:51 +02:00
|
|
|
|
name: playerLabel(p),
|
2026-07-26 15:48:07 +02:00
|
|
|
|
userId: p.user_id,
|
|
|
|
|
|
guestEmail: p.guest_email,
|
|
|
|
|
|
isOwner: p.is_owner,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
gender: apiGenderToUi(p.gender),
|
|
|
|
|
|
hcp: p.handicap_index_snapshot,
|
2026-07-26 15:48:07 +02:00
|
|
|
|
courseHandicap: p.course_handicap_snapshot,
|
2026-07-26 09:18:52 +02:00
|
|
|
|
teeName: p.tee_name_snapshot,
|
2026-07-26 08:22:41 +02:00
|
|
|
|
isSelf: viewerId !== null ? p.user_id === viewerId : p.is_owner,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
countsForHandicap: p.counts_for_handicap,
|
|
|
|
|
|
scoreDifferential: p.score_differential,
|
2026-07-24 06:27:35 +02:00
|
|
|
|
statLevel: p.stat_level,
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
excludeFromHandicap: p.exclude_from_handicap,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
roundSideId: p.round_side_id,
|
|
|
|
|
|
playingHandicap: p.playing_handicap,
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
lineupOrder: p.lineup_order,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
})) ?? []
|
|
|
|
|
|
|
2026-07-26 08:22:41 +02:00
|
|
|
|
// Kun eieren kan forvalte runden (rediger/slett/legge til/fjerne
|
|
|
|
|
|
// medspillere) -- en lenket medspiller kan se runden og registrere
|
|
|
|
|
|
// score for hele flighten, men ikke dette (ADR-036 fase 3-utvidelsen).
|
|
|
|
|
|
const isOwnerViewer = viewerId !== null && (round?.participants.some((p) => p.is_owner && p.user_id === viewerId) ?? false)
|
|
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
const activePlayer = players.find((p) => p.id === activePlayerId) ?? players[0] ?? null
|
|
|
|
|
|
const apiHoles = activePlayerId ? holesByParticipant[activePlayerId] : undefined
|
|
|
|
|
|
const holes: Hole[] = (apiHoles ?? []).map((h) => ({ holeNumber: h.hole_number, par: h.par, index: h.stroke_index }))
|
2026-07-24 06:27:35 +02:00
|
|
|
|
// Alltid en tallverdi å regne videre på selv rett etter mount, FØR
|
|
|
|
|
|
// loadRound() har satt currentHole fra round.start_hole.
|
|
|
|
|
|
const activeHole = currentHole ?? round?.start_hole ?? 1
|
|
|
|
|
|
const hole = holes.find((h) => h.holeNumber === activeHole) ?? null
|
|
|
|
|
|
|
2026-07-24 11:21:39 +02:00
|
|
|
|
// Navigasjonsrekkefølge starter på øktens starthull og går rundt
|
|
|
|
|
|
// (sirkulært over 18 hull), men KUN så mange hull som runden faktisk
|
|
|
|
|
|
// planlegger (holes_planned) -- en 9-hulls runde 10-18 skal aldri vise
|
|
|
|
|
|
// hull 1-9 i det hele tatt, og "Forrige" fra hull 10 skal gå til hull
|
|
|
|
|
|
// 18 (siste i settet), ikke hull 9 (funnet 2026-07-24: brukte tidligere
|
|
|
|
|
|
// alltid 18, uansett holes_planned).
|
|
|
|
|
|
const holeOrder = round
|
|
|
|
|
|
? Array.from({ length: round.holes_planned }, (_, i) => ((round.start_hole - 1 + i) % 18) + 1)
|
|
|
|
|
|
: []
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
|
2026-07-26 19:54:18 +02:00
|
|
|
|
// Veiviserens mål (2026-07-26) -- HVEM sin score som registreres akkurat
|
|
|
|
|
|
// nå, uavhengig av `activePlayerId` (som fortsatt kun styrer hvem sin
|
|
|
|
|
|
// "Så langt i runden"-oversikt lenger ned som vises).
|
|
|
|
|
|
const wizardPlayer = players.find((p) => p.id === wizardPlayerId) ?? null
|
|
|
|
|
|
const wizardApiHole = wizardPlayerId ? (holesByParticipant[wizardPlayerId]?.find((h) => h.hole_number === activeHole) ?? null) : null
|
|
|
|
|
|
const wizardStat = wizardApiHole ? apiHoleToStat(wizardApiHole) : emptyStat()
|
|
|
|
|
|
|
|
|
|
|
|
async function updateWizardStat(patch: Partial<HoleStat>) {
|
|
|
|
|
|
if (readOnly || !wizardPlayerId) return
|
|
|
|
|
|
const merged: HoleStat = { ...wizardStat, ...patch }
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
const body = statToPatchBody(merged)
|
|
|
|
|
|
if (!navigator.onLine) {
|
|
|
|
|
|
await queueParticipantHoleWrite(wizardPlayerId, activeHole, body)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
let res: Response
|
|
|
|
|
|
try {
|
|
|
|
|
|
res = await fetch(`/rounds/${roundId}/participants/${wizardPlayerId}/holes/${activeHole}`, {
|
|
|
|
|
|
method: "PATCH",
|
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
|
credentials: "include",
|
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// Ekte nettverksfeil (ikke bare et avvist svar) -- køordne i stedet
|
|
|
|
|
|
// for å tape registreringen, se ADR-028.
|
|
|
|
|
|
await queueParticipantHoleWrite(wizardPlayerId, activeHole, body)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
if (!res.ok) return
|
|
|
|
|
|
const updated: ApiHole = await res.json()
|
|
|
|
|
|
setHolesByParticipant((prev) => ({
|
|
|
|
|
|
...prev,
|
2026-07-26 19:54:18 +02:00
|
|
|
|
[wizardPlayerId]: (prev[wizardPlayerId] ?? []).map((h) => (h.hole_number === updated.hole_number ? updated : h)),
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}))
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
setFormatResultRefreshTick((n) => n + 1)
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
function goPrev() {
|
2026-07-24 06:27:35 +02:00
|
|
|
|
const i = holeOrder.indexOf(activeHole)
|
2026-07-24 11:21:39 +02:00
|
|
|
|
setCurrentHole(holeOrder[(i - 1 + holeOrder.length) % holeOrder.length])
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
scrollToHolePanel()
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
function goNext() {
|
2026-07-24 06:27:35 +02:00
|
|
|
|
const i = holeOrder.indexOf(activeHole)
|
2026-07-24 11:21:39 +02:00
|
|
|
|
setCurrentHole(holeOrder[(i + 1) % holeOrder.length])
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
scrollToHolePanel()
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-26 19:54:18 +02:00
|
|
|
|
// Samlebånd-fremdrift (2026-07-26): kalles fra veiviserens siste steg for
|
|
|
|
|
|
// siste spiller på hullet -- bytter til NESTE spiller i rekkefølgen (samme
|
|
|
|
|
|
// veiviser, nullstilt til steg 1), eller -- er aktiv spiller den siste --
|
|
|
|
|
|
// lukker veiviseren og går videre til neste hull, klar for spiller 1 igjen.
|
|
|
|
|
|
function advanceWizardPlayer() {
|
|
|
|
|
|
const idx = players.findIndex((p) => p.id === wizardPlayerId)
|
|
|
|
|
|
const isLast = idx === -1 || idx === players.length - 1
|
|
|
|
|
|
if (!isLast) {
|
|
|
|
|
|
const next = players[idx + 1]
|
|
|
|
|
|
setActivePlayerId(next.id)
|
|
|
|
|
|
setWizardPlayerId(next.id)
|
2026-07-26 15:48:07 +02:00
|
|
|
|
} else {
|
2026-07-26 19:54:18 +02:00
|
|
|
|
setWizardPlayerId(null)
|
2026-07-26 15:48:07 +02:00
|
|
|
|
setActivePlayerId(players[0]?.id ?? null)
|
|
|
|
|
|
goNext()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
// ADR-039 Beslutning C -- delt-ball-formater (foursome/greensome/scramble)
|
|
|
|
|
|
// scorer PER SIDE, ikke per spiller. Egen, enklere veiviser-tilstand og
|
|
|
|
|
|
// -oppdatering (kun slagtall, ingen putt/detalj-steg -- gir ikke entydig
|
|
|
|
|
|
// mening for en delt ball).
|
|
|
|
|
|
const isSharedBall = round ? SHARED_BALL_FORMATS.has(round.play_format) : false
|
|
|
|
|
|
const wizardSide = round?.sides.find((s) => s.id === wizardSideId) ?? null
|
|
|
|
|
|
const wizardSideHole = wizardSideId ? (holesBySide[wizardSideId]?.find((h) => h.hole_number === activeHole) ?? null) : null
|
|
|
|
|
|
// Par/index for aktivt hull -- samme for begge sider (samme bane), henter
|
|
|
|
|
|
// fra hvilken som helst av de to som allerede har lastet inn hullene sine.
|
|
|
|
|
|
const sharedHole: Hole | null = (() => {
|
|
|
|
|
|
for (const s of round?.sides ?? []) {
|
|
|
|
|
|
const h = holesBySide[s.id]?.find((x) => x.hole_number === activeHole)
|
|
|
|
|
|
if (h) return { holeNumber: h.hole_number, par: h.par, index: h.stroke_index }
|
|
|
|
|
|
}
|
|
|
|
|
|
return null
|
|
|
|
|
|
})()
|
|
|
|
|
|
|
2026-07-30 09:36:52 +02:00
|
|
|
|
// Auto-prompt "Fullfør runde" (2026-07-30): er ALLE hull ført for ALLE
|
|
|
|
|
|
// deltakere (individuelle formater) eller for BEGGE sider (delt-ball) --
|
|
|
|
|
|
// vis en tydelig CTA på Score-fanen i stedet for å stole på at brukeren
|
|
|
|
|
|
// selv husker den tonet-ned knappen i "Spillere og runde"-fanen. Samme
|
|
|
|
|
|
// `setup_complete`-gate som selve knappen (unngår å reklamere for en
|
|
|
|
|
|
// handling som uansett ville vært deaktivert).
|
|
|
|
|
|
const allHolesEnteredForEveryone =
|
|
|
|
|
|
!completed &&
|
|
|
|
|
|
round !== null &&
|
|
|
|
|
|
round.setup_complete &&
|
|
|
|
|
|
holeOrder.length > 0 &&
|
|
|
|
|
|
(isSharedBall
|
|
|
|
|
|
? round.sides.length >= 2 &&
|
|
|
|
|
|
round.sides.every((s) => {
|
|
|
|
|
|
const list = holesBySide[s.id]
|
|
|
|
|
|
return list !== undefined && holeOrder.every((n) => list.find((h) => h.hole_number === n)?.played === true)
|
|
|
|
|
|
})
|
|
|
|
|
|
: players.length > 0 &&
|
|
|
|
|
|
players.every((p) => {
|
|
|
|
|
|
const list = holesByParticipant[p.id]
|
|
|
|
|
|
return list !== undefined && holeOrder.every((n) => list.find((h) => h.hole_number === n)?.played === true)
|
|
|
|
|
|
}))
|
|
|
|
|
|
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
// Begge feltene (score OG selected_participant_id) sendes ALLTID sammen
|
|
|
|
|
|
// (samme "full overwrite hvert kall"-kontrakt som played/score alltid har
|
|
|
|
|
|
// hatt) -- ellers ville en score-oppdatering stille nullstilt en allerede
|
|
|
|
|
|
// registrert utslags-valg (migrasjon 036), siden backend ikke gjør en
|
|
|
|
|
|
// delvis PATCH her.
|
|
|
|
|
|
async function patchSideHole(score: number | null, selectedParticipantId: string | null) {
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
if (readOnly || !wizardSideId) return
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
const body = { played: true, score, selected_participant_id: selectedParticipantId }
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
if (!navigator.onLine) {
|
|
|
|
|
|
await queueSideHoleWrite(wizardSideId, activeHole, body)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
let res: Response
|
|
|
|
|
|
try {
|
|
|
|
|
|
res = await fetch(`/rounds/${roundId}/sides/${wizardSideId}/holes/${activeHole}`, {
|
|
|
|
|
|
method: "PATCH",
|
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
|
credentials: "include",
|
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
await queueSideHoleWrite(wizardSideId, activeHole, body)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
if (!res.ok) return
|
|
|
|
|
|
const updated: ApiSideHole = await res.json()
|
|
|
|
|
|
setHolesBySide((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[wizardSideId]: (prev[wizardSideId] ?? []).map((h) => (h.hole_number === updated.hole_number ? updated : h)),
|
|
|
|
|
|
}))
|
|
|
|
|
|
setFormatResultRefreshTick((n) => n + 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
function updateWizardSideScore(score: number | null) {
|
|
|
|
|
|
void patchSideHole(score, wizardSideHole?.selected_participant_id ?? null)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Scramble/greensome: hvem sitt utslag ble valgt (migrasjon 036) -- helt
|
|
|
|
|
|
// valgfritt, egen handling fra selve slagregistreringen slik at man kan
|
|
|
|
|
|
// sette/endre valget uten å røre et allerede registrert slagtall.
|
|
|
|
|
|
function updateWizardSideSelection(participantId: string | null) {
|
|
|
|
|
|
void patchSideHole(wizardSideHole?.score ?? null, participantId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
function advanceWizardSide() {
|
|
|
|
|
|
if (!round) return
|
|
|
|
|
|
const idx = round.sides.findIndex((s) => s.id === wizardSideId)
|
|
|
|
|
|
const isLast = idx === -1 || idx === round.sides.length - 1
|
|
|
|
|
|
if (!isLast) {
|
|
|
|
|
|
setWizardSideId(round.sides[idx + 1].id)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setWizardSideId(null)
|
|
|
|
|
|
goNext()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 06:27:35 +02:00
|
|
|
|
async function addGuest(name: string, gender: Gender, hcp: number | null, statLevel: StatLevel) {
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
const res = await fetch(`/rounds/${roundId}/participants`, {
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
|
credentials: "include",
|
2026-07-24 06:27:35 +02:00
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
guest_name: name,
|
|
|
|
|
|
gender: uiGenderToApi(gender),
|
|
|
|
|
|
handicap_index: hcp,
|
|
|
|
|
|
stat_level: statLevel,
|
|
|
|
|
|
}),
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
})
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
setError("Klarte ikke å legge til spilleren. Sjekk at banen har en rating for valgt kjønn.")
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
setShowAddGuest(false)
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
const created: ApiParticipant = await res.json()
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
await loadRound()
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
setActivePlayerId(created.id)
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-26 08:22:41 +02:00
|
|
|
|
async function addSearchedParticipant(userId: string, statLevel: StatLevel): Promise<string | null> {
|
|
|
|
|
|
const res = await fetch(`/rounds/${roundId}/participants`, {
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
|
credentials: "include",
|
|
|
|
|
|
body: JSON.stringify({ user_id: userId, stat_level: statLevel }),
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
const body = await res.json().catch(() => null)
|
|
|
|
|
|
return body?.detail?.message ?? "Klarte ikke å legge til spilleren."
|
|
|
|
|
|
}
|
|
|
|
|
|
setShowAddGuest(false)
|
|
|
|
|
|
const created: ApiParticipant = await res.json()
|
|
|
|
|
|
await loadRound()
|
|
|
|
|
|
setActivePlayerId(created.id)
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
async function removeGuest(id: string) {
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
if (!confirm("Fjerne denne spilleren fra runden?")) return
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
const res = await fetch(`/rounds/${roundId}/participants/${id}`, { method: "DELETE", credentials: "include" })
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
if (!res.ok) return
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
if (activePlayerId === id) setActivePlayerId(round?.participants.find((p) => p.is_owner)?.id ?? null)
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
await loadRound()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-26 15:48:07 +02:00
|
|
|
|
// Utslag+HCP+statistikknivå (og navn/kjønn/e-post for en gjest) for en
|
|
|
|
|
|
// enkelt deltaker (2026-07-26) -- gjelder KUN denne
|
2026-07-26 09:18:52 +02:00
|
|
|
|
// runden, rører aldri spillerens egen profil. Bygget for å dekke at en
|
|
|
|
|
|
// medspiller kan spille fra et annet utslag enn eieren, og at en foreslått
|
|
|
|
|
|
// HCP (auto-fylt fra profilen ved søk) kan være feil for akkurat denne dagen.
|
|
|
|
|
|
async function patchParticipant(
|
|
|
|
|
|
participantId: string,
|
|
|
|
|
|
body: Record<string, unknown>,
|
|
|
|
|
|
): Promise<{ ok: true } | { ok: false; message: string }> {
|
|
|
|
|
|
const res = await fetch(`/rounds/${roundId}/participants/${participantId}`, {
|
|
|
|
|
|
method: "PATCH",
|
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
|
credentials: "include",
|
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
const errBody = await res.json().catch(() => null)
|
|
|
|
|
|
return { ok: false, message: errBody?.detail?.message ?? "Klarte ikke å oppdatere spilleren. Prøv igjen." }
|
|
|
|
|
|
}
|
|
|
|
|
|
const updated: ApiParticipant = await res.json()
|
|
|
|
|
|
setRound((prev) =>
|
|
|
|
|
|
prev ? { ...prev, participants: prev.participants.map((p) => (p.id === updated.id ? updated : p)) } : prev,
|
|
|
|
|
|
)
|
|
|
|
|
|
return { ok: true }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
async function finishRound() {
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
if (!confirm("Fullføre runden? Du kan fortsatt se den, men ikke lenger endre registrerte hull.")) return
|
|
|
|
|
|
setCompleting(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(`/rounds/${roundId}/complete`, { method: "POST", credentials: "include" })
|
|
|
|
|
|
if (!res.ok) throw new Error()
|
|
|
|
|
|
setRound(await res.json())
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
setError("Klarte ikke å fullføre runden. Prøv igjen.")
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setCompleting(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 11:21:39 +02:00
|
|
|
|
async function deleteRound() {
|
|
|
|
|
|
if (!confirm("Slette denne runden permanent? Dette kan ikke angres, og gjelder for alle spillerne i runden.")) return
|
|
|
|
|
|
setDeleting(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(`/rounds/${roundId}`, { method: "DELETE", credentials: "include" })
|
|
|
|
|
|
if (!res.ok) throw new Error()
|
|
|
|
|
|
router.replace("/my-rounds")
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
setError("Klarte ikke å slette runden. Prøv igjen.")
|
|
|
|
|
|
setDeleting(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Retter opp feil bane/utslag eller feil antall hull underveis i runden
|
|
|
|
|
|
// (2026-07-24) -- rører ALDRI allerede registrerte slag/putter/etc.,
|
|
|
|
|
|
// kun rating-grunnlaget. Backend avviser tydelig (400) hvis en deltaker
|
|
|
|
|
|
// ville mistet HCP-sporing, uten å skrive noe.
|
|
|
|
|
|
async function patchRound(body: Record<string, unknown>): Promise<{ ok: true } | { ok: false; message: string }> {
|
|
|
|
|
|
const res = await fetch(`/rounds/${roundId}`, {
|
|
|
|
|
|
method: "PATCH",
|
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
|
credentials: "include",
|
|
|
|
|
|
body: JSON.stringify(body),
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
const errBody = await res.json().catch(() => null)
|
|
|
|
|
|
return { ok: false, message: errBody?.detail?.message ?? "Klarte ikke å oppdatere runden. Prøv igjen." }
|
|
|
|
|
|
}
|
|
|
|
|
|
const updated: ApiRound = await res.json()
|
|
|
|
|
|
setRound(updated)
|
|
|
|
|
|
// Par/stroke-indeks kan ha endret seg for ALLE deltakere -- hent hull-
|
|
|
|
|
|
// dataen på nytt for alle vi allerede har lastet, ikke bare aktiv spiller.
|
|
|
|
|
|
const participantIds = Object.keys(holesByParticipant)
|
|
|
|
|
|
for (const pid of participantIds) {
|
|
|
|
|
|
await loadHoles(pid)
|
|
|
|
|
|
}
|
|
|
|
|
|
return { ok: true }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
if (error) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex min-h-[100dvh] flex-col items-center justify-center gap-4 bg-background px-5 text-center">
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
<p className="text-base font-medium text-destructive">{error}</p>
|
2026-07-27 10:32:51 +02:00
|
|
|
|
<Link
|
|
|
|
|
|
href="/my-rounds"
|
|
|
|
|
|
className="flex min-h-11 items-center text-base font-semibold text-primary underline underline-offset-2"
|
|
|
|
|
|
>
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
Tilbake til egne runder
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
if (!round || !activePlayer) {
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex min-h-[100dvh] flex-col items-center justify-center bg-background">
|
|
|
|
|
|
<div aria-hidden="true" className="size-10 animate-spin rounded-full border-4 border-primary/20 border-t-primary" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-08-02 16:34:54 +02:00
|
|
|
|
<RoundPageShell roundId={roundId} activeTab="score">
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
<main className="mx-auto w-full max-w-3xl flex-1 px-5 py-6 sm:py-8">
|
|
|
|
|
|
{error && (
|
|
|
|
|
|
<p role="alert" className="mb-4 text-base font-medium text-destructive">
|
|
|
|
|
|
{error}
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
</p>
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
)}
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
|
Ja, den gir mye mening — og jeg sjekket to av påstandene direkte mot koden i stedet for å ta dem på tro:
Bekreftet, reelt: "Deg Deg"-duplikaten er ekte. playerLabel() erstatter selve NAVNET med teksten "Deg" for din egen rad, OG kortet legger på en egen "Deg"-badge ved siden av — du ser den bokstavelig to ganger, akkurat som i skjermbildet du delte tidligere. Samme sted i koden (spillerkortet OG den nye scoringslisten).
Også bekreftet: score-sirkelen i den nye listen er rounded-full + grønn UANSETT om resultatet er under, over eller på par — den følger ikke golfscore-språket i det hele tatt (sirkel skal bety under par, firkant over par). Det forklarer noe jeg ikke helt klarte å sette fingeren på selv: fargen grønn er kraftig overbrukt — aktiv hull-fane, valgt tall, denne sirkelen, "Deg"-badgen, rangeringsmerket for lederen, primærknapper — alt er grønt, uansett om det faktisk betyr "aktiv/positivt" eller bare er standard-stilen. Det er en skarpere diagnose enn noe jeg selv har formulert denne økten, og den forklarer trolig mye av "fortsatt rotete"-følelsen din utover selve tettheten.
Resten (tetthetsprinsippet, "verktøy ikke nettside", scorekort-som-grid) er en sammenhengende, godt begrunnet retning — men det er en STØRRE strukturell endring (bytte fra dagens "faner + kompakt liste" til et fullt scorekort-grid som primærvisning, med veiviseren åpnet fra en hvilken som helst celle).
Jeg foreslår å dele dette i to:
Compliance-passet (seksjon 3) nå — konkrete, allerede bekreftede feil, lav risiko, rask gevinst.
Grid-redesignet som egen, større runde — fortjener et bevisst ja fra deg først, siden det er en helt annen informasjonsarkitektur enn det jeg bygde i går.
2026-07-27 09:03:15 +02:00
|
|
|
|
{/* To faner: "Score" (default, KUN hull-navigasjon + registrering --
|
|
|
|
|
|
det eneste de fleste trenger midt i en runde) og "Spillere og
|
|
|
|
|
|
runde" (alt administrativt, samlet ett sted i stedet for å ligge
|
|
|
|
|
|
i veien øverst på scoringssiden). */}
|
|
|
|
|
|
<div className="mb-5 inline-flex gap-1 rounded-full bg-muted p-1">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setPageTab("score")}
|
|
|
|
|
|
aria-pressed={pageTab === "score"}
|
|
|
|
|
|
className={cn(
|
2026-07-27 10:32:51 +02:00
|
|
|
|
"min-h-11 rounded-full px-4 text-sm font-bold transition-colors",
|
Ja, den gir mye mening — og jeg sjekket to av påstandene direkte mot koden i stedet for å ta dem på tro:
Bekreftet, reelt: "Deg Deg"-duplikaten er ekte. playerLabel() erstatter selve NAVNET med teksten "Deg" for din egen rad, OG kortet legger på en egen "Deg"-badge ved siden av — du ser den bokstavelig to ganger, akkurat som i skjermbildet du delte tidligere. Samme sted i koden (spillerkortet OG den nye scoringslisten).
Også bekreftet: score-sirkelen i den nye listen er rounded-full + grønn UANSETT om resultatet er under, over eller på par — den følger ikke golfscore-språket i det hele tatt (sirkel skal bety under par, firkant over par). Det forklarer noe jeg ikke helt klarte å sette fingeren på selv: fargen grønn er kraftig overbrukt — aktiv hull-fane, valgt tall, denne sirkelen, "Deg"-badgen, rangeringsmerket for lederen, primærknapper — alt er grønt, uansett om det faktisk betyr "aktiv/positivt" eller bare er standard-stilen. Det er en skarpere diagnose enn noe jeg selv har formulert denne økten, og den forklarer trolig mye av "fortsatt rotete"-følelsen din utover selve tettheten.
Resten (tetthetsprinsippet, "verktøy ikke nettside", scorekort-som-grid) er en sammenhengende, godt begrunnet retning — men det er en STØRRE strukturell endring (bytte fra dagens "faner + kompakt liste" til et fullt scorekort-grid som primærvisning, med veiviseren åpnet fra en hvilken som helst celle).
Jeg foreslår å dele dette i to:
Compliance-passet (seksjon 3) nå — konkrete, allerede bekreftede feil, lav risiko, rask gevinst.
Grid-redesignet som egen, større runde — fortjener et bevisst ja fra deg først, siden det er en helt annen informasjonsarkitektur enn det jeg bygde i går.
2026-07-27 09:03:15 +02:00
|
|
|
|
pageTab === "score" ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground",
|
2026-07-26 08:22:41 +02:00
|
|
|
|
)}
|
Ja, den gir mye mening — og jeg sjekket to av påstandene direkte mot koden i stedet for å ta dem på tro:
Bekreftet, reelt: "Deg Deg"-duplikaten er ekte. playerLabel() erstatter selve NAVNET med teksten "Deg" for din egen rad, OG kortet legger på en egen "Deg"-badge ved siden av — du ser den bokstavelig to ganger, akkurat som i skjermbildet du delte tidligere. Samme sted i koden (spillerkortet OG den nye scoringslisten).
Også bekreftet: score-sirkelen i den nye listen er rounded-full + grønn UANSETT om resultatet er under, over eller på par — den følger ikke golfscore-språket i det hele tatt (sirkel skal bety under par, firkant over par). Det forklarer noe jeg ikke helt klarte å sette fingeren på selv: fargen grønn er kraftig overbrukt — aktiv hull-fane, valgt tall, denne sirkelen, "Deg"-badgen, rangeringsmerket for lederen, primærknapper — alt er grønt, uansett om det faktisk betyr "aktiv/positivt" eller bare er standard-stilen. Det er en skarpere diagnose enn noe jeg selv har formulert denne økten, og den forklarer trolig mye av "fortsatt rotete"-følelsen din utover selve tettheten.
Resten (tetthetsprinsippet, "verktøy ikke nettside", scorekort-som-grid) er en sammenhengende, godt begrunnet retning — men det er en STØRRE strukturell endring (bytte fra dagens "faner + kompakt liste" til et fullt scorekort-grid som primærvisning, med veiviseren åpnet fra en hvilken som helst celle).
Jeg foreslår å dele dette i to:
Compliance-passet (seksjon 3) nå — konkrete, allerede bekreftede feil, lav risiko, rask gevinst.
Grid-redesignet som egen, større runde — fortjener et bevisst ja fra deg først, siden det er en helt annen informasjonsarkitektur enn det jeg bygde i går.
2026-07-27 09:03:15 +02:00
|
|
|
|
>
|
|
|
|
|
|
Score
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setPageTab("manage")}
|
|
|
|
|
|
aria-pressed={pageTab === "manage"}
|
|
|
|
|
|
className={cn(
|
2026-07-27 10:32:51 +02:00
|
|
|
|
"min-h-11 rounded-full px-4 text-sm font-bold transition-colors",
|
Ja, den gir mye mening — og jeg sjekket to av påstandene direkte mot koden i stedet for å ta dem på tro:
Bekreftet, reelt: "Deg Deg"-duplikaten er ekte. playerLabel() erstatter selve NAVNET med teksten "Deg" for din egen rad, OG kortet legger på en egen "Deg"-badge ved siden av — du ser den bokstavelig to ganger, akkurat som i skjermbildet du delte tidligere. Samme sted i koden (spillerkortet OG den nye scoringslisten).
Også bekreftet: score-sirkelen i den nye listen er rounded-full + grønn UANSETT om resultatet er under, over eller på par — den følger ikke golfscore-språket i det hele tatt (sirkel skal bety under par, firkant over par). Det forklarer noe jeg ikke helt klarte å sette fingeren på selv: fargen grønn er kraftig overbrukt — aktiv hull-fane, valgt tall, denne sirkelen, "Deg"-badgen, rangeringsmerket for lederen, primærknapper — alt er grønt, uansett om det faktisk betyr "aktiv/positivt" eller bare er standard-stilen. Det er en skarpere diagnose enn noe jeg selv har formulert denne økten, og den forklarer trolig mye av "fortsatt rotete"-følelsen din utover selve tettheten.
Resten (tetthetsprinsippet, "verktøy ikke nettside", scorekort-som-grid) er en sammenhengende, godt begrunnet retning — men det er en STØRRE strukturell endring (bytte fra dagens "faner + kompakt liste" til et fullt scorekort-grid som primærvisning, med veiviseren åpnet fra en hvilken som helst celle).
Jeg foreslår å dele dette i to:
Compliance-passet (seksjon 3) nå — konkrete, allerede bekreftede feil, lav risiko, rask gevinst.
Grid-redesignet som egen, større runde — fortjener et bevisst ja fra deg først, siden det er en helt annen informasjonsarkitektur enn det jeg bygde i går.
2026-07-27 09:03:15 +02:00
|
|
|
|
pageTab === "manage" ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground",
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
)}
|
Ja, den gir mye mening — og jeg sjekket to av påstandene direkte mot koden i stedet for å ta dem på tro:
Bekreftet, reelt: "Deg Deg"-duplikaten er ekte. playerLabel() erstatter selve NAVNET med teksten "Deg" for din egen rad, OG kortet legger på en egen "Deg"-badge ved siden av — du ser den bokstavelig to ganger, akkurat som i skjermbildet du delte tidligere. Samme sted i koden (spillerkortet OG den nye scoringslisten).
Også bekreftet: score-sirkelen i den nye listen er rounded-full + grønn UANSETT om resultatet er under, over eller på par — den følger ikke golfscore-språket i det hele tatt (sirkel skal bety under par, firkant over par). Det forklarer noe jeg ikke helt klarte å sette fingeren på selv: fargen grønn er kraftig overbrukt — aktiv hull-fane, valgt tall, denne sirkelen, "Deg"-badgen, rangeringsmerket for lederen, primærknapper — alt er grønt, uansett om det faktisk betyr "aktiv/positivt" eller bare er standard-stilen. Det er en skarpere diagnose enn noe jeg selv har formulert denne økten, og den forklarer trolig mye av "fortsatt rotete"-følelsen din utover selve tettheten.
Resten (tetthetsprinsippet, "verktøy ikke nettside", scorekort-som-grid) er en sammenhengende, godt begrunnet retning — men det er en STØRRE strukturell endring (bytte fra dagens "faner + kompakt liste" til et fullt scorekort-grid som primærvisning, med veiviseren åpnet fra en hvilken som helst celle).
Jeg foreslår å dele dette i to:
Compliance-passet (seksjon 3) nå — konkrete, allerede bekreftede feil, lav risiko, rask gevinst.
Grid-redesignet som egen, større runde — fortjener et bevisst ja fra deg først, siden det er en helt annen informasjonsarkitektur enn det jeg bygde i går.
2026-07-27 09:03:15 +02:00
|
|
|
|
>
|
|
|
|
|
|
Spillere og runde
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
{/* Offline scoreregistrering (ADR-028-utvidelse) -- synlig uansett
|
|
|
|
|
|
fane, siden offline-tilstanden er global for hele siden. */}
|
|
|
|
|
|
{!isOnline && (
|
|
|
|
|
|
<div className="mb-4 flex items-center gap-2 rounded-2xl border border-amber-300 bg-amber-50 px-4 py-3 text-sm font-medium text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-200">
|
|
|
|
|
|
<WifiOff aria-hidden="true" className="size-4 shrink-0" />
|
|
|
|
|
|
<span>Du er offline. Endringer lagres lokalt og sendes automatisk når du er tilbake på nett.</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{pendingCount > 0 && (
|
|
|
|
|
|
<div className="mb-4 flex items-center justify-between gap-2 rounded-2xl border border-border bg-card px-4 py-3 text-sm font-medium text-foreground">
|
|
|
|
|
|
<span>
|
|
|
|
|
|
{pendingCount} {pendingCount === 1 ? "endring venter" : "endringer venter"} på synkronisering.
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{isOnline && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => void flushPending()}
|
|
|
|
|
|
disabled={syncing}
|
|
|
|
|
|
className="inline-flex shrink-0 items-center gap-1 font-semibold text-primary disabled:opacity-50"
|
|
|
|
|
|
>
|
|
|
|
|
|
<RefreshCw aria-hidden="true" className={cn("size-3.5", syncing && "animate-spin")} />
|
|
|
|
|
|
Synkroniser nå
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
Ja, den gir mye mening — og jeg sjekket to av påstandene direkte mot koden i stedet for å ta dem på tro:
Bekreftet, reelt: "Deg Deg"-duplikaten er ekte. playerLabel() erstatter selve NAVNET med teksten "Deg" for din egen rad, OG kortet legger på en egen "Deg"-badge ved siden av — du ser den bokstavelig to ganger, akkurat som i skjermbildet du delte tidligere. Samme sted i koden (spillerkortet OG den nye scoringslisten).
Også bekreftet: score-sirkelen i den nye listen er rounded-full + grønn UANSETT om resultatet er under, over eller på par — den følger ikke golfscore-språket i det hele tatt (sirkel skal bety under par, firkant over par). Det forklarer noe jeg ikke helt klarte å sette fingeren på selv: fargen grønn er kraftig overbrukt — aktiv hull-fane, valgt tall, denne sirkelen, "Deg"-badgen, rangeringsmerket for lederen, primærknapper — alt er grønt, uansett om det faktisk betyr "aktiv/positivt" eller bare er standard-stilen. Det er en skarpere diagnose enn noe jeg selv har formulert denne økten, og den forklarer trolig mye av "fortsatt rotete"-følelsen din utover selve tettheten.
Resten (tetthetsprinsippet, "verktøy ikke nettside", scorekort-som-grid) er en sammenhengende, godt begrunnet retning — men det er en STØRRE strukturell endring (bytte fra dagens "faner + kompakt liste" til et fullt scorekort-grid som primærvisning, med veiviseren åpnet fra en hvilken som helst celle).
Jeg foreslår å dele dette i to:
Compliance-passet (seksjon 3) nå — konkrete, allerede bekreftede feil, lav risiko, rask gevinst.
Grid-redesignet som egen, større runde — fortjener et bevisst ja fra deg først, siden det er en helt annen informasjonsarkitektur enn det jeg bygde i går.
2026-07-27 09:03:15 +02:00
|
|
|
|
{pageTab === "manage" && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{/* Kompakt leaderboard-forhåndsvisning (2026-07-26) -- kun for
|
|
|
|
|
|
runder med flere enn én deltaker (komponenten returnerer selv
|
|
|
|
|
|
null ellers), lenke til hele leaderboardet. */}
|
|
|
|
|
|
<RoundLeaderboardMini roundId={roundId} />
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
{/* Løpende matchstatus (to-sidede formater) eller skins-tavle
|
|
|
|
|
|
(ADR-039) -- avledet ved lesing, aldri lagret. */}
|
2026-07-29 15:46:49 +02:00
|
|
|
|
<FormatResultPanel round={round} players={players} result={formatResult} />
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
{/* Nassau Match Play (2026-07-30) -- tre parallelle vinduer
|
|
|
|
|
|
(front9/back9/overall) av det samme to-sidede formatet,
|
|
|
|
|
|
kun for 18-hulls runder. Ren tilleggsvisning, ikke et eget
|
|
|
|
|
|
play_format. */}
|
|
|
|
|
|
{TWO_SIDED_FORMATS.has(round.play_format) && round.holes_planned === 18 && (
|
|
|
|
|
|
<NassauPanel roundId={roundId} formatResultRefreshTick={formatResultRefreshTick} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
{/* Flere flighter i én "vanlig" runde (migrasjon 035) -- løs
|
|
|
|
|
|
gruppering, se FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md.
|
|
|
|
|
|
Eieren tar ansvar for å sette opp ALLE flightene i én
|
|
|
|
|
|
handling ("+ Legg til en flight til" -> gjenbruker hele
|
|
|
|
|
|
opprett-runde-flyten, forhåndsutfylt fra denne runden). */}
|
|
|
|
|
|
<FlightGroupPanel roundId={roundId} round={round} isOwnerViewer={isOwnerViewer} />
|
|
|
|
|
|
|
Ja, den gir mye mening — og jeg sjekket to av påstandene direkte mot koden i stedet for å ta dem på tro:
Bekreftet, reelt: "Deg Deg"-duplikaten er ekte. playerLabel() erstatter selve NAVNET med teksten "Deg" for din egen rad, OG kortet legger på en egen "Deg"-badge ved siden av — du ser den bokstavelig to ganger, akkurat som i skjermbildet du delte tidligere. Samme sted i koden (spillerkortet OG den nye scoringslisten).
Også bekreftet: score-sirkelen i den nye listen er rounded-full + grønn UANSETT om resultatet er under, over eller på par — den følger ikke golfscore-språket i det hele tatt (sirkel skal bety under par, firkant over par). Det forklarer noe jeg ikke helt klarte å sette fingeren på selv: fargen grønn er kraftig overbrukt — aktiv hull-fane, valgt tall, denne sirkelen, "Deg"-badgen, rangeringsmerket for lederen, primærknapper — alt er grønt, uansett om det faktisk betyr "aktiv/positivt" eller bare er standard-stilen. Det er en skarpere diagnose enn noe jeg selv har formulert denne økten, og den forklarer trolig mye av "fortsatt rotete"-følelsen din utover selve tettheten.
Resten (tetthetsprinsippet, "verktøy ikke nettside", scorekort-som-grid) er en sammenhengende, godt begrunnet retning — men det er en STØRRE strukturell endring (bytte fra dagens "faner + kompakt liste" til et fullt scorekort-grid som primærvisning, med veiviseren åpnet fra en hvilken som helst celle).
Jeg foreslår å dele dette i to:
Compliance-passet (seksjon 3) nå — konkrete, allerede bekreftede feil, lav risiko, rask gevinst.
Grid-redesignet som egen, større runde — fortjener et bevisst ja fra deg først, siden det er en helt annen informasjonsarkitektur enn det jeg bygde i går.
2026-07-27 09:03:15 +02:00
|
|
|
|
<div className="mb-5 flex flex-col gap-3">
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
{isOwnerViewer && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setShowEditRound((v) => !v)}
|
|
|
|
|
|
aria-expanded={showEditRound}
|
|
|
|
|
|
className="inline-flex min-h-11 items-center gap-1.5 rounded-xl border border-dashed border-border bg-card px-3 text-sm font-semibold text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Settings2 aria-hidden="true" className="size-4" />
|
|
|
|
|
|
Rediger runde
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{!completed && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
disabled={completing || !round.setup_complete}
|
Ja, den gir mye mening — og jeg sjekket to av påstandene direkte mot koden i stedet for å ta dem på tro:
Bekreftet, reelt: "Deg Deg"-duplikaten er ekte. playerLabel() erstatter selve NAVNET med teksten "Deg" for din egen rad, OG kortet legger på en egen "Deg"-badge ved siden av — du ser den bokstavelig to ganger, akkurat som i skjermbildet du delte tidligere. Samme sted i koden (spillerkortet OG den nye scoringslisten).
Også bekreftet: score-sirkelen i den nye listen er rounded-full + grønn UANSETT om resultatet er under, over eller på par — den følger ikke golfscore-språket i det hele tatt (sirkel skal bety under par, firkant over par). Det forklarer noe jeg ikke helt klarte å sette fingeren på selv: fargen grønn er kraftig overbrukt — aktiv hull-fane, valgt tall, denne sirkelen, "Deg"-badgen, rangeringsmerket for lederen, primærknapper — alt er grønt, uansett om det faktisk betyr "aktiv/positivt" eller bare er standard-stilen. Det er en skarpere diagnose enn noe jeg selv har formulert denne økten, og den forklarer trolig mye av "fortsatt rotete"-følelsen din utover selve tettheten.
Resten (tetthetsprinsippet, "verktøy ikke nettside", scorekort-som-grid) er en sammenhengende, godt begrunnet retning — men det er en STØRRE strukturell endring (bytte fra dagens "faner + kompakt liste" til et fullt scorekort-grid som primærvisning, med veiviseren åpnet fra en hvilken som helst celle).
Jeg foreslår å dele dette i to:
Compliance-passet (seksjon 3) nå — konkrete, allerede bekreftede feil, lav risiko, rask gevinst.
Grid-redesignet som egen, større runde — fortjener et bevisst ja fra deg først, siden det er en helt annen informasjonsarkitektur enn det jeg bygde i går.
2026-07-27 09:03:15 +02:00
|
|
|
|
onClick={finishRound}
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
title={!round.setup_complete ? round.setup_message ?? undefined : undefined}
|
Ja, den gir mye mening — og jeg sjekket to av påstandene direkte mot koden i stedet for å ta dem på tro:
Bekreftet, reelt: "Deg Deg"-duplikaten er ekte. playerLabel() erstatter selve NAVNET med teksten "Deg" for din egen rad, OG kortet legger på en egen "Deg"-badge ved siden av — du ser den bokstavelig to ganger, akkurat som i skjermbildet du delte tidligere. Samme sted i koden (spillerkortet OG den nye scoringslisten).
Også bekreftet: score-sirkelen i den nye listen er rounded-full + grønn UANSETT om resultatet er under, over eller på par — den følger ikke golfscore-språket i det hele tatt (sirkel skal bety under par, firkant over par). Det forklarer noe jeg ikke helt klarte å sette fingeren på selv: fargen grønn er kraftig overbrukt — aktiv hull-fane, valgt tall, denne sirkelen, "Deg"-badgen, rangeringsmerket for lederen, primærknapper — alt er grønt, uansett om det faktisk betyr "aktiv/positivt" eller bare er standard-stilen. Det er en skarpere diagnose enn noe jeg selv har formulert denne økten, og den forklarer trolig mye av "fortsatt rotete"-følelsen din utover selve tettheten.
Resten (tetthetsprinsippet, "verktøy ikke nettside", scorekort-som-grid) er en sammenhengende, godt begrunnet retning — men det er en STØRRE strukturell endring (bytte fra dagens "faner + kompakt liste" til et fullt scorekort-grid som primærvisning, med veiviseren åpnet fra en hvilken som helst celle).
Jeg foreslår å dele dette i to:
Compliance-passet (seksjon 3) nå — konkrete, allerede bekreftede feil, lav risiko, rask gevinst.
Grid-redesignet som egen, større runde — fortjener et bevisst ja fra deg først, siden det er en helt annen informasjonsarkitektur enn det jeg bygde i går.
2026-07-27 09:03:15 +02:00
|
|
|
|
className="inline-flex min-h-11 items-center gap-1.5 rounded-xl border border-dashed border-border bg-card px-3 text-sm font-semibold text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground disabled:opacity-60"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Check aria-hidden="true" className="size-4" />
|
|
|
|
|
|
{completing ? "Fullfører…" : "Fullfør runde"}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{isOwnerViewer && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={deleting}
|
|
|
|
|
|
onClick={deleteRound}
|
|
|
|
|
|
className="inline-flex min-h-11 items-center gap-1.5 rounded-xl border border-dashed border-border bg-card px-3 text-sm font-semibold text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive disabled:opacity-60"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 aria-hidden="true" className="size-4" />
|
|
|
|
|
|
{deleting ? "Sletter…" : "Slett runde"}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{showEditRound && isOwnerViewer && (
|
|
|
|
|
|
<EditRoundPanel round={round} onPatch={patchRound} onClose={() => setShowEditRound(false)} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
{/* Sider (ADR-039 Beslutning A) -- kun for formater som faktisk
|
|
|
|
|
|
bruker sider (match/fourball/foursome/greensome/scramble).
|
|
|
|
|
|
Opprett/slett sider + fordel deltakerne. */}
|
|
|
|
|
|
{TWO_SIDED_FORMATS.has(round.play_format) && (
|
|
|
|
|
|
<SidesPanel
|
|
|
|
|
|
roundId={roundId}
|
|
|
|
|
|
round={round}
|
|
|
|
|
|
players={players}
|
|
|
|
|
|
canManage={isOwnerViewer && !readOnly}
|
|
|
|
|
|
onSidesChanged={loadRound}
|
|
|
|
|
|
onPatchParticipant={patchParticipant}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{!round.setup_complete && !completed && (
|
|
|
|
|
|
<p role="status" className="mb-5 rounded-2xl border border-dashed border-border bg-muted/40 px-4 py-3 text-sm font-medium text-muted-foreground">
|
|
|
|
|
|
{round.setup_message ?? "Oppsettet er ikke komplett ennå."}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
Ja, den gir mye mening — og jeg sjekket to av påstandene direkte mot koden i stedet for å ta dem på tro:
Bekreftet, reelt: "Deg Deg"-duplikaten er ekte. playerLabel() erstatter selve NAVNET med teksten "Deg" for din egen rad, OG kortet legger på en egen "Deg"-badge ved siden av — du ser den bokstavelig to ganger, akkurat som i skjermbildet du delte tidligere. Samme sted i koden (spillerkortet OG den nye scoringslisten).
Også bekreftet: score-sirkelen i den nye listen er rounded-full + grønn UANSETT om resultatet er under, over eller på par — den følger ikke golfscore-språket i det hele tatt (sirkel skal bety under par, firkant over par). Det forklarer noe jeg ikke helt klarte å sette fingeren på selv: fargen grønn er kraftig overbrukt — aktiv hull-fane, valgt tall, denne sirkelen, "Deg"-badgen, rangeringsmerket for lederen, primærknapper — alt er grønt, uansett om det faktisk betyr "aktiv/positivt" eller bare er standard-stilen. Det er en skarpere diagnose enn noe jeg selv har formulert denne økten, og den forklarer trolig mye av "fortsatt rotete"-følelsen din utover selve tettheten.
Resten (tetthetsprinsippet, "verktøy ikke nettside", scorekort-som-grid) er en sammenhengende, godt begrunnet retning — men det er en STØRRE strukturell endring (bytte fra dagens "faner + kompakt liste" til et fullt scorekort-grid som primærvisning, med veiviseren åpnet fra en hvilken som helst celle).
Jeg foreslår å dele dette i to:
Compliance-passet (seksjon 3) nå — konkrete, allerede bekreftede feil, lav risiko, rask gevinst.
Grid-redesignet som egen, større runde — fortjener et bevisst ja fra deg først, siden det er en helt annen informasjonsarkitektur enn det jeg bygde i går.
2026-07-27 09:03:15 +02:00
|
|
|
|
{/* Spillerliste (2026-07-26, erstatter tidligere horisontal
|
|
|
|
|
|
fane-rad) -- vertikal, ett kort per spiller. Utslag/HCP/
|
|
|
|
|
|
statistikknivå + "Rediger" ligger nå INNI hvert kort (samme
|
|
|
|
|
|
kort er også selve velg-aktiv-spiller-trykkflaten), i stedet
|
|
|
|
|
|
for en egen rad under listen som kun gjaldt aktiv spiller. */}
|
|
|
|
|
|
<PlayerList
|
|
|
|
|
|
roundId={roundId}
|
|
|
|
|
|
players={players}
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
sides={round.sides}
|
Ja, den gir mye mening — og jeg sjekket to av påstandene direkte mot koden i stedet for å ta dem på tro:
Bekreftet, reelt: "Deg Deg"-duplikaten er ekte. playerLabel() erstatter selve NAVNET med teksten "Deg" for din egen rad, OG kortet legger på en egen "Deg"-badge ved siden av — du ser den bokstavelig to ganger, akkurat som i skjermbildet du delte tidligere. Samme sted i koden (spillerkortet OG den nye scoringslisten).
Også bekreftet: score-sirkelen i den nye listen er rounded-full + grønn UANSETT om resultatet er under, over eller på par — den følger ikke golfscore-språket i det hele tatt (sirkel skal bety under par, firkant over par). Det forklarer noe jeg ikke helt klarte å sette fingeren på selv: fargen grønn er kraftig overbrukt — aktiv hull-fane, valgt tall, denne sirkelen, "Deg"-badgen, rangeringsmerket for lederen, primærknapper — alt er grønt, uansett om det faktisk betyr "aktiv/positivt" eller bare er standard-stilen. Det er en skarpere diagnose enn noe jeg selv har formulert denne økten, og den forklarer trolig mye av "fortsatt rotete"-følelsen din utover selve tettheten.
Resten (tetthetsprinsippet, "verktøy ikke nettside", scorekort-som-grid) er en sammenhengende, godt begrunnet retning — men det er en STØRRE strukturell endring (bytte fra dagens "faner + kompakt liste" til et fullt scorekort-grid som primærvisning, med veiviseren åpnet fra en hvilken som helst celle).
Jeg foreslår å dele dette i to:
Compliance-passet (seksjon 3) nå — konkrete, allerede bekreftede feil, lav risiko, rask gevinst.
Grid-redesignet som egen, større runde — fortjener et bevisst ja fra deg først, siden det er en helt annen informasjonsarkitektur enn det jeg bygde i går.
2026-07-27 09:03:15 +02:00
|
|
|
|
activePlayerId={activePlayer.id}
|
|
|
|
|
|
onSelect={setActivePlayerId}
|
|
|
|
|
|
onRemove={removeGuest}
|
|
|
|
|
|
onAdd={() => setShowAddGuest((v) => !v)}
|
|
|
|
|
|
addOpen={showAddGuest}
|
|
|
|
|
|
readOnly={readOnly}
|
|
|
|
|
|
canManage={isOwnerViewer}
|
|
|
|
|
|
editingPlayerId={editingPlayerId}
|
|
|
|
|
|
onToggleEdit={(id) => setEditingPlayerId((prev) => (prev === id ? null : id))}
|
|
|
|
|
|
onPatchParticipant={patchParticipant}
|
|
|
|
|
|
onAddGuest={addGuest}
|
|
|
|
|
|
onAddSearched={addSearchedParticipant}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Flyttet HIT (2026-07-26, samme redesign-runde) -- lå tidligere
|
|
|
|
|
|
på selve scoringssiden (først øverst, deretter lenger ned),
|
|
|
|
|
|
men er detaljert stats-innsyn (fairway/innspill/kølle-
|
|
|
|
|
|
fordeling for VALGT spiller), ikke selve registrerings-
|
|
|
|
|
|
oppgaven. Hver spillers rad i scoringslisten viser allerede
|
|
|
|
|
|
løpende til-par, så dette er nå rendyrket "se mer detaljer"
|
|
|
|
|
|
-- hører hjemme i administrasjons-fanen, ikke i veien for
|
|
|
|
|
|
score-registreringen. */}
|
|
|
|
|
|
{!completed && <ScoreSoFar holes={apiHoles} order={holeOrder} />}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2026-07-24 11:21:39 +02:00
|
|
|
|
|
Ja, den gir mye mening — og jeg sjekket to av påstandene direkte mot koden i stedet for å ta dem på tro:
Bekreftet, reelt: "Deg Deg"-duplikaten er ekte. playerLabel() erstatter selve NAVNET med teksten "Deg" for din egen rad, OG kortet legger på en egen "Deg"-badge ved siden av — du ser den bokstavelig to ganger, akkurat som i skjermbildet du delte tidligere. Samme sted i koden (spillerkortet OG den nye scoringslisten).
Også bekreftet: score-sirkelen i den nye listen er rounded-full + grønn UANSETT om resultatet er under, over eller på par — den følger ikke golfscore-språket i det hele tatt (sirkel skal bety under par, firkant over par). Det forklarer noe jeg ikke helt klarte å sette fingeren på selv: fargen grønn er kraftig overbrukt — aktiv hull-fane, valgt tall, denne sirkelen, "Deg"-badgen, rangeringsmerket for lederen, primærknapper — alt er grønt, uansett om det faktisk betyr "aktiv/positivt" eller bare er standard-stilen. Det er en skarpere diagnose enn noe jeg selv har formulert denne økten, og den forklarer trolig mye av "fortsatt rotete"-følelsen din utover selve tettheten.
Resten (tetthetsprinsippet, "verktøy ikke nettside", scorekort-som-grid) er en sammenhengende, godt begrunnet retning — men det er en STØRRE strukturell endring (bytte fra dagens "faner + kompakt liste" til et fullt scorekort-grid som primærvisning, med veiviseren åpnet fra en hvilken som helst celle).
Jeg foreslår å dele dette i to:
Compliance-passet (seksjon 3) nå — konkrete, allerede bekreftede feil, lav risiko, rask gevinst.
Grid-redesignet som egen, større runde — fortjener et bevisst ja fra deg først, siden det er en helt annen informasjonsarkitektur enn det jeg bygde i går.
2026-07-27 09:03:15 +02:00
|
|
|
|
{pageTab === "score" && (
|
|
|
|
|
|
<>
|
2026-07-29 12:44:51 +02:00
|
|
|
|
{/* Matchstatus/skins-tavle øverst på Score-fanen også (2026-07-29,
|
|
|
|
|
|
etterspurt av bruker -- fantes fra før KUN under "Spillere og
|
|
|
|
|
|
runde", lett å overse siden scoring naturlig skjer her).
|
|
|
|
|
|
Samme komponent, ikke duplisert logikk. */}
|
2026-07-29 15:46:49 +02:00
|
|
|
|
<FormatResultPanel round={round} players={players} result={formatResult} />
|
2026-07-29 12:44:51 +02:00
|
|
|
|
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
{/* Bingo Bango Bongo (2026-07-30) -- MANUELL per-hull-observasjon
|
|
|
|
|
|
(ikke GPS/live-sporet), separat fra selve slag-/putt-
|
|
|
|
|
|
registreringen (som fortsatt skjer via vanlig ScoringWizard,
|
|
|
|
|
|
for stat_level/GIR/HCP-formål -- BBB-poengene er UAVHENGIGE
|
|
|
|
|
|
av gross_strokes). */}
|
|
|
|
|
|
{round.play_format === "bbb" && !readOnly && (
|
|
|
|
|
|
<BBBHolePicker
|
|
|
|
|
|
players={players}
|
|
|
|
|
|
holeNumber={activeHole}
|
|
|
|
|
|
selection={bbbHoles[activeHole] ?? null}
|
|
|
|
|
|
onSet={patchBbbHole}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Money Ball (2026-07-30) -- rekkefølgen (0-3) må være tildelt for
|
|
|
|
|
|
alle fire FØR scoring/fullføring gir mening (avgjør hvem som er
|
|
|
|
|
|
"money ball" på hvilket hull). Vises kun til rekkefølgen er
|
|
|
|
|
|
komplett, deretter automatisk skjult. */}
|
|
|
|
|
|
{round.play_format === "money_ball" && isOwnerViewer && !readOnly && (
|
|
|
|
|
|
<MoneyBallLineupPanel players={players} onPatchParticipant={patchParticipant} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-07-30 09:36:52 +02:00
|
|
|
|
{/* Auto-prompt "Fullfør runde" (2026-07-30) -- kun synlig når alle
|
|
|
|
|
|
hull faktisk er ført for alle, forsvinner selv idet runden er
|
|
|
|
|
|
fullført (completed flipper til true). */}
|
|
|
|
|
|
{allHolesEnteredForEveryone && <AllHolesEnteredBanner completing={completing} onComplete={finishRound} />}
|
|
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
{/* Completed summary banner */}
|
2026-07-24 11:21:39 +02:00
|
|
|
|
{completed && (
|
|
|
|
|
|
<CompletedBanner
|
|
|
|
|
|
players={players}
|
|
|
|
|
|
duration={round.started_at && round.completed_at ? formatDuration(round.started_at, round.completed_at) : null}
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
roundId={roundId}
|
2026-07-24 11:21:39 +02:00
|
|
|
|
/>
|
|
|
|
|
|
)}
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
{/* ADR-039 Beslutning C -- delt-ball-formater scorer PER SIDE, ikke
|
|
|
|
|
|
per spiller. Vises kun når begge sider faktisk er opprettet
|
|
|
|
|
|
(SidesPanel i "Spillere og runde" setter dette opp). */}
|
|
|
|
|
|
{isSharedBall && round.sides.length < 2 ? (
|
|
|
|
|
|
<p className="mt-5 rounded-2xl border border-dashed border-border bg-card p-4 text-sm text-muted-foreground">
|
|
|
|
|
|
Opprett begge sidene under "Spillere og runde" før du kan registrere score.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : isSharedBall ? (
|
|
|
|
|
|
round.sides.some((s) => !holesBySide[s.id]) ? (
|
|
|
|
|
|
<div className="mt-5 flex justify-center py-8">
|
|
|
|
|
|
<div aria-hidden="true" className="size-8 animate-spin rounded-full border-4 border-primary/20 border-t-primary" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<section ref={holePanelRef} className="mt-5 flex scroll-mt-28 flex-col gap-4">
|
|
|
|
|
|
<SideScorecardGrid
|
|
|
|
|
|
sides={round.sides}
|
|
|
|
|
|
players={players}
|
|
|
|
|
|
holesBySide={holesBySide}
|
|
|
|
|
|
holeOrder={holeOrder}
|
|
|
|
|
|
holesPlanned={round.holes_planned}
|
|
|
|
|
|
currentHole={activeHole}
|
|
|
|
|
|
readOnly={readOnly}
|
2026-07-29 15:46:49 +02:00
|
|
|
|
isTwoSided={TWO_SIDED_FORMATS.has(round.play_format)}
|
|
|
|
|
|
formatResult={formatResult}
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
onSelectHole={(n) => {
|
|
|
|
|
|
setCurrentHole(n)
|
|
|
|
|
|
scrollToHolePanel()
|
|
|
|
|
|
}}
|
|
|
|
|
|
onOpenEntry={(sideId, holeNumber) => {
|
|
|
|
|
|
setCurrentHole(holeNumber)
|
|
|
|
|
|
setWizardSideId(sideId)
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
|
<Button type="button" variant="outline" onClick={goPrev} className="h-14 rounded-2xl text-base font-bold">
|
|
|
|
|
|
<ChevronLeft aria-hidden="true" className="size-5" />
|
|
|
|
|
|
Forrige hull
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button type="button" variant="outline" onClick={goNext} className="h-14 rounded-2xl text-base font-bold">
|
|
|
|
|
|
Neste hull
|
|
|
|
|
|
<ChevronRight aria-hidden="true" className="size-5" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
)
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
) : (
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
<>
|
|
|
|
|
|
{/* Scorekort-grid (§1 i teecup-scorekort-og-entry-spec.md, bygget
|
|
|
|
|
|
2026-07-27 -- erstatter den forrige rundens ett-hull-om-gangen-
|
|
|
|
|
|
liste, bruker eksplisitt bekreftet "JA" på at grid-redesignet er
|
|
|
|
|
|
en egen, større runde). Spillere som rader (sticky venstre
|
|
|
|
|
|
navnekolonne), hull som horisontalt scrollbare kolonner --
|
|
|
|
|
|
HELE runden synlig samtidig, ikke bare det aktive hullet. */}
|
|
|
|
|
|
{holes.length === 0 ? (
|
|
|
|
|
|
<div className="mt-5 flex justify-center py-8">
|
|
|
|
|
|
<div aria-hidden="true" className="size-8 animate-spin rounded-full border-4 border-primary/20 border-t-primary" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<section ref={holePanelRef} className="mt-5 flex scroll-mt-28 flex-col gap-4">
|
|
|
|
|
|
<ScorecardGrid
|
|
|
|
|
|
players={players}
|
|
|
|
|
|
holesByParticipant={holesByParticipant}
|
|
|
|
|
|
holeOrder={holeOrder}
|
|
|
|
|
|
holesPlanned={round.holes_planned}
|
|
|
|
|
|
currentHole={activeHole}
|
|
|
|
|
|
readOnly={readOnly}
|
2026-07-29 15:46:49 +02:00
|
|
|
|
sides={round.sides}
|
|
|
|
|
|
isTwoSided={TWO_SIDED_FORMATS.has(round.play_format)}
|
|
|
|
|
|
formatResult={formatResult}
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
onSelectHole={(n) => {
|
|
|
|
|
|
setCurrentHole(n)
|
|
|
|
|
|
scrollToHolePanel()
|
|
|
|
|
|
}}
|
|
|
|
|
|
onOpenEntry={(playerId, holeNumber) => {
|
|
|
|
|
|
setCurrentHole(holeNumber)
|
|
|
|
|
|
setActivePlayerId(playerId)
|
|
|
|
|
|
setWizardPlayerId(playerId)
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
{/* Rask sekvensiell hull-navigasjon -- gridet viser allerede
|
|
|
|
|
|
alle hull som kolonner (tapp en kolonneoverskrift for å
|
|
|
|
|
|
velge), men disse to gir en rask vei videre uten å måtte
|
|
|
|
|
|
scrolle bredt for hvert eneste hull. */}
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
|
<Button type="button" variant="outline" onClick={goPrev} className="h-14 rounded-2xl text-base font-bold">
|
|
|
|
|
|
<ChevronLeft aria-hidden="true" className="size-5" />
|
|
|
|
|
|
Forrige hull
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button type="button" variant="outline" onClick={goNext} className="h-14 rounded-2xl text-base font-bold">
|
|
|
|
|
|
Neste hull
|
|
|
|
|
|
<ChevronRight aria-hidden="true" className="size-5" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
)}
|
Ja, den gir mye mening — og jeg sjekket to av påstandene direkte mot koden i stedet for å ta dem på tro:
Bekreftet, reelt: "Deg Deg"-duplikaten er ekte. playerLabel() erstatter selve NAVNET med teksten "Deg" for din egen rad, OG kortet legger på en egen "Deg"-badge ved siden av — du ser den bokstavelig to ganger, akkurat som i skjermbildet du delte tidligere. Samme sted i koden (spillerkortet OG den nye scoringslisten).
Også bekreftet: score-sirkelen i den nye listen er rounded-full + grønn UANSETT om resultatet er under, over eller på par — den følger ikke golfscore-språket i det hele tatt (sirkel skal bety under par, firkant over par). Det forklarer noe jeg ikke helt klarte å sette fingeren på selv: fargen grønn er kraftig overbrukt — aktiv hull-fane, valgt tall, denne sirkelen, "Deg"-badgen, rangeringsmerket for lederen, primærknapper — alt er grønt, uansett om det faktisk betyr "aktiv/positivt" eller bare er standard-stilen. Det er en skarpere diagnose enn noe jeg selv har formulert denne økten, og den forklarer trolig mye av "fortsatt rotete"-følelsen din utover selve tettheten.
Resten (tetthetsprinsippet, "verktøy ikke nettside", scorekort-som-grid) er en sammenhengende, godt begrunnet retning — men det er en STØRRE strukturell endring (bytte fra dagens "faner + kompakt liste" til et fullt scorekort-grid som primærvisning, med veiviseren åpnet fra en hvilken som helst celle).
Jeg foreslår å dele dette i to:
Compliance-passet (seksjon 3) nå — konkrete, allerede bekreftede feil, lav risiko, rask gevinst.
Grid-redesignet som egen, større runde — fortjener et bevisst ja fra deg først, siden det er en helt annen informasjonsarkitektur enn det jeg bygde i går.
2026-07-27 09:03:15 +02:00
|
|
|
|
</>
|
|
|
|
|
|
)}
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
</main>
|
2026-07-26 19:54:18 +02:00
|
|
|
|
|
|
|
|
|
|
{/* Skjermovertagende scoringsveiviser (2026-07-26) -- rendres HELT
|
|
|
|
|
|
UTENFOR <main>, øverst i DOM-treet, slik at den dekker hele
|
|
|
|
|
|
skjermen uavhengig av hvor langt man har skrollet i bakgrunnen. */}
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
{wizardSide && sharedHole && (
|
|
|
|
|
|
<SideScoreWizard
|
|
|
|
|
|
sides={round.sides}
|
|
|
|
|
|
side={wizardSide}
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
sideParticipants={players.filter((p) => p.roundSideId === wizardSide.id)}
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
hole={sharedHole}
|
|
|
|
|
|
score={wizardSideHole?.played ? wizardSideHole.score : null}
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
selectedParticipantId={wizardSideHole?.selected_participant_id ?? null}
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
onSave={updateWizardSideScore}
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
onSelectParticipant={updateWizardSideSelection}
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
onClose={() => setWizardSideId(null)}
|
|
|
|
|
|
onAdvance={advanceWizardSide}
|
|
|
|
|
|
readOnly={readOnly}
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
isPending={pendingSideHoles.has(`${wizardSide.id}:${activeHole}`)}
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-07-26 19:54:18 +02:00
|
|
|
|
{wizardPlayer && hole && (
|
|
|
|
|
|
<ScoringWizard
|
|
|
|
|
|
players={players}
|
|
|
|
|
|
player={wizardPlayer}
|
|
|
|
|
|
hole={hole}
|
|
|
|
|
|
stat={wizardStat}
|
|
|
|
|
|
onUpdate={updateWizardStat}
|
|
|
|
|
|
onClose={() => setWizardPlayerId(null)}
|
|
|
|
|
|
onAdvance={advanceWizardPlayer}
|
|
|
|
|
|
readOnly={readOnly}
|
|
|
|
|
|
ownBagClubs={ownBagClubs}
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
isPending={pendingParticipantHoles.has(`${wizardPlayer.id}:${activeHole}`)}
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
playFormat={round.play_format}
|
|
|
|
|
|
strokesReceived={wizardApiHole?.strokes_received ?? null}
|
2026-07-26 19:54:18 +02:00
|
|
|
|
/>
|
|
|
|
|
|
)}
|
2026-08-02 16:34:54 +02:00
|
|
|
|
</RoundPageShell>
|
2026-07-26 19:54:18 +02:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 15:32:02 +02:00
|
|
|
|
// --- Scoringsveiviser (2026-07-26, auto-hopp lagt til 2026-07-28) ----------
|
2026-07-26 19:54:18 +02:00
|
|
|
|
// Skjermovertagende, ett-spiller-om-gangen registreringsflyt -- inspirert av
|
|
|
|
|
|
// en konkurrentapps samlebåndsflyt (video studert bilde for bilde), men
|
|
|
|
|
|
// BEVISST forenklet til TeeCups eget stat_level-drevne steg-antall i stedet
|
|
|
|
|
|
// for å kopiere konkurrentens nøyaktige skjermrekkefølge/design:
|
|
|
|
|
|
// - "strokes_only": ett steg (Slag)
|
2026-07-28 15:32:02 +02:00
|
|
|
|
// - "strokes_and_putts": to steg (Slag, Putter, Avstand første putt)
|
|
|
|
|
|
// - "full": fire steg (Slag, Putter, Avstand første putt, resten samlet)
|
|
|
|
|
|
// Slag/Putter hopper AUTOMATISK videre i det øyeblikket en verdi velges --
|
|
|
|
|
|
// ingen "Neste"-trykk nødvendig (etterspurt av bruker 2026-07-28: "Avstand
|
|
|
|
|
|
// første putt" lå tidligere PÅ SAMME steg som selve putt-tallet, som ville
|
|
|
|
|
|
// gjort feltet uoppnåelig hvis auto-hopp fyrte idet putt-tallet ble valgt --
|
|
|
|
|
|
// derfor et eget, eget steg for avstanden, som ELLERS ville vært umulig å
|
|
|
|
|
|
// nå). "Avstand"/"detaljer" er valgfrie/flerfelts og auto-hopper derfor
|
|
|
|
|
|
// ikke -- "Neste"/"Forrige"-knappene i bunnen fungerer fortsatt som
|
|
|
|
|
|
// manuell reserve for disse (og for å rette et allerede registrert steg).
|
|
|
|
|
|
// På siste steg blir knappen "Neste: {navn}" (bytter til neste spiller,
|
|
|
|
|
|
// nullstiller til steg 1) eller "Ferdig" (er spilleren den siste, lukker
|
|
|
|
|
|
// veiviseren og varsler forelderen om å gå videre til neste hull).
|
|
|
|
|
|
|
|
|
|
|
|
type WizardStep = "strokes" | "putts" | "puttDistance" | "details"
|
2026-07-26 19:54:18 +02:00
|
|
|
|
|
|
|
|
|
|
function wizardStepsFor(statLevel: StatLevel): WizardStep[] {
|
|
|
|
|
|
if (statLevel === "strokes_only") return ["strokes"]
|
2026-07-28 15:32:02 +02:00
|
|
|
|
if (statLevel === "strokes_and_putts") return ["strokes", "putts", "puttDistance"]
|
|
|
|
|
|
return ["strokes", "putts", "puttDistance", "details"]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Hvilket HoleStat-felt (om noen) som "eier" auto-hopp for et gitt steg --
|
|
|
|
|
|
// null for steg uten noe entydig enkeltfelt å trigge på (flerfelts/valgfritt).
|
|
|
|
|
|
function autoAdvanceFieldFor(step: WizardStep): "strokes" | "putts" | "firstPuttBucket" | null {
|
|
|
|
|
|
if (step === "strokes") return "strokes"
|
|
|
|
|
|
if (step === "putts") return "putts"
|
|
|
|
|
|
if (step === "puttDistance") return "firstPuttBucket"
|
|
|
|
|
|
return null
|
2026-07-26 19:54:18 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ScoringWizard({
|
|
|
|
|
|
players,
|
|
|
|
|
|
player,
|
|
|
|
|
|
hole,
|
|
|
|
|
|
stat,
|
|
|
|
|
|
onUpdate,
|
|
|
|
|
|
onClose,
|
|
|
|
|
|
onAdvance,
|
|
|
|
|
|
readOnly,
|
|
|
|
|
|
ownBagClubs,
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
isPending,
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
playFormat,
|
|
|
|
|
|
strokesReceived,
|
2026-07-26 19:54:18 +02:00
|
|
|
|
}: {
|
|
|
|
|
|
players: Player[]
|
|
|
|
|
|
player: Player
|
|
|
|
|
|
hole: Hole
|
|
|
|
|
|
stat: HoleStat
|
|
|
|
|
|
onUpdate: (patch: Partial<HoleStat>) => void
|
|
|
|
|
|
onClose: () => void
|
|
|
|
|
|
onAdvance: () => void
|
|
|
|
|
|
readOnly: boolean
|
|
|
|
|
|
ownBagClubs: string[]
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
// Offline scoreregistrering (ADR-028-utvidelse, 2026-07-28) -- sann når
|
|
|
|
|
|
// gjeldende hull for AKTIV spiller ligger i den lokale IndexedDB-køen,
|
|
|
|
|
|
// ikke bekreftet av serveren ennå.
|
|
|
|
|
|
isPending: boolean
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
// "Plukket opp" (migrasjon 038, Stableford) -- kun meningsfullt for
|
|
|
|
|
|
// slagspill/Stableford (match/skins/par-lag har egne, ubyggede
|
|
|
|
|
|
// hull-avgjørelsesmekanismer -- konsedering, ikke dette). Krever en
|
|
|
|
|
|
// kjent strokes_received for AKTUELT hull for at serveren skal kunne
|
|
|
|
|
|
// regne ut Net Double Bogey-scoren.
|
|
|
|
|
|
playFormat: string
|
|
|
|
|
|
strokesReceived: number | null
|
2026-07-26 19:54:18 +02:00
|
|
|
|
}) {
|
|
|
|
|
|
const playerIndex = players.findIndex((p) => p.id === player.id)
|
|
|
|
|
|
const isLastPlayer = playerIndex === -1 || playerIndex === players.length - 1
|
|
|
|
|
|
const nextPlayer = !isLastPlayer ? players[playerIndex + 1] : null
|
|
|
|
|
|
|
|
|
|
|
|
const steps = wizardStepsFor(player.statLevel)
|
|
|
|
|
|
const [stepIndex, setStepIndex] = useState(0)
|
|
|
|
|
|
// Nullstill til steg 1 hver gang veiviseren åpnes for en NY spiller --
|
|
|
|
|
|
// uten dette ville "Neste: {navn}" hoppet rett til der FORRIGE spiller
|
|
|
|
|
|
// slapp, i stedet for å starte på Slag igjen for den nye spilleren.
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setStepIndex(0)
|
|
|
|
|
|
}, [player.id])
|
|
|
|
|
|
|
|
|
|
|
|
const step = steps[Math.min(stepIndex, steps.length - 1)]
|
|
|
|
|
|
const isFirstStep = stepIndex === 0
|
|
|
|
|
|
const isLastStep = stepIndex === steps.length - 1
|
|
|
|
|
|
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
const canProceed =
|
|
|
|
|
|
step === "strokes" ? stat.strokes !== null || stat.pickedUp : step === "putts" ? stat.putts !== null : true
|
2026-07-26 19:54:18 +02:00
|
|
|
|
|
2026-07-28 15:32:02 +02:00
|
|
|
|
// Auto-hopp (2026-07-28): så snart steget sitt eget felt får en verdi,
|
|
|
|
|
|
// hopp videre av seg selv -- ingen "Neste"-trykk nødvendig. `enteredWith
|
|
|
|
|
|
// ValueRef` fanger om feltet ALLEREDE hadde en verdi idet vi ankom steget
|
|
|
|
|
|
// (et allerede utfylt hull skal ikke hoppe videre bare fordi veiviseren
|
|
|
|
|
|
// åpnes) -- kun en FERSK endring gjort AV BRUKEREN akkurat nå skal utløse
|
|
|
|
|
|
// hoppet. `firedRef` hindrer at samme hopp trigges to ganger.
|
|
|
|
|
|
const autoField = autoAdvanceFieldFor(step)
|
|
|
|
|
|
const enteredWithValueRef = useRef(false)
|
|
|
|
|
|
const firedRef = useRef(false)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
firedRef.current = false
|
|
|
|
|
|
enteredWithValueRef.current = autoField !== null && stat[autoField] !== null
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
|
}, [step, player.id, hole.holeNumber])
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!autoField || firedRef.current || enteredWithValueRef.current) return
|
|
|
|
|
|
if (stat[autoField] !== null) {
|
|
|
|
|
|
firedRef.current = true
|
|
|
|
|
|
forward()
|
|
|
|
|
|
}
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
|
}, [stat.strokes, stat.putts, stat.firstPuttBucket])
|
|
|
|
|
|
|
2026-07-30 09:36:52 +02:00
|
|
|
|
// GIR-auto-inferens (2026-07-30): score - putts <= par - 2 GARANTERER
|
|
|
|
|
|
// greentreff i regulering (samme formel som round-stats.tsx sin isGir/
|
|
|
|
|
|
// round-detail.tsx sin showGir, som allerede styrer GIR-STATISTIKKEN) --
|
|
|
|
|
|
// da er det unødvendig å i tillegg kreve et manuelt "Innspill: Traff"-
|
|
|
|
|
|
// valg i DirectionCross-feltet under. Kun en FORHÅNDSUTFYLLING: rører
|
|
|
|
|
|
// ALDRI et allerede satt (manuelt ELLER tidligere auto-satt) approach-
|
|
|
|
|
|
// valg, og gjelder aldri motsatt vei (GIR usann sier ingenting om
|
|
|
|
|
|
// retning -- en scrambling-birdie gir identisk slag/putt-mønster som en
|
|
|
|
|
|
// ekte GIR). Trigges når "details"-steget nås, siden slag+putt begge må
|
|
|
|
|
|
// være kjent (kommer tidligere i wizard-rekkefølgen).
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (step !== "details") return
|
|
|
|
|
|
if (stat.approach !== null) return
|
|
|
|
|
|
if (stat.strokes === null || stat.putts === null) return
|
|
|
|
|
|
if (stat.strokes - stat.putts <= hole.par - 2) {
|
|
|
|
|
|
onUpdate({ approach: "hit" })
|
|
|
|
|
|
}
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
|
}, [step, stat.strokes, stat.putts, stat.approach, hole.par])
|
|
|
|
|
|
|
2026-07-26 19:54:18 +02:00
|
|
|
|
function back() {
|
|
|
|
|
|
if (isFirstStep) {
|
|
|
|
|
|
onClose()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
setStepIndex((i) => i - 1)
|
|
|
|
|
|
}
|
|
|
|
|
|
function forward() {
|
|
|
|
|
|
if (!isLastStep) {
|
|
|
|
|
|
setStepIndex((i) => i + 1)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
onAdvance()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="fixed inset-0 z-50 flex flex-col bg-background">
|
|
|
|
|
|
<header className="flex min-h-14 shrink-0 items-center gap-2 border-b border-border px-3">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
|
aria-label="Lukk veiviseren"
|
|
|
|
|
|
className="flex size-11 shrink-0 items-center justify-center rounded-xl text-foreground transition-colors hover:bg-accent"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X aria-hidden="true" className="size-5" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div className="flex min-w-0 flex-1 flex-col items-center text-center">
|
|
|
|
|
|
<span className="truncate text-base font-extrabold text-foreground">
|
|
|
|
|
|
Hull {hole.holeNumber} · Par {hole.par}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="truncate text-sm font-semibold text-muted-foreground">{player.name}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="size-11 shrink-0" aria-hidden="true" />
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Kontekst-rad: alle spillerne, aktiv fremhevet -- samme prinsipp som
|
|
|
|
|
|
konkurrentappen: man mister aldri oversikten over hvem som allerede
|
|
|
|
|
|
er ferdig på hullet. Ren informasjon, ikke en snarvei -- bytte
|
|
|
|
|
|
spiller midt i et steg ville latt uferdige felt henge løst. */}
|
|
|
|
|
|
<div className="-mx-1 flex shrink-0 gap-2 overflow-x-auto border-b border-border px-4 py-2.5">
|
|
|
|
|
|
{players.map((p) => (
|
|
|
|
|
|
<span
|
|
|
|
|
|
key={p.id}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex shrink-0 items-center rounded-full border px-3 py-1.5 text-sm font-bold",
|
|
|
|
|
|
p.id === player.id
|
|
|
|
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
|
|
|
|
: "border-border bg-card text-muted-foreground",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{p.name}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
{isPending && (
|
|
|
|
|
|
<p className="shrink-0 border-b border-border bg-muted/50 px-4 py-1.5 text-center text-xs font-medium text-muted-foreground">
|
|
|
|
|
|
Lagret lokalt · venter på synk
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-07-26 19:54:18 +02:00
|
|
|
|
<main className="flex-1 overflow-y-auto p-5 sm:p-6">
|
|
|
|
|
|
<div className="mx-auto flex max-w-sm flex-col gap-6">
|
|
|
|
|
|
{step === "strokes" && (
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
<>
|
|
|
|
|
|
<NumberPicker
|
|
|
|
|
|
label="Slag"
|
|
|
|
|
|
value={stat.strokes}
|
|
|
|
|
|
directValues={[1, 2, 3, 4, 5, 6, 7, 8, 9]}
|
|
|
|
|
|
expandValues={[10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]}
|
|
|
|
|
|
expandLabel="10+"
|
|
|
|
|
|
parValue={hole.par}
|
|
|
|
|
|
showGolfTerms
|
|
|
|
|
|
onChange={(v) => onUpdate({ strokes: v, played: true, pickedUp: false })}
|
|
|
|
|
|
readOnly={readOnly}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* "Plukket opp" (migrasjon 038, Stableford, 2026-07-29) --
|
|
|
|
|
|
kun for slagspill/Stableford, og kun når serveren faktisk
|
|
|
|
|
|
kan regne ut Net Double Bogey (krever kjent strokes_received). */}
|
|
|
|
|
|
{(playFormat === "stroke" || playFormat === "stableford") && strokesReceived !== null && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={readOnly}
|
|
|
|
|
|
aria-pressed={stat.pickedUp}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
if (stat.pickedUp) {
|
|
|
|
|
|
onUpdate({ pickedUp: false, strokes: null, played: false })
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
onUpdate({ pickedUp: true, strokes: null, played: true })
|
|
|
|
|
|
forward()
|
|
|
|
|
|
}}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex min-h-11 items-center justify-center gap-2 rounded-2xl border px-4 text-base font-bold transition-colors disabled:opacity-60",
|
|
|
|
|
|
stat.pickedUp
|
|
|
|
|
|
? "border-brand-orange bg-brand-orange text-brand-orange-foreground"
|
|
|
|
|
|
: "border-dashed border-border bg-card text-muted-foreground hover:bg-accent/50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{stat.pickedUp ? "Plukket opp — trykk for å angre" : "Plukket opp (0 poeng)"}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
2026-07-26 19:54:18 +02:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{step === "putts" && (
|
2026-07-28 15:32:02 +02:00
|
|
|
|
<NumberPicker
|
|
|
|
|
|
label="Putter"
|
|
|
|
|
|
value={stat.putts}
|
|
|
|
|
|
directValues={[0, 1, 2, 3, 4, 5, 6]}
|
|
|
|
|
|
expandValues={[7, 8, 9, 10]}
|
|
|
|
|
|
expandLabel="7+"
|
|
|
|
|
|
maxValue={stat.strokes ?? undefined}
|
|
|
|
|
|
onChange={(v) => onUpdate({ putts: v })}
|
|
|
|
|
|
readOnly={readOnly}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{step === "puttDistance" && (
|
|
|
|
|
|
<ChoiceRow
|
|
|
|
|
|
label="Avstand første putt"
|
|
|
|
|
|
options={[
|
|
|
|
|
|
{ value: "<1m", label: "<1m" },
|
|
|
|
|
|
{ value: "<2m", label: "<2m" },
|
|
|
|
|
|
{ value: "<3m", label: "<3m" },
|
|
|
|
|
|
{ value: "<5m", label: "<5m" },
|
|
|
|
|
|
{ value: "<8m", label: "<8m" },
|
|
|
|
|
|
{ value: "8m+", label: "8m+" },
|
|
|
|
|
|
]}
|
|
|
|
|
|
value={stat.firstPuttBucket}
|
|
|
|
|
|
onChange={(v) => onUpdate({ firstPuttBucket: v as PuttBucket })}
|
|
|
|
|
|
readOnly={readOnly}
|
|
|
|
|
|
/>
|
2026-07-26 19:54:18 +02:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{step === "details" && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<Label className="text-base font-semibold">Kølle brukt ved utslaget</Label>
|
|
|
|
|
|
{player.isSelf && ownBagClubs.length > 0 ? (
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
{ownBagClubs.map((club) => {
|
|
|
|
|
|
const selected = stat.club === club
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={club}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={readOnly}
|
|
|
|
|
|
onClick={() => onUpdate({ club })}
|
|
|
|
|
|
aria-pressed={selected}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex min-h-11 items-center justify-center rounded-xl border px-3 text-sm font-bold transition-colors disabled:opacity-100",
|
|
|
|
|
|
selected
|
|
|
|
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
|
|
|
|
: "border-border bg-card text-foreground hover:bg-accent/50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{club}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={stat.club}
|
|
|
|
|
|
onChange={(e) => onUpdate({ club: e.target.value })}
|
|
|
|
|
|
placeholder="F.eks. Driver, 3-jern"
|
|
|
|
|
|
disabled={readOnly}
|
|
|
|
|
|
className="h-12 rounded-2xl text-base"
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{hole.par !== 3 && (
|
|
|
|
|
|
<DirectionCross
|
|
|
|
|
|
label="Utslag"
|
|
|
|
|
|
variant="horizontal"
|
|
|
|
|
|
centerLabel="Fairway"
|
|
|
|
|
|
value={stat.teeShot}
|
|
|
|
|
|
onChange={(v) => onUpdate({ teeShot: v as TeeShot })}
|
|
|
|
|
|
readOnly={readOnly}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<DirectionCross
|
|
|
|
|
|
label="Innspill"
|
|
|
|
|
|
variant="full"
|
|
|
|
|
|
centerLabel="Traff"
|
|
|
|
|
|
value={stat.approach}
|
|
|
|
|
|
onChange={(v) => onUpdate({ approach: v as Approach })}
|
|
|
|
|
|
readOnly={readOnly}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-07-30 08:16:40 +02:00
|
|
|
|
{/* Alltid stablet, ALDRI et sm:grid-cols-3-sprang: denne wizard-
|
|
|
|
|
|
seksjonen er fast begrenset til max-w-sm (384px, se
|
|
|
|
|
|
containeren over) uansett hvor bred selve VIEWPORTET er --
|
|
|
|
|
|
et viewport-basert grid-cols-3-brudd ved 640px trigges
|
|
|
|
|
|
derfor på skjermer godt over 640px bredde mens
|
|
|
|
|
|
innholdskolonnen fortsatt kun er 384px, som får de tre
|
|
|
|
|
|
stepperne (hver ~190px minimum pga. faste 44px-knapper) til
|
|
|
|
|
|
å overlappe hverandre i stedet for å faktisk ha plass. */}
|
|
|
|
|
|
<div className="grid grid-cols-1 gap-3">
|
2026-07-26 19:54:18 +02:00
|
|
|
|
<Stepper label="Chip" value={stat.chip} onChange={(v) => onUpdate({ chip: v })} readOnly={readOnly} max={stat.strokes ?? undefined} />
|
|
|
|
|
|
<Stepper label="Bunker" value={stat.bunker} onChange={(v) => onUpdate({ bunker: v })} readOnly={readOnly} max={stat.strokes ?? undefined} />
|
|
|
|
|
|
<Stepper label="Straffeslag" value={stat.penalty} onChange={(v) => onUpdate({ penalty: v })} readOnly={readOnly} max={stat.strokes ?? undefined} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<NumberPicker
|
|
|
|
|
|
label="Anywayslag"
|
|
|
|
|
|
value={stat.anywayStrokes}
|
|
|
|
|
|
directValues={[0, 1, 2, 3, 4, 5, 6]}
|
|
|
|
|
|
expandValues={[7, 8, 9, 10]}
|
|
|
|
|
|
expandLabel="7+"
|
|
|
|
|
|
maxValue={stat.strokes ?? undefined}
|
|
|
|
|
|
onChange={(v) => onUpdate({ anywayStrokes: v })}
|
|
|
|
|
|
readOnly={readOnly}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
|
|
<footer className="grid shrink-0 grid-cols-2 gap-3 border-t border-border p-4">
|
|
|
|
|
|
<Button type="button" variant="outline" onClick={back} className="h-14 rounded-2xl text-base font-bold">
|
|
|
|
|
|
{isFirstStep ? "Avbryt" : "Forrige"}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={!canProceed}
|
|
|
|
|
|
onClick={forward}
|
|
|
|
|
|
className="h-14 rounded-2xl text-base font-bold disabled:opacity-40"
|
|
|
|
|
|
>
|
|
|
|
|
|
{isLastStep ? (isLastPlayer ? "Ferdig" : `Neste: ${nextPlayer?.name}`) : "Neste"}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</footer>
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
// --- Completed banner ------------------------------------------------------
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
|
2026-07-30 09:36:52 +02:00
|
|
|
|
function AllHolesEnteredBanner({ completing, onComplete }: { completing: boolean; onComplete: () => void }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mb-6 flex flex-col gap-3 rounded-3xl border border-primary/30 bg-primary/10 p-5 sm:flex-row sm:items-center sm:justify-between sm:p-6">
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
<div className="flex size-12 shrink-0 items-center justify-center rounded-2xl bg-primary text-primary-foreground shadow-sm">
|
|
|
|
|
|
<Check aria-hidden="true" className="size-6" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex flex-col">
|
|
|
|
|
|
<span className="text-xs font-semibold uppercase tracking-wide text-primary">Alle hull er ført</span>
|
|
|
|
|
|
<h2 className="text-lg font-extrabold tracking-tight text-foreground">Klar til å fullføre runden?</h2>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={completing}
|
|
|
|
|
|
onClick={onComplete}
|
|
|
|
|
|
className="flex min-h-12 items-center justify-center gap-1.5 rounded-2xl bg-primary px-5 text-base font-bold text-primary-foreground shadow-sm transition-colors hover:bg-primary/90 disabled:opacity-60 sm:shrink-0"
|
|
|
|
|
|
>
|
|
|
|
|
|
{completing ? "Fullfører…" : "Fullfør runden"}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
function CompletedBanner({
|
|
|
|
|
|
players,
|
|
|
|
|
|
duration,
|
|
|
|
|
|
roundId,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
players: Player[]
|
|
|
|
|
|
duration: string | null
|
|
|
|
|
|
roundId: string
|
|
|
|
|
|
}) {
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
return (
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
<div className="mb-6 flex flex-col gap-4 rounded-3xl border border-primary/30 bg-primary/10 p-5 sm:p-6">
|
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
|
<div className="flex size-12 shrink-0 items-center justify-center rounded-2xl bg-primary text-primary-foreground shadow-sm">
|
|
|
|
|
|
<Trophy aria-hidden="true" className="size-6" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex flex-col">
|
|
|
|
|
|
<span className="text-xs font-semibold uppercase tracking-wide text-primary">Ferdigspilt</span>
|
|
|
|
|
|
<h2 className="text-xl font-extrabold tracking-tight text-foreground">Runde fullført</h2>
|
|
|
|
|
|
</div>
|
2026-07-24 11:21:39 +02:00
|
|
|
|
{duration && (
|
|
|
|
|
|
<div className="ml-auto flex flex-col items-end">
|
|
|
|
|
|
<span className="text-xs font-medium text-muted-foreground">Tid brukt</span>
|
|
|
|
|
|
<span className="flex items-center gap-1.5 text-lg font-extrabold tabular-nums text-foreground">
|
|
|
|
|
|
<Clock aria-hidden="true" className="size-4 text-primary" />
|
|
|
|
|
|
{duration}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
</div>
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
<ul className="flex flex-col divide-y divide-primary/20 overflow-hidden rounded-2xl border border-primary/20 bg-card">
|
|
|
|
|
|
{players.map((player) => {
|
|
|
|
|
|
const counts = player.countsForHandicap && player.scoreDifferential !== null
|
|
|
|
|
|
return (
|
|
|
|
|
|
<li key={player.id} className="flex items-center justify-between gap-3 px-4 py-3">
|
|
|
|
|
|
<span className="text-base font-bold text-foreground">{player.name}</span>
|
|
|
|
|
|
{counts ? (
|
|
|
|
|
|
<span className="flex items-baseline gap-1.5">
|
All done and verified live in the browser. Summary of this round:
"Kommende runder" → "Runder" on the dashboard.
Tjøme course-name duplicate fixed in the real database (one round's stale course_name_snapshot corrected) — all three of your rounds now group under one "Tjøme Golfklubb" entry.
Two new colors, per your request for two: lifted the already-validated --chart-3 (blue → --info) and --chart-4 (gold → --gold) into core design tokens. First real uses: a blue "Hcp spilt til X" badge on round cards, and a gold "Personlig rekord" badge on your best completed round.
Clicking a course under "Spilte baner" now opens a new page listing every round played there (/my-rounds/course/[name]) — caught and fixed a real double-encoding bug here via an actual browser screenshot before shipping.
Aggregated statistics, clickable from the dashboard's "Statistikk" section (/my-rounds/stats): rounds completed, avg to-par, putts/18 holes (implementing your padding rule exactly — unplayed holes count as 2 putts, only for rounds where putt-tracking was on), fairway%, GIR%, one-putt%, scrambling%, sand save%, and chip/bunker/penalty/anywayslag averages per round.
Everything is typechecked, the putts-padding logic is verified against a hand-computed synthetic dataset, and all new screens were checked live in the browser with no console errors.
2026-07-28 07:04:56 +02:00
|
|
|
|
<span className="text-xs font-medium text-muted-foreground">Hcp spilt til</span>
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
<span className="text-lg font-extrabold tabular-nums text-foreground">
|
|
|
|
|
|
{player.scoreDifferential!.toFixed(1).replace(".", ",")}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="text-sm font-semibold text-muted-foreground">Telte ikke mot HCP</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</li>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</ul>
|
2026-08-02 16:34:54 +02:00
|
|
|
|
{/* Scorekort og statistikk slått sammen til ÉN side (ADR-040
|
|
|
|
|
|
Beslutning D) -- ett felles knapp i stedet for to separate. */}
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href={`/my-rounds/${roundId}/scorecard`}
|
|
|
|
|
|
className="flex min-h-12 items-center justify-center gap-1.5 rounded-2xl bg-primary px-5 text-base font-bold text-primary-foreground shadow-sm transition-colors hover:bg-primary/90"
|
|
|
|
|
|
>
|
|
|
|
|
|
Se scorekort og statistikk
|
|
|
|
|
|
<ChevronRight aria-hidden="true" className="size-5" />
|
|
|
|
|
|
</Link>
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Score så langt (kompakt linje + utvidbar full oversikt) --------------
|
|
|
|
|
|
// Etterspurt av bruker 2026-07-25 med referanse til en annen golf-app --
|
|
|
|
|
|
// samme "Vis full oversikt"-mønster som allerede etablert i
|
|
|
|
|
|
// session-scorecard.tsx (turnering-scoring), tilpasset til en enkelt
|
|
|
|
|
|
// spillers frittstående runde (ingen motstander-side å vise her).
|
|
|
|
|
|
//
|
|
|
|
|
|
// Utvidet SAMME DAG, etterspurt av bruker: netto-/stableford-sum, putt-/
|
|
|
|
|
|
// kølle-statistikk-totaler, og grafisk fairway-/innspill-fordeling +
|
|
|
|
|
|
// gjennomsnittlig score-til-par splittet på treff/bom. Appen har ingen
|
|
|
|
|
|
// egen "spilleform"-innstilling for frittstående runder -- stableford
|
|
|
|
|
|
// beregnes derfor alltid ut fra netto score der det er mulig (krever
|
|
|
|
|
|
// registrert HCP), uavhengig av om brukeren "egentlig" spiller slagspill
|
|
|
|
|
|
// eller stableford -- rent informativt, ikke en offisiell poengsum.
|
|
|
|
|
|
|
|
|
|
|
|
// Standard stableford-poengtabell: netto dobbel bogey eller dårligere = 0,
|
|
|
|
|
|
// netto par = 2, hvert slag bedre/dårligere enn par gir ett poeng mer/mindre.
|
|
|
|
|
|
function stablefordPoints(netScore: number, par: number): number {
|
|
|
|
|
|
return Math.max(0, 2 - (netScore - par))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function avgScoreToPar(holes: ApiHole[]): number | null {
|
|
|
|
|
|
const played = holes.filter((h) => h.played && h.score !== null)
|
|
|
|
|
|
if (played.length === 0) return null
|
|
|
|
|
|
const diffSum = played.reduce((sum, h) => sum + ((h.score as number) - h.par), 0)
|
|
|
|
|
|
return diffSum / played.length
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatSignedAvg(n: number): string {
|
|
|
|
|
|
const fixed = Math.abs(n).toFixed(2)
|
|
|
|
|
|
return n > 0 ? `+${fixed}` : n < 0 ? `-${fixed}` : fixed
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function ScoreSoFar({ holes, order }: { holes: ApiHole[] | undefined; order: number[] }) {
|
|
|
|
|
|
const [expanded, setExpanded] = useState(false)
|
|
|
|
|
|
if (!holes || holes.length === 0) return null
|
|
|
|
|
|
|
|
|
|
|
|
const orderedHoles = order
|
|
|
|
|
|
.map((n) => holes.find((h) => h.hole_number === n))
|
|
|
|
|
|
.filter((h): h is ApiHole => h !== undefined)
|
|
|
|
|
|
const playedSoFar = orderedHoles.filter((h) => h.played && h.score !== null)
|
|
|
|
|
|
if (playedSoFar.length === 0) return null
|
|
|
|
|
|
|
|
|
|
|
|
const totalStrokes = playedSoFar.reduce((sum, h) => sum + (h.score ?? 0), 0)
|
|
|
|
|
|
const totalPar = playedSoFar.reduce((sum, h) => sum + h.par, 0)
|
|
|
|
|
|
const toPar = totalStrokes - totalPar
|
|
|
|
|
|
const toParLabel = toPar === 0 ? "Par" : toPar > 0 ? `+${toPar}` : `${toPar}`
|
|
|
|
|
|
|
|
|
|
|
|
// Netto/stableford krever at deltakeren har en beregnet course handicap
|
|
|
|
|
|
// (strokes_received er da satt på ALLE hull, ellers på ingen) -- uten
|
|
|
|
|
|
// det vises rett og slett ikke disse to (f.eks. en gjest uten HCP).
|
|
|
|
|
|
const withNetto = playedSoFar.filter((h) => h.strokes_received !== null)
|
|
|
|
|
|
const hasNetto = withNetto.length > 0
|
|
|
|
|
|
const nettoTotal = hasNetto
|
|
|
|
|
|
? withNetto.reduce((sum, h) => sum + ((h.score as number) - (h.strokes_received as number)), 0)
|
|
|
|
|
|
: null
|
|
|
|
|
|
const stablefordTotal = hasNetto
|
|
|
|
|
|
? withNetto.reduce(
|
|
|
|
|
|
(sum, h) => sum + stablefordPoints((h.score as number) - (h.strokes_received as number), h.par),
|
|
|
|
|
|
0,
|
|
|
|
|
|
)
|
|
|
|
|
|
: null
|
|
|
|
|
|
|
|
|
|
|
|
const puttsPlayed = orderedHoles.filter((h) => h.putts !== null)
|
|
|
|
|
|
const puttsTotal = puttsPlayed.length > 0 ? puttsPlayed.reduce((sum, h) => sum + (h.putts as number), 0) : null
|
|
|
|
|
|
const chipPlayed = orderedHoles.filter((h) => h.chip_count !== null)
|
|
|
|
|
|
const chipTotal = chipPlayed.length > 0 ? chipPlayed.reduce((sum, h) => sum + (h.chip_count as number), 0) : null
|
|
|
|
|
|
const bunkerPlayed = orderedHoles.filter((h) => h.bunker_shot_count !== null)
|
|
|
|
|
|
const bunkerTotal =
|
|
|
|
|
|
bunkerPlayed.length > 0 ? bunkerPlayed.reduce((sum, h) => sum + (h.bunker_shot_count as number), 0) : null
|
|
|
|
|
|
const penaltyPlayed = orderedHoles.filter((h) => h.penalty_strokes !== null)
|
|
|
|
|
|
const penaltyTotal =
|
|
|
|
|
|
penaltyPlayed.length > 0 ? penaltyPlayed.reduce((sum, h) => sum + (h.penalty_strokes as number), 0) : null
|
|
|
|
|
|
const anywayPlayed = orderedHoles.filter((h) => h.anyway_strokes !== null)
|
|
|
|
|
|
const anywayTotal =
|
|
|
|
|
|
anywayPlayed.length > 0 ? anywayPlayed.reduce((sum, h) => sum + (h.anyway_strokes as number), 0) : null
|
|
|
|
|
|
|
|
|
|
|
|
// Fairwaytreff spores kun på hull som ikke er par 3 (samme regel som
|
|
|
|
|
|
// registrerings-skjemaet -- DirectionCross for Utslag vises ikke der).
|
|
|
|
|
|
const fairwayTracked = orderedHoles.filter((h) => h.par !== 3 && h.tee_shot_result !== null)
|
|
|
|
|
|
const fairwayHit = fairwayTracked.filter((h) => h.tee_shot_result === "fairway")
|
|
|
|
|
|
const fairwayLeft = fairwayTracked.filter((h) => h.tee_shot_result === "left")
|
|
|
|
|
|
const fairwayRight = fairwayTracked.filter((h) => h.tee_shot_result === "right")
|
|
|
|
|
|
const fairwayMiss = [...fairwayLeft, ...fairwayRight]
|
|
|
|
|
|
const avgToParFairwayHit = avgScoreToPar(fairwayHit)
|
|
|
|
|
|
const avgToParFairwayMiss = avgScoreToPar(fairwayMiss)
|
|
|
|
|
|
|
|
|
|
|
|
const approachTracked = orderedHoles.filter((h) => h.approach_result !== null)
|
|
|
|
|
|
const approachHit = approachTracked.filter((h) => h.approach_result === "hit")
|
|
|
|
|
|
const approachShort = approachTracked.filter((h) => h.approach_result === "short")
|
|
|
|
|
|
const approachLong = approachTracked.filter((h) => h.approach_result === "long")
|
|
|
|
|
|
const approachLeft = approachTracked.filter((h) => h.approach_result === "left")
|
|
|
|
|
|
const approachRight = approachTracked.filter((h) => h.approach_result === "right")
|
|
|
|
|
|
const approachMiss = [...approachShort, ...approachLong, ...approachLeft, ...approachRight]
|
|
|
|
|
|
const avgToParApproachHit = avgScoreToPar(approachHit)
|
|
|
|
|
|
const avgToParApproachMiss = avgScoreToPar(approachMiss)
|
|
|
|
|
|
|
|
|
|
|
|
let running = 0
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mb-4 overflow-hidden rounded-2xl border border-border bg-card">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setExpanded((v) => !v)}
|
|
|
|
|
|
aria-expanded={expanded}
|
|
|
|
|
|
className="flex w-full items-center justify-between gap-3 px-4 py-3 text-left"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground text-pretty">
|
|
|
|
|
|
Så langt: {playedSoFar.length} hull · {totalStrokes} slag · {toParLabel} til par
|
|
|
|
|
|
{hasNetto && ` · Netto ${nettoTotal! > 0 ? "+" : ""}${nettoTotal}`}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="shrink-0 text-sm font-semibold text-primary">{expanded ? "Skjul" : "Vis full oversikt"}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{expanded && (
|
|
|
|
|
|
<div className="border-t border-border">
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2 p-4 sm:grid-cols-3">
|
|
|
|
|
|
<StatPill label="Slag" value={String(totalStrokes)} />
|
|
|
|
|
|
<StatPill label="Til par" value={toParLabel} />
|
|
|
|
|
|
{hasNetto && <StatPill label="Netto" value={nettoTotal! > 0 ? `+${nettoTotal}` : String(nettoTotal)} />}
|
|
|
|
|
|
{hasNetto && <StatPill label="Stableford" value={`${stablefordTotal} poeng`} />}
|
|
|
|
|
|
{puttsTotal !== null && <StatPill label="Putt" value={String(puttsTotal)} />}
|
|
|
|
|
|
{chipTotal !== null && <StatPill label="Chip" value={String(chipTotal)} />}
|
|
|
|
|
|
{bunkerTotal !== null && <StatPill label="Bunker" value={String(bunkerTotal)} />}
|
|
|
|
|
|
{penaltyTotal !== null && <StatPill label="Straffeslag" value={String(penaltyTotal)} />}
|
|
|
|
|
|
{anywayTotal !== null && <StatPill label="Anywayslag" value={String(anywayTotal)} />}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="overflow-x-auto border-t border-border">
|
|
|
|
|
|
<table className="w-full min-w-[420px] text-sm">
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr className="border-b border-border text-left text-xs font-bold uppercase tracking-wide text-muted-foreground">
|
|
|
|
|
|
<th className="px-3 py-2">Hull</th>
|
|
|
|
|
|
<th className="px-3 py-2">Par</th>
|
|
|
|
|
|
<th className="px-3 py-2">Score</th>
|
|
|
|
|
|
<th className="px-3 py-2">Netto</th>
|
|
|
|
|
|
{hasNetto && <th className="px-3 py-2">Stableford</th>}
|
|
|
|
|
|
<th className="px-3 py-2">Sum</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
{orderedHoles.map((h) => {
|
|
|
|
|
|
const played = h.played && h.score !== null
|
|
|
|
|
|
if (played) running += h.score as number
|
|
|
|
|
|
const netto = played && h.strokes_received !== null ? (h.score as number) - h.strokes_received : null
|
|
|
|
|
|
const points = played && netto !== null ? stablefordPoints(netto, h.par) : null
|
|
|
|
|
|
return (
|
|
|
|
|
|
<tr key={h.hole_number} className="border-b border-border last:border-b-0">
|
|
|
|
|
|
<td className="px-3 py-2 font-bold tabular-nums text-foreground">{h.hole_number}</td>
|
|
|
|
|
|
<td className="px-3 py-2 tabular-nums text-muted-foreground">{h.par}</td>
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
<td className="px-3 py-2 tabular-nums font-semibold text-foreground">
|
|
|
|
|
|
{played ? (h.picked_up ? "PU" : h.score) : "–"}
|
|
|
|
|
|
</td>
|
Nytt horisontalt scorekort (/my-rounds/[id]/scorecard) — ekte HTML-tabell med faste kolonnebredder, ingen scroll i det hele tatt. Score-cellene bruker form (sirkel = under par, firkant = over par) + fylt/ufylt i stedet for farge alene, med egen symbolforklaring. Ut/Inn-blokker for 18-hulls runder, én blokk for 9-hulls. Egen dedikert side (ikke sammenslått med statistikk-siden), lenket fra både "Runde fullført"-banneret og fra statistikk-siden. Byttet ut V0s egen forenklede HCP-formel med backends allerede beregnede strokes_received, så det ikke finnes to ulike slagfordelings-implementasjoner i appen. Den gamle vertikale scorekort-tabellen i statistikk-siden er fjernet — nå bor det rå scorekortet kun ett sted.
Anywayslag-fiksen din: lagt til som en tredje flis ved siden av Bunkerslag/Straffeslag, og seksjonen omdøpt fra "Chip, bunker og straffeslag" til "Annet" — notert i .md-filene at et notatfelt trolig kommer dit senere.
Ekte typesjekket build, ingen backend-endring nødvendig, teeoff.no upåvirket.
2026-07-24 23:33:41 +02:00
|
|
|
|
<td className="px-3 py-2 tabular-nums text-muted-foreground">{netto ?? "–"}</td>
|
|
|
|
|
|
{hasNetto && (
|
|
|
|
|
|
<td className="px-3 py-2 tabular-nums text-muted-foreground">{points ?? "–"}</td>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<td className="px-3 py-2 tabular-nums font-bold text-foreground">{played ? running : "–"}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{fairwayTracked.length > 0 && (
|
|
|
|
|
|
<div className="flex flex-col gap-2 border-t border-border p-4">
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">Fairwaytreff</span>
|
|
|
|
|
|
<DistributionBar
|
|
|
|
|
|
segments={[
|
|
|
|
|
|
{ label: "Fairway", count: fairwayHit.length, colorClass: "bg-emerald-500" },
|
|
|
|
|
|
{ label: "Venstre", count: fairwayLeft.length, colorClass: "bg-amber-500" },
|
|
|
|
|
|
{ label: "Høyre", count: fairwayRight.length, colorClass: "bg-rose-500" },
|
|
|
|
|
|
]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{(avgToParFairwayHit !== null || avgToParFairwayMiss !== null) && (
|
|
|
|
|
|
<div className="mt-1 flex flex-wrap gap-x-4 gap-y-1 text-xs font-semibold text-muted-foreground">
|
|
|
|
|
|
{avgToParFairwayHit !== null && (
|
|
|
|
|
|
<span>
|
|
|
|
|
|
Til par ved fairwaytreff:{" "}
|
|
|
|
|
|
<strong className="text-foreground">{formatSignedAvg(avgToParFairwayHit)}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{avgToParFairwayMiss !== null && (
|
|
|
|
|
|
<span>
|
|
|
|
|
|
Til par ved bom på fairway:{" "}
|
|
|
|
|
|
<strong className="text-foreground">{formatSignedAvg(avgToParFairwayMiss)}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{approachTracked.length > 0 && (
|
|
|
|
|
|
<div className="flex flex-col gap-2 border-t border-border p-4">
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">Innspill</span>
|
|
|
|
|
|
<DistributionBar
|
|
|
|
|
|
segments={[
|
|
|
|
|
|
{ label: "Traff", count: approachHit.length, colorClass: "bg-emerald-500" },
|
|
|
|
|
|
{ label: "Kort", count: approachShort.length, colorClass: "bg-amber-500" },
|
|
|
|
|
|
{ label: "Langt", count: approachLong.length, colorClass: "bg-orange-500" },
|
|
|
|
|
|
{ label: "Venstre", count: approachLeft.length, colorClass: "bg-sky-500" },
|
|
|
|
|
|
{ label: "Høyre", count: approachRight.length, colorClass: "bg-rose-500" },
|
|
|
|
|
|
]}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{(avgToParApproachHit !== null || avgToParApproachMiss !== null) && (
|
|
|
|
|
|
<div className="mt-1 flex flex-wrap gap-x-4 gap-y-1 text-xs font-semibold text-muted-foreground">
|
|
|
|
|
|
{avgToParApproachHit !== null && (
|
|
|
|
|
|
<span>
|
|
|
|
|
|
Til par ved treff på innspill:{" "}
|
|
|
|
|
|
<strong className="text-foreground">{formatSignedAvg(avgToParApproachHit)}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{avgToParApproachMiss !== null && (
|
|
|
|
|
|
<span>
|
|
|
|
|
|
Til par ved bom på innspill:{" "}
|
|
|
|
|
|
<strong className="text-foreground">{formatSignedAvg(avgToParApproachMiss)}</strong>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function StatPill({ label, value }: { label: string; value: string }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex flex-col items-center justify-center gap-0.5 rounded-xl border border-border bg-background px-2 py-2.5 text-center">
|
|
|
|
|
|
<span className="text-lg font-extrabold tabular-nums text-foreground">{value}</span>
|
|
|
|
|
|
<span className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">{label}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Ett rektangel delt i fargede segmenter proporsjonalt med antall --
|
|
|
|
|
|
// samme visuelle idé som SegmentedBar i tournament-leaderboard.tsx, men
|
|
|
|
|
|
// generalisert til N kategorier (ikke bare to lag).
|
|
|
|
|
|
function DistributionBar({ segments }: { segments: { label: string; count: number; colorClass: string }[] }) {
|
|
|
|
|
|
const total = segments.reduce((sum, s) => sum + s.count, 0)
|
|
|
|
|
|
if (total === 0) return null
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<div className="flex h-5 w-full overflow-hidden rounded-full border border-border">
|
|
|
|
|
|
{segments
|
|
|
|
|
|
.filter((s) => s.count > 0)
|
|
|
|
|
|
.map((s) => (
|
|
|
|
|
|
<div key={s.label} aria-hidden="true" style={{ width: `${(s.count / total) * 100}%` }} className={s.colorClass} />
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex flex-wrap gap-x-4 gap-y-1">
|
|
|
|
|
|
{segments
|
|
|
|
|
|
.filter((s) => s.count > 0)
|
|
|
|
|
|
.map((s) => (
|
|
|
|
|
|
<span key={s.label} className="flex items-center gap-1.5 text-xs font-semibold text-muted-foreground">
|
|
|
|
|
|
<span aria-hidden="true" className={cn("inline-block size-2.5 rounded-full", s.colorClass)} />
|
|
|
|
|
|
{s.label} {Math.round((s.count / total) * 100)}% ({s.count})
|
|
|
|
|
|
</span>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
// --- Sider (ADR-039 Beslutning A) -------------------------------------------
|
|
|
|
|
|
// Kun for match/fourball/foursome/greensome/scramble -- nøyaktig to sider,
|
|
|
|
|
|
// hver med et forventet spillerantall avhengig av format. Sidene opprettes
|
|
|
|
|
|
// tomme (POST .../sides), deltakere tildeles/flyttes deretter via PATCH
|
|
|
|
|
|
// .../participants/{id} med round_side_id.
|
|
|
|
|
|
|
|
|
|
|
|
function SidesPanel({
|
|
|
|
|
|
roundId,
|
|
|
|
|
|
round,
|
|
|
|
|
|
players,
|
|
|
|
|
|
canManage,
|
|
|
|
|
|
onSidesChanged,
|
|
|
|
|
|
onPatchParticipant,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
roundId: string
|
|
|
|
|
|
round: ApiRound
|
|
|
|
|
|
players: Player[]
|
|
|
|
|
|
canManage: boolean
|
|
|
|
|
|
onSidesChanged: () => Promise<void>
|
|
|
|
|
|
onPatchParticipant: (participantId: string, body: Record<string, unknown>) => Promise<{ ok: true } | { ok: false; message: string }>
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const [labelDrafts, setLabelDrafts] = useState<Record<number, string>>({})
|
|
|
|
|
|
const [busy, setBusy] = useState(false)
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
const expected = SIDE_PLAYER_COUNT[round.play_format] ?? 1
|
|
|
|
|
|
const slots: (ApiSide | null)[] = [round.sides[0] ?? null, round.sides[1] ?? null]
|
|
|
|
|
|
const unassigned = players.filter((p) => p.roundSideId === null)
|
|
|
|
|
|
|
|
|
|
|
|
async function createSide(slotIndex: number) {
|
|
|
|
|
|
setBusy(true)
|
|
|
|
|
|
setError(null)
|
|
|
|
|
|
const res = await fetch(`/rounds/${roundId}/sides`, {
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
|
credentials: "include",
|
|
|
|
|
|
body: JSON.stringify({ label: labelDrafts[slotIndex]?.trim() || null }),
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
const body = await res.json().catch(() => null)
|
|
|
|
|
|
setError(body?.detail?.message ?? "Klarte ikke å opprette siden.")
|
|
|
|
|
|
setBusy(false)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
await onSidesChanged()
|
|
|
|
|
|
setBusy(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function deleteSide(sideId: string) {
|
|
|
|
|
|
if (!confirm("Slette denne siden? Deltakerne blir liggende i runden, uten side-tilhørighet.")) return
|
|
|
|
|
|
setBusy(true)
|
|
|
|
|
|
setError(null)
|
|
|
|
|
|
const res = await fetch(`/rounds/${roundId}/sides/${sideId}`, { method: "DELETE", credentials: "include" })
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
const body = await res.json().catch(() => null)
|
|
|
|
|
|
setError(body?.detail?.message ?? "Klarte ikke å slette siden.")
|
|
|
|
|
|
}
|
|
|
|
|
|
await onSidesChanged()
|
|
|
|
|
|
setBusy(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function assign(participantId: string, sideId: string | null) {
|
|
|
|
|
|
setBusy(true)
|
|
|
|
|
|
setError(null)
|
|
|
|
|
|
const result = await onPatchParticipant(participantId, { round_side_id: sideId })
|
|
|
|
|
|
if (!result.ok) {
|
|
|
|
|
|
setError(result.message)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// patchParticipant oppdaterer kun deltaker-listen lokalt -- setup_complete/
|
|
|
|
|
|
// setup_message/playing_handicap er beregnet SERVER-side og krever en
|
|
|
|
|
|
// full runde-refetch for å ikke bli stående utdatert (2026-07-28, funnet
|
|
|
|
|
|
// under browserverifisering: meldingen "ikke tildelt en side" ble
|
|
|
|
|
|
// stående etter en vellykket tildeling).
|
|
|
|
|
|
await onSidesChanged()
|
|
|
|
|
|
}
|
|
|
|
|
|
setBusy(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!canManage && round.sides.length === 0) return null
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mb-5 flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 sm:p-5">
|
|
|
|
|
|
<div className="flex items-center justify-between gap-2">
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">
|
|
|
|
|
|
Sider · {expected} spiller{expected !== 1 ? "e" : ""} per side
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{error && <p className="text-sm font-medium text-destructive">{error}</p>}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
|
|
|
|
{slots.map((side, slotIndex) => {
|
|
|
|
|
|
const assignedPlayers = side ? players.filter((p) => p.roundSideId === side.id) : []
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={slotIndex} className="flex flex-col gap-2 rounded-xl border border-dashed border-border p-3">
|
|
|
|
|
|
{side ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="flex items-center justify-between gap-2">
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">
|
|
|
|
|
|
{side.label?.trim() || `Side ${slotIndex === 0 ? "A" : "B"}`}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{canManage && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={busy}
|
|
|
|
|
|
onClick={() => deleteSide(side.id)}
|
|
|
|
|
|
aria-label="Slett siden"
|
|
|
|
|
|
className="flex min-h-8 min-w-8 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive disabled:opacity-60"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 aria-hidden="true" className="size-4" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span className="text-xs font-semibold tabular-nums text-muted-foreground">
|
|
|
|
|
|
{assignedPlayers.length}/{expected} spillere
|
|
|
|
|
|
{assignedPlayers.some((p) => p.playingHandicap !== null) && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{" · "}
|
|
|
|
|
|
Match-hcp{" "}
|
|
|
|
|
|
{assignedPlayers
|
|
|
|
|
|
.filter((p) => p.playingHandicap !== null)
|
|
|
|
|
|
.map((p) => p.playingHandicap)
|
|
|
|
|
|
.join("/")}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{assignedPlayers.length === 0 ? (
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">Ingen spillere ennå.</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ul className="flex flex-wrap gap-1.5">
|
|
|
|
|
|
{assignedPlayers.map((p) => (
|
|
|
|
|
|
<li
|
|
|
|
|
|
key={p.id}
|
|
|
|
|
|
className="inline-flex min-h-8 items-center gap-1.5 rounded-full border border-border bg-background px-2.5 text-sm font-semibold text-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
{p.name}
|
|
|
|
|
|
{canManage && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={busy}
|
|
|
|
|
|
onClick={() => assign(p.id, null)}
|
|
|
|
|
|
aria-label={`Fjern ${p.name} fra siden`}
|
|
|
|
|
|
className="flex size-5 items-center justify-center rounded-full text-muted-foreground hover:bg-accent hover:text-destructive"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X aria-hidden="true" className="size-3" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : canManage ? (
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={labelDrafts[slotIndex] ?? ""}
|
|
|
|
|
|
onChange={(e) => setLabelDrafts((prev) => ({ ...prev, [slotIndex]: e.target.value }))}
|
|
|
|
|
|
placeholder={`Navn på side (valgfritt)`}
|
|
|
|
|
|
className="h-10 rounded-xl text-sm"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
disabled={busy}
|
|
|
|
|
|
onClick={() => createSide(slotIndex)}
|
|
|
|
|
|
className="h-10 rounded-xl text-sm font-bold"
|
|
|
|
|
|
>
|
|
|
|
|
|
Opprett side
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">Ikke opprettet ennå.</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{canManage && unassigned.length > 0 && (
|
|
|
|
|
|
<div className="flex flex-col gap-2 border-t border-border pt-3">
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">Ikke tildelt en side</span>
|
|
|
|
|
|
<ul className="flex flex-col gap-2">
|
|
|
|
|
|
{unassigned.map((p) => (
|
|
|
|
|
|
<li key={p.id} className="flex flex-wrap items-center gap-2">
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">{p.name}</span>
|
|
|
|
|
|
{slots.map((side, slotIndex) => {
|
|
|
|
|
|
if (!side) return null
|
|
|
|
|
|
const count = players.filter((x) => x.roundSideId === side.id).length
|
|
|
|
|
|
const full = count >= expected
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={slotIndex}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={busy || full}
|
|
|
|
|
|
onClick={() => assign(p.id, side.id)}
|
|
|
|
|
|
className="inline-flex min-h-8 items-center rounded-full border border-border bg-card px-3 text-xs font-bold text-foreground transition-colors hover:bg-accent/50 disabled:opacity-40"
|
|
|
|
|
|
>
|
|
|
|
|
|
→ {side.label?.trim() || `Side ${slotIndex === 0 ? "A" : "B"}`}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Løpende matchstatus / skins-tavle (ADR-039) ----------------------------
|
|
|
|
|
|
// Avledet ved LESING (aldri lagret) fra GET .../format-result -- samme
|
|
|
|
|
|
// filosofi som resten av appen (leaderboard/HCP m.fl.). Slagspill trenger
|
|
|
|
|
|
// ingen egen visning her (eksisterende leaderboard dekker det).
|
|
|
|
|
|
|
2026-07-29 15:46:49 +02:00
|
|
|
|
// Per-hull-oppløsning (2026-07-29, samme kontrakt som round-scorecard.tsx sin
|
|
|
|
|
|
// ApiFormatHoleEntry/ApiFormatHoleOut) -- gjør det mulig å farge hver
|
|
|
|
|
|
// scorecelle etter hvem som VANT hullet, og å regne en løpende Stilling-rad,
|
|
|
|
|
|
// i stedet for kun et sluttresultat.
|
|
|
|
|
|
type ApiFormatHoleEntry = {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
label: string
|
|
|
|
|
|
side: "a" | "b" | null
|
|
|
|
|
|
gross: number | null
|
|
|
|
|
|
net: number | null
|
|
|
|
|
|
strokes_received: number | null
|
|
|
|
|
|
counted: boolean | null
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
// Københavner (2026-07-30) -- poeng denne deltakeren fikk PÅ DETTE hullet.
|
|
|
|
|
|
points: number | null
|
2026-07-29 15:46:49 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type ApiFormatHoleOut = {
|
|
|
|
|
|
hole_number: number
|
|
|
|
|
|
par: number
|
|
|
|
|
|
stroke_index: number
|
|
|
|
|
|
entries: ApiFormatHoleEntry[]
|
|
|
|
|
|
result: string | null
|
|
|
|
|
|
winner_ids: string[] | null
|
|
|
|
|
|
carried: boolean | null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
type ApiFlagResult = {
|
|
|
|
|
|
holes_completed: number
|
|
|
|
|
|
ran_out: boolean
|
|
|
|
|
|
strokes_remaining: number
|
|
|
|
|
|
label: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type ApiBBBHole = {
|
|
|
|
|
|
hole_number: number
|
|
|
|
|
|
bingo_participant_id: string | null
|
|
|
|
|
|
bango_participant_id: string | null
|
|
|
|
|
|
bongo_participant_id: string | null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
type ApiFormatResult = {
|
|
|
|
|
|
play_format: string
|
|
|
|
|
|
ready: boolean
|
|
|
|
|
|
match_lead: number | null
|
|
|
|
|
|
match_holes_played: number | null
|
|
|
|
|
|
match_holes_remaining: number | null
|
|
|
|
|
|
match_is_closed: boolean | null
|
|
|
|
|
|
match_is_dormie: boolean | null
|
|
|
|
|
|
match_status_text: string | null
|
|
|
|
|
|
hole_results: string[] | null
|
|
|
|
|
|
skins_won: Record<string, number> | null
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
// Åtte nye formater (2026-07-30) -- se app/routers/rounds.py sin
|
|
|
|
|
|
// RoundFormatResultOut for full kontrakt.
|
|
|
|
|
|
copenhagen_points: Record<string, number> | null
|
|
|
|
|
|
bbb_points: Record<string, number> | null
|
|
|
|
|
|
flag_results: Record<string, ApiFlagResult> | null
|
|
|
|
|
|
shamble_team_score: number | null
|
|
|
|
|
|
money_ball_team_score: number | null
|
|
|
|
|
|
hlh_points_a: number | null
|
|
|
|
|
|
hlh_points_b: number | null
|
2026-07-29 15:46:49 +02:00
|
|
|
|
holes: ApiFormatHoleOut[] | null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Kort statustekst UTEN backend sin bokstavelige "(A)"/"(B)"-suffiks --
|
|
|
|
|
|
// identitetsbanneret viser navnet separat (se IdentitySide), så suffikset
|
|
|
|
|
|
// ville vært en duplikat. Speiler handicap_engine.py sin
|
|
|
|
|
|
// MatchState.describe() presist.
|
|
|
|
|
|
function formatMatchStatus(lead: number, holesRemaining: number, isClosed: boolean, isDormie: boolean): string {
|
|
|
|
|
|
const margin = Math.abs(lead)
|
|
|
|
|
|
if (isClosed) return holesRemaining === 0 && margin > 0 ? `${margin} UP` : `${margin}&${holesRemaining}`
|
|
|
|
|
|
if (lead === 0) return "AS"
|
|
|
|
|
|
return isDormie ? `Dormie ${margin}` : `${margin} UP`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Speiler samme funksjon i handicap_engine.py/round-scorecard.tsx, men
|
|
|
|
|
|
// regnet ETT PREFIKS om gangen langs `holeOrder` (spillerekkefølgen, ikke
|
|
|
|
|
|
// nødvendigvis fysisk hullnummer) -- gir en løpende stilling PER HULL i
|
|
|
|
|
|
// stedet for kun sluttresultatet. Stopper (viser ingen verdi videre) ved
|
|
|
|
|
|
// første hull uten et registrert resultat, siden hull avgjøres sekvensielt.
|
|
|
|
|
|
function matchStillingByHole(
|
|
|
|
|
|
holeOrder: number[],
|
|
|
|
|
|
formatResult: ApiFormatResult | null,
|
|
|
|
|
|
totalHoles: number,
|
|
|
|
|
|
): Map<number, { label: string; tone: "a" | "b" | "neutral" }> {
|
|
|
|
|
|
const out = new Map<number, { label: string; tone: "a" | "b" | "neutral" }>()
|
|
|
|
|
|
if (!formatResult?.holes) return out
|
|
|
|
|
|
const byHole = new Map(formatResult.holes.map((h) => [h.hole_number, h]))
|
|
|
|
|
|
let lead = 0
|
|
|
|
|
|
let holesPlayed = 0
|
|
|
|
|
|
for (const n of holeOrder) {
|
|
|
|
|
|
const h = byHole.get(n)
|
|
|
|
|
|
if (!h || !h.result) break
|
|
|
|
|
|
holesPlayed += 1
|
|
|
|
|
|
lead += h.result === "a" ? 1 : h.result === "b" ? -1 : 0
|
|
|
|
|
|
const holesRemaining = totalHoles - holesPlayed
|
|
|
|
|
|
const isClosed = Math.abs(lead) > holesRemaining
|
|
|
|
|
|
const isDormie = !isClosed && holesRemaining > 0 && Math.abs(lead) === holesRemaining
|
|
|
|
|
|
out.set(n, {
|
|
|
|
|
|
label: formatMatchStatus(lead, holesRemaining, isClosed, isDormie),
|
|
|
|
|
|
tone: lead > 0 ? "a" : lead < 0 ? "b" : "neutral",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
return out
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-29 15:46:49 +02:00
|
|
|
|
// Identitet (navn+HCP) for en side i banneret/gridet -- viser SPILLERENS
|
|
|
|
|
|
// navn når nøyaktig én er tildelt siden (match, eller en komplett foursome-
|
|
|
|
|
|
// side sett fra én spiller... nei, alltid nøyaktig 1 for match), ellers
|
|
|
|
|
|
// sidens eget (evt. generiske) navn uten noen entydig HCP å vise.
|
|
|
|
|
|
function sideIdentity(round: ApiRound, players: Player[], side: "a" | "b"): { name: string; hcp: number | null } {
|
|
|
|
|
|
const idx = side === "a" ? 0 : 1
|
|
|
|
|
|
const sideId = round.sides[idx]?.id
|
|
|
|
|
|
const sidePlayers = players.filter((p) => p.roundSideId === sideId)
|
|
|
|
|
|
if (sidePlayers.length === 1) return { name: sidePlayers[0].name, hcp: sidePlayers[0].playingHandicap }
|
|
|
|
|
|
return { name: round.sides[idx]?.label?.trim() || (side === "a" ? "Side A" : "Side B"), hcp: null }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function IdentitySide({ identity, side }: { identity: { name: string; hcp: number | null }; side: "a" | "b" }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className={cn("flex min-w-0 flex-1 flex-col gap-0.5", side === "a" ? "items-start" : "items-end")}>
|
|
|
|
|
|
<div className={cn("flex min-w-0 items-center gap-1.5", side === "b" && "flex-row-reverse")}>
|
|
|
|
|
|
<span
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
className={cn("size-2 shrink-0 rounded-full", side === "a" ? "bg-primary" : "bg-brand-orange")}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="min-w-0 truncate text-sm font-extrabold text-foreground">{identity.name}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{identity.hcp !== null && (
|
|
|
|
|
|
<span className="text-xs font-bold tabular-nums text-muted-foreground">HCP {identity.hcp}</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-29 16:26:31 +02:00
|
|
|
|
// Territorium-bar (2026-07-29, erstatter det tidligere flate, likt-delte
|
|
|
|
|
|
// banneret) -- den ledende sidens sone strekker seg PROPORSJONALT forbi
|
|
|
|
|
|
// midtlinjen inn på motstanderens halvdel (brukerens eget forslag: "hele
|
|
|
|
|
|
// den halve delen som tilhører den som er en opp er markert over på
|
|
|
|
|
|
// motstanderens halvdel"), i stedet for en statisk 50/50-boks med tekst i
|
|
|
|
|
|
// midten. Ved AS (eller ingen hull spilt ennå) er begge soner nøyaktig like
|
|
|
|
|
|
// store OG likt (nøytralt) farget -- ingen side skal se ut til å lede.
|
|
|
|
|
|
// Bruker samme proporsjonale to-fargede-bar-idé som SegmentedBar
|
|
|
|
|
|
// (tournament-leaderboard.tsx), tilpasset lead-i-hull i stedet for poeng.
|
|
|
|
|
|
function leadZoneFraction(lead: number, totalHoles: number): number {
|
|
|
|
|
|
if (totalHoles <= 0) return 0.5
|
|
|
|
|
|
const fraction = 0.5 + (lead / totalHoles) * 0.5
|
|
|
|
|
|
return Math.min(0.82, Math.max(0.18, fraction))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function LeadZone({
|
|
|
|
|
|
identity,
|
|
|
|
|
|
side,
|
|
|
|
|
|
dominant,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
identity: { name: string; hcp: number | null }
|
|
|
|
|
|
side: "a" | "b"
|
|
|
|
|
|
dominant: boolean
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const textTone = dominant ? (side === "a" ? "text-primary-foreground" : "text-brand-orange-foreground") : "text-foreground"
|
|
|
|
|
|
const subTone = dominant
|
|
|
|
|
|
? side === "a"
|
|
|
|
|
|
? "text-primary-foreground/75"
|
|
|
|
|
|
: "text-brand-orange-foreground/75"
|
|
|
|
|
|
: "text-muted-foreground"
|
|
|
|
|
|
// Merk: den ytre boksen STREKKER SEG (ingen items-start/items-end her --
|
|
|
|
|
|
// det ville sizet boksen etter innholdets egen bredde, ikke etter sonens
|
|
|
|
|
|
// faktiske tildelte bredde, og kunne visuelt lekke inn i midt-kolonnen på
|
|
|
|
|
|
// smale skjermer). Venstre/høyre-justering skjer i stedet med
|
|
|
|
|
|
// justify-start/justify-end og text-align INNI en boks som alltid har
|
|
|
|
|
|
// sonens fulle, korrekte bredde -- truncate virker da presist.
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex h-full min-w-0 w-full flex-col justify-center gap-0.5 px-3">
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex min-w-0 items-center gap-1.5",
|
|
|
|
|
|
side === "a" ? "justify-start" : "justify-end",
|
|
|
|
|
|
side === "b" && "flex-row-reverse",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{!dominant && (
|
|
|
|
|
|
<span
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
className={cn("size-2 shrink-0 rounded-full", side === "a" ? "bg-primary" : "bg-brand-orange")}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<span className={cn("min-w-0 truncate text-sm font-extrabold", textTone)}>{identity.name}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{identity.hcp !== null && (
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"truncate text-xs font-bold tabular-nums",
|
|
|
|
|
|
subTone,
|
|
|
|
|
|
side === "a" ? "text-left" : "text-right",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
HCP {identity.hcp}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
type ApiFlightSummary = {
|
|
|
|
|
|
round_id: string
|
|
|
|
|
|
name: string | null
|
|
|
|
|
|
course_name_snapshot: string
|
|
|
|
|
|
played_at: string
|
|
|
|
|
|
started_at: string | null
|
|
|
|
|
|
completed_at: string | null
|
|
|
|
|
|
participant_count: number
|
|
|
|
|
|
is_anchor: boolean
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Flere flighter i én "vanlig" runde (migrasjon 035, retning 1 -- løs
|
|
|
|
|
|
// gruppering av separate `round`-rader, se FEATURE_BACKLOG.md). "+ Legg
|
|
|
|
|
|
// til en flight til" gjenbruker HELE eksisterende opprett-runde-flyten
|
|
|
|
|
|
// (/my-rounds/new) i stedet for et eget skjema -- forhåndsutfylt med denne
|
|
|
|
|
|
// flightens dato/starthull/hullantall/spilleform via URL-parametre, men
|
|
|
|
|
|
// banen velges på nytt (samme lave friksjon som normal bane-søk ellers).
|
|
|
|
|
|
function FlightGroupPanel({
|
|
|
|
|
|
roundId,
|
|
|
|
|
|
round,
|
|
|
|
|
|
isOwnerViewer,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
roundId: string
|
|
|
|
|
|
round: ApiRound
|
|
|
|
|
|
isOwnerViewer: boolean
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const [flights, setFlights] = useState<ApiFlightSummary[] | null>(null)
|
|
|
|
|
|
const [adding, setAdding] = useState(false)
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
fetch(`/rounds/${roundId}/flight-group`, { credentials: "include" })
|
|
|
|
|
|
.then((res) => (res.ok ? res.json() : null))
|
|
|
|
|
|
.then((data: { flight_group_id: string | null; flights: ApiFlightSummary[] } | null) => {
|
|
|
|
|
|
if (!cancelled) setFlights(data?.flights ?? null)
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {
|
|
|
|
|
|
if (!cancelled) setFlights(null)
|
|
|
|
|
|
})
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [roundId, round.flight_group_id])
|
|
|
|
|
|
|
|
|
|
|
|
async function addFlight() {
|
|
|
|
|
|
if (!isOwnerViewer) return
|
|
|
|
|
|
setAdding(true)
|
|
|
|
|
|
try {
|
|
|
|
|
|
let groupId = round.flight_group_id
|
|
|
|
|
|
if (!groupId) {
|
|
|
|
|
|
groupId = crypto.randomUUID()
|
|
|
|
|
|
await fetch(`/rounds/${roundId}`, {
|
|
|
|
|
|
method: "PATCH",
|
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
|
credentials: "include",
|
|
|
|
|
|
body: JSON.stringify({ flight_group_id: groupId }),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
|
|
flightGroupId: groupId,
|
|
|
|
|
|
playedAt: round.played_at,
|
|
|
|
|
|
startHole: String(round.start_hole),
|
|
|
|
|
|
holes: String(round.holes_planned),
|
|
|
|
|
|
playFormat: round.play_format,
|
|
|
|
|
|
})
|
|
|
|
|
|
router.push(`/my-rounds/new?${params.toString()}`)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setAdding(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const others = (flights ?? []).filter((f) => f.round_id !== roundId)
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mb-5 flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 sm:p-5">
|
|
|
|
|
|
<div className="flex items-center justify-between gap-2">
|
|
|
|
|
|
<span className="inline-flex items-center gap-1.5 text-sm font-bold text-foreground">
|
|
|
|
|
|
<Users aria-hidden="true" className="size-4 text-primary" />
|
|
|
|
|
|
Flighter
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{isOwnerViewer && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={addFlight}
|
|
|
|
|
|
disabled={adding}
|
|
|
|
|
|
className="inline-flex min-h-11 items-center gap-1.5 rounded-lg border border-dashed border-border px-3 text-sm font-semibold text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground disabled:opacity-60"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus aria-hidden="true" className="size-4" />
|
|
|
|
|
|
{adding ? "Setter opp…" : "Legg til en flight til"}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{others.length === 0 ? (
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
Kun denne flighten er satt opp. Legg til en flight til hvis flere spiller sammen med dere samme dag --
|
|
|
|
|
|
de deler da leaderboard med denne flighten.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<ul className="flex flex-col divide-y divide-border overflow-hidden rounded-xl border border-border">
|
|
|
|
|
|
{(flights ?? []).map((f) => (
|
|
|
|
|
|
<li key={f.round_id} className="flex items-center justify-between gap-3 px-4 py-2.5">
|
|
|
|
|
|
{f.round_id === roundId ? (
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">
|
|
|
|
|
|
{f.name || f.course_name_snapshot}{" "}
|
|
|
|
|
|
<span className="font-normal text-muted-foreground">(denne)</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href={`/my-rounds/${f.round_id}`}
|
|
|
|
|
|
className="text-sm font-semibold text-primary hover:underline"
|
|
|
|
|
|
>
|
|
|
|
|
|
{f.name || f.course_name_snapshot}
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<span className="text-xs font-semibold tabular-nums text-muted-foreground">
|
|
|
|
|
|
{f.participant_count} spiller{f.participant_count !== 1 ? "e" : ""}
|
|
|
|
|
|
{f.completed_at ? " · Ferdig" : ""}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
<Link
|
|
|
|
|
|
href={`/my-rounds/${roundId}/flights`}
|
|
|
|
|
|
className="inline-flex min-h-11 items-center gap-1.5 self-start text-sm font-bold text-primary hover:underline"
|
|
|
|
|
|
>
|
|
|
|
|
|
Se samlet leaderboard for flightene
|
|
|
|
|
|
<ArrowRight aria-hidden="true" className="size-4" />
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
function FormatResultPanel({
|
|
|
|
|
|
round,
|
|
|
|
|
|
players,
|
2026-07-29 15:46:49 +02:00
|
|
|
|
result,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
}: {
|
|
|
|
|
|
round: ApiRound
|
|
|
|
|
|
players: Player[]
|
2026-07-29 15:46:49 +02:00
|
|
|
|
result: ApiFormatResult | null
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
}) {
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
if (round.play_format === "stroke" || round.play_format === "stableford" || !result) return null
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
|
|
|
|
|
|
if (round.play_format === "skins") {
|
|
|
|
|
|
const won = result.skins_won ?? {}
|
|
|
|
|
|
const rows = players
|
|
|
|
|
|
.map((p) => ({ player: p, skins: won[p.id] ?? 0 }))
|
|
|
|
|
|
.sort((a, b) => b.skins - a.skins)
|
|
|
|
|
|
const totalAwarded = Object.values(won).reduce((sum, v) => sum + v, 0)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mb-5 flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 sm:p-5">
|
|
|
|
|
|
<div className="flex items-center justify-between gap-2">
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">Skins-tavle</span>
|
|
|
|
|
|
<span className="text-xs font-semibold tabular-nums text-muted-foreground">
|
|
|
|
|
|
{totalAwarded} skin{totalAwarded !== 1 ? "s" : ""} avgjort
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<ul className="flex flex-col divide-y divide-border overflow-hidden rounded-xl border border-border">
|
|
|
|
|
|
{rows.map(({ player, skins }) => (
|
|
|
|
|
|
<li key={player.id} className="flex items-center justify-between gap-3 px-4 py-2.5">
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">{player.name}</span>
|
|
|
|
|
|
<span className="text-base font-extrabold tabular-nums text-foreground">{skins}</span>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
// Københavner (2026-07-30) -- flatt felt, poengtabell (6 poeng deles
|
|
|
|
|
|
// per hull mellom nøyaktig 3 spillere -- flest poeng totalt vinner).
|
|
|
|
|
|
if (round.play_format === "copenhagen") {
|
|
|
|
|
|
const points = result.copenhagen_points ?? {}
|
|
|
|
|
|
const rows = players
|
|
|
|
|
|
.map((p) => ({ player: p, points: points[p.id] ?? 0 }))
|
|
|
|
|
|
.sort((a, b) => b.points - a.points)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mb-5 flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 sm:p-5">
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">Københavner -- poengtavle</span>
|
|
|
|
|
|
<ul className="flex flex-col divide-y divide-border overflow-hidden rounded-xl border border-border">
|
|
|
|
|
|
{rows.map(({ player, points: pts }) => (
|
|
|
|
|
|
<li key={player.id} className="flex items-center justify-between gap-3 px-4 py-2.5">
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">{player.name}</span>
|
|
|
|
|
|
<span className="text-base font-extrabold tabular-nums text-foreground">{pts}</span>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Bingo Bango Bongo (2026-07-30) -- flatt felt, poengtabell (registreres
|
|
|
|
|
|
// per hull via egen BBBHolePicker, se lenger ned i Score-fanen).
|
|
|
|
|
|
if (round.play_format === "bbb") {
|
|
|
|
|
|
const points = result.bbb_points ?? {}
|
|
|
|
|
|
const rows = players
|
|
|
|
|
|
.map((p) => ({ player: p, points: points[p.id] ?? 0 }))
|
|
|
|
|
|
.sort((a, b) => b.points - a.points)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mb-5 flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 sm:p-5">
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">Bingo Bango Bongo -- poengtavle</span>
|
|
|
|
|
|
<ul className="flex flex-col divide-y divide-border overflow-hidden rounded-xl border border-border">
|
|
|
|
|
|
{rows.map(({ player, points: pts }) => (
|
|
|
|
|
|
<li key={player.id} className="flex items-center justify-between gap-3 px-4 py-2.5">
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">{player.name}</span>
|
|
|
|
|
|
<span className="text-base font-extrabold tabular-nums text-foreground">{pts}</span>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Flaggturnering (2026-07-30) -- flatt felt, individuelt slagbudsjett.
|
|
|
|
|
|
// Flest fullførte hull vinner (ran_out=true, gikk tom for slag).
|
|
|
|
|
|
if (round.play_format === "flag") {
|
|
|
|
|
|
const results = result.flag_results ?? {}
|
|
|
|
|
|
const rows = Object.entries(results)
|
|
|
|
|
|
.map(([id, r]) => ({ id, ...r }))
|
|
|
|
|
|
.sort((a, b) => b.holes_completed - a.holes_completed)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mb-5 flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 sm:p-5">
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">Flaggturnering</span>
|
|
|
|
|
|
{rows.length === 0 ? (
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">Venter på at handicap beregnes for minst én spiller.</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ul className="flex flex-col divide-y divide-border overflow-hidden rounded-xl border border-border">
|
|
|
|
|
|
{rows.map((r) => (
|
|
|
|
|
|
<li key={r.id} className="flex items-center justify-between gap-3 px-4 py-2.5">
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">{r.label}</span>
|
|
|
|
|
|
<span className="flex flex-col items-end">
|
|
|
|
|
|
<span className="text-base font-extrabold tabular-nums text-foreground">
|
|
|
|
|
|
{r.holes_completed} hull
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-xs tabular-nums text-muted-foreground">
|
|
|
|
|
|
{r.ran_out
|
|
|
|
|
|
? "gikk tom for slag"
|
|
|
|
|
|
: `${r.strokes_remaining} slag igjen`}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Shamble/Money Ball (2026-07-30) -- flatt felt, ETT lag = hele rundens
|
|
|
|
|
|
// deltakersett (ingen motstander innad -- flere lag kobles via
|
|
|
|
|
|
// flight_group_id-leaderboardet). Viser kun lagets samlede sum.
|
|
|
|
|
|
if (round.play_format === "shamble" || round.play_format === "money_ball") {
|
|
|
|
|
|
const teamScore = round.play_format === "shamble" ? result.shamble_team_score : result.money_ball_team_score
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mb-5 flex flex-col items-center gap-1 rounded-2xl border border-border bg-card p-5 text-center">
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">
|
|
|
|
|
|
{FORMAT_LABELS[round.play_format]} -- lagets samlede score
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-4xl font-extrabold tabular-nums text-foreground">{teamScore ?? "–"}</span>
|
|
|
|
|
|
{teamScore === null && (
|
|
|
|
|
|
<p className="text-xs text-muted-foreground">Venter på at alle spillerne registrerer på minst ett hull.</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// High-low-high (2026-07-30) -- literal poengsum (IKKE match-play
|
|
|
|
|
|
// "X UP" -- poeng fordeles 0-2 per hull via to uavhengige duelløer,
|
|
|
|
|
|
// se handicap_engine.py sin high_low_high_points_for_hole). Samme
|
|
|
|
|
|
// territorium-bar-visuelle språk som resten av to-sidede formater,
|
|
|
|
|
|
// men banneret viser "{poeng A}-{poeng B}" i stedet for en "X UP"-tekst.
|
|
|
|
|
|
if (round.play_format === "high_low_high") {
|
|
|
|
|
|
if (!result.ready) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mb-5 rounded-2xl border border-dashed border-border bg-card p-4 text-sm text-muted-foreground sm:p-5">
|
|
|
|
|
|
Venter på at begge sider er komplette og har beregnet handicap før High-low-high-status kan vises.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
const pa = result.hlh_points_a ?? 0
|
|
|
|
|
|
const pb = result.hlh_points_b ?? 0
|
|
|
|
|
|
const holesPlayed = result.match_holes_played ?? 0
|
|
|
|
|
|
const identityA = sideIdentity(round, players, "a")
|
|
|
|
|
|
const identityB = sideIdentity(round, players, "b")
|
|
|
|
|
|
const tone: "a" | "b" | "neutral" = pa > pb ? "a" : pb > pa ? "b" : "neutral"
|
|
|
|
|
|
const widthA = pa + pb > 0 ? (pa / (pa + pb)) * 100 : 50
|
|
|
|
|
|
const widthB = 100 - widthA
|
|
|
|
|
|
const zoneAClass = tone === "a" ? "bg-primary" : tone === "b" ? "bg-primary/12" : "bg-muted"
|
|
|
|
|
|
const zoneBClass = tone === "b" ? "bg-brand-orange" : tone === "a" ? "bg-brand-orange/12" : "bg-muted"
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mb-5 flex flex-col gap-2">
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">High-low-high</span>
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="grid h-[76px] w-full overflow-hidden rounded-2xl shadow-md shadow-black/8 sm:h-20"
|
|
|
|
|
|
style={{ gridTemplateColumns: `minmax(0, ${widthA}fr) auto minmax(0, ${widthB}fr)` }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className={cn("h-full min-w-0 transition-[grid-template-columns] duration-300", zoneAClass)}>
|
|
|
|
|
|
<LeadZone identity={identityA} side="a" dominant={tone === "a"} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex h-full items-center justify-center px-2">
|
|
|
|
|
|
<div className="flex flex-col items-center rounded-xl border border-border/60 bg-card/95 px-3 py-1.5 shadow-sm shadow-black/10 backdrop-blur-sm">
|
|
|
|
|
|
{holesPlayed > 0 ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"text-xl font-extrabold leading-none tabular-nums sm:text-2xl",
|
|
|
|
|
|
tone === "a" ? "text-primary" : tone === "b" ? "text-brand-orange" : "text-foreground",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{pa}-{pb}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="mt-1 text-[11px] font-semibold text-muted-foreground">{holesPlayed} hull spilt</span>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="text-xs font-bold text-muted-foreground text-pretty">Ingen hull spilt ennå</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className={cn("h-full min-w-0 transition-[grid-template-columns] duration-300", zoneBClass)}>
|
|
|
|
|
|
<LeadZone identity={identityB} side="b" dominant={tone === "b"} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// To-sidede formater (match/fourball/foursome/greensome/scramble/chapman).
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
if (!result.ready) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mb-5 rounded-2xl border border-dashed border-border bg-card p-4 text-sm text-muted-foreground sm:p-5">
|
|
|
|
|
|
Venter på at begge sider er komplette og har beregnet handicap før {FORMAT_LABELS[round.play_format] ?? round.play_format}-status kan vises.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-29 16:26:31 +02:00
|
|
|
|
// Identitetsbanner (2026-07-29, territorium-bar-versjon -- se leadZoneFraction
|
|
|
|
|
|
// over). Navn+HCP i hver sone, stor sentrert løpende status flytende oppå
|
|
|
|
|
|
// begge soner, "Ferdig"-merke når avgjort -- samme visuelle språk (nå) som
|
2026-07-29 15:46:49 +02:00
|
|
|
|
// MatchScorecardGrid (round-scorecard.tsx) og TournamentMatchGrid
|
|
|
|
|
|
// (session-scorecard.tsx), egen lokal kopi per prosjektets konvensjon.
|
|
|
|
|
|
const identityA = sideIdentity(round, players, "a")
|
|
|
|
|
|
const identityB = sideIdentity(round, players, "b")
|
|
|
|
|
|
const lead = result.match_lead ?? 0
|
|
|
|
|
|
const holesPlayed = result.match_holes_played ?? 0
|
|
|
|
|
|
const holesRemaining = result.match_holes_remaining ?? 0
|
|
|
|
|
|
const isClosed = result.match_is_closed ?? false
|
|
|
|
|
|
const isDormie = result.match_is_dormie ?? false
|
|
|
|
|
|
const tone: "a" | "b" | "neutral" = lead > 0 ? "a" : lead < 0 ? "b" : "neutral"
|
|
|
|
|
|
const statusLabel = holesPlayed > 0 ? formatMatchStatus(lead, holesRemaining, isClosed, isDormie) : null
|
2026-07-29 16:26:31 +02:00
|
|
|
|
const totalHoles = holesPlayed + holesRemaining
|
|
|
|
|
|
const widthA = holesPlayed > 0 ? leadZoneFraction(lead, totalHoles) * 100 : 50
|
|
|
|
|
|
const widthB = 100 - widthA
|
|
|
|
|
|
const zoneAClass = tone === "a" ? "bg-primary" : tone === "b" ? "bg-primary/12" : "bg-muted"
|
|
|
|
|
|
const zoneBClass = tone === "b" ? "bg-brand-orange" : tone === "a" ? "bg-brand-orange/12" : "bg-muted"
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
|
|
|
|
|
|
return (
|
2026-07-29 15:46:49 +02:00
|
|
|
|
<div className="mb-5 flex flex-col gap-2">
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">{FORMAT_LABELS[round.play_format] ?? round.play_format}</span>
|
2026-07-29 16:26:31 +02:00
|
|
|
|
{/* CSS Grid, ikke absolutt-posisjonert overlegg (2026-07-29-fiks) --
|
|
|
|
|
|
en tidligere versjon la statusen som en absolutt sentrert boks OPPÅ
|
|
|
|
|
|
navnetekst, som kunne overlappe et langt navn på smale skjermer.
|
|
|
|
|
|
Med grid-kolonner `minmax(0,X fr) auto minmax(0,Y fr)` reserverer
|
|
|
|
|
|
nettleseren ALLTID nøyaktig midt-kolonnens egen bredde til status-
|
|
|
|
|
|
boksen -- soneandelen forblir proporsjonal (X:Y), men kan aldri
|
|
|
|
|
|
overlappe, og lange navn trunkeres i stedet (samme `truncate`-
|
|
|
|
|
|
mekanisme som ellers). */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="grid h-[76px] w-full overflow-hidden rounded-2xl shadow-md shadow-black/8 sm:h-20"
|
|
|
|
|
|
style={{ gridTemplateColumns: `minmax(0, ${widthA}fr) auto minmax(0, ${widthB}fr)` }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className={cn("h-full min-w-0 transition-[grid-template-columns] duration-300", zoneAClass)}>
|
|
|
|
|
|
<LeadZone identity={identityA} side="a" dominant={tone === "a"} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex h-full items-center justify-center px-2">
|
|
|
|
|
|
<div className="flex flex-col items-center rounded-xl border border-border/60 bg-card/95 px-3 py-1.5 shadow-sm shadow-black/10 backdrop-blur-sm">
|
|
|
|
|
|
{statusLabel ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"text-xl font-extrabold leading-none tabular-nums sm:text-2xl",
|
|
|
|
|
|
tone === "a" ? "text-primary" : tone === "b" ? "text-brand-orange" : "text-foreground",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{statusLabel}
|
2026-07-29 15:46:49 +02:00
|
|
|
|
</span>
|
2026-07-29 16:26:31 +02:00
|
|
|
|
{isClosed ? (
|
|
|
|
|
|
<span className="mt-1 rounded-full bg-muted px-2 py-0.5 text-[10px] font-extrabold uppercase tracking-wide text-muted-foreground">
|
|
|
|
|
|
Ferdig
|
|
|
|
|
|
</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="mt-1 text-[11px] font-semibold text-muted-foreground">
|
|
|
|
|
|
{holesPlayed} hull spilt
|
|
|
|
|
|
{holesRemaining > 0 && isDormie && " · dormie"}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="text-xs font-bold text-muted-foreground text-pretty">Ingen hull spilt ennå</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className={cn("h-full min-w-0 transition-[grid-template-columns] duration-300", zoneBClass)}>
|
|
|
|
|
|
<LeadZone identity={identityB} side="b" dominant={tone === "b"} />
|
2026-07-29 15:46:49 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
// --- Nassau Match Play (2026-07-30) -- selvstendig, henter sin egen data --
|
|
|
|
|
|
// tre parallelle vinduer av et EKSISTERENDE to-sidet format, ingen egen
|
|
|
|
|
|
// lagring (se app/routers/rounds.py sin get_round_nassau_result).
|
|
|
|
|
|
|
|
|
|
|
|
type ApiNassauWindow = {
|
|
|
|
|
|
lead: number
|
|
|
|
|
|
holes_played: number
|
|
|
|
|
|
holes_remaining: number
|
|
|
|
|
|
is_closed: boolean
|
|
|
|
|
|
is_dormie: boolean
|
|
|
|
|
|
status_text: string
|
|
|
|
|
|
}
|
|
|
|
|
|
type ApiNassauResult = {
|
|
|
|
|
|
ready: boolean
|
|
|
|
|
|
front9: ApiNassauWindow | null
|
|
|
|
|
|
back9: ApiNassauWindow | null
|
|
|
|
|
|
overall: ApiNassauWindow | null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function NassauWindowCard({ label, window }: { label: string; window: ApiNassauWindow | null }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex flex-col items-center gap-1 rounded-xl border border-border bg-background px-3 py-3">
|
|
|
|
|
|
<span className="text-xs font-bold uppercase tracking-wide text-muted-foreground">{label}</span>
|
|
|
|
|
|
{window && window.holes_played > 0 ? (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<span className="text-lg font-extrabold tabular-nums text-foreground">{window.status_text}</span>
|
|
|
|
|
|
<span className="text-[11px] text-muted-foreground">{window.holes_played} hull spilt</span>
|
|
|
|
|
|
</>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="text-sm text-muted-foreground">–</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function NassauPanel({ roundId, formatResultRefreshTick }: { roundId: string; formatResultRefreshTick: number }) {
|
|
|
|
|
|
const [result, setResult] = useState<ApiNassauResult | null>(null)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
fetch(`/rounds/${roundId}/format-result/nassau`, { credentials: "include" })
|
|
|
|
|
|
.then((res) => (res.ok ? res.json() : null))
|
|
|
|
|
|
.then((data: ApiNassauResult | null) => {
|
|
|
|
|
|
if (!cancelled) setResult(data)
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {})
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [roundId, formatResultRefreshTick])
|
|
|
|
|
|
|
|
|
|
|
|
if (!result?.ready) return null
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mb-5 flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 sm:p-5">
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">Nassau</span>
|
|
|
|
|
|
<div className="grid grid-cols-3 gap-2">
|
|
|
|
|
|
<NassauWindowCard label="Ut (1-9)" window={result.front9} />
|
|
|
|
|
|
<NassauWindowCard label="Inn (10-18)" window={result.back9} />
|
|
|
|
|
|
<NassauWindowCard label="Totalt" window={result.overall} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Bingo Bango Bongo -- per-hull-observasjon (2026-07-30) -----------------
|
|
|
|
|
|
// Tre uavhengige kategorier (først på green/nærmest hull/først i hull),
|
|
|
|
|
|
// manuelt registrert av den som fører score -- ikke GPS/live-sporet. Én
|
|
|
|
|
|
// spiller kan vinne flere/alle kategorier på samme hull (sistnevnte utløser
|
|
|
|
|
|
// evt. sweep-bonusen, se compute_bbb). Klikk på en allerede valgt spiller
|
|
|
|
|
|
// fjerner valget (samme "toggle av/på"-mønster som resten av appen).
|
|
|
|
|
|
|
|
|
|
|
|
function BBBCategoryRow({
|
|
|
|
|
|
label,
|
|
|
|
|
|
players,
|
|
|
|
|
|
selectedId,
|
|
|
|
|
|
onSelect,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
label: string
|
|
|
|
|
|
players: Player[]
|
|
|
|
|
|
selectedId: string | null
|
|
|
|
|
|
onSelect: (id: string | null) => void
|
|
|
|
|
|
}) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">{label}</span>
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
{players.map((p) => {
|
|
|
|
|
|
const active = selectedId === p.id
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={p.id}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => onSelect(active ? null : p.id)}
|
|
|
|
|
|
aria-pressed={active}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"min-h-11 rounded-xl border px-3 text-sm font-semibold transition-colors",
|
|
|
|
|
|
active
|
|
|
|
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
|
|
|
|
: "border-border bg-background text-foreground hover:bg-accent/50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{p.name}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function BBBHolePicker({
|
|
|
|
|
|
players,
|
|
|
|
|
|
holeNumber,
|
|
|
|
|
|
selection,
|
|
|
|
|
|
onSet,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
players: Player[]
|
|
|
|
|
|
holeNumber: number
|
|
|
|
|
|
selection: ApiBBBHole | null
|
|
|
|
|
|
onSet: (holeNumber: number, body: Record<string, string | null>) => Promise<void>
|
|
|
|
|
|
}) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mb-5 flex flex-col gap-4 rounded-2xl border border-border bg-card p-4 sm:p-5">
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">Bingo Bango Bongo -- hull {holeNumber}</span>
|
|
|
|
|
|
<BBBCategoryRow
|
|
|
|
|
|
label="Bingo (først på green)"
|
|
|
|
|
|
players={players}
|
|
|
|
|
|
selectedId={selection?.bingo_participant_id ?? null}
|
|
|
|
|
|
onSelect={(id) => void onSet(holeNumber, { bingo_participant_id: id })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<BBBCategoryRow
|
|
|
|
|
|
label="Bango (nærmest hull, når alle er på green)"
|
|
|
|
|
|
players={players}
|
|
|
|
|
|
selectedId={selection?.bango_participant_id ?? null}
|
|
|
|
|
|
onSelect={(id) => void onSet(holeNumber, { bango_participant_id: id })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<BBBCategoryRow
|
|
|
|
|
|
label="Bongo (først i hull)"
|
|
|
|
|
|
players={players}
|
|
|
|
|
|
selectedId={selection?.bongo_participant_id ?? null}
|
|
|
|
|
|
onSelect={(id) => void onSet(holeNumber, { bongo_participant_id: id })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Money Ball -- lineup_order-tildeling (2026-07-30) ----------------------
|
|
|
|
|
|
// Fast rotasjonsrekkefølge (0-3), avgjør hvem som er "money ball" på hvilket
|
|
|
|
|
|
// hull. Gjenbruker eksisterende patchParticipant (samme generiske PATCH som
|
|
|
|
|
|
// resten av spiller-redigeringen) -- ingen egen fetch-logikk her.
|
|
|
|
|
|
|
|
|
|
|
|
function MoneyBallLineupPanel({
|
|
|
|
|
|
players,
|
|
|
|
|
|
onPatchParticipant,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
players: Player[]
|
|
|
|
|
|
onPatchParticipant: (participantId: string, body: Record<string, unknown>) => Promise<{ ok: true } | { ok: false; message: string }>
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
|
|
const allAssigned = players.length === 4 && players.every((p) => p.lineupOrder !== null)
|
|
|
|
|
|
|
|
|
|
|
|
async function assign(playerId: string, order: number) {
|
|
|
|
|
|
setError(null)
|
|
|
|
|
|
const res = await onPatchParticipant(playerId, { lineup_order: order })
|
|
|
|
|
|
if (!res.ok) setError(res.message)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (allAssigned) return null
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mb-5 flex flex-col gap-3 rounded-2xl border border-border bg-card p-4 sm:p-5">
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">Money Ball -- rekkefølge</span>
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
Tildel hver spiller en plass (0-3) -- avgjør hvem som er "money ball" på hvilket hull.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{error && <p className="text-sm font-medium text-destructive">{error}</p>}
|
|
|
|
|
|
<ul className="flex flex-col gap-2">
|
|
|
|
|
|
{players.map((p) => (
|
|
|
|
|
|
<li key={p.id} className="flex items-center justify-between gap-3 rounded-xl border border-border px-4 py-2.5">
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">{p.name}</span>
|
|
|
|
|
|
<div className="flex gap-1.5">
|
|
|
|
|
|
{[0, 1, 2, 3].map((n) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={n}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => void assign(p.id, n)}
|
|
|
|
|
|
aria-pressed={p.lineupOrder === n}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex size-9 items-center justify-center rounded-lg border text-sm font-bold transition-colors",
|
|
|
|
|
|
p.lineupOrder === n
|
|
|
|
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
|
|
|
|
: "border-border bg-background text-foreground hover:bg-accent/50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{n}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
// --- Player tabs -----------------------------------------------------------
|
|
|
|
|
|
|
2026-07-26 15:48:07 +02:00
|
|
|
|
// --- Spillerliste (2026-07-26, erstatter tidligere horisontal fane-rad) ---
|
|
|
|
|
|
// Vertikal liste av spillerkort -- hvert kort er BÅDE "velg meg som aktiv
|
|
|
|
|
|
// spiller" (hovedtrykkflaten) og bærer et eget "Rediger"-panel (utslag/HCP/
|
|
|
|
|
|
// statistikk, pluss navn/kjønn/e-post for en gjest) som en utvidbar
|
|
|
|
|
|
// underseksjon. Skalerer til mange spillere uten fane-scroll.
|
|
|
|
|
|
|
|
|
|
|
|
function PlayerList({
|
|
|
|
|
|
roundId,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
players,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
sides,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
activePlayerId,
|
|
|
|
|
|
onSelect,
|
|
|
|
|
|
onRemove,
|
|
|
|
|
|
onAdd,
|
|
|
|
|
|
addOpen,
|
|
|
|
|
|
readOnly,
|
2026-07-26 08:22:41 +02:00
|
|
|
|
canManage,
|
2026-07-26 15:48:07 +02:00
|
|
|
|
editingPlayerId,
|
|
|
|
|
|
onToggleEdit,
|
|
|
|
|
|
onPatchParticipant,
|
|
|
|
|
|
onAddGuest,
|
|
|
|
|
|
onAddSearched,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
}: {
|
2026-07-26 15:48:07 +02:00
|
|
|
|
roundId: string
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
players: Player[]
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
// ADR-039 -- kun brukt til å slå opp side-LABEL for badgen; selve
|
|
|
|
|
|
// tildelingen skjer i SidesPanel, ikke her.
|
|
|
|
|
|
sides: ApiSide[]
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
activePlayerId: string
|
|
|
|
|
|
onSelect: (id: string) => void
|
|
|
|
|
|
onRemove: (id: string) => void
|
|
|
|
|
|
onAdd: () => void
|
|
|
|
|
|
addOpen: boolean
|
|
|
|
|
|
readOnly: boolean
|
2026-07-26 15:48:07 +02:00
|
|
|
|
// Kun runde-eieren kan legge til/fjerne/redigere medspillere (ADR-036
|
|
|
|
|
|
// fase 3-utvidelsen, 2026-07-26) -- en lenket medspiller kan fortsatt
|
|
|
|
|
|
// VELGE kort for å registrere score for hele flighten.
|
2026-07-26 08:22:41 +02:00
|
|
|
|
canManage: boolean
|
2026-07-26 15:48:07 +02:00
|
|
|
|
editingPlayerId: string | null
|
|
|
|
|
|
onToggleEdit: (id: string) => void
|
|
|
|
|
|
onPatchParticipant: (participantId: string, body: Record<string, unknown>) => Promise<{ ok: true } | { ok: false; message: string }>
|
|
|
|
|
|
onAddGuest: (name: string, gender: Gender, hcp: number | null, statLevel: StatLevel) => void
|
|
|
|
|
|
onAddSearched: (userId: string, statLevel: StatLevel) => Promise<string | null>
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
}) {
|
|
|
|
|
|
return (
|
2026-07-26 15:48:07 +02:00
|
|
|
|
<div className="flex flex-col gap-3" role="tablist" aria-label="Velg spiller">
|
|
|
|
|
|
{players.map((player) => {
|
|
|
|
|
|
const active = player.id === activePlayerId
|
|
|
|
|
|
const editing = editingPlayerId === player.id
|
|
|
|
|
|
const canRemove = !player.isSelf && !readOnly && canManage
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
// ADR-038 -- full redigering (utslag/HCP/navn/statistikk) er
|
|
|
|
|
|
// fortsatt eier-only OG sperret etter fullføring. Men enhver
|
|
|
|
|
|
// lenket deltaker (eier ELLER medspiller, kun sin EGEN rad) skal
|
|
|
|
|
|
// kunne endre "ekskluder fra faktisk HCP" UANSETT fullført-status
|
|
|
|
|
|
// -- dekket av en RESTRIKTERT variant av samme panel, kun
|
|
|
|
|
|
// meningsfullt for en spiller med egen konto (userId).
|
|
|
|
|
|
const canEditFull = canManage && !readOnly
|
|
|
|
|
|
const canEditRestricted = (canManage || player.isSelf) && player.userId !== null
|
|
|
|
|
|
const canEdit = canEditFull || canEditRestricted
|
2026-07-26 15:48:07 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={player.id}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"overflow-hidden rounded-2xl border transition-colors",
|
|
|
|
|
|
active ? "border-primary" : "border-border",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className={cn("flex items-stretch gap-1", active ? "bg-primary/10" : "bg-card")}>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
role="tab"
|
|
|
|
|
|
aria-selected={active}
|
|
|
|
|
|
onClick={() => onSelect(player.id)}
|
|
|
|
|
|
className="flex min-h-[68px] flex-1 flex-col items-start justify-center gap-0.5 overflow-hidden rounded-xl px-4 py-2 text-left transition-colors hover:bg-accent/50"
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* Fast kort-høyde uansett innhold (2026-07-26, rapportert av
|
|
|
|
|
|
bruker) -- begge rader er ÉN linje med trunkering (ikke
|
|
|
|
|
|
wrap), slik at kort med ulikt antall merker/lange
|
|
|
|
|
|
navn/utslag likevel blir like høye, og listen samlet tar
|
|
|
|
|
|
minst mulig plass over score-tastaturet lenger ned. */}
|
|
|
|
|
|
<span className="flex w-full min-w-0 items-center gap-2">
|
|
|
|
|
|
<span className="min-w-0 truncate text-base font-bold text-foreground">{player.name}</span>
|
|
|
|
|
|
{player.isSelf && (
|
|
|
|
|
|
<Badge variant="default" className="shrink-0">
|
|
|
|
|
|
Deg
|
|
|
|
|
|
</Badge>
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
)}
|
2026-07-26 15:48:07 +02:00
|
|
|
|
{player.isOwner && (
|
|
|
|
|
|
<Badge variant="outline" className="shrink-0">
|
|
|
|
|
|
Eier
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
)}
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
{player.roundSideId && (
|
|
|
|
|
|
<Badge variant="outline" className="shrink-0">
|
|
|
|
|
|
{(() => {
|
|
|
|
|
|
const idx = sides.findIndex((s) => s.id === player.roundSideId)
|
|
|
|
|
|
const side = sides[idx]
|
|
|
|
|
|
return side?.label?.trim() || `Side ${idx === 0 ? "A" : "B"}`
|
|
|
|
|
|
})()}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
)}
|
2026-07-26 15:48:07 +02:00
|
|
|
|
</span>
|
|
|
|
|
|
<span className="w-full truncate text-sm text-muted-foreground">
|
|
|
|
|
|
Utslag: <span className="font-semibold text-foreground">{player.teeName}</span>
|
|
|
|
|
|
{" · "}
|
|
|
|
|
|
HCP:{" "}
|
2026-07-27 10:32:51 +02:00
|
|
|
|
<span className="font-semibold tabular-nums text-foreground">
|
2026-07-26 15:48:07 +02:00
|
|
|
|
{player.hcp !== null ? player.hcp.toFixed(1).replace(".", ",") : "ikke satt"}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{/* Tildelte slag (course handicap) -- KUN vist når HCP
|
|
|
|
|
|
faktisk spores for spilleren (samme betingelse som
|
|
|
|
|
|
"spilles med hcp" -- er den null, spores ikke HCP her). */}
|
|
|
|
|
|
{player.courseHandicap !== null && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{" · "}
|
2026-07-27 10:32:51 +02:00
|
|
|
|
Tildelte slag: <span className="font-semibold tabular-nums text-foreground">{player.courseHandicap}</span>
|
2026-07-26 15:48:07 +02:00
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div className="flex shrink-0 flex-col items-center justify-center gap-1 py-1.5 pr-1.5">
|
|
|
|
|
|
{canEdit && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => onToggleEdit(player.id)}
|
|
|
|
|
|
aria-expanded={editing}
|
|
|
|
|
|
aria-label={`Rediger ${player.name} for denne runden`}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex min-h-11 min-w-11 items-center justify-center gap-1 rounded-xl px-2.5 text-sm font-semibold transition-colors",
|
|
|
|
|
|
editing ? "bg-primary text-primary-foreground" : "text-foreground hover:bg-accent",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Settings2 aria-hidden="true" className="size-4" />
|
|
|
|
|
|
<span className="hidden sm:inline">Rediger</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{canRemove && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => onRemove(player.id)}
|
|
|
|
|
|
aria-label={`Fjern ${player.name}`}
|
|
|
|
|
|
className="flex min-h-11 min-w-11 items-center justify-center rounded-xl text-muted-foreground transition-colors hover:bg-accent hover:text-destructive"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X aria-hidden="true" className="size-4" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
</div>
|
2026-07-26 15:48:07 +02:00
|
|
|
|
|
|
|
|
|
|
{editing && canEdit && (
|
|
|
|
|
|
<EditParticipantPanel
|
|
|
|
|
|
roundId={roundId}
|
|
|
|
|
|
player={player}
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
restricted={!canEditFull}
|
2026-07-26 15:48:07 +02:00
|
|
|
|
onPatch={onPatchParticipant}
|
|
|
|
|
|
onClose={() => onToggleEdit(player.id)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
|
|
{!readOnly && canManage && (
|
|
|
|
|
|
addOpen ? (
|
|
|
|
|
|
<AddParticipantForm onAddGuest={onAddGuest} onAddSearched={onAddSearched} onCancel={onAdd} />
|
|
|
|
|
|
) : (
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={onAdd}
|
2026-07-26 08:22:41 +02:00
|
|
|
|
aria-label="Legg til medspiller"
|
2026-07-26 15:48:07 +02:00
|
|
|
|
className="flex min-h-14 items-center justify-center gap-1.5 rounded-2xl border border-dashed border-border bg-card text-base font-semibold text-foreground transition-colors hover:bg-accent/50"
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
>
|
|
|
|
|
|
<Plus aria-hidden="true" className="size-5" />
|
2026-07-26 08:22:41 +02:00
|
|
|
|
Medspiller
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
</button>
|
2026-07-26 15:48:07 +02:00
|
|
|
|
)
|
|
|
|
|
|
)}
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-26 08:22:41 +02:00
|
|
|
|
// --- Legg til medspiller: søk etter ekte bruker, eller gjest uten konto ----
|
|
|
|
|
|
// (ADR-036 fase 3-utvidelsen, 2026-07-26 -- "+ Gjest" søkte tidligere ikke
|
|
|
|
|
|
// etter spillere i det hele tatt, kun et rent tekstfelt.)
|
|
|
|
|
|
|
|
|
|
|
|
function AddParticipantForm({
|
|
|
|
|
|
onAddGuest,
|
|
|
|
|
|
onAddSearched,
|
|
|
|
|
|
onCancel,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
onAddGuest: (name: string, gender: Gender, hcp: number | null, statLevel: StatLevel) => void
|
|
|
|
|
|
onAddSearched: (userId: string, statLevel: StatLevel) => Promise<string | null>
|
|
|
|
|
|
onCancel: () => void
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const [mode, setMode] = useState<"search" | "guest">("search")
|
|
|
|
|
|
const [query, setQuery] = useState("")
|
|
|
|
|
|
const [results, setResults] = useState<PersonMatch[]>([])
|
|
|
|
|
|
const [searching, setSearching] = useState(false)
|
|
|
|
|
|
const [adding, setAdding] = useState(false)
|
|
|
|
|
|
const [searchError, setSearchError] = useState<string | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const trimmed = query.trim()
|
|
|
|
|
|
if (trimmed.length < 2) {
|
|
|
|
|
|
setResults([])
|
|
|
|
|
|
setSearching(false)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
setSearching(true)
|
|
|
|
|
|
const handle = setTimeout(() => {
|
|
|
|
|
|
fetch(`/people/search?q=${encodeURIComponent(trimmed)}`, { credentials: "include" })
|
|
|
|
|
|
.then((res) => (res.ok ? res.json() : []))
|
|
|
|
|
|
.then((data: PersonMatch[]) => setResults(data))
|
|
|
|
|
|
.catch(() => setResults([]))
|
|
|
|
|
|
.finally(() => setSearching(false))
|
|
|
|
|
|
}, 250)
|
|
|
|
|
|
return () => clearTimeout(handle)
|
|
|
|
|
|
}, [query])
|
|
|
|
|
|
|
|
|
|
|
|
async function handlePick(person: PersonMatch) {
|
|
|
|
|
|
setAdding(true)
|
|
|
|
|
|
setSearchError(null)
|
|
|
|
|
|
const errorMessage = await onAddSearched(person.id, "strokes_only")
|
|
|
|
|
|
setAdding(false)
|
|
|
|
|
|
if (errorMessage) setSearchError(errorMessage)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (mode === "guest") {
|
|
|
|
|
|
return <AddGuestForm onAdd={onAddGuest} onCancel={onCancel} onBack={() => setMode("search")} />
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="mt-3 flex flex-col gap-4 rounded-2xl border border-dashed border-border bg-card/50 p-4 sm:p-5">
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<Label htmlFor="participant-search" className="text-base font-semibold">
|
|
|
|
|
|
Søk etter medspiller
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<div className="relative">
|
|
|
|
|
|
<Search aria-hidden="true" className="pointer-events-none absolute left-3.5 top-1/2 size-5 -translate-y-1/2 text-muted-foreground" />
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="participant-search"
|
|
|
|
|
|
autoFocus
|
|
|
|
|
|
value={query}
|
|
|
|
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
|
|
|
|
placeholder="Navn på medspilleren"
|
|
|
|
|
|
className="h-12 rounded-2xl pl-11 text-base"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{query.trim().length > 0 && query.trim().length < 2 && (
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">Skriv minst 2 tegn for å søke.</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{searchError && (
|
|
|
|
|
|
<p role="alert" className="text-sm font-medium text-destructive">
|
|
|
|
|
|
{searchError}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{results.length > 0 && (
|
|
|
|
|
|
<ul className="flex flex-col gap-2">
|
|
|
|
|
|
{results.map((person) => (
|
|
|
|
|
|
<li key={person.id}>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={adding}
|
|
|
|
|
|
onClick={() => handlePick(person)}
|
|
|
|
|
|
className="flex min-h-14 w-full items-center gap-3 rounded-2xl border border-border bg-card px-3 text-left transition-colors hover:border-primary/60 hover:bg-accent/50 disabled:opacity-60"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span
|
|
|
|
|
|
aria-hidden="true"
|
2026-08-02 16:34:54 +02:00
|
|
|
|
className="flex size-10 shrink-0 items-center justify-center rounded-full bg-muted text-sm font-bold text-muted-foreground"
|
2026-07-26 08:22:41 +02:00
|
|
|
|
>
|
|
|
|
|
|
{person.first_name[0]}
|
|
|
|
|
|
{person.last_name[0]}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="flex min-w-0 flex-col">
|
|
|
|
|
|
<span className="truncate text-base font-bold text-foreground">
|
|
|
|
|
|
{person.first_name} {person.last_name}
|
|
|
|
|
|
</span>
|
2026-07-26 09:18:52 +02:00
|
|
|
|
<span className="truncate text-sm text-muted-foreground">
|
|
|
|
|
|
{person.home_club}
|
|
|
|
|
|
{person.home_club && person.handicap_index !== null && " · "}
|
|
|
|
|
|
{person.handicap_index !== null && `HCP ${person.handicap_index.toFixed(1).replace(".", ",")}`}
|
|
|
|
|
|
</span>
|
2026-07-26 08:22:41 +02:00
|
|
|
|
</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{!searching && query.trim().length >= 2 && results.length === 0 && (
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">Fant ingen med det navnet.</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center justify-between gap-3 border-t border-border pt-4">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setMode("guest")}
|
2026-07-27 10:32:51 +02:00
|
|
|
|
className="flex min-h-11 items-center text-sm font-semibold text-foreground underline-offset-2 hover:underline"
|
2026-07-26 08:22:41 +02:00
|
|
|
|
>
|
|
|
|
|
|
Legg til uten konto (gjest)
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<Button type="button" variant="ghost" onClick={onCancel} className="h-11 rounded-2xl px-5 text-base font-semibold">
|
|
|
|
|
|
Avbryt
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Add guest form (fallback for spillere uten TeeCup-konto) --------------
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
|
|
|
|
|
|
function AddGuestForm({
|
|
|
|
|
|
onAdd,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
onCancel,
|
2026-07-26 08:22:41 +02:00
|
|
|
|
onBack,
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}: {
|
2026-07-24 06:27:35 +02:00
|
|
|
|
onAdd: (name: string, gender: Gender, hcp: number | null, statLevel: StatLevel) => void
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
onCancel: () => void
|
2026-07-26 08:22:41 +02:00
|
|
|
|
onBack?: () => void
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}) {
|
|
|
|
|
|
const [name, setName] = useState("")
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
const [gender, setGender] = useState<Gender>("male")
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
const [hcp, setHcp] = useState("")
|
2026-07-24 06:27:35 +02:00
|
|
|
|
const [statLevel, setStatLevel] = useState<StatLevel>("strokes_only")
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
|
|
|
|
|
|
function handleSubmit(e: React.FormEvent) {
|
|
|
|
|
|
e.preventDefault()
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
const trimmed = name.trim()
|
|
|
|
|
|
if (!trimmed) return
|
|
|
|
|
|
const parsedHcp = hcp.trim() === "" ? null : Number(hcp.replace(",", "."))
|
2026-07-24 06:27:35 +02:00
|
|
|
|
onAdd(trimmed, gender, parsedHcp !== null && !Number.isNaN(parsedHcp) ? parsedHcp : null, statLevel)
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
<form onSubmit={handleSubmit} className="mt-3 flex flex-col gap-5 rounded-2xl border border-dashed border-border bg-card/50 p-4 sm:p-5">
|
2026-07-26 08:22:41 +02:00
|
|
|
|
{onBack && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={onBack}
|
2026-07-27 10:32:51 +02:00
|
|
|
|
className="flex min-h-11 items-center self-start text-sm font-semibold text-muted-foreground hover:text-foreground"
|
2026-07-26 08:22:41 +02:00
|
|
|
|
>
|
|
|
|
|
|
← Søk i stedet
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
<div className="flex flex-col gap-2">
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
<Label htmlFor="guest-name" className="text-base font-semibold">
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
Navn
|
|
|
|
|
|
</Label>
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
<Input id="guest-name" autoFocus value={name} onChange={(e) => setName(e.target.value)} placeholder="Gjestens navn" className="h-12 rounded-2xl text-base" />
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
</div>
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
|
|
|
|
|
|
<ChoiceRow
|
|
|
|
|
|
label="Kjønn"
|
|
|
|
|
|
options={[
|
|
|
|
|
|
{ value: "male", label: "Mann" },
|
|
|
|
|
|
{ value: "female", label: "Kvinne" },
|
|
|
|
|
|
{ value: "other", label: "Annet" },
|
|
|
|
|
|
]}
|
|
|
|
|
|
value={gender}
|
|
|
|
|
|
onChange={(v) => setGender(v as Gender)}
|
|
|
|
|
|
readOnly={false}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<Label htmlFor="guest-hcp" className="text-base font-semibold">
|
|
|
|
|
|
HCP <span className="font-normal text-muted-foreground">(valgfritt)</span>
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="guest-hcp"
|
|
|
|
|
|
inputMode="decimal"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
step="0.1"
|
|
|
|
|
|
value={hcp}
|
|
|
|
|
|
onChange={(e) => setHcp(e.target.value)}
|
|
|
|
|
|
placeholder="F.eks. 18"
|
|
|
|
|
|
className="h-12 rounded-2xl text-base"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-24 06:27:35 +02:00
|
|
|
|
<ChoiceRow
|
|
|
|
|
|
label="Statistikk for denne spilleren"
|
|
|
|
|
|
options={[
|
|
|
|
|
|
{ value: "strokes_only", label: "Kun slag" },
|
|
|
|
|
|
{ value: "strokes_and_putts", label: "Slag og putter" },
|
|
|
|
|
|
{ value: "full", label: "All statistikk" },
|
|
|
|
|
|
]}
|
|
|
|
|
|
value={statLevel}
|
|
|
|
|
|
onChange={(v) => setStatLevel(v as StatLevel)}
|
|
|
|
|
|
readOnly={false}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
<div className="flex gap-3">
|
|
|
|
|
|
<Button type="submit" className="h-12 flex-1 rounded-2xl text-base font-bold">
|
|
|
|
|
|
Legg til
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button type="button" variant="ghost" onClick={onCancel} className="h-12 rounded-2xl px-6 text-base font-semibold">
|
|
|
|
|
|
Avbryt
|
|
|
|
|
|
</Button>
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 11:21:39 +02:00
|
|
|
|
// --- Rediger runde (bane/utslag/antall hull) -------------------------------
|
|
|
|
|
|
// Retter opp feil bane eller feil antall hull underveis -- rører ALDRI
|
|
|
|
|
|
// allerede registrerte slag/putter/etc. (backend garanterer dette, se
|
|
|
|
|
|
// PATCH /rounds/{id}). Kun tilgjengelig før runden er fullført.
|
|
|
|
|
|
|
|
|
|
|
|
type ApiFacility = { slug: string; name: string; city: string | null; county: string | null }
|
|
|
|
|
|
type ApiOfficialCourseOption = { teeoff_course_id: number; name: string; is_main_course: boolean; tees: { name: string }[] }
|
|
|
|
|
|
type ApiPersonalCourseSummary = { id: string; name: string }
|
|
|
|
|
|
type ApiPersonalCourseDetail = { id: string; name: string; tees: { name: string }[] }
|
|
|
|
|
|
|
|
|
|
|
|
// Konverterer til/fra <input type="datetime-local">-verdiformatet
|
|
|
|
|
|
// ("YYYY-MM-DDTHH:mm", lokal tid) -- selve lagringen skjer som ekte
|
|
|
|
|
|
// tidsstempler (UTC), kun visningen er lokal.
|
|
|
|
|
|
function toDatetimeLocalValue(iso: string | null): string {
|
|
|
|
|
|
if (!iso) return ""
|
|
|
|
|
|
const d = new Date(iso)
|
|
|
|
|
|
if (Number.isNaN(d.getTime())) return ""
|
|
|
|
|
|
const local = new Date(d.getTime() - d.getTimezoneOffset() * 60000)
|
|
|
|
|
|
return local.toISOString().slice(0, 16)
|
|
|
|
|
|
}
|
|
|
|
|
|
function fromDatetimeLocalValue(value: string): string | null {
|
|
|
|
|
|
if (!value) return null
|
|
|
|
|
|
const d = new Date(value)
|
|
|
|
|
|
return Number.isNaN(d.getTime()) ? null : d.toISOString()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function EditRoundPanel({
|
|
|
|
|
|
round,
|
|
|
|
|
|
onPatch,
|
|
|
|
|
|
onClose,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
round: ApiRound
|
|
|
|
|
|
onPatch: (body: Record<string, unknown>) => Promise<{ ok: true } | { ok: false; message: string }>
|
|
|
|
|
|
onClose: () => void
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const isCompleted = round.completed_at !== null
|
2026-07-25 06:29:09 +02:00
|
|
|
|
const [name, setName] = useState(round.name ?? "")
|
2026-07-24 11:21:39 +02:00
|
|
|
|
const [holesPlanned, setHolesPlanned] = useState<9 | 18>(round.holes_planned === 9 ? 9 : 18)
|
|
|
|
|
|
const [startHole, setStartHole] = useState(String(round.start_hole))
|
|
|
|
|
|
const [startedAt, setStartedAt] = useState(toDatetimeLocalValue(round.started_at))
|
|
|
|
|
|
const [completedAt, setCompletedAt] = useState(toDatetimeLocalValue(round.completed_at))
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
// ADR-038, utvidet med Stableford (migrasjon 038) -- ren metadata, aldri
|
|
|
|
|
|
// sperret av fullført-status (rører aldri AGS/differensial-matematikken
|
|
|
|
|
|
// selv).
|
|
|
|
|
|
const currentEditablePlayFormat: "stroke" | "match" | "stableford" =
|
|
|
|
|
|
round.play_format === "match" ? "match" : round.play_format === "stableford" ? "stableford" : "stroke"
|
|
|
|
|
|
const [playFormat, setPlayFormat] = useState<"stroke" | "match" | "stableford">(currentEditablePlayFormat)
|
2026-07-28 19:25:37 +02:00
|
|
|
|
// ADR-036 Beslutning B (fase 2, 2026-07-28) -- ren metadata, aldri sperret
|
|
|
|
|
|
// av fullført-status (samme begrunnelse som playFormat over).
|
|
|
|
|
|
const [visibilityMode, setVisibilityMode] = useState<"private" | "public" | "friends">(
|
|
|
|
|
|
round.visibility_mode === "public" || round.visibility_mode === "friends" ? round.visibility_mode : "private",
|
|
|
|
|
|
)
|
|
|
|
|
|
const [visibleCategories, setVisibleCategories] = useState<string[]>(round.visible_categories)
|
2026-07-24 11:21:39 +02:00
|
|
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
|
|
const [showChangeCourse, setShowChangeCourse] = useState(false)
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
const sameCategories =
|
|
|
|
|
|
visibleCategories.length === round.visible_categories.length &&
|
|
|
|
|
|
visibleCategories.every((c) => round.visible_categories.includes(c))
|
|
|
|
|
|
|
2026-07-24 11:21:39 +02:00
|
|
|
|
const hasMetadataChanges =
|
2026-07-25 06:29:09 +02:00
|
|
|
|
name.trim() !== (round.name ?? "") ||
|
2026-07-24 11:21:39 +02:00
|
|
|
|
(!isCompleted && (holesPlanned !== round.holes_planned || Number(startHole) !== round.start_hole)) ||
|
|
|
|
|
|
toDatetimeLocalValue(round.started_at) !== startedAt ||
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
(isCompleted && toDatetimeLocalValue(round.completed_at) !== completedAt) ||
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
playFormat !== currentEditablePlayFormat ||
|
2026-07-28 19:25:37 +02:00
|
|
|
|
visibilityMode !== round.visibility_mode ||
|
|
|
|
|
|
(visibilityMode === "friends" && !sameCategories)
|
2026-07-24 11:21:39 +02:00
|
|
|
|
|
|
|
|
|
|
async function saveMetadata() {
|
|
|
|
|
|
setSaving(true)
|
|
|
|
|
|
setError(null)
|
|
|
|
|
|
const body: Record<string, unknown> = {}
|
2026-07-25 06:29:09 +02:00
|
|
|
|
// Tom streng betyr "fjern navnet" (backend-kontrakt) -- send den derfor
|
|
|
|
|
|
// alltid med når feltet faktisk er endret, aldri utelatt for å tømme.
|
|
|
|
|
|
if (name.trim() !== (round.name ?? "")) body.name = name.trim()
|
2026-07-24 11:21:39 +02:00
|
|
|
|
if (!isCompleted) {
|
|
|
|
|
|
if (holesPlanned !== round.holes_planned) body.holes_planned = holesPlanned
|
|
|
|
|
|
if (Number(startHole) !== round.start_hole) body.start_hole = Number(startHole)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (toDatetimeLocalValue(round.started_at) !== startedAt) body.started_at = fromDatetimeLocalValue(startedAt)
|
|
|
|
|
|
if (isCompleted && toDatetimeLocalValue(round.completed_at) !== completedAt) {
|
|
|
|
|
|
body.completed_at = fromDatetimeLocalValue(completedAt)
|
|
|
|
|
|
}
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
if (playFormat !== currentEditablePlayFormat) body.play_format = playFormat
|
2026-07-28 19:25:37 +02:00
|
|
|
|
if (visibilityMode !== round.visibility_mode) body.visibility_mode = visibilityMode
|
|
|
|
|
|
if (visibilityMode === "friends" && !sameCategories) body.visible_categories = visibleCategories
|
2026-07-24 11:21:39 +02:00
|
|
|
|
const result = await onPatch(body)
|
|
|
|
|
|
if (!result.ok) setError(result.message)
|
|
|
|
|
|
setSaving(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex flex-col gap-4 rounded-2xl border border-border bg-card p-4 sm:p-5">
|
|
|
|
|
|
<div className="flex items-center justify-between gap-2">
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">Rediger runde</span>
|
2026-07-27 10:32:51 +02:00
|
|
|
|
<Button type="button" variant="ghost" size="icon" onClick={onClose} className="size-11 rounded-lg text-muted-foreground" aria-label="Lukk">
|
2026-07-24 11:21:39 +02:00
|
|
|
|
<X aria-hidden="true" className="size-4" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{error && <p className="text-sm font-medium text-destructive">{error}</p>}
|
|
|
|
|
|
|
2026-07-25 06:29:09 +02:00
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<Label htmlFor="edit-round-name" className="text-sm font-semibold">
|
|
|
|
|
|
Navn på runden <span className="font-normal text-muted-foreground">(valgfritt)</span>
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="edit-round-name"
|
|
|
|
|
|
value={name}
|
|
|
|
|
|
onChange={(e) => setName(e.target.value)}
|
|
|
|
|
|
placeholder={round.course_name_snapshot}
|
|
|
|
|
|
maxLength={200}
|
|
|
|
|
|
className="h-11 rounded-xl text-base"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">Spilleform</span>
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
{(
|
|
|
|
|
|
[
|
|
|
|
|
|
{ value: "stroke" as const, label: "Slagspill" },
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
{ value: "stableford" as const, label: "Stableford" },
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
{ value: "match" as const, label: "Matchspill" },
|
|
|
|
|
|
]
|
|
|
|
|
|
).map((opt) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={opt.value}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setPlayFormat(opt.value)}
|
|
|
|
|
|
aria-pressed={playFormat === opt.value}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"h-11 flex-1 rounded-xl border text-base font-bold transition-colors",
|
|
|
|
|
|
playFormat === opt.value
|
|
|
|
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
|
|
|
|
: "border-border bg-background text-foreground hover:bg-accent/50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{opt.label}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-28 19:25:37 +02:00
|
|
|
|
{/* Synlighet (ADR-036 Beslutning B, fase 2) -- styrer KUN tredjeparts
|
|
|
|
|
|
innsyn (venner/offentligheten), aldri en faktisk lagt-til
|
|
|
|
|
|
medspiller (som alltid ser runden uansett). */}
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">Synlighet</span>
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
{(
|
|
|
|
|
|
[
|
|
|
|
|
|
{ value: "private" as const, label: "Privat" },
|
|
|
|
|
|
{ value: "friends" as const, label: "Venner" },
|
|
|
|
|
|
{ value: "public" as const, label: "Offentlig" },
|
|
|
|
|
|
]
|
|
|
|
|
|
).map((opt) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={opt.value}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setVisibilityMode(opt.value)}
|
|
|
|
|
|
aria-pressed={visibilityMode === opt.value}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"h-11 flex-1 rounded-xl border text-base font-bold transition-colors",
|
|
|
|
|
|
visibilityMode === opt.value
|
|
|
|
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
|
|
|
|
: "border-border bg-background text-foreground hover:bg-accent/50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{opt.label}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{visibilityMode === "friends" && (
|
|
|
|
|
|
<div className="flex flex-col gap-2 rounded-xl border border-border bg-background p-3">
|
|
|
|
|
|
<p className="text-xs font-semibold text-muted-foreground">
|
|
|
|
|
|
Hvilke av dine venne-kategorier skal kunne se runden?
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
{VISIBILITY_CATEGORY_OPTIONS.map(({ code, label }) => {
|
|
|
|
|
|
const active = visibleCategories.includes(code)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={code}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() =>
|
|
|
|
|
|
setVisibleCategories((prev) =>
|
|
|
|
|
|
active ? prev.filter((c) => c !== code) : [...prev, code],
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
aria-pressed={active}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"min-h-[44px] rounded-full border px-3 text-sm font-semibold transition-colors",
|
|
|
|
|
|
active
|
|
|
|
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
|
|
|
|
: "border-border bg-card text-muted-foreground hover:bg-accent/50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{label}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-07-24 11:21:39 +02:00
|
|
|
|
{!isCompleted && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">Antall hull</span>
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
{([9, 18] as const).map((n) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={n}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setHolesPlanned(n)}
|
|
|
|
|
|
aria-pressed={holesPlanned === n}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"h-11 flex-1 rounded-xl border text-base font-bold transition-colors",
|
|
|
|
|
|
holesPlanned === n
|
|
|
|
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
|
|
|
|
: "border-border bg-background text-foreground hover:bg-accent/50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{n} hull
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<Label htmlFor="edit-start-hole" className="text-sm font-semibold">
|
|
|
|
|
|
Starthull
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
id="edit-start-hole"
|
|
|
|
|
|
value={startHole}
|
|
|
|
|
|
onChange={(e) => setStartHole(e.target.value)}
|
|
|
|
|
|
className="h-11 rounded-xl border border-border bg-background px-3 text-base font-semibold text-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
{Array.from({ length: 18 }, (_, i) => i + 1).map((n) => (
|
|
|
|
|
|
<option key={n} value={String(n)}>
|
|
|
|
|
|
Hull {n}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<Label htmlFor="edit-started-at" className="text-sm font-semibold">
|
|
|
|
|
|
Utslagstid
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="edit-started-at"
|
|
|
|
|
|
type="datetime-local"
|
|
|
|
|
|
value={startedAt}
|
|
|
|
|
|
onChange={(e) => setStartedAt(e.target.value)}
|
|
|
|
|
|
className="h-11 rounded-xl text-base"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{isCompleted && (
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<Label htmlFor="edit-completed-at" className="text-sm font-semibold">
|
|
|
|
|
|
Fullført-tidspunkt
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="edit-completed-at"
|
|
|
|
|
|
type="datetime-local"
|
|
|
|
|
|
value={completedAt}
|
|
|
|
|
|
onChange={(e) => setCompletedAt(e.target.value)}
|
|
|
|
|
|
className="h-11 rounded-xl text-base"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{hasMetadataChanges && (
|
|
|
|
|
|
<Button type="button" disabled={saving} onClick={saveMetadata} className="h-11 rounded-xl text-sm font-bold">
|
|
|
|
|
|
{saving ? "Lagrer…" : "Lagre endringer"}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{!isCompleted && (
|
|
|
|
|
|
<div className="border-t border-border pt-4">
|
|
|
|
|
|
{showChangeCourse ? (
|
|
|
|
|
|
<ChangeCourseForm onPatch={onPatch} onCancel={() => setShowChangeCourse(false)} />
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setShowChangeCourse(true)}
|
|
|
|
|
|
className="inline-flex min-h-11 items-center gap-1.5 text-sm font-semibold text-primary"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Search aria-hidden="true" className="size-4" />
|
|
|
|
|
|
Bytt bane
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-26 09:18:52 +02:00
|
|
|
|
// --- Rediger utslag+HCP for én deltaker, KUN for denne runden (2026-07-26) -
|
|
|
|
|
|
|
|
|
|
|
|
function EditParticipantPanel({
|
|
|
|
|
|
roundId,
|
|
|
|
|
|
player,
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
restricted,
|
2026-07-26 09:18:52 +02:00
|
|
|
|
onPatch,
|
|
|
|
|
|
onClose,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
roundId: string
|
|
|
|
|
|
player: Player
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
// ADR-038 -- true når viewer IKKE har full redigeringsrett her (en
|
|
|
|
|
|
// ikke-eier som redigerer sin EGEN rad, ELLER eieren etter at runden er
|
|
|
|
|
|
// fullført) -- viser da KUN eksklusjons-toggelen, ingen av de andre
|
|
|
|
|
|
// feltene under.
|
|
|
|
|
|
restricted: boolean
|
2026-07-26 09:18:52 +02:00
|
|
|
|
onPatch: (participantId: string, body: Record<string, unknown>) => Promise<{ ok: true } | { ok: false; message: string }>
|
|
|
|
|
|
onClose: () => void
|
|
|
|
|
|
}) {
|
2026-07-26 15:48:07 +02:00
|
|
|
|
// En "midlertidig spiller" (gjest uten TeeCup-konto) har ikke egen profil
|
|
|
|
|
|
// -- navn/kjønn/e-post kan derfor redigeres her. En lenket bruker har sin
|
|
|
|
|
|
// egen konto, og disse feltene gir ingen mening (backend avviser dem også
|
|
|
|
|
|
// eksplisitt, 2026-07-26).
|
|
|
|
|
|
const isGuest = player.userId === null
|
|
|
|
|
|
|
2026-07-26 09:18:52 +02:00
|
|
|
|
const [teeOptions, setTeeOptions] = useState<ApiTeeOption[] | null>(null)
|
|
|
|
|
|
const [teeName, setTeeName] = useState(player.teeName)
|
|
|
|
|
|
const [hcp, setHcp] = useState(player.hcp !== null ? String(player.hcp).replace(".", ",") : "")
|
2026-07-26 15:48:07 +02:00
|
|
|
|
const [statLevel, setStatLevel] = useState<StatLevel>(player.statLevel)
|
2026-07-27 10:32:51 +02:00
|
|
|
|
const [guestName, setGuestName] = useState(player.name)
|
2026-07-26 15:48:07 +02:00
|
|
|
|
const [gender, setGender] = useState<Gender>(player.gender)
|
|
|
|
|
|
const [guestEmail, setGuestEmail] = useState(player.guestEmail ?? "")
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
const [excludeFromHandicap, setExcludeFromHandicap] = useState(player.excludeFromHandicap)
|
2026-07-26 09:18:52 +02:00
|
|
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
if (restricted) return
|
2026-07-26 09:18:52 +02:00
|
|
|
|
let cancelled = false
|
|
|
|
|
|
fetch(`/rounds/${roundId}/tee-options`, { credentials: "include" })
|
|
|
|
|
|
.then((res) => (res.ok ? res.json() : []))
|
|
|
|
|
|
.then((data: ApiTeeOption[]) => {
|
|
|
|
|
|
if (!cancelled) setTeeOptions(data)
|
|
|
|
|
|
})
|
|
|
|
|
|
.catch(() => {
|
|
|
|
|
|
if (!cancelled) setTeeOptions([])
|
|
|
|
|
|
})
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
}
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
}, [roundId, restricted])
|
2026-07-26 09:18:52 +02:00
|
|
|
|
|
2026-07-26 15:48:07 +02:00
|
|
|
|
// Reaktivt filter (2026-07-26): en gjest kan endre eget kjønn her, som da
|
|
|
|
|
|
// umiddelbart oppdaterer hvilke utslag som er valgbare -- for en lenket
|
|
|
|
|
|
// bruker er `gender` uendret gjennom hele panelets levetid.
|
|
|
|
|
|
const apiGender = uiGenderToApi(gender)
|
2026-07-26 09:18:52 +02:00
|
|
|
|
const availableTees = (teeOptions ?? []).filter((t) => t.genders.includes(apiGender))
|
|
|
|
|
|
|
|
|
|
|
|
async function save() {
|
|
|
|
|
|
setSaving(true)
|
|
|
|
|
|
setError(null)
|
|
|
|
|
|
const body: Record<string, unknown> = {}
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
if (!restricted) {
|
|
|
|
|
|
if (teeName !== player.teeName) body.tee_name = teeName
|
|
|
|
|
|
const parsedHcp = hcp.trim() === "" ? null : Number(hcp.replace(",", "."))
|
|
|
|
|
|
const normalizedHcp = parsedHcp !== null && !Number.isNaN(parsedHcp) ? parsedHcp : null
|
|
|
|
|
|
if (normalizedHcp !== player.hcp) body.handicap_index = normalizedHcp
|
|
|
|
|
|
if (statLevel !== player.statLevel) body.stat_level = statLevel
|
|
|
|
|
|
if (isGuest) {
|
|
|
|
|
|
const trimmedName = guestName.trim()
|
|
|
|
|
|
if (trimmedName && trimmedName !== player.name) body.guest_name = trimmedName
|
|
|
|
|
|
if (gender !== player.gender) body.gender = apiGender
|
|
|
|
|
|
const trimmedEmail = guestEmail.trim()
|
|
|
|
|
|
const normalizedEmail = trimmedEmail === "" ? null : trimmedEmail
|
|
|
|
|
|
if (normalizedEmail !== (player.guestEmail ?? null)) body.guest_email = normalizedEmail
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!isGuest && excludeFromHandicap !== player.excludeFromHandicap) {
|
|
|
|
|
|
body.exclude_from_handicap = excludeFromHandicap
|
2026-07-26 15:48:07 +02:00
|
|
|
|
}
|
2026-07-26 09:18:52 +02:00
|
|
|
|
if (Object.keys(body).length === 0) {
|
|
|
|
|
|
setSaving(false)
|
|
|
|
|
|
onClose()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const result = await onPatch(player.id, body)
|
|
|
|
|
|
setSaving(false)
|
|
|
|
|
|
if (!result.ok) {
|
|
|
|
|
|
setError(result.message)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
onClose()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-07-26 15:48:07 +02:00
|
|
|
|
<div className="flex flex-col gap-4 border-t border-border bg-muted/30 p-4 sm:p-5">
|
|
|
|
|
|
{error && <p className="text-sm font-medium text-destructive">{error}</p>}
|
|
|
|
|
|
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
{restricted && (
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
Runden er fullført, eller du har ikke rett til å endre de andre feltene her -- du kan
|
|
|
|
|
|
likevel endre om {player.isSelf ? "din egen deltakelse" : "spillerens deltakelse"} skal
|
|
|
|
|
|
telle mot {player.isSelf ? "ditt" : "spillerens"} faktiske HCP.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{!restricted && (
|
2026-07-26 15:48:07 +02:00
|
|
|
|
<>
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
{isGuest && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<Label htmlFor={`edit-guest-name-${player.id}`} className="text-sm font-semibold">
|
|
|
|
|
|
Navn
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id={`edit-guest-name-${player.id}`}
|
|
|
|
|
|
value={guestName}
|
|
|
|
|
|
onChange={(e) => setGuestName(e.target.value)}
|
|
|
|
|
|
className="h-11 rounded-xl text-base"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<ChoiceRow
|
|
|
|
|
|
label="Kjønn"
|
|
|
|
|
|
options={[
|
|
|
|
|
|
{ value: "male", label: "Mann" },
|
|
|
|
|
|
{ value: "female", label: "Kvinne" },
|
|
|
|
|
|
{ value: "other", label: "Annet" },
|
|
|
|
|
|
]}
|
|
|
|
|
|
value={gender}
|
|
|
|
|
|
onChange={(v) => setGender(v as Gender)}
|
|
|
|
|
|
readOnly={false}
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<Label htmlFor={`edit-guest-email-${player.id}`} className="text-sm font-semibold">
|
|
|
|
|
|
E-post <span className="font-normal text-muted-foreground">(valgfritt)</span>
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id={`edit-guest-email-${player.id}`}
|
|
|
|
|
|
type="email"
|
|
|
|
|
|
value={guestEmail}
|
|
|
|
|
|
onChange={(e) => setGuestEmail(e.target.value)}
|
|
|
|
|
|
placeholder="navn@eksempel.no"
|
|
|
|
|
|
className="h-11 rounded-xl text-base"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="border-t border-border" />
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
|
Utslag/HCP/statistikk gjelder kun denne runden -- endrer ikke {player.isSelf ? "din egen" : "spillerens"} profil.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
2026-07-26 15:48:07 +02:00
|
|
|
|
<div className="flex flex-col gap-2">
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
<Label htmlFor={`edit-tee-${player.id}`} className="text-sm font-semibold">
|
|
|
|
|
|
Utslagssted
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
id={`edit-tee-${player.id}`}
|
|
|
|
|
|
value={teeName}
|
|
|
|
|
|
onChange={(e) => setTeeName(e.target.value)}
|
|
|
|
|
|
disabled={teeOptions === null}
|
|
|
|
|
|
className="h-11 rounded-xl border border-border bg-background px-3 text-base font-semibold text-foreground disabled:opacity-60"
|
|
|
|
|
|
>
|
|
|
|
|
|
{!availableTees.some((t) => t.name === teeName) && <option value={teeName}>{teeName}</option>}
|
|
|
|
|
|
{availableTees.map((t) => (
|
|
|
|
|
|
<option key={t.name} value={t.name}>
|
|
|
|
|
|
{t.name}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
{teeOptions !== null && availableTees.length === 0 && (
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">Fant ingen utslag med rating for dette kjønnet på denne banen.</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<Label htmlFor={`edit-hcp-${player.id}`} className="text-sm font-semibold">
|
|
|
|
|
|
HCP for denne runden <span className="font-normal text-muted-foreground">(valgfritt)</span>
|
2026-07-26 15:48:07 +02:00
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
id={`edit-hcp-${player.id}`}
|
|
|
|
|
|
inputMode="decimal"
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
step="0.1"
|
|
|
|
|
|
value={hcp}
|
|
|
|
|
|
onChange={(e) => setHcp(e.target.value)}
|
|
|
|
|
|
placeholder="F.eks. 18"
|
|
|
|
|
|
className="h-12 rounded-2xl text-base"
|
2026-07-26 15:48:07 +02:00
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<ChoiceRow
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
label="Statistikk for denne spilleren"
|
2026-07-26 15:48:07 +02:00
|
|
|
|
options={[
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
{ value: "strokes_only", label: "Kun slag" },
|
|
|
|
|
|
{ value: "strokes_and_putts", label: "Slag og putter" },
|
|
|
|
|
|
{ value: "full", label: "All statistikk" },
|
2026-07-26 15:48:07 +02:00
|
|
|
|
]}
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
value={statLevel}
|
|
|
|
|
|
onChange={(v) => setStatLevel(v as StatLevel)}
|
2026-07-26 15:48:07 +02:00
|
|
|
|
readOnly={false}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2026-07-26 09:18:52 +02:00
|
|
|
|
|
Alt er ferdig, verifisert og live. Oppsummert:
Hullet lukket: hele WHS-HCP-motoren fantes og var testet, men ble aldri kalt — score_differential ble lagret per runde, men ingen indeks ble noensinne regnet ut. Nå har du:
To atskilte tall: manuelt satt HCP (uendret bruk) og et nytt, automatisk beregnet «faktisk HCP» (beste 8-av-≤20 nyeste tellende runder, WHS Rule 5.2, med Low-HCP-cap).
«Bruk som mitt HCP»-knapp på /account for å eksplisitt overføre.
Eksklusjon per deltaker — hver innlogget spiller (ikke bare eieren) styrer selv om egen deltakelse skal telle, uansett fullført-status.
Selvdeklarert spilleform (slagspill/matchspill) — matchspill forhåndsforeslår (ikke tvinger) eksklusjon, begrunnet i WHS sin «most likely score»-regel som TeeCup ikke kan garantere presist.
Verifisert med 111/111 scratch-sjekker (inkl. håndregnet WHS-matte) og en ekte nettleser-gjennomgang av hele flyten. Migrasjon 030 kjørt mot ekte teecup_db, begge containere redeployet, alt grønt, teeoff.no upåvirket.
Gjenstår (dokumentert i CLAUDE.md/FEATURE_BACKLOG.md, ikke bygget nå): offline-kø for frittstående runder, Stableford, rundedeling/visibility, flere flighter i én runde.
2026-07-28 11:04:51 +02:00
|
|
|
|
{!isGuest && (
|
|
|
|
|
|
<label className="flex min-h-11 items-start gap-3 rounded-xl border border-border bg-card px-4 py-3">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={excludeFromHandicap}
|
|
|
|
|
|
onChange={(e) => setExcludeFromHandicap(e.target.checked)}
|
|
|
|
|
|
className="mt-1 size-5 shrink-0 accent-primary"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="flex flex-col gap-0.5">
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">
|
|
|
|
|
|
Ekskluder fra {player.isSelf ? "mitt" : "spillerens"} faktiske HCP
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="text-sm text-muted-foreground">
|
|
|
|
|
|
F.eks. ved matchspill, eller andre runder som ikke bør telle med i HCP-grunnlaget.
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
)}
|
2026-07-26 15:48:07 +02:00
|
|
|
|
|
|
|
|
|
|
<div className="flex justify-end gap-2 pt-1">
|
|
|
|
|
|
<Button type="button" variant="ghost" onClick={onClose} className="h-11 rounded-xl px-5 text-sm font-semibold">
|
|
|
|
|
|
Avbryt
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button type="button" disabled={saving} onClick={save} className="h-11 rounded-xl px-5 text-sm font-bold">
|
|
|
|
|
|
{saving ? "Lagrer…" : "Lagre endringer"}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
2026-07-26 09:18:52 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 11:21:39 +02:00
|
|
|
|
function ChangeCourseForm({
|
|
|
|
|
|
onPatch,
|
|
|
|
|
|
onCancel,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
onPatch: (body: Record<string, unknown>) => Promise<{ ok: true } | { ok: false; message: string }>
|
|
|
|
|
|
onCancel: () => void
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const [source, setSource] = useState<"teeoff" | "custom" | null>(null)
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
|
|
const [submitting, setSubmitting] = useState(false)
|
|
|
|
|
|
|
|
|
|
|
|
// Teeoff-søk
|
|
|
|
|
|
const [query, setQuery] = useState("")
|
|
|
|
|
|
const [facilities, setFacilities] = useState<ApiFacility[] | null>(null)
|
|
|
|
|
|
const [selectedFacility, setSelectedFacility] = useState<ApiFacility | null>(null)
|
|
|
|
|
|
const [officialCourses, setOfficialCourses] = useState<ApiOfficialCourseOption[] | null>(null)
|
|
|
|
|
|
const [selectedOfficialCourse, setSelectedOfficialCourse] = useState<ApiOfficialCourseOption | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
// Egen bane-søk
|
|
|
|
|
|
const [customQuery, setCustomQuery] = useState("")
|
|
|
|
|
|
const [customResults, setCustomResults] = useState<ApiPersonalCourseSummary[]>([])
|
|
|
|
|
|
const [selectedCustomCourse, setSelectedCustomCourse] = useState<ApiPersonalCourseDetail | null>(null)
|
|
|
|
|
|
|
|
|
|
|
|
const [teeName, setTeeName] = useState("")
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (source !== "custom") return
|
|
|
|
|
|
let cancelled = false
|
|
|
|
|
|
const timer = setTimeout(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(`/personal-courses?q=${encodeURIComponent(customQuery.trim())}`, { credentials: "include" })
|
|
|
|
|
|
if (res.ok && !cancelled) setCustomResults(await res.json())
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// Stille -- listen blir bare uendret, ingen kritisk feil å vise her.
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 250)
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
cancelled = true
|
|
|
|
|
|
clearTimeout(timer)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [source, customQuery])
|
|
|
|
|
|
|
|
|
|
|
|
async function searchFacilities() {
|
|
|
|
|
|
setError(null)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(`/rounds/official-search?q=${encodeURIComponent(query.trim())}`, { credentials: "include" })
|
|
|
|
|
|
if (!res.ok) throw new Error()
|
|
|
|
|
|
setFacilities(await res.json())
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
setError("Klarte ikke å søke i teeoff sine baner akkurat nå.")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function pickFacility(facility: ApiFacility) {
|
|
|
|
|
|
setSelectedFacility(facility)
|
|
|
|
|
|
setError(null)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(`/rounds/official-search/${facility.slug}`, { credentials: "include" })
|
|
|
|
|
|
if (!res.ok) throw new Error()
|
|
|
|
|
|
const detail: { courses: ApiOfficialCourseOption[] } = await res.json()
|
|
|
|
|
|
setOfficialCourses(detail.courses)
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
setError("Klarte ikke å hente baner for dette anlegget.")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function pickCustomCourse(course: ApiPersonalCourseSummary) {
|
|
|
|
|
|
setError(null)
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(`/personal-courses/${course.id}`, { credentials: "include" })
|
|
|
|
|
|
if (!res.ok) throw new Error()
|
|
|
|
|
|
setSelectedCustomCourse(await res.json())
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
setError("Klarte ikke å hente banedetaljer.")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const availableTees = selectedOfficialCourse?.tees ?? selectedCustomCourse?.tees ?? []
|
|
|
|
|
|
|
|
|
|
|
|
async function handleSubmit() {
|
|
|
|
|
|
if (!teeName) return
|
|
|
|
|
|
setSubmitting(true)
|
|
|
|
|
|
setError(null)
|
|
|
|
|
|
const body =
|
|
|
|
|
|
source === "teeoff" && selectedFacility && selectedOfficialCourse
|
|
|
|
|
|
? {
|
|
|
|
|
|
course_source: "teeoff",
|
|
|
|
|
|
teeoff_facility_slug: selectedFacility.slug,
|
|
|
|
|
|
teeoff_course_id: selectedOfficialCourse.teeoff_course_id,
|
|
|
|
|
|
tee_name: teeName,
|
|
|
|
|
|
}
|
|
|
|
|
|
: selectedCustomCourse
|
|
|
|
|
|
? { course_source: "custom", personal_course_id: selectedCustomCourse.id, tee_name: teeName }
|
|
|
|
|
|
: null
|
|
|
|
|
|
if (!body) {
|
|
|
|
|
|
setSubmitting(false)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const result = await onPatch(body)
|
|
|
|
|
|
setSubmitting(false)
|
|
|
|
|
|
if (!result.ok) {
|
|
|
|
|
|
setError(result.message)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
onCancel()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex flex-col gap-3">
|
|
|
|
|
|
<p className="text-xs leading-relaxed text-muted-foreground text-pretty">
|
|
|
|
|
|
Endrer kun banen og utslaget videre -- allerede registrerte slag/putter for spilte hull røres ikke.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
{error && <p className="text-sm font-medium text-destructive">{error}</p>}
|
|
|
|
|
|
|
|
|
|
|
|
{source === null && (
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
|
<Button type="button" variant="outline" onClick={() => setSource("teeoff")} className="h-11 rounded-xl text-sm font-bold">
|
|
|
|
|
|
Offisiell bane
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button type="button" variant="outline" onClick={() => setSource("custom")} className="h-11 rounded-xl text-sm font-bold">
|
|
|
|
|
|
Egen bane
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{source === "teeoff" && !selectedFacility && (
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
autoFocus
|
|
|
|
|
|
value={query}
|
|
|
|
|
|
onChange={(e) => setQuery(e.target.value)}
|
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
|
if (e.key === "Enter") {
|
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
searchFacilities()
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
placeholder="Søk klubbnavn…"
|
|
|
|
|
|
className="h-11 flex-1 rounded-xl text-base"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Button type="button" onClick={searchFacilities} className="h-11 shrink-0 rounded-xl text-sm font-bold">
|
|
|
|
|
|
Søk
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{facilities && (
|
|
|
|
|
|
<ul className="flex max-h-56 flex-col overflow-auto rounded-xl border border-border">
|
|
|
|
|
|
{facilities.map((f) => (
|
|
|
|
|
|
<li key={f.slug} className="border-b border-border last:border-b-0">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => pickFacility(f)}
|
2026-07-27 10:32:51 +02:00
|
|
|
|
className="flex min-h-11 w-full flex-col px-3 py-2.5 text-left hover:bg-accent/60"
|
2026-07-24 11:21:39 +02:00
|
|
|
|
>
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">{f.name}</span>
|
|
|
|
|
|
{(f.city || f.county) && (
|
|
|
|
|
|
<span className="text-xs text-muted-foreground">{[f.city, f.county].filter(Boolean).join(", ")}</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{facilities.length === 0 && <li className="px-3 py-2.5 text-center text-sm text-muted-foreground">Ingen treff.</li>}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{source === "teeoff" && selectedFacility && !selectedOfficialCourse && (
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
2026-07-27 10:32:51 +02:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setSelectedFacility(null)}
|
|
|
|
|
|
className="flex min-h-11 items-center self-start text-sm font-semibold text-muted-foreground"
|
|
|
|
|
|
>
|
2026-07-24 11:21:39 +02:00
|
|
|
|
← {selectedFacility.name}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
{officialCourses === null ? (
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">Laster baner…</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ul className="flex flex-col overflow-hidden rounded-xl border border-border">
|
|
|
|
|
|
{officialCourses.map((c) => (
|
|
|
|
|
|
<li key={c.teeoff_course_id} className="border-b border-border last:border-b-0">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setSelectedOfficialCourse(c)}
|
2026-07-27 10:32:51 +02:00
|
|
|
|
className="flex min-h-11 w-full items-center justify-between px-3 py-2.5 text-left text-sm font-semibold text-foreground hover:bg-accent/60"
|
2026-07-24 11:21:39 +02:00
|
|
|
|
>
|
|
|
|
|
|
{c.name}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{source === "custom" && !selectedCustomCourse && (
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<Input
|
|
|
|
|
|
autoFocus
|
|
|
|
|
|
value={customQuery}
|
|
|
|
|
|
onChange={(e) => setCustomQuery(e.target.value)}
|
|
|
|
|
|
placeholder="Søk egendefinert bane…"
|
|
|
|
|
|
className="h-11 rounded-xl text-base"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<ul className="flex max-h-56 flex-col overflow-auto rounded-xl border border-border">
|
|
|
|
|
|
{customResults.map((c) => (
|
|
|
|
|
|
<li key={c.id} className="border-b border-border last:border-b-0">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => pickCustomCourse(c)}
|
2026-07-27 10:32:51 +02:00
|
|
|
|
className="flex min-h-11 w-full items-center px-3 py-2.5 text-left text-sm font-semibold text-foreground hover:bg-accent/60"
|
2026-07-24 11:21:39 +02:00
|
|
|
|
>
|
|
|
|
|
|
{c.name}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{customResults.length === 0 && (
|
|
|
|
|
|
<li className="px-3 py-2.5 text-center text-sm text-muted-foreground">Ingen treff.</li>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{(selectedOfficialCourse || selectedCustomCourse) && (
|
|
|
|
|
|
<div className="flex flex-col gap-3">
|
|
|
|
|
|
<span className="text-sm font-bold text-foreground">
|
|
|
|
|
|
{selectedOfficialCourse?.name ?? selectedCustomCourse?.name}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">Utslag</span>
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
{availableTees.map((t) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={t.name}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setTeeName(t.name)}
|
|
|
|
|
|
aria-pressed={teeName === t.name}
|
|
|
|
|
|
className={cn(
|
2026-07-27 10:32:51 +02:00
|
|
|
|
"h-11 rounded-lg border px-3 text-sm font-semibold transition-colors",
|
2026-07-24 11:21:39 +02:00
|
|
|
|
teeName === t.name
|
|
|
|
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
|
|
|
|
: "border-border bg-background text-foreground hover:bg-accent/50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{t.name}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Button type="button" disabled={submitting || !teeName} onClick={handleSubmit} className="h-11 rounded-xl text-sm font-bold">
|
|
|
|
|
|
{submitting ? "Bytter bane…" : "Bytt bane"}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-07-27 10:32:51 +02:00
|
|
|
|
<Button type="button" variant="ghost" onClick={onCancel} className="h-11 self-start rounded-xl text-sm font-semibold text-muted-foreground">
|
2026-07-24 11:21:39 +02:00
|
|
|
|
Avbryt
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
// --- Scorekort-grid (§1 i teecup-scorekort-og-entry-spec.md, 2026-07-27) ---
|
|
|
|
|
|
// Spillere = rader (sticky venstre navnekolonne), hull = horisontalt
|
|
|
|
|
|
// scrollbare kolonner (sticky Ut/Inn/Sum til høyre) -- erstatter den
|
|
|
|
|
|
// forrige ett-hull-om-gangen-listen. Tapp en kolonneoverskrift for å velge
|
|
|
|
|
|
// "gjeldende" hull (kun navigasjon), tapp en spillers navnecelle eller en
|
|
|
|
|
|
// score-celle for å åpne `ScoringWizard` for AKKURAT den (spiller, hull)-
|
|
|
|
|
|
// kombinasjonen -- samme veiviser som før, bare adressert direkte fra
|
|
|
|
|
|
// gridet i stedet for via ett enkelt "aktivt hull"-panel.
|
|
|
|
|
|
// Par/stroke-indeks er samme fysiske hull for alle spillere -- hentes fra
|
|
|
|
|
|
// HVILKEN SOM HELST allerede lastet spillers hull-liste (ikke bare aktiv
|
|
|
|
|
|
// spiller), robust mot i hvilken rekkefølge deltakernes hull lastes inn.
|
|
|
|
|
|
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
// Sorteringsnøkkel for lagvis gruppering (2026-08-02) -- side A (sides[0])
|
|
|
|
|
|
// først, side B (sides[1]) deretter, ingen/ukjent side sist. Brukt med en
|
|
|
|
|
|
// STABIL sort, så original rekkefølge innad i hver gruppe bevares.
|
|
|
|
|
|
function sideRank(roundSideId: string | null, sides: ApiSide[]): number {
|
|
|
|
|
|
if (roundSideId === sides[0]?.id) return 0
|
|
|
|
|
|
if (roundSideId === sides[1]?.id) return 1
|
|
|
|
|
|
return 2
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
function ScorecardGrid({
|
|
|
|
|
|
players,
|
|
|
|
|
|
holesByParticipant,
|
|
|
|
|
|
holeOrder,
|
|
|
|
|
|
holesPlanned,
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
currentHole,
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
readOnly,
|
2026-07-29 15:46:49 +02:00
|
|
|
|
sides,
|
|
|
|
|
|
isTwoSided,
|
|
|
|
|
|
formatResult,
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
onSelectHole,
|
|
|
|
|
|
onOpenEntry,
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}: {
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
players: Player[]
|
|
|
|
|
|
holesByParticipant: Record<string, ApiHole[]>
|
|
|
|
|
|
holeOrder: number[]
|
|
|
|
|
|
holesPlanned: number
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
currentHole: number
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
readOnly: boolean
|
2026-07-29 15:46:49 +02:00
|
|
|
|
// Match-aware fargelegging + Stilling-rad (2026-07-29) -- kun for
|
|
|
|
|
|
// to-sidede individuell-ball-formater (match/fourball), aldri slagspill/
|
|
|
|
|
|
// skins (ingen sider/lead-begrep der).
|
|
|
|
|
|
sides: ApiSide[]
|
|
|
|
|
|
isTwoSided: boolean
|
|
|
|
|
|
formatResult: ApiFormatResult | null
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
onSelectHole: (holeNumber: number) => void
|
|
|
|
|
|
onOpenEntry: (playerId: string, holeNumber: number) => void
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}) {
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
const holeMeta: Record<number, { par: number; index: number }> = {}
|
|
|
|
|
|
for (const list of Object.values(holesByParticipant)) {
|
|
|
|
|
|
for (const h of list) {
|
|
|
|
|
|
if (!(h.hole_number in holeMeta)) holeMeta[h.hole_number] = { par: h.par, index: h.stroke_index }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Ut(1-9)/Inn(10-18) er alltid gruppert på FYSISK hullnummer (tradisjonell
|
|
|
|
|
|
// scorekort-konvensjon), ikke på spillerekkefølgen -- riktig uansett
|
|
|
|
|
|
// hvilket starthull runden faktisk begynte på. Kun meningsfullt å splitte
|
|
|
|
|
|
// for en 18-hulls runde; en 9-hulls runde får kun én samlet Sum-kolonne.
|
|
|
|
|
|
const showSplit = holesPlanned === 18
|
|
|
|
|
|
const front = holeOrder.filter((n) => n <= 9)
|
|
|
|
|
|
const back = holeOrder.filter((n) => n >= 10)
|
|
|
|
|
|
|
|
|
|
|
|
function sumFor(playerId: string, numbers: number[]): { strokes: number; par: number } | null {
|
|
|
|
|
|
const list = holesByParticipant[playerId]
|
|
|
|
|
|
if (!list || numbers.length === 0) return null
|
|
|
|
|
|
const played = numbers
|
|
|
|
|
|
.map((n) => list.find((h) => h.hole_number === n))
|
|
|
|
|
|
.filter((h): h is ApiHole => Boolean(h) && h!.played && h!.score !== null)
|
|
|
|
|
|
if (played.length === 0) return null
|
|
|
|
|
|
return {
|
|
|
|
|
|
strokes: played.reduce((sum, h) => sum + (h.score as number), 0),
|
|
|
|
|
|
par: played.reduce((sum, h) => sum + h.par, 0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const parRowTotal = holeOrder.reduce((sum, n) => sum + (holeMeta[n]?.par ?? 0), 0)
|
|
|
|
|
|
|
2026-07-29 15:46:49 +02:00
|
|
|
|
// Match-aware fargelegging (2026-07-29): entries[].id == round_participant.id
|
|
|
|
|
|
// for individuell-ball-formater (match/fourball) -- se _build_format_result
|
|
|
|
|
|
// i app/routers/rounds.py. Kun relevant når formatResult faktisk er klart.
|
|
|
|
|
|
const showMatchColors = isTwoSided && Boolean(formatResult?.ready) && Boolean(formatResult?.holes)
|
|
|
|
|
|
const formatHolesByNumber = new Map((formatResult?.holes ?? []).map((h) => [h.hole_number, h]))
|
|
|
|
|
|
const stillingByHole = showMatchColors ? matchStillingByHole(holeOrder, formatResult, holesPlanned) : new Map()
|
|
|
|
|
|
function sideOf(playerId: string): "a" | "b" | null {
|
|
|
|
|
|
const p = players.find((x) => x.id === playerId)
|
|
|
|
|
|
if (!p?.roundSideId) return null
|
|
|
|
|
|
return p.roundSideId === sides[0]?.id ? "a" : "b"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
// Lagvis gruppering (2026-08-02, "det store grepet") -- et to-sidet
|
|
|
|
|
|
// format (fourball/match/foursome osv.) skal vise side A samlet, deretter
|
|
|
|
|
|
// side B samlet, ikke ren innsettingsrekkefølge (A, B, C, D uansett hvem
|
|
|
|
|
|
// som faktisk er lagkamerater). Stabil sortering bevarer original
|
|
|
|
|
|
// rekkefølge INNAD i hver gruppe -- kun selve grupperingen endres. Kun
|
|
|
|
|
|
// meningsfullt når begge sider faktisk finnes; ellers uendret rekkefølge.
|
|
|
|
|
|
const sortedPlayers =
|
|
|
|
|
|
sides.length === 2
|
|
|
|
|
|
? [...players].sort((a, b) => sideRank(a.roundSideId, sides) - sideRank(b.roundSideId, sides))
|
|
|
|
|
|
: players
|
|
|
|
|
|
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
return (
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
<div className="overflow-x-auto rounded-2xl border border-border">
|
|
|
|
|
|
<table className="w-full border-separate border-spacing-0 text-sm">
|
|
|
|
|
|
<colgroup>
|
|
|
|
|
|
<col className="w-[130px]" />
|
|
|
|
|
|
{holeOrder.map((n) => (
|
|
|
|
|
|
<col key={n} className="w-[52px]" />
|
|
|
|
|
|
))}
|
|
|
|
|
|
{showSplit && <col className="w-[60px]" />}
|
|
|
|
|
|
{showSplit && <col className="w-[60px]" />}
|
|
|
|
|
|
<col className="w-[60px]" />
|
|
|
|
|
|
</colgroup>
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr className="border-b border-border">
|
|
|
|
|
|
<th
|
|
|
|
|
|
scope="col"
|
|
|
|
|
|
className="sticky left-0 z-10 border-r border-border bg-card px-3 py-2 text-left text-xs font-bold uppercase tracking-wide text-muted-foreground"
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
>
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
Spiller
|
|
|
|
|
|
</th>
|
|
|
|
|
|
{holeOrder.map((n) => {
|
|
|
|
|
|
const active = n === currentHole
|
|
|
|
|
|
return (
|
|
|
|
|
|
<th key={n} scope="col" className="border-l border-border p-0">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => onSelectHole(n)}
|
|
|
|
|
|
aria-current={active ? "true" : undefined}
|
|
|
|
|
|
aria-label={`Velg hull ${n}`}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex min-h-11 w-full items-center justify-center text-sm font-extrabold tabular-nums transition-colors",
|
|
|
|
|
|
active ? "bg-primary text-primary-foreground" : "bg-card text-foreground hover:bg-accent/50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{n}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</th>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
{showSplit && (
|
|
|
|
|
|
<th scope="col" className="border-l border-border bg-muted px-1 py-2 text-xs font-bold text-muted-foreground">
|
|
|
|
|
|
Ut
|
|
|
|
|
|
</th>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{showSplit && (
|
|
|
|
|
|
<th scope="col" className="border-l border-border bg-muted px-1 py-2 text-xs font-bold text-muted-foreground">
|
|
|
|
|
|
Inn
|
|
|
|
|
|
</th>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<th scope="col" className="border-l border-border bg-muted px-1 py-2 text-xs font-bold text-muted-foreground">
|
|
|
|
|
|
Sum
|
|
|
|
|
|
</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
{/* Hcp- og Par-referanserader (samme mønster som den allerede
|
|
|
|
|
|
shippede round-scorecard.tsx) -- rene informasjonsrader, ikke
|
|
|
|
|
|
trykkbare. */}
|
|
|
|
|
|
<tr className="border-b border-border">
|
|
|
|
|
|
<th scope="row" className="sticky left-0 z-10 border-r border-border bg-card px-3 py-1.5 text-left text-xs font-semibold text-muted-foreground">
|
|
|
|
|
|
Hcp
|
|
|
|
|
|
</th>
|
|
|
|
|
|
{holeOrder.map((n) => (
|
|
|
|
|
|
<td key={n} className="border-l border-border py-1.5 text-center text-xs font-semibold tabular-nums text-muted-foreground">
|
|
|
|
|
|
{holeMeta[n]?.index ?? "–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{showSplit && <td className="border-l border-border bg-muted" />}
|
|
|
|
|
|
{showSplit && <td className="border-l border-border bg-muted" />}
|
|
|
|
|
|
<td className="border-l border-border bg-muted" />
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr className="border-b-2 border-border">
|
|
|
|
|
|
<th scope="row" className="sticky left-0 z-10 border-r border-border bg-card px-3 py-1.5 text-left text-xs font-semibold text-muted-foreground">
|
|
|
|
|
|
Par
|
|
|
|
|
|
</th>
|
|
|
|
|
|
{holeOrder.map((n) => (
|
|
|
|
|
|
<td key={n} className="border-l border-border py-1.5 text-center text-xs font-semibold tabular-nums text-muted-foreground">
|
|
|
|
|
|
{holeMeta[n]?.par ?? "–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{showSplit && (
|
|
|
|
|
|
<td className="border-l border-border bg-muted text-center text-xs font-bold tabular-nums text-foreground">
|
|
|
|
|
|
{front.reduce((sum, n) => sum + (holeMeta[n]?.par ?? 0), 0) || "–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{showSplit && (
|
|
|
|
|
|
<td className="border-l border-border bg-muted text-center text-xs font-bold tabular-nums text-foreground">
|
|
|
|
|
|
{back.reduce((sum, n) => sum + (holeMeta[n]?.par ?? 0), 0) || "–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<td className="border-l border-border bg-muted text-center text-xs font-bold tabular-nums text-foreground">
|
|
|
|
|
|
{parRowTotal || "–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
|
Ja, jeg forsto rekkefølgen — steg 1 og 2 er nå ferdig og scratch-verifisert:
Steg 1 (backend, HCP-prosent + Match-HCP): ny migrasjon 051_round_allowance_override.sql (round.allowance_override jsonb), koblet inn i _recompute_side_handicaps/_relative_strokes_for_round + POST/PATCH /rounds. Verifisert presist i scratch: 100%→50%-prosent ga playing_handicap 24/10 → 12/5 nøyaktig som beregnet for hånd, og use_matchplay_handicap slått av ga rå 12/5 i stedet for differensial 7/0. Slagspill upåvirket (regresjon bekreftet).
Steg 2 (kode-fiks, lag-gruppering): ScorecardGrid (live-scoring) og MatchScorecardGrid (post-runde/fremtidig sammenslått side) sorterer nå stabilt på round_side_id før rendering. Bygget en fersk scratch-fourball-runde med akkurat ditt bug-scenario (A→Rødt, B→Blått, C→Rødt, D→Blått, lagt til i den rekkefølgen) og bekreftet visuelt i nettleseren i begge visningene: Rødt lag vises samlet, deretter Blått — ikke lenger interleaved.
Typesjekk + full produksjonsbuild kompilerte rent gjennom hele runden.
Gjenstår før jeg går videre til V0-promptene: migrasjon 051 er kun kjørt i scratch, ikke mot ekte teecup_db. Planen er:
docker exec -i teeoff_db psql -U teeoff_admin -d teecup_db -v ON_ERROR_STOP=1 < 051_round_allowance_override.sql
— ett rent additivt ALTER TABLE round ADD COLUMN allowance_override jsonb, ingen eksisterende rader berøres. Deretter docker compose up -d --build teecup_api teecup_frontend (backend for allowance-koblingen, frontend for gruppering-fiksen).
Bekreft så kjører jeg dette, og går videre til V0-prompt 1 (rundeoppsett-veiviseren).
2026-08-02 08:24:01 +02:00
|
|
|
|
{sortedPlayers.map((player) => {
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
const list = holesByParticipant[player.id]
|
|
|
|
|
|
const frontSum = showSplit ? sumFor(player.id, front) : null
|
|
|
|
|
|
const backSum = showSplit ? sumFor(player.id, back) : null
|
|
|
|
|
|
const totalSum = sumFor(player.id, holeOrder)
|
2026-07-29 15:46:49 +02:00
|
|
|
|
const playerSide = showMatchColors ? sideOf(player.id) : null
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<tr key={player.id} className="border-b border-border last:border-b-0">
|
|
|
|
|
|
<th scope="row" className="sticky left-0 z-10 border-r border-border bg-card p-0 text-left font-normal">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={readOnly}
|
|
|
|
|
|
onClick={() => onOpenEntry(player.id, currentHole)}
|
|
|
|
|
|
aria-label={`Registrer score for ${player.name} på hull ${currentHole}`}
|
|
|
|
|
|
className="flex min-h-12 w-[130px] flex-col justify-center gap-0.5 px-3 py-1.5 text-left transition-colors hover:bg-accent/50 disabled:opacity-60"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="flex items-center gap-1.5">
|
2026-07-29 15:46:49 +02:00
|
|
|
|
{playerSide && (
|
|
|
|
|
|
<span
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"size-2 shrink-0 rounded-full",
|
|
|
|
|
|
playerSide === "a" ? "bg-primary" : "bg-brand-orange",
|
|
|
|
|
|
)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
<span className="truncate text-sm font-bold text-foreground">{player.name}</span>
|
|
|
|
|
|
{player.isSelf && (
|
|
|
|
|
|
<Badge variant="default" className="shrink-0 px-1.5 py-0 text-[10px]">
|
|
|
|
|
|
Deg
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="truncate text-xs tabular-nums text-muted-foreground">
|
|
|
|
|
|
HCP {player.hcp !== null ? player.hcp.toFixed(1).replace(".", ",") : "–"}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</th>
|
|
|
|
|
|
{holeOrder.map((n) => {
|
|
|
|
|
|
const apiHole = list?.find((h) => h.hole_number === n) ?? null
|
|
|
|
|
|
const meta = holeMeta[n]
|
|
|
|
|
|
const hasScore = Boolean(apiHole?.played) && apiHole?.score !== null
|
|
|
|
|
|
const diff = hasScore && meta ? (apiHole!.score as number) - meta.par : null
|
|
|
|
|
|
const net =
|
|
|
|
|
|
hasScore && apiHole?.strokes_received != null ? (apiHole!.score as number) - apiHole!.strokes_received : null
|
2026-07-29 15:46:49 +02:00
|
|
|
|
const formatHole = showMatchColors ? formatHolesByNumber.get(n) : undefined
|
|
|
|
|
|
const entry = formatHole?.entries.find((e) => e.id === player.id)
|
|
|
|
|
|
const won = Boolean(entry && playerSide && formatHole?.result === playerSide && entry.counted !== false)
|
|
|
|
|
|
const dimmed = Boolean(entry && entry.counted === false)
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<td key={n} className="border-l border-border p-0 text-center">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={readOnly}
|
|
|
|
|
|
onClick={() => onOpenEntry(player.id, n)}
|
|
|
|
|
|
aria-label={
|
|
|
|
|
|
hasScore
|
2026-07-29 15:46:49 +02:00
|
|
|
|
? `${player.name}, hull ${n}, ${apiHole?.score} slag${diff !== null && meta ? `, ${golfTermForScore(apiHole!.score as number, meta.par) ?? ""}` : ""}${won ? ", vant hullet" : ""}`
|
2026-07-30 09:36:52 +02:00
|
|
|
|
: `Registrer ${player.name} sin score på hull ${n}${apiHole?.strokes_received ? `, mottar ${apiHole.strokes_received} slag` : ""}`
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
}
|
|
|
|
|
|
className="flex min-h-12 w-full flex-col items-center justify-center gap-0.5 py-1 transition-colors hover:bg-accent/40 disabled:opacity-60"
|
|
|
|
|
|
>
|
2026-07-29 15:46:49 +02:00
|
|
|
|
{showMatchColors ? (
|
|
|
|
|
|
<MatchScorecardCell
|
|
|
|
|
|
value={hasScore ? (apiHole!.score as number) : null}
|
|
|
|
|
|
side={playerSide}
|
|
|
|
|
|
won={won}
|
|
|
|
|
|
dimmed={dimmed}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
<ScorecardCell
|
|
|
|
|
|
value={hasScore ? (apiHole!.score as number) : null}
|
|
|
|
|
|
diff={diff}
|
|
|
|
|
|
pickedUp={apiHole?.picked_up}
|
|
|
|
|
|
/>
|
2026-07-29 15:46:49 +02:00
|
|
|
|
)}
|
|
|
|
|
|
{hasScore ? (
|
|
|
|
|
|
net !== null && <span className="text-[10px] font-semibold tabular-nums text-muted-foreground">{net}</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
// Mottatte slag vist FØR hullet er fylt ut (2026-07-29,
|
2026-07-30 09:36:52 +02:00
|
|
|
|
// etterspurt av bruker) -- kun når spilleren faktisk
|
2026-07-29 15:46:49 +02:00
|
|
|
|
// mottar minst ett slag her (ingen støy på de andre).
|
2026-07-30 09:36:52 +02:00
|
|
|
|
// Prikker i stedet for "−N"-tekst (2026-07-30).
|
2026-07-29 15:46:49 +02:00
|
|
|
|
apiHole?.strokes_received != null &&
|
2026-07-30 09:36:52 +02:00
|
|
|
|
apiHole.strokes_received > 0 && <StrokeDots count={apiHole.strokes_received} />
|
2026-07-29 15:46:49 +02:00
|
|
|
|
)}
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
</button>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
{showSplit && (
|
|
|
|
|
|
<td className="border-l border-border bg-muted px-1 py-1 text-center text-sm font-extrabold tabular-nums text-foreground">
|
|
|
|
|
|
{frontSum ? frontSum.strokes : "–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{showSplit && (
|
|
|
|
|
|
<td className="border-l border-border bg-muted px-1 py-1 text-center text-sm font-extrabold tabular-nums text-foreground">
|
|
|
|
|
|
{backSum ? backSum.strokes : "–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<td className="border-l border-border bg-muted px-1 py-1 text-center">
|
|
|
|
|
|
{totalSum ? (
|
|
|
|
|
|
<span className="flex flex-col items-center leading-tight">
|
|
|
|
|
|
<span className="text-sm font-extrabold tabular-nums text-foreground">{totalSum.strokes}</span>
|
|
|
|
|
|
<span className="text-[10px] font-semibold tabular-nums text-muted-foreground">
|
|
|
|
|
|
{formatToPar(totalSum.strokes - totalSum.par)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
"–"
|
|
|
|
|
|
)}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
{/* High-low-high (2026-07-30) har INGEN "X UP"-lead-begrep -- poeng
|
|
|
|
|
|
fordeles 0-2 per hull (to uavhengige duelløer), ikke ett
|
|
|
|
|
|
lead-poeng per vunnet hull som resten av to-sidede formater
|
|
|
|
|
|
(denne raden ville da vist en misvisende match-play-tekst
|
|
|
|
|
|
basert på hull VUNNET, ikke faktiske poeng). Literal poengsum
|
|
|
|
|
|
vises i stedet i FormatResultPanel over selve gridet --
|
|
|
|
|
|
per-hull-fargeleggingen (won/dimmed) er fortsatt riktig og
|
|
|
|
|
|
vises uendret. Formatet er uansett aldri delt-ball, så denne
|
|
|
|
|
|
sjekken er kun reelt aktiv i ScorecardGrid, ikke her. */}
|
|
|
|
|
|
{showMatchColors && formatResult?.play_format !== "high_low_high" && (
|
2026-07-29 15:46:49 +02:00
|
|
|
|
<tr className="border-b border-border bg-muted/30 last:border-b-0">
|
|
|
|
|
|
<th scope="row" className="sticky left-0 z-10 border-r border-border bg-muted/30 px-3 py-1.5 text-left text-xs font-extrabold text-foreground">
|
|
|
|
|
|
Stilling
|
|
|
|
|
|
</th>
|
|
|
|
|
|
{holeOrder.map((n) => {
|
|
|
|
|
|
const s = stillingByHole.get(n)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<td
|
|
|
|
|
|
key={n}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"border-l border-border py-1.5 text-center text-xs font-extrabold tabular-nums",
|
|
|
|
|
|
!s ? "text-muted-foreground" : s.tone === "a" ? "text-primary" : s.tone === "b" ? "text-brand-orange" : "text-foreground",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{s?.label ?? "–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
{showSplit && <td className="border-l border-border bg-muted" />}
|
|
|
|
|
|
{showSplit && <td className="border-l border-border bg-muted" />}
|
|
|
|
|
|
<td className="border-l border-border bg-muted" />
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
)}
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
// Golfscore-språket for en LITEN tabellcelle -- samme eksakte klassifisering
|
|
|
|
|
|
// og Tailwind-klasser som `ScoreMark`/`classify` i round-scorecard.tsx
|
|
|
|
|
|
// (bekreftet ved å lese kildekoden der direkte, egen kopi her per
|
|
|
|
|
|
// prosjektets etablerte "lokale komponentmønstre per fil"-konvensjon, se
|
|
|
|
|
|
// DESIGN_SYSTEM.md). Sirkel under par, "firkant" (`rounded-[4px]`) over
|
|
|
|
|
|
// par, ren tekst på par, fylt ved 2+ slag fra par.
|
2026-07-30 09:36:52 +02:00
|
|
|
|
// Mottatte slag FØR hullet er fylt ut, vist som prikker i stedet for tekst
|
|
|
|
|
|
// (2026-07-30, etterspurt av bruker som en kompakt erstatning for "−N") --
|
|
|
|
|
|
// ren visuell hint, den faktiske informasjonen ligger i knappens eget
|
|
|
|
|
|
// aria-label (§tilgjengelighet: aldri kun et visuelt symbol uten tekst for
|
|
|
|
|
|
// noe som faktisk betyr noe).
|
|
|
|
|
|
function StrokeDots({ count }: { count: number }) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span className="flex items-center justify-center gap-0.5" aria-hidden="true">
|
|
|
|
|
|
{Array.from({ length: count }, (_, i) => (
|
|
|
|
|
|
<span key={i} className="size-1.5 rounded-full bg-info" />
|
|
|
|
|
|
))}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
function ScorecardCell({
|
|
|
|
|
|
value,
|
|
|
|
|
|
diff,
|
|
|
|
|
|
pickedUp,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
value: number | null
|
|
|
|
|
|
diff: number | null
|
|
|
|
|
|
pickedUp?: boolean
|
|
|
|
|
|
}) {
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
const shared = "flex size-7 items-center justify-center text-sm font-extrabold tabular-nums"
|
Stableford is now a real spilleform for frittstående runder, alongside Slagspill/Match: selectable at round creation, editable afterward, shown as a badge everywhere the round appears.
"Plukket opp" — a new button in the scoring wizard's Slag step (only for Slagspill/Stableford, when handicap is known). Tapping it auto-hops forward just like picking a stroke count, and the server writes the exact Net Double Bogey score (par+2+mottatte slag) — so it flows through the existing HCP/AGS pipeline unchanged, always nets to exactly 0 Stableford points. Shown everywhere as a "PU" badge instead of a raw number: scorekortet, leaderboardet, "Så langt i runden".
Along the way, browser testing caught two real bugs (not just theoretical): the new-round form briefly showed a nonsensical "set up sides" notice under Stableford, and — more seriously — the match/skins territory-bar panel was rendering empty "Side A/Side B" bars on a Stableford round. Both traced to the same root cause (=== "stroke" checks that needed to also exclude the new format), and a full grep caught three more of the same pattern in watch-round.tsx before they could reach production.
Everything verified in scratch (hand-calculated math matched exactly) and in the browser (all four display surfaces, plus a regression check on an existing match round). Migration 038 applied to the real database, both containers redeployed, teeoff.no unaffected.
2026-07-29 19:46:17 +02:00
|
|
|
|
// "Plukket opp" (migrasjon 038, Stableford) -- 0 poeng er impliserte,
|
|
|
|
|
|
// vises som "PU" i stedet for det (allerede kjente) Net Double Bogey-
|
|
|
|
|
|
// slagtallet, som ellers ville sett ut som en vilkårlig høy score.
|
|
|
|
|
|
if (pickedUp) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span className={cn(shared, "rounded-[4px] border-2 border-brand-orange bg-brand-orange/10 text-[10px] text-brand-orange")}>
|
|
|
|
|
|
PU
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
Update Todos
Log in as user in chrome-devtools browser (magic link)
Screenshot scorekort-grid page at mobile viewport
Fix critical sticky-column overlap bug found via real browser test
Verify fix by scrolling grid + re-screenshotting
Assess density and golfscore-language compliance
Update CLAUDE.md/FEATURE_BACKLOG.md with the real bug+fix
Report findings to user
Good thing you asked me to actually look — I found and fixed a real, serious bug.
What was broken: position: sticky on table cells, combined with sticky columns on both the left (player names) and right (Ut/Inn/Sum) at once, rendered completely garbled in Chrome — the sticky right columns got pulled to the visible edge immediately and overlapped/bled into the hole columns underneath (you'd have seen overlapping digits like "436"/"472"). My first fix attempt (border-collapse → border-separate) didn't touch the real cause and the second screenshot still showed the same mess.
Root cause: combining sticky-left and sticky-right in one table wider than the viewport is inherently unstable — at scroll position 0, the sticky-right cells are already pinned to the screen edge, sitting visually on top of whatever hole columns happen to be there in normal document flow, only partially hidden by their translucent background.
Fix: dropped sticky positioning from the Ut/Inn/Sum columns entirely — they now scroll with the rest of the holes, same well-supported pattern as the (correctly-working) sticky name column. Verified for real this time:
Screenshot at scroll-left=0: all numbers clean, correct circle/square/plain-text score marks (bogey/double-bogey/par all visually distinct).
Scrolled the grid all the way right via a script: holes 14-18 plus Ut/Inn/Sum all render cleanly, name column still correctly pinned — Ut 36 / Inn 36 / Sum 72 on the Par row, which is exactly right for an 18-hole par-72 course.
Accessibility tree confirms the golf-term labels ("Bogey", "Dobbel bogey", "Par") are actually present in aria-labels, so §Golfscore-språket's "never color alone" rule holds in practice, not just in code.
No dead space concerns — density looks reasonable against §0.1's tolerance, and green stays reserved for active-state/under-par per §0.2 (no stray "grønt-på-grønt" spotted).
Redeployed twice (once for the failed attempt, once for the real fix); both times /health → 200, teeoff.no unaffected. Docs updated with the full story, since this is the first time this session a real browser was available to verify hand-coded frontend work — worth remembering for anything else built blind this week that you haven't clicked through yet.
2026-07-27 14:34:18 +02:00
|
|
|
|
if (value === null) return <span className={cn(shared, "text-muted-foreground")}>–</span>
|
|
|
|
|
|
if (diff === null || diff === 0) return <span className={cn(shared, "font-bold text-foreground")}>{value}</span>
|
|
|
|
|
|
if (diff <= -2) return <span className={cn(shared, "rounded-full bg-primary text-primary-foreground")}>{value}</span>
|
|
|
|
|
|
if (diff === -1) return <span className={cn(shared, "rounded-full border-2 border-primary bg-primary/10 text-primary")}>{value}</span>
|
|
|
|
|
|
if (diff === 1) {
|
|
|
|
|
|
return <span className={cn(shared, "rounded-[4px] border-2 border-brand-orange bg-brand-orange/10 text-brand-orange")}>{value}</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
return <span className={cn(shared, "rounded-[4px] bg-brand-orange text-brand-orange-foreground")}>{value}</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-29 15:46:49 +02:00
|
|
|
|
// Match-aware variant av ScorecardCell (2026-07-29) -- fargen følger HVEM SOM
|
|
|
|
|
|
// VANT hullet (grønn=side A, oransje=side B), ikke over/under par, siden
|
|
|
|
|
|
// det er det som betyr noe i en match. Nedtonet tekst for en fourball-
|
|
|
|
|
|
// partner sin ikke-tellende score (laveste netto teller for siden).
|
|
|
|
|
|
function MatchScorecardCell({
|
|
|
|
|
|
value,
|
|
|
|
|
|
side,
|
|
|
|
|
|
won,
|
|
|
|
|
|
dimmed,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
value: number | null
|
|
|
|
|
|
side: "a" | "b" | null
|
|
|
|
|
|
won: boolean
|
|
|
|
|
|
dimmed: boolean
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const shared = "flex size-7 items-center justify-center text-sm font-extrabold tabular-nums"
|
|
|
|
|
|
if (value === null) return <span className={cn(shared, "text-muted-foreground")}>–</span>
|
|
|
|
|
|
if (won) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
shared,
|
|
|
|
|
|
"rounded-full",
|
|
|
|
|
|
side === "a" ? "bg-primary text-primary-foreground" : "bg-brand-orange text-brand-orange-foreground",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{value}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
return <span className={cn(shared, dimmed ? "text-muted-foreground/50" : "font-bold text-foreground")}>{value}</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 06:27:35 +02:00
|
|
|
|
// --- Statistikknivå-velger ---------------------------------------------------
|
|
|
|
|
|
// "Hullet er spilt" fantes tidligere som egen avkrysning, men var reelt
|
|
|
|
|
|
// overflødig -- score settes allerede automatisk til "spilt" idet et
|
|
|
|
|
|
// slagtall velges (se onChange på Slag-NumberPicker under). Fjernet
|
|
|
|
|
|
// 2026-07-24 på brukerens eksplisitte bekreftelse.
|
|
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
// --- Number picker ---------------------------------------------------------
|
|
|
|
|
|
|
2026-07-26 15:48:07 +02:00
|
|
|
|
// Golf-term per slagtall relativt til par (2026-07-26, inspirert av en
|
|
|
|
|
|
// konkurrentapps taltastatur der HVER knapp er merket kontekstuelt --
|
|
|
|
|
|
// fjerner behovet for å regne "er 6 slag på et par 4-hull en bogey eller
|
|
|
|
|
|
// dobbel bogey?" i hodet). Kun brukt for Slag-velgeren.
|
|
|
|
|
|
function golfTermForScore(n: number, par: number): string | null {
|
|
|
|
|
|
const diff = n - par
|
|
|
|
|
|
if (diff <= -3) return "Albatross"
|
|
|
|
|
|
if (diff === -2) return "Eagle"
|
|
|
|
|
|
if (diff === -1) return "Birdie"
|
|
|
|
|
|
if (diff === 0) return "Par"
|
|
|
|
|
|
if (diff === 1) return "Bogey"
|
|
|
|
|
|
if (diff === 2) return "Dobbel bogey"
|
|
|
|
|
|
return null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
// --- Delt-ball-scorekort (ADR-039 Beslutning C) -----------------------------
|
|
|
|
|
|
// Samme grid-idé som ScorecardGrid, men rader = SIDER (ikke spillere) -- kun
|
|
|
|
|
|
// ett tall per hull (ingen putt/netto-linje, en delt ball har ingen entydig
|
|
|
|
|
|
// netto-visning per spiller her).
|
|
|
|
|
|
|
|
|
|
|
|
function SideScorecardGrid({
|
|
|
|
|
|
sides,
|
|
|
|
|
|
players,
|
|
|
|
|
|
holesBySide,
|
|
|
|
|
|
holeOrder,
|
|
|
|
|
|
holesPlanned,
|
|
|
|
|
|
currentHole,
|
|
|
|
|
|
readOnly,
|
2026-07-29 15:46:49 +02:00
|
|
|
|
isTwoSided,
|
|
|
|
|
|
formatResult,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
onSelectHole,
|
|
|
|
|
|
onOpenEntry,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
sides: ApiSide[]
|
|
|
|
|
|
players: Player[]
|
|
|
|
|
|
holesBySide: Record<string, ApiSideHole[]>
|
|
|
|
|
|
holeOrder: number[]
|
|
|
|
|
|
holesPlanned: number
|
|
|
|
|
|
currentHole: number
|
|
|
|
|
|
readOnly: boolean
|
2026-07-29 15:46:49 +02:00
|
|
|
|
// Match-aware fargelegging + Stilling-rad (2026-07-29) -- delt-ball-
|
|
|
|
|
|
// formater (foursome/greensome/scramble) er ALLE to-sidede per ADR-039,
|
|
|
|
|
|
// så denne er reelt alltid sann her, men holdt eksplisitt for symmetri
|
|
|
|
|
|
// med ScorecardGrid.
|
|
|
|
|
|
isTwoSided: boolean
|
|
|
|
|
|
formatResult: ApiFormatResult | null
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
onSelectHole: (holeNumber: number) => void
|
|
|
|
|
|
onOpenEntry: (sideId: string, holeNumber: number) => void
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const holeMeta: Record<number, { par: number; index: number }> = {}
|
|
|
|
|
|
for (const list of Object.values(holesBySide)) {
|
|
|
|
|
|
for (const h of list) {
|
|
|
|
|
|
if (!(h.hole_number in holeMeta)) holeMeta[h.hole_number] = { par: h.par, index: h.stroke_index }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const showSplit = holesPlanned === 18
|
|
|
|
|
|
const front = holeOrder.filter((n) => n <= 9)
|
|
|
|
|
|
const back = holeOrder.filter((n) => n >= 10)
|
|
|
|
|
|
|
|
|
|
|
|
function sumFor(sideId: string, numbers: number[]): { strokes: number; par: number } | null {
|
|
|
|
|
|
const list = holesBySide[sideId]
|
|
|
|
|
|
if (!list || numbers.length === 0) return null
|
|
|
|
|
|
const played = numbers
|
|
|
|
|
|
.map((n) => list.find((h) => h.hole_number === n))
|
|
|
|
|
|
.filter((h): h is ApiSideHole => Boolean(h) && h!.played && h!.score !== null)
|
|
|
|
|
|
if (played.length === 0) return null
|
|
|
|
|
|
return {
|
|
|
|
|
|
strokes: played.reduce((sum, h) => sum + (h.score as number), 0),
|
|
|
|
|
|
par: played.reduce((sum, h) => sum + h.par, 0),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const parRowTotal = holeOrder.reduce((sum, n) => sum + (holeMeta[n]?.par ?? 0), 0)
|
|
|
|
|
|
|
2026-07-29 15:46:49 +02:00
|
|
|
|
// entries[].id == round_side.id for delt-ball-formater (samme
|
|
|
|
|
|
// _build_format_result-kontrakt som ScorecardGrid bruker for spillere).
|
|
|
|
|
|
const showMatchColors = isTwoSided && Boolean(formatResult?.ready) && Boolean(formatResult?.holes)
|
|
|
|
|
|
const formatHolesByNumber = new Map((formatResult?.holes ?? []).map((h) => [h.hole_number, h]))
|
|
|
|
|
|
const stillingByHole = showMatchColors ? matchStillingByHole(holeOrder, formatResult, holesPlanned) : new Map()
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="overflow-x-auto rounded-2xl border border-border">
|
|
|
|
|
|
<table className="w-full border-separate border-spacing-0 text-sm">
|
|
|
|
|
|
<colgroup>
|
|
|
|
|
|
<col className="w-[130px]" />
|
|
|
|
|
|
{holeOrder.map((n) => (
|
|
|
|
|
|
<col key={n} className="w-[52px]" />
|
|
|
|
|
|
))}
|
|
|
|
|
|
{showSplit && <col className="w-[60px]" />}
|
|
|
|
|
|
{showSplit && <col className="w-[60px]" />}
|
|
|
|
|
|
<col className="w-[60px]" />
|
|
|
|
|
|
</colgroup>
|
|
|
|
|
|
<thead>
|
|
|
|
|
|
<tr className="border-b border-border">
|
|
|
|
|
|
<th
|
|
|
|
|
|
scope="col"
|
|
|
|
|
|
className="sticky left-0 z-10 border-r border-border bg-card px-3 py-2 text-left text-xs font-bold uppercase tracking-wide text-muted-foreground"
|
|
|
|
|
|
>
|
|
|
|
|
|
Side
|
|
|
|
|
|
</th>
|
|
|
|
|
|
{holeOrder.map((n) => {
|
|
|
|
|
|
const active = n === currentHole
|
|
|
|
|
|
return (
|
|
|
|
|
|
<th key={n} scope="col" className="border-l border-border p-0">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => onSelectHole(n)}
|
|
|
|
|
|
aria-current={active ? "true" : undefined}
|
|
|
|
|
|
aria-label={`Velg hull ${n}`}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex min-h-11 w-full items-center justify-center text-sm font-extrabold tabular-nums transition-colors",
|
|
|
|
|
|
active ? "bg-primary text-primary-foreground" : "bg-card text-foreground hover:bg-accent/50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{n}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</th>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
{showSplit && (
|
|
|
|
|
|
<th scope="col" className="border-l border-border bg-muted px-1 py-2 text-xs font-bold text-muted-foreground">
|
|
|
|
|
|
Ut
|
|
|
|
|
|
</th>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{showSplit && (
|
|
|
|
|
|
<th scope="col" className="border-l border-border bg-muted px-1 py-2 text-xs font-bold text-muted-foreground">
|
|
|
|
|
|
Inn
|
|
|
|
|
|
</th>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<th scope="col" className="border-l border-border bg-muted px-1 py-2 text-xs font-bold text-muted-foreground">
|
|
|
|
|
|
Sum
|
|
|
|
|
|
</th>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
</thead>
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
<tr className="border-b border-border">
|
|
|
|
|
|
<th scope="row" className="sticky left-0 z-10 border-r border-border bg-card px-3 py-1.5 text-left text-xs font-semibold text-muted-foreground">
|
|
|
|
|
|
Hcp
|
|
|
|
|
|
</th>
|
|
|
|
|
|
{holeOrder.map((n) => (
|
|
|
|
|
|
<td key={n} className="border-l border-border py-1.5 text-center text-xs font-semibold tabular-nums text-muted-foreground">
|
|
|
|
|
|
{holeMeta[n]?.index ?? "–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{showSplit && <td className="border-l border-border bg-muted" />}
|
|
|
|
|
|
{showSplit && <td className="border-l border-border bg-muted" />}
|
|
|
|
|
|
<td className="border-l border-border bg-muted" />
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
<tr className="border-b-2 border-border">
|
|
|
|
|
|
<th scope="row" className="sticky left-0 z-10 border-r border-border bg-card px-3 py-1.5 text-left text-xs font-semibold text-muted-foreground">
|
|
|
|
|
|
Par
|
|
|
|
|
|
</th>
|
|
|
|
|
|
{holeOrder.map((n) => (
|
|
|
|
|
|
<td key={n} className="border-l border-border py-1.5 text-center text-xs font-semibold tabular-nums text-muted-foreground">
|
|
|
|
|
|
{holeMeta[n]?.par ?? "–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{showSplit && (
|
|
|
|
|
|
<td className="border-l border-border bg-muted text-center text-xs font-bold tabular-nums text-foreground">
|
|
|
|
|
|
{front.reduce((sum, n) => sum + (holeMeta[n]?.par ?? 0), 0) || "–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{showSplit && (
|
|
|
|
|
|
<td className="border-l border-border bg-muted text-center text-xs font-bold tabular-nums text-foreground">
|
|
|
|
|
|
{back.reduce((sum, n) => sum + (holeMeta[n]?.par ?? 0), 0) || "–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<td className="border-l border-border bg-muted text-center text-xs font-bold tabular-nums text-foreground">
|
|
|
|
|
|
{parRowTotal || "–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
|
|
|
|
{sides.map((side, sideIndex) => {
|
|
|
|
|
|
const list = holesBySide[side.id]
|
|
|
|
|
|
const frontSum = showSplit ? sumFor(side.id, front) : null
|
|
|
|
|
|
const backSum = showSplit ? sumFor(side.id, back) : null
|
|
|
|
|
|
const totalSum = sumFor(side.id, holeOrder)
|
|
|
|
|
|
const assignedNames = players.filter((p) => p.roundSideId === side.id).map((p) => p.name)
|
2026-07-29 15:46:49 +02:00
|
|
|
|
const sideTone: "a" | "b" = sideIndex === 0 ? "a" : "b"
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<tr key={side.id} className="border-b border-border last:border-b-0">
|
|
|
|
|
|
<th scope="row" className="sticky left-0 z-10 border-r border-border bg-card p-0 text-left font-normal">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={readOnly}
|
|
|
|
|
|
onClick={() => onOpenEntry(side.id, currentHole)}
|
|
|
|
|
|
aria-label={`Registrer score for ${side.label?.trim() || `Side ${sideIndex === 0 ? "A" : "B"}`} på hull ${currentHole}`}
|
|
|
|
|
|
className="flex min-h-12 w-[130px] flex-col justify-center gap-0.5 px-3 py-1.5 text-left transition-colors hover:bg-accent/50 disabled:opacity-60"
|
|
|
|
|
|
>
|
2026-07-29 15:46:49 +02:00
|
|
|
|
<span className="flex items-center gap-1.5">
|
|
|
|
|
|
{showMatchColors && (
|
|
|
|
|
|
<span
|
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
|
className={cn("size-2 shrink-0 rounded-full", sideTone === "a" ? "bg-primary" : "bg-brand-orange")}
|
|
|
|
|
|
/>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<span className="truncate text-sm font-bold text-foreground">
|
|
|
|
|
|
{side.label?.trim() || `Side ${sideIndex === 0 ? "A" : "B"}`}
|
|
|
|
|
|
</span>
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
</span>
|
|
|
|
|
|
<span className="truncate text-xs text-muted-foreground">
|
|
|
|
|
|
{assignedNames.length > 0 ? assignedNames.join(" · ") : "Ingen spillere"}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</th>
|
|
|
|
|
|
{holeOrder.map((n) => {
|
|
|
|
|
|
const apiHole = list?.find((h) => h.hole_number === n) ?? null
|
|
|
|
|
|
const meta = holeMeta[n]
|
|
|
|
|
|
const hasScore = Boolean(apiHole?.played) && apiHole?.score !== null
|
|
|
|
|
|
const diff = hasScore && meta ? (apiHole!.score as number) - meta.par : null
|
2026-07-29 15:46:49 +02:00
|
|
|
|
const formatHole = showMatchColors ? formatHolesByNumber.get(n) : undefined
|
|
|
|
|
|
const entry = formatHole?.entries.find((e) => e.id === side.id)
|
|
|
|
|
|
const won = Boolean(entry && formatHole?.result === sideTone)
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<td key={n} className="border-l border-border p-0 text-center">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={readOnly}
|
|
|
|
|
|
onClick={() => onOpenEntry(side.id, n)}
|
|
|
|
|
|
aria-label={
|
|
|
|
|
|
hasScore
|
2026-07-29 15:46:49 +02:00
|
|
|
|
? `${side.label ?? "Side"}, hull ${n}, ${apiHole?.score} slag${won ? ", vant hullet" : ""}`
|
2026-07-30 09:36:52 +02:00
|
|
|
|
: `Registrer score for hull ${n}${apiHole?.strokes_received ? `, mottar ${apiHole.strokes_received} slag` : ""}`
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
}
|
2026-07-29 15:46:49 +02:00
|
|
|
|
className="flex min-h-12 w-full flex-col items-center justify-center gap-0.5 py-1 transition-colors hover:bg-accent/40 disabled:opacity-60"
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
>
|
2026-07-29 15:46:49 +02:00
|
|
|
|
{showMatchColors ? (
|
|
|
|
|
|
<MatchScorecardCell
|
|
|
|
|
|
value={hasScore ? (apiHole!.score as number) : null}
|
|
|
|
|
|
side={sideTone}
|
|
|
|
|
|
won={won}
|
|
|
|
|
|
dimmed={false}
|
|
|
|
|
|
/>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<ScorecardCell value={hasScore ? (apiHole!.score as number) : null} diff={diff} />
|
|
|
|
|
|
)}
|
|
|
|
|
|
{/* Mottatte slag vist FØR hullet er fylt ut (2026-07-29) --
|
|
|
|
|
|
samme kombinerte side-verdi som ville blitt trukket fra
|
2026-07-30 09:36:52 +02:00
|
|
|
|
ved utfylling. Prikker i stedet for "−N"-tekst (2026-07-30). */}
|
2026-07-29 15:46:49 +02:00
|
|
|
|
{!hasScore && apiHole?.strokes_received != null && apiHole.strokes_received > 0 && (
|
2026-07-30 09:36:52 +02:00
|
|
|
|
<StrokeDots count={apiHole.strokes_received} />
|
2026-07-29 15:46:49 +02:00
|
|
|
|
)}
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
</button>
|
|
|
|
|
|
</td>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
{showSplit && (
|
|
|
|
|
|
<td className="border-l border-border bg-muted px-1 py-1 text-center text-sm font-extrabold tabular-nums text-foreground">
|
|
|
|
|
|
{frontSum ? frontSum.strokes : "–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{showSplit && (
|
|
|
|
|
|
<td className="border-l border-border bg-muted px-1 py-1 text-center text-sm font-extrabold tabular-nums text-foreground">
|
|
|
|
|
|
{backSum ? backSum.strokes : "–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<td className="border-l border-border bg-muted px-1 py-1 text-center">
|
|
|
|
|
|
{totalSum ? (
|
|
|
|
|
|
<span className="flex flex-col items-center leading-tight">
|
|
|
|
|
|
<span className="text-sm font-extrabold tabular-nums text-foreground">{totalSum.strokes}</span>
|
|
|
|
|
|
<span className="text-[10px] font-semibold tabular-nums text-muted-foreground">
|
|
|
|
|
|
{formatToPar(totalSum.strokes - totalSum.par)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
"–"
|
|
|
|
|
|
)}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
Update Todos
Chapman: format-valg i new-round.tsx + tournament-program.tsx (session)
Nassau: nytt vindu-resultatvisning i round-detail.tsx + session-scorecard.tsx
Københavner: format-valg + poengtabell-visning i individual-tournament-detail.tsx
Bingo Bango Bongo: format-valg + per-hull picker + poengtabell
Flaggturnering: format-valg + nedtelling/resultatvisning
Shamble: format-valg + best_n + lagvisning (frittstående + org-lag)
Money Ball: format-valg + lineup_order + lagvisning (frittstående + org-lag)
High-low-high: format-valg + løpende poeng-resultatvisning (alle 3 flater)
Ekte produksjonsbuild -- FERDIG, kompilerte rent (27 ruter)
Browserverifisert alle åtte formatene -- fant og fikset ekte backend-bug (side-handicap-recompute)
Rullet ut mot ekte teecup_api/teecup_frontend -- FERDIG, health checks grønne, teeoff.no upåvirket
Rullet ut live 2026-07-30. Frontend for alle åtte nye turneringsformatene (Chapman, Nassau, Københavner, Bingo Bango Bongo, Flaggturnering, Shamble, Money Ball, High-low-high) er nå bygget, browserverifisert og live — dekker alle tre flatene (frittstående runder, org-lagturneringer, org-individuelle turneringer). Fant og fikset én reell backend-bug underveis (side-handicap ble ikke regnet på nytt når en deltakers HCP ble satt/endret etter at de allerede var tildelt en side). Ingen migrasjon i denne runden, teeoff.no upåvirket. ADR-039/åtte-formater-arbeidet er dermed helt ferdig, backend og frontend.
2026-08-01 06:51:53 +02:00
|
|
|
|
{/* High-low-high (2026-07-30) har INGEN "X UP"-lead-begrep -- poeng
|
|
|
|
|
|
fordeles 0-2 per hull (to uavhengige duelløer), ikke ett
|
|
|
|
|
|
lead-poeng per vunnet hull som resten av to-sidede formater
|
|
|
|
|
|
(denne raden ville da vist en misvisende match-play-tekst
|
|
|
|
|
|
basert på hull VUNNET, ikke faktiske poeng). Literal poengsum
|
|
|
|
|
|
vises i stedet i FormatResultPanel over selve gridet --
|
|
|
|
|
|
per-hull-fargeleggingen (won/dimmed) er fortsatt riktig og
|
|
|
|
|
|
vises uendret. Formatet er uansett aldri delt-ball, så denne
|
|
|
|
|
|
sjekken er kun reelt aktiv i ScorecardGrid, ikke her. */}
|
|
|
|
|
|
{showMatchColors && formatResult?.play_format !== "high_low_high" && (
|
2026-07-29 15:46:49 +02:00
|
|
|
|
<tr className="border-b border-border bg-muted/30 last:border-b-0">
|
|
|
|
|
|
<th scope="row" className="sticky left-0 z-10 border-r border-border bg-muted/30 px-3 py-1.5 text-left text-xs font-extrabold text-foreground">
|
|
|
|
|
|
Stilling
|
|
|
|
|
|
</th>
|
|
|
|
|
|
{holeOrder.map((n) => {
|
|
|
|
|
|
const s = stillingByHole.get(n)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<td
|
|
|
|
|
|
key={n}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"border-l border-border py-1.5 text-center text-xs font-extrabold tabular-nums",
|
|
|
|
|
|
!s ? "text-muted-foreground" : s.tone === "a" ? "text-primary" : s.tone === "b" ? "text-brand-orange" : "text-foreground",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{s?.label ?? "–"}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
{showSplit && <td className="border-l border-border bg-muted" />}
|
|
|
|
|
|
{showSplit && <td className="border-l border-border bg-muted" />}
|
|
|
|
|
|
<td className="border-l border-border bg-muted" />
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
)}
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Delt-ball-scoreveiviser (ADR-039 Beslutning C) --------------------------
|
|
|
|
|
|
// Enklere enn ScoringWizard -- kun ett tall (slagtall for hele siden), ingen
|
|
|
|
|
|
// putt-/detalj-steg. Samme "Neste side" → "Neste hull"-samlebåndsprinsipp.
|
|
|
|
|
|
|
|
|
|
|
|
function SideScoreWizard({
|
|
|
|
|
|
sides,
|
|
|
|
|
|
side,
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
sideParticipants,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
hole,
|
|
|
|
|
|
score,
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
selectedParticipantId,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
onSave,
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
onSelectParticipant,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
onClose,
|
|
|
|
|
|
onAdvance,
|
|
|
|
|
|
readOnly,
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
isPending,
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
}: {
|
|
|
|
|
|
sides: ApiSide[]
|
|
|
|
|
|
side: ApiSide
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
// Scramble/greensome: hvem sitt utslag ble valgt (migrasjon 036).
|
|
|
|
|
|
sideParticipants: Player[]
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
hole: Hole
|
|
|
|
|
|
score: number | null
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
selectedParticipantId: string | null
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
onSave: (score: number | null) => void
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
onSelectParticipant: (participantId: string | null) => void
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
onClose: () => void
|
|
|
|
|
|
onAdvance: () => void
|
|
|
|
|
|
readOnly: boolean
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
// Offline scoreregistrering (ADR-028-utvidelse, 2026-07-28) -- se
|
|
|
|
|
|
// ScoringWizard sin samme prop for full begrunnelse.
|
|
|
|
|
|
isPending: boolean
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
}) {
|
|
|
|
|
|
const sideIndex = sides.findIndex((s) => s.id === side.id)
|
|
|
|
|
|
const isLast = sideIndex === -1 || sideIndex === sides.length - 1
|
|
|
|
|
|
const nextSide = !isLast ? sides[sideIndex + 1] : null
|
|
|
|
|
|
|
|
|
|
|
|
function sideLabel(s: ApiSide, idx: number) {
|
|
|
|
|
|
return s.label?.trim() || `Side ${idx === 0 ? "A" : "B"}`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-28 15:32:02 +02:00
|
|
|
|
// Auto-hopp (2026-07-28, samme mønster som ScoringWizard) -- registrer
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
// slagtall for siden, hopp umiddelbart videre til neste side/hull. KUN
|
|
|
|
|
|
// når det ikke finnes noe utslags-valg å ta stilling til også (migrasjon
|
|
|
|
|
|
// 036) -- ellers ville auto-hoppet revet brukeren videre FØR de rakk å
|
|
|
|
|
|
// velge hvem sitt utslag som ble brukt (det valgfrie feltet under).
|
|
|
|
|
|
const autoAdvance = sideParticipants.length === 0
|
2026-07-28 15:32:02 +02:00
|
|
|
|
const enteredWithValueRef = useRef(score !== null)
|
|
|
|
|
|
const firedRef = useRef(false)
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
firedRef.current = false
|
|
|
|
|
|
enteredWithValueRef.current = score !== null
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
|
}, [side.id, hole.holeNumber])
|
|
|
|
|
|
useEffect(() => {
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
if (!autoAdvance || firedRef.current || enteredWithValueRef.current) return
|
2026-07-28 15:32:02 +02:00
|
|
|
|
if (score !== null) {
|
|
|
|
|
|
firedRef.current = true
|
|
|
|
|
|
onAdvance()
|
|
|
|
|
|
}
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
}, [score, autoAdvance])
|
2026-07-28 15:32:02 +02:00
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="fixed inset-0 z-50 flex flex-col bg-background">
|
|
|
|
|
|
<header className="flex min-h-14 shrink-0 items-center gap-2 border-b border-border px-3">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
|
aria-label="Lukk veiviseren"
|
|
|
|
|
|
className="flex size-11 shrink-0 items-center justify-center rounded-xl text-foreground transition-colors hover:bg-accent"
|
|
|
|
|
|
>
|
|
|
|
|
|
<X aria-hidden="true" className="size-5" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div className="flex min-w-0 flex-1 flex-col items-center text-center">
|
|
|
|
|
|
<span className="truncate text-base font-extrabold text-foreground">
|
|
|
|
|
|
Hull {hole.holeNumber} · Par {hole.par}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="truncate text-sm font-semibold text-muted-foreground">{sideLabel(side, sideIndex)}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="size-11 shrink-0" aria-hidden="true" />
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="-mx-1 flex shrink-0 gap-2 overflow-x-auto border-b border-border px-4 py-2.5">
|
|
|
|
|
|
{sides.map((s, i) => (
|
|
|
|
|
|
<span
|
|
|
|
|
|
key={s.id}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex shrink-0 items-center rounded-full border px-3 py-1.5 text-sm font-bold",
|
|
|
|
|
|
s.id === side.id
|
|
|
|
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
|
|
|
|
: "border-border bg-card text-muted-foreground",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{sideLabel(s, i)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
Ferdig og live. Oppsummert:
Offline-køen fra ADR-028 (turnering-scorekortet) er nå portert til frittstående runder — begge scoringsveivisere (vanlig og delt-ball) fungerer offline.
Portingen ble faktisk enklere enn originalen: siden PATCH-endepunktene for frittstående runder erstatter hele hull-raden per kall, kunne en køet skriving speiles direkte inn i lokal state uten egne verdi-overlays.
Samme mønster som originalen: navigator.onLine-sjekk, nettverksfeil fanges og køes, automatisk synk når nettet kommer tilbake, manuell "Synkroniser nå"-knapp, og en "Lagret lokalt · venter på synk"-indikator i selve veiviseren.
Faktisk browserverifisert denne gangen (i motsetning til den opprinnelige ADR-028-runden, som aldri ble testet i praksis) — brukte Chrome DevTools sin ekte offline-emulering: registrerte score offline, bekreftet kø i IndexedDB, gikk online igjen, bekreftet automatisk synk og at serveren faktisk mottok riktige verdier. Gjort for begge veivisertypene.
Ingen migrasjon — rullet ut mot ekte systemer, begge containere boot-et rent, teeoff.no upåvirket.
2026-07-28 18:17:49 +02:00
|
|
|
|
{isPending && (
|
|
|
|
|
|
<p className="shrink-0 border-b border-border bg-muted/50 px-4 py-1.5 text-center text-xs font-medium text-muted-foreground">
|
|
|
|
|
|
Lagret lokalt · venter på synk
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
<main className="flex-1 overflow-y-auto p-5 sm:p-6">
|
|
|
|
|
|
<div className="mx-auto flex max-w-sm flex-col gap-6">
|
|
|
|
|
|
<NumberPicker
|
|
|
|
|
|
label="Slag"
|
|
|
|
|
|
value={score}
|
|
|
|
|
|
directValues={[1, 2, 3, 4, 5, 6, 7, 8, 9]}
|
|
|
|
|
|
expandValues={[10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]}
|
|
|
|
|
|
expandLabel="10+"
|
|
|
|
|
|
parValue={hole.par}
|
|
|
|
|
|
showGolfTerms
|
|
|
|
|
|
onChange={(v) => onSave(v)}
|
|
|
|
|
|
readOnly={readOnly}
|
|
|
|
|
|
/>
|
Update Todos
Bygg PWA-installasjonsoppfordring (Android beforeinstallprompt + iOS instruksjonsbanner)
Design og bygg flere flighter i én frittstående runde (løs gruppering, retning 1)
Design og bygg scramble/greensome-statistikk (valgt utslag per spiller)
Bygg push-varsler til telefonens OS (VAPID, push_subscription, service worker)
Scratch-verifiser backend for alle fire (27/27 sjekker gjort)
Browserverifiser alle fire i nettleser mot scratch-backend
Rull ut mot ekte systemer (migrasjoner + containere), med bekreftelse
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Alle fire er ferdigstilt, verifisert og live. Kort oppsummert:
PWA-installasjonsoppfordring — ekte beforeinstallprompt-fanging, Android-knapp + iOS-instruksjonsbanner, vist på dashbordet. Bekreftet at Chrome faktisk fyrte eventet i testøkten.
Flere flighter i én frittstående runde — løs gruppering av separate runder (delt UUID), "+ Legg til en flight til" gjenbruker hele opprett-flyten forhåndsutfylt, nytt kombinert leaderboard på tvers av flighter. Testet fra knappetrykk til ferdig runde, bekreftet i database.
Scramble/greensome: valgt utslag per spiller — nytt felt i hull-registreringen (helt valgfritt), "Utslag brukt"-oppsummering på statistikksiden. Bygget for frittstående runder; org-turneringer bevisst utenfor omfang denne runden (notert i backloggen).
Push-varsler til telefonens OS — Web Push/VAPID, ny av/på-bryter i kontoinnstillinger, dekker alle fire varseltyper automatisk. VAPID-nøkler generert og lagt i .env uten å vises i chatten. Én ærlig begrensning: ekte OS-varsel-levering med innvilget tillatelse kunne ikke testes i denne automatiserte nettleserøkten (tillatelsen var forhåndsblokkert av miljøet) — verdt å teste selv på en ekte enhet.
27/27 scratch-sjekker, full nettleser-gjennomgang uten konsollfeil, migrasjoner 035–037 kjørt mot ekte teecup_db, begge containere redeployet og verifisert, teeoff.no upåvirket. Alle tre .md-filene er oppdatert med full detalj.
2026-07-28 22:34:16 +02:00
|
|
|
|
|
|
|
|
|
|
{/* Scramble/greensome: hvem sitt utslag ble valgt (migrasjon 036,
|
|
|
|
|
|
FEATURE_BACKLOG.md "Scramble: statistikk over utslag brukt per
|
|
|
|
|
|
spiller"). Helt valgfritt -- egen handling, rører aldri
|
|
|
|
|
|
slagtallet. */}
|
|
|
|
|
|
{sideParticipants.length > 0 && (
|
|
|
|
|
|
<fieldset className="flex flex-col gap-2">
|
|
|
|
|
|
<legend className="mb-1 text-sm font-bold text-foreground">
|
|
|
|
|
|
Hvem sitt utslag ble brukt? <span className="font-normal text-muted-foreground">(valgfritt)</span>
|
|
|
|
|
|
</legend>
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
{sideParticipants.map((p) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={p.id}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={readOnly}
|
|
|
|
|
|
onClick={() => onSelectParticipant(selectedParticipantId === p.id ? null : p.id)}
|
|
|
|
|
|
aria-pressed={selectedParticipantId === p.id}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"inline-flex min-h-11 items-center rounded-xl border px-4 text-sm font-bold transition-colors disabled:opacity-60",
|
|
|
|
|
|
selectedParticipantId === p.id
|
|
|
|
|
|
? "border-primary bg-primary text-primary-foreground"
|
|
|
|
|
|
: "border-border bg-card text-foreground hover:bg-accent/50",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{p.name}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</fieldset>
|
|
|
|
|
|
)}
|
Ferdig og live. Kort oppsummert:
Bygget: hele frontend for ADR-039-spillformene — utvidet spilleform-velgeren i "Ny runde" til alle åtte formater med skins-konfig, en ny SidesPanel i "Spillere og runde" (opprett/slett sider, tildel/fjern spillere med sanntids kapasitetssperre), en ny FormatResultPanel (løpende matchstatus for match/fourball/foursome/greensome/scramble, skins-tavle for skins), setup_complete-gating av "Fullfør runde", og et eget delt-ball-scorekort+veiviser for foursome/greensome/scramble.
Testet reelt i nettleser (Chrome DevTools mot en isolert scratch-backend, ikke bare typesjekk) — spilte gjennom en komplett match-, skins- og foursome-runde fra bunnen av. Fant og fikset to reelle stale-state-buger underveis (setup-melding og matchstatus ble stående utdatert etter side-tildeling til de ikke lenger refetchet runden). Skins- og foursome-handicap-matematikken kryssjekket for hånd og stemte eksakt.
Rullet ut mot ekte systemer — ingen migrasjon, kun teecup_frontend bygget på nytt, begge containere boot-et rent, teeoff.no upåvirket. ADR-039 er dermed helt ferdig, backend og frontend.
2026-07-28 13:00:50 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
|
|
<footer className="grid shrink-0 grid-cols-2 gap-3 border-t border-border p-4">
|
|
|
|
|
|
<Button type="button" variant="outline" onClick={onClose} className="h-14 rounded-2xl text-base font-bold">
|
|
|
|
|
|
Avbryt
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={score === null}
|
|
|
|
|
|
onClick={onAdvance}
|
|
|
|
|
|
className="h-14 rounded-2xl text-base font-bold disabled:opacity-40"
|
|
|
|
|
|
>
|
|
|
|
|
|
{isLast ? "Ferdig" : `Neste: ${sideLabel(nextSide!, sideIndex + 1)}`}
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</footer>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
function NumberPicker({
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
label,
|
|
|
|
|
|
value,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
directValues,
|
|
|
|
|
|
expandValues,
|
|
|
|
|
|
expandLabel,
|
2026-07-24 11:21:39 +02:00
|
|
|
|
parValue,
|
2026-07-26 15:48:07 +02:00
|
|
|
|
showGolfTerms,
|
2026-07-24 11:21:39 +02:00
|
|
|
|
maxValue,
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
onChange,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
readOnly,
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}: {
|
|
|
|
|
|
label: string
|
|
|
|
|
|
value: number | null
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
directValues: number[]
|
|
|
|
|
|
expandValues: number[]
|
|
|
|
|
|
expandLabel: string
|
2026-07-26 15:48:07 +02:00
|
|
|
|
// Hullets par -- brukt til golf-term-bildeteksten når showGolfTerms er satt.
|
2026-07-24 11:21:39 +02:00
|
|
|
|
parValue?: number
|
2026-07-26 15:48:07 +02:00
|
|
|
|
// Viser Eagle/Birdie/Par/Bogey/Dobbel bogey under HVERT tall (relativt til
|
|
|
|
|
|
// parValue), i stedet for kun å merke selve par-knappen -- kun meningsfullt
|
|
|
|
|
|
// for Slag-velgeren, ikke Putter/Anywayslag.
|
|
|
|
|
|
showGolfTerms?: boolean
|
2026-07-24 11:21:39 +02:00
|
|
|
|
// Skjuler valg høyere enn dette (f.eks. Putter kan aldri overstige
|
|
|
|
|
|
// antall slag registrert på hullet) -- ingen vits i å tilby et tall som
|
|
|
|
|
|
// uansett ville vært selvmotsigende.
|
|
|
|
|
|
maxValue?: number
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
onChange: (value: number) => void
|
|
|
|
|
|
readOnly: boolean
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}) {
|
2026-07-24 11:21:39 +02:00
|
|
|
|
const cap = (values: number[]) => (maxValue === undefined ? values : values.filter((n) => n <= maxValue))
|
|
|
|
|
|
const cappedDirect = cap(directValues)
|
|
|
|
|
|
const cappedExpand = cap(expandValues)
|
|
|
|
|
|
|
|
|
|
|
|
const valueInExpand = value !== null && cappedExpand.includes(value)
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
const [expanded, setExpanded] = useState(false)
|
|
|
|
|
|
const showExpanded = expanded || valueInExpand
|
|
|
|
|
|
|
2026-07-24 11:21:39 +02:00
|
|
|
|
const visibleValues = showExpanded ? [...cappedDirect, ...cappedExpand] : cappedDirect
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
return (
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<span className="text-base font-semibold text-foreground">{label}</span>
|
2026-07-24 11:21:39 +02:00
|
|
|
|
{/* Numpad: tre kolonner, knappene fyller bredden -- store trykkflater
|
|
|
|
|
|
for bruk utendørs, ofte med sol på skjermen (V0-runde 2026-07-24). */}
|
|
|
|
|
|
<div className="grid max-w-sm grid-cols-3 gap-2.5">
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
{visibleValues.map((n) => {
|
|
|
|
|
|
const selected = value === n
|
2026-07-26 15:48:07 +02:00
|
|
|
|
const term = showGolfTerms && parValue !== undefined ? golfTermForScore(n, parValue) : n === parValue ? "par" : null
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={n}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={readOnly}
|
|
|
|
|
|
onClick={() => onChange(n)}
|
2026-07-26 15:48:07 +02:00
|
|
|
|
aria-label={term ? `${label} ${n}, ${term}` : `${label} ${n}`}
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
aria-pressed={selected}
|
|
|
|
|
|
className={cn(
|
2026-07-24 11:21:39 +02:00
|
|
|
|
"flex h-16 flex-col items-center justify-center gap-0 rounded-2xl border text-2xl font-extrabold leading-none tabular-nums transition-colors disabled:opacity-100",
|
|
|
|
|
|
selected
|
|
|
|
|
|
? "border-primary bg-primary text-primary-foreground shadow-sm"
|
|
|
|
|
|
: "border-border bg-card text-foreground hover:bg-accent/50",
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
readOnly && !selected && "opacity-40",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
2026-07-24 11:21:39 +02:00
|
|
|
|
<span>{n}</span>
|
2026-07-26 15:48:07 +02:00
|
|
|
|
{term && (
|
2026-07-24 11:21:39 +02:00
|
|
|
|
<span className={cn("text-[11px] font-bold leading-none", selected ? "text-primary-foreground/80" : "text-muted-foreground")}>
|
2026-07-26 15:48:07 +02:00
|
|
|
|
{term}
|
2026-07-24 11:21:39 +02:00
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
</button>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
2026-07-24 11:21:39 +02:00
|
|
|
|
{!showExpanded && !readOnly && cappedExpand.length > 0 && (
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setExpanded(true)}
|
2026-07-24 11:21:39 +02:00
|
|
|
|
className="col-span-3 flex h-14 items-center justify-center rounded-2xl border border-dashed border-border bg-card text-lg font-bold text-foreground transition-colors hover:bg-accent/50"
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
>
|
|
|
|
|
|
{expandLabel}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 11:21:39 +02:00
|
|
|
|
// --- Retningskors (D-pad for Utslag / Innspill) -----------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function DirButton({
|
|
|
|
|
|
icon: Icon,
|
|
|
|
|
|
label,
|
|
|
|
|
|
selected,
|
|
|
|
|
|
onClick,
|
|
|
|
|
|
readOnly,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
icon: typeof ArrowUp
|
|
|
|
|
|
label: string
|
|
|
|
|
|
selected: boolean
|
|
|
|
|
|
onClick: () => void
|
|
|
|
|
|
readOnly: boolean
|
|
|
|
|
|
}) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={readOnly}
|
|
|
|
|
|
onClick={onClick}
|
|
|
|
|
|
aria-pressed={selected}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex min-h-16 flex-col items-center justify-center gap-1 rounded-2xl border px-2 py-2 transition-colors disabled:opacity-100",
|
|
|
|
|
|
selected
|
|
|
|
|
|
? "border-primary bg-primary text-primary-foreground shadow-sm"
|
|
|
|
|
|
: "border-border bg-card text-foreground hover:bg-accent/50",
|
|
|
|
|
|
readOnly && !selected && "opacity-40",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Icon aria-hidden="true" className="size-6" />
|
|
|
|
|
|
<span className="text-sm font-bold">{label}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function DirectionCross({
|
|
|
|
|
|
label,
|
|
|
|
|
|
value,
|
|
|
|
|
|
onChange,
|
|
|
|
|
|
readOnly,
|
|
|
|
|
|
variant,
|
|
|
|
|
|
centerLabel,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
label: string
|
|
|
|
|
|
value: string | null
|
|
|
|
|
|
onChange: (value: string) => void
|
|
|
|
|
|
readOnly: boolean
|
|
|
|
|
|
// "horizontal" = kun venstre/senter/høyre (Utslag); "full" = 5-veis (Innspill)
|
|
|
|
|
|
variant: "horizontal" | "full"
|
|
|
|
|
|
centerLabel: string
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const spacer = <div aria-hidden="true" />
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<span className="text-base font-semibold text-foreground">{label}</span>
|
|
|
|
|
|
<div className="grid max-w-sm grid-cols-3 gap-2.5">
|
|
|
|
|
|
{variant === "full" && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{spacer}
|
|
|
|
|
|
<DirButton icon={ArrowUp} label="Langt" selected={value === "long"} onClick={() => onChange("long")} readOnly={readOnly} />
|
|
|
|
|
|
{spacer}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<DirButton icon={ArrowLeft} label="Venstre" selected={value === "left"} onClick={() => onChange("left")} readOnly={readOnly} />
|
|
|
|
|
|
<DirButton
|
|
|
|
|
|
icon={Target}
|
|
|
|
|
|
label={centerLabel}
|
|
|
|
|
|
selected={value === (variant === "full" ? "hit" : "fairway")}
|
|
|
|
|
|
onClick={() => onChange(variant === "full" ? "hit" : "fairway")}
|
|
|
|
|
|
readOnly={readOnly}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<DirButton icon={ArrowRight} label="Høyre" selected={value === "right"} onClick={() => onChange("right")} readOnly={readOnly} />
|
|
|
|
|
|
{variant === "full" && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
{spacer}
|
|
|
|
|
|
<DirButton icon={ArrowDown} label="Kort" selected={value === "short"} onClick={() => onChange("short")} readOnly={readOnly} />
|
|
|
|
|
|
{spacer}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
// --- Choice row (segmented buttons) ----------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
function ChoiceRow({
|
|
|
|
|
|
label,
|
|
|
|
|
|
options,
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
value,
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
onChange,
|
|
|
|
|
|
readOnly,
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}: {
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
label: string
|
|
|
|
|
|
options: { value: string; label: string }[]
|
|
|
|
|
|
value: string | null
|
|
|
|
|
|
onChange: (value: string) => void
|
|
|
|
|
|
readOnly: boolean
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
<span className="text-base font-semibold text-foreground">{label}</span>
|
|
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
|
|
|
{options.map((opt) => {
|
|
|
|
|
|
const selected = value === opt.value
|
|
|
|
|
|
return (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={opt.value}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={readOnly}
|
|
|
|
|
|
onClick={() => onChange(opt.value)}
|
|
|
|
|
|
aria-pressed={selected}
|
|
|
|
|
|
className={cn(
|
|
|
|
|
|
"flex min-h-12 flex-1 items-center justify-center rounded-2xl border px-4 text-base font-bold transition-colors disabled:opacity-100",
|
|
|
|
|
|
selected ? "border-primary bg-primary text-primary-foreground" : "border-border bg-card text-foreground hover:bg-accent/50",
|
|
|
|
|
|
readOnly && !selected && "opacity-40",
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{opt.label}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
// --- Stepper (+/-) ---------------------------------------------------------
|
|
|
|
|
|
|
2026-07-24 11:21:39 +02:00
|
|
|
|
function Stepper({
|
|
|
|
|
|
label,
|
|
|
|
|
|
value,
|
|
|
|
|
|
onChange,
|
|
|
|
|
|
readOnly,
|
|
|
|
|
|
max,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
label: string
|
|
|
|
|
|
value: number
|
|
|
|
|
|
onChange: (value: number) => void
|
|
|
|
|
|
readOnly: boolean
|
|
|
|
|
|
// Kan ikke telle høyere enn antall slag registrert på hullet -- f.eks.
|
|
|
|
|
|
// umulig å ha chippet flere ganger enn totalt antall slag.
|
|
|
|
|
|
max?: number
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const atMax = max !== undefined && value >= max
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<span className="text-base font-semibold text-foreground">{label}</span>
|
|
|
|
|
|
<div className="flex items-center justify-between gap-2 rounded-2xl border border-border bg-card p-1.5">
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={readOnly || value <= 0}
|
|
|
|
|
|
onClick={() => onChange(Math.max(0, value - 1))}
|
|
|
|
|
|
aria-label={`Færre ${label}`}
|
|
|
|
|
|
className="flex size-11 shrink-0 items-center justify-center rounded-xl border border-border bg-background text-foreground transition-colors hover:bg-accent/50 disabled:opacity-40"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Minus aria-hidden="true" className="size-5" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span className="min-w-8 text-center text-xl font-extrabold tabular-nums text-foreground">{value}</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
2026-07-24 11:21:39 +02:00
|
|
|
|
disabled={readOnly || atMax}
|
V0-integrasjonen er ferdig. Kort om hva som skjedde:
Diff mot levende tre (samme rutine som alltid): alle tre eksportene var fulle re-eksporter av hele prosjektet, bare fire filer var faktisk nye/endret for dette formålet: round-card.tsx, own-rounds.tsx, new-round.tsx, round-detail.tsx (+ tre app/rounds*/page.tsx-ruter). Alt annet (login-form, dashboard, config osv.) var forventede full-reverts og ble ikke rørt. Ett kjent V0-avvik dukket opp igjen (app/clubs/[id]/page.tsx med feil parameternavn — samme feil som ble rettet i klubbsiderunden) — hoppet bevisst over den, som vanlig.
Erstattet mine hånd-bygde skjermer med V0s presentasjon, datalag skrevet om fra mock til ekte fetch, blant annet:
Egen-bane-søket er nå et ekte to-stegs oppslag (søk → hent detalj med utslag) siden V0s modell antok baner allerede hadde utslagene ferdig innbakt i søkeresultatet.
La til «Annet» som et tredje kjønnsvalg for gjestespillere (V0 hadde kun mann/kvinne) — matcher appens ellers etablerte konvensjon.
Beholdt merge-før-PATCH-sikringen fra forrige runde (hull-endepunktet skriver alle felt hver gang).
Fjernet V0s dev-only forhåndsvisningsbrytere («Forhåndsvis: Pågår/Fullført» osv.) — samme opprydning som i tidligere runder.
Verifisert: ekte typesjekket produksjonsbuild (docker build --target builder) kompilerte rent, alle ruter listet. Ingen backend-endring i denne runden, så API-kontrakten er allerede scratch-bevist fra forrige runde — jeg har ikke kjørt en ny interaktiv nettleser-test (intet slikt verktøy tilgjengelig), kun kodegjennomgang + typesjekk.
Zip-filene er slettet fra prosjektroten.
2026-07-23 11:27:37 +02:00
|
|
|
|
onClick={() => onChange(value + 1)}
|
|
|
|
|
|
aria-label={`Flere ${label}`}
|
|
|
|
|
|
className="flex size-11 shrink-0 items-center justify-center rounded-xl border border-border bg-background text-foreground transition-colors hover:bg-accent/50 disabled:opacity-40"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus aria-hidden="true" className="size-5" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
Update Todos
Add non-org-scoped teeoff search + personal-course detail endpoints to rounds.py
Build /rounds list page + component
Build /rounds/new creation flow (course search/create, tee, date, holes)
Build /rounds/[id] detail page (participants, hole-by-hole stats, complete)
Link new feature from dashboard
Scratch-verify backend additions + typecheck production build
Deploy live after user confirmation
Update ADR-033/FEATURE_BACKLOG/CLAUDE.md status
Frontend for standalone round tracking is built and fully verified in scratch — 22 backend contract checks plus a live end-to-end test against both course sources (custom bane and real teeoff lookup), and a clean typechecked production build (docker build --target builder) showing all new routes (/rounds, /rounds/new, /rounds/[id]).
What's new, in summary:
Backend: three additions to app/routers/rounds.py — non-org-scoped teeoff facility/course search (/rounds/official-search*), a personal-course detail endpoint exposing tee/gender info, and a GET .../holes endpoint (needed since RoundOut never carried hole-level data). Also changed the hole-PATCH endpoint to return the full updated row instead of {"ok": true}. No migration — no schema touched.
Frontend: /rounds (list), /rounds/new (course search or create, tee/date/holes-planned), /rounds/[id] (participants, hole-by-hole stat entry with GIR auto-derived, complete-round flow with HCP differential summary). Linked from the dashboard both as a header shortcut and a prominent "Egne runder" entry card.
One thing worth flagging: the hole-PATCH endpoint replaces all fields every call rather than doing a true partial update, so the frontend always merges the current hole state before sending a patch — I confirmed this contract explicitly in scratch (a naive partial PATCH silently nulls out unrelated fields).
2026-07-23 05:51:26 +02:00
|
|
|
|
}
|