Begge implementert, browserverifisert og live:
Lenke scorekort → statistikk lagt til nederst på scorekortsiden — symmetrisk med den eksisterende lenken tilbake. Prikker for mottatte slag i stedet for "−N"-tekst, i live-registreringen (begge grid-typer) — verifisert med en HCP 28-spiller som viste nøyaktig 2 prikker på de rette hullene. Selve spørsmålet om å slå alt sammen til én visning er bevisst latt åpent til dere har fått testet med ekte brukere — notert tydelig i FEATURE_BACKLOG.md slik at det ikke går i glemmeboken.
This commit is contained in:
parent
bcb98a8625
commit
facefd9951
6 changed files with 480 additions and 55 deletions
116
CLAUDE.md
116
CLAUDE.md
|
|
@ -5739,6 +5739,122 @@ Ferdig og verifisert:
|
||||||
bekreftet nåbare over ekte https med riktig `content-type`, `teeoff.no`
|
bekreftet nåbare over ekte https med riktig `content-type`, `teeoff.no`
|
||||||
upåvirket.
|
upåvirket.
|
||||||
|
|
||||||
|
- **Individuelle turneringer (ADR-037): scoring-autorisasjon strammet inn,
|
||||||
|
BYGGET, SCRATCH-VERIFISERT OG LIVE (2026-07-30), samme dag som frontend-
|
||||||
|
runden:** direkte oppfølging av det noterte hullet fra byggerunden
|
||||||
|
tidligere samme dag — ethvert org-medlem kunne skrive score for HVEM SOM
|
||||||
|
HELST i en individuell turnering, ikke bare sin egen.
|
||||||
|
Ny `user_is_own_tournament_participant` i `app/team_authz.py` — samme
|
||||||
|
mønster som `user_is_match_participant` (ADR-023): krever at brukeren ER
|
||||||
|
spilleren bak `tournament_participant`-raden (via `player.user_id`),
|
||||||
|
eller er org-eier/admin. Brukt av `individual_tournaments.py` sin
|
||||||
|
`update_hole` (eneste endepunkt strammet inn). Runde-/deltaker-OPPSETT
|
||||||
|
(opprett/slett runde, legg til/fjern turnering-/rundedeltaker) forblir
|
||||||
|
bevisst på vanlig org-medlemsnivå — matcher presedensen fra `session`-
|
||||||
|
opprettelse og `team_roster`-tilføyelse i tournaments.py, som heller
|
||||||
|
aldri har vært captain-/admin-gatet.
|
||||||
|
**Scratch-verifisert grundig** (isolert `teecup_app_scratch`-rolle +
|
||||||
|
isolert scratch-MinIO + engangs API-container, alle 40 migrasjoner kjørt
|
||||||
|
friskt): full regresjon av den eksisterende 52-punkts testsuiten (uendret
|
||||||
|
grønn — org-eier brukes gjennomgående der, rammes ikke av innstrammingen),
|
||||||
|
pluss 13 nye målrettede sjekker: et fremmed org-medlem (uten kobling til
|
||||||
|
noen av deltakerne) NEKTES å score for både en annen deltaker OG en
|
||||||
|
tredje deltaker (403 `NOT_TOURNAMENT_PARTICIPANT`), en spiller KOBLET til
|
||||||
|
sin egen `tournament_participant` (via `player.email` + ADR-017s
|
||||||
|
kontokobling ved innlogging) FÅR score seg selv men NEKTES å score for en
|
||||||
|
annen, org-eier beholder uendret admin-fallback for begge, og lesing
|
||||||
|
(`GET .../holes`) er bekreftet uendret tilgjengelig for et vanlig
|
||||||
|
org-medlem (kun skriving er strammet inn, ikke lesing).
|
||||||
|
**Rullet ut live 2026-07-30**, bruker bekreftet eksplisitt: ingen
|
||||||
|
migrasjon (ren Python-logikk), `docker compose up -d --build
|
||||||
|
teecup_api`. Containeren boot-et rent (`Application startup complete`),
|
||||||
|
`/health`/`/dashboard` → 200, `teeoff.no` upåvirket.
|
||||||
|
|
||||||
|
- **To notater fra brukeren, BEGGE BYGGET, GRUNDIG BROWSERVERIFISERT OG
|
||||||
|
LIVE (2026-07-30), samme dag som ADR-037-autorisasjonsfiksen:**
|
||||||
|
1. **GIR-auto-inferens for "Innspill: Traff":** ny `useEffect` i
|
||||||
|
`ScoringWizard` (`round-detail.tsx`) -- idet "flere detaljer"-steget
|
||||||
|
nås, settes `stat.approach = "hit"` automatisk når
|
||||||
|
`stat.strokes - stat.putts <= hole.par - 2` (samme formel som den
|
||||||
|
allerede eksisterende GIR-STATISTIKK-inferensen i `round-stats.tsx`
|
||||||
|
sin `isGir`, nå også koblet til selve REGISTRERINGEN) -- men KUN når
|
||||||
|
`stat.approach` fortsatt er `null` (rører aldri et allerede satt
|
||||||
|
manuelt ELLER tidligere auto-satt valg).
|
||||||
|
2. **Auto-prompt "Fullfør runde":** ny `allHolesEnteredForEveryone`-
|
||||||
|
beregning + `AllHolesEnteredBanner`-komponent i `RoundDetail` -- viser
|
||||||
|
en tydelig CTA øverst på Score-fanen ("Alle hull er ført — Klar til å
|
||||||
|
fullføre runden?") så snart ALLE spillere (eller BEGGE sider for
|
||||||
|
delt-ball-formater) har `played=true` på alle hull i `holeOrder`,
|
||||||
|
gatet på `round.setup_complete`. Kaller samme `finishRound()` som den
|
||||||
|
eksisterende, tidligere passive knappen.
|
||||||
|
**Browserverifisert grundig i en isolert scratch-nettleserøkt** (fersk
|
||||||
|
`teecup_scratch`-database + isolert scratch-MinIO + engangs API-
|
||||||
|
container + en isolert `next dev`-frontend-container med KOPIERT, ikke
|
||||||
|
bind-mountet, kildekode -- bind-mount ga gjentatte Turbopack-panics i
|
||||||
|
denne økten, løst ved å `tar`-kopiere kildetreet inn i en isolert
|
||||||
|
container-filsystem i stedet): GIR-auto-inferens bekreftet BÅDE positivt
|
||||||
|
(birdie+1-putt -> "Traff" auto-merket umiddelbart, uten klikk, bekreftet
|
||||||
|
visuelt OG direkte i databasen) og negativt (bogey+1-putt -> "Traff"
|
||||||
|
korrekt IKKE forhåndsmerket). Auto-prompt-banneret bekreftet å dukke opp
|
||||||
|
automatisk idet siste hull ble fylt for begge spillerne i en 18-hulls
|
||||||
|
2-spiller-runde, og "Fullfør runden"-knappen i banneret bekreftet å
|
||||||
|
fullføre runden korrekt (håndterte den native `confirm()`-dialogen via
|
||||||
|
Chrome DevTools -- HCP-differensialer beregnet og vist etterpå). Ingen
|
||||||
|
konsollfeil i noen av rundene.
|
||||||
|
**Rullet ut live 2026-07-30**, bruker bekreftet eksplisitt: ingen
|
||||||
|
migrasjon (ren frontend), `docker compose up -d --build
|
||||||
|
teecup_frontend` (gjenskapte også `teecup_api` som vanlig bivirkning,
|
||||||
|
ingen backend-kode rørt). Begge containere boot-et rent, `/health`/
|
||||||
|
`/dashboard` → 200, `teeoff.no` upåvirket.
|
||||||
|
|
||||||
|
- **Symbolforklaringen fjernet fra scorekortet + to design-notater fanget
|
||||||
|
opp (2026-07-30), samme dag:** brukeren viste et skjermbilde av
|
||||||
|
`round-scorecard.tsx` med "Under par (sirkel)/Over par (firkant)/Fylt
|
||||||
|
symbol..."-forklaringen sirklet inn og ba om at den fjernes — `Legend`-
|
||||||
|
komponenten (kun ett bruksted) fjernet fullstendig. Samtidig reist:
|
||||||
|
(1) spilleren bør kunne se historikk/statistikk for NØYAKTIG hullet som
|
||||||
|
spilles/er spilt (ikke bygget — krever en ny, ikke-triviell "aggreger på
|
||||||
|
tvers av runder, filtrert til banenavn+hullnummer"-spørring, notert i
|
||||||
|
FEATURE_BACKLOG.md); (2) manglende lenke scorekort→statistikk, og et
|
||||||
|
åpent spørsmål om scorekort/statistikk/live-registrering burde slås
|
||||||
|
sammen til én visning, med mottatte slag vist som prikker. Svarte
|
||||||
|
direkte (ikke bygget): behold scorekort og statistikk som to separate
|
||||||
|
sider (bevisst skilt 2026-07-25 nettopp for å unngå én lang/rotete
|
||||||
|
side) — legg heller til den manglende lenken; behold også selve
|
||||||
|
live-registrerings-gridet uendret (bygget 2026-07-27 som en AKTIV
|
||||||
|
data-entry-flate, ikke en lesevisning — et vertikalt front9/back9-delt
|
||||||
|
format ville svekket registreringsergonomikken). "Prikker for mottatte
|
||||||
|
slag" vurdert som en god, uavhengig senere polish-oppgave. Full
|
||||||
|
begrunnelse i FEATURE_BACKLOG.md.
|
||||||
|
**Rullet ut live 2026-07-30**, bruker bekreftet eksplisitt: ekte
|
||||||
|
typesjekket produksjonsbuild kompilerte rent, `docker compose up -d
|
||||||
|
--build teecup_frontend` (gjenskapte også `teecup_api` som vanlig
|
||||||
|
bivirkning). Begge containere boot-et rent, `/health` → 200,
|
||||||
|
scorekort-siden bekreftet 200, `teeoff.no` upåvirket.
|
||||||
|
|
||||||
|
- **De to avgrensede scorekort-oppfølgerne fra samme dag BYGGET,
|
||||||
|
BROWSERVERIFISERT OG LIVE (2026-07-30):** (1) ny "Se full
|
||||||
|
rundestatistikk"-lenke nederst på `round-scorecard.tsx` (symmetrisk med
|
||||||
|
den eksisterende motsatte lenken på statistikksiden); (2) mottatte
|
||||||
|
slag-hintet (vist FØR et hull er fylt ut) endret fra "−N"-tekst til
|
||||||
|
prikker (`StrokeDots`, `round-detail.tsx`, i BÅDE `ScorecardGrid` og
|
||||||
|
`SideScorecardGrid`) — selve tallet bevart i `aria-label` for
|
||||||
|
tilgjengelighet. Selve spørsmålet om å slå sammen scorekort/statistikk/
|
||||||
|
live-registrering til én visning er BEVISST IKKE avgjort — bruker
|
||||||
|
ønsker en fremtidig brukertest først.
|
||||||
|
**Browserverifisert grundig i en isolert scratch-nettleserøkt** (samme
|
||||||
|
mønster som resten av uken): en HCP 28-spiller (course handicap 30)
|
||||||
|
bekreftet å vise nøyaktig to prikker på hull 1-12 i live-
|
||||||
|
registreringsgridet (riktig ut fra 30-18=12 ekstra slag), den nye
|
||||||
|
lenken bekreftet klikkbar og navigerte korrekt til `/stats`. Ekte
|
||||||
|
typesjekket produksjonsbuild kompilerte rent. Ingen konsollfeil (kun en
|
||||||
|
godartet, urelatert WebSocket-advarsel fra rask sidenavigasjon).
|
||||||
|
**Rullet ut live 2026-07-30**, bruker bekreftet eksplisitt: ingen
|
||||||
|
migrasjon, `docker compose up -d --build teecup_frontend` (gjenskapte
|
||||||
|
også `teecup_api` som vanlig bivirkning). Begge containere boot-et
|
||||||
|
rent, `/health`/`/dashboard`/scorekort-siden → 200, `teeoff.no`
|
||||||
|
upåvirket.
|
||||||
|
|
||||||
Neste steg:
|
Neste steg:
|
||||||
0a. **Spillerliste-redesign — nå FAKTISK nettleser-bekreftet
|
0a. **Spillerliste-redesign — nå FAKTISK nettleser-bekreftet
|
||||||
(2026-07-27, full 22-skjerms gjennomgang):** rendrer korrekt, ingen
|
(2026-07-27, full 22-skjerms gjennomgang):** rendrer korrekt, ingen
|
||||||
|
|
|
||||||
|
|
@ -733,11 +733,16 @@ deltakere avvist 409, turnering-deltaker fortsatt referert av en
|
||||||
rundedeltaker avvist 400 RESTRICT-FK). `test_isolation.sql` 12/12
|
rundedeltaker avvist 400 RESTRICT-FK). `test_isolation.sql` 12/12
|
||||||
uendret (additiv migrasjon).
|
uendret (additiv migrasjon).
|
||||||
|
|
||||||
**Autorisasjon** er fortsatt bredt org-medlemskap for ALT inkl. selve
|
**Autorisasjon — ✅ SCORING STRAMMET INN, SCRATCH-VERIFISERT OG LIVE
|
||||||
scoreregistreringen -- en senere innstramming (analogt ADR-023s
|
2026-07-30** (samme dag, egen del-runde etter frontend-utrullingen): ny
|
||||||
kaptein-only for lagturneringer) er en naturlig, men separat oppfølger.
|
`user_is_own_tournament_participant` (`app/team_authz.py`, samme mønster
|
||||||
De fem konkrete formatene (Københavner m.fl.) og Order of Merit fortsatt
|
som ADR-023s `user_is_match_participant`) brukt av `update_hole` — kun
|
||||||
ikke designet.
|
deltakeren selv eller org-admin kan nå skrive score for en gitt
|
||||||
|
rundedeltaker. Runde-/deltaker-oppsett forblir bevisst på org-medlemsnivå
|
||||||
|
(samme presedens som `session`/`team_roster` i tournaments.py). 13 nye
|
||||||
|
scratch-sjekker + full 52-punkts regresjon, se CLAUDE.md-status
|
||||||
|
2026-07-30 for full detalj. De fem konkrete formatene (Københavner m.fl.)
|
||||||
|
og Order of Merit fortsatt ikke designet.
|
||||||
|
|
||||||
### Oppdatering 2026-07-30, samme dag: frontend HÅNDKODET og LIVE
|
### Oppdatering 2026-07-30, samme dag: frontend HÅNDKODET og LIVE
|
||||||
|
|
||||||
|
|
@ -1559,6 +1564,96 @@ i CLAUDE.md sin statuslogg (2026-07-27) — ikke duplisert her.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Symbolforklaringen på scorekortet fjernet — ✅ BYGGET OG LIVE 2026-07-30
|
||||||
|
|
||||||
|
Brukeren pekte direkte på "Under par (sirkel) / Over par (firkant) / Fylt
|
||||||
|
symbol = 2 slag eller mer"-forklaringen på `round-scorecard.tsx` og ba om
|
||||||
|
at den fjernes. `Legend`-komponenten (kun brukt ett sted) fjernet
|
||||||
|
fullstendig, ikke bare skjult. Den andre, urelaterte inline-teksten på
|
||||||
|
samme fil ("Fylt sirkel = vunnet hull ...", brukt av match-scorekortet)
|
||||||
|
er en annen streng, ikke rørt. Ekte typesjekket produksjonsbuild kompilerte
|
||||||
|
rent. Rullet ut (kun `teecup_frontend`), ingen migrasjon, `teeoff.no`
|
||||||
|
upåvirket.
|
||||||
|
|
||||||
|
## Notat 2026-07-30: per-hull historikk/statistikk for spilleren — 📋 NOTERT, IKKE bygget
|
||||||
|
|
||||||
|
Reist av brukeren: spilleren bør kunne se all historikk/statistikk for
|
||||||
|
NØYAKTIG det hullet vedkommende skal spille (eller har spilt) — f.eks. "du
|
||||||
|
har i snitt brukt X slag/Y putter på hull 7 på denne banen, GIR Z% av
|
||||||
|
gangene" — ikke bare hva som skjedde DENNE runden.
|
||||||
|
|
||||||
|
**Ikke trivielt, verdt å notere hvorfor:** dagens data er scoped PER
|
||||||
|
RUNDE (`round_hole`), ingen eksisterende spørring aggregerer "alle mine
|
||||||
|
tidligere runder på DENNE banen, filtrert til DETTE hullnummeret". Banen
|
||||||
|
identifiseres i dag kun ved navn-snapshot (`round.course_name_snapshot`,
|
||||||
|
se `PlayedCourses`/`course-rounds.tsx` sin eksisterende gruppering på
|
||||||
|
akkurat dette navnet) — samme identifikator kan trolig gjenbrukes til en
|
||||||
|
ny spørring: `GET /rounds/holes/{course_name}/{hole_number}/history` (eller
|
||||||
|
tilsvarende), som slår sammen `round_hole`-rader på tvers av alle
|
||||||
|
fullførte runder brukeren eier/har spilt på den banen. Naturlig plassering
|
||||||
|
i UI-et: en liten utvidbar seksjon i `ScoringWizard` sitt "strokes"-steg
|
||||||
|
(`round-detail.tsx`), og/eller i `round-scorecard.tsx`/`round-stats.tsx`
|
||||||
|
sine hull-visninger. Ingen design/ADR skrevet ennå — kun fanget opp her.
|
||||||
|
|
||||||
|
## Notat 2026-07-30: lenke til rundestatistikk fra scorekortet, og hvorvidt scorekort/statistikk/live-registrering bør slås sammen til én visning — lenke + prikker ✅ BYGGET, BROWSERVERIFISERT OG LIVE; sammenslåing bevisst UTSATT til brukertesting
|
||||||
|
|
||||||
|
Brukeren observerte at `round-scorecard.tsx` (det tradisjonelle,
|
||||||
|
front9/back9-delte scorekortet) mangler en lenke videre til
|
||||||
|
`/my-rounds/{id}/stats` (som selv HAR en lenke tilbake til scorekortet,
|
||||||
|
`CompletedBanner`) — og spurte om det heller burde vært ÉN samlet visning
|
||||||
|
i stedet for to, og i så fall om selve LIVE-registrerings-gridet
|
||||||
|
(`ScorecardGrid` i `round-detail.tsx`, bygget som spec-dokumentets §1
|
||||||
|
2026-07-27) burde erstattes av det samme front9/back9-delte formatet, med
|
||||||
|
mottatte slag vist som prikker i stedet for tekst.
|
||||||
|
|
||||||
|
**Svart direkte i chatten (ikke bygget), gjengitt her for historikken:**
|
||||||
|
anbefalte å IKKE slå sammen scorekort og statistikk til én visning — de
|
||||||
|
ble bevisst SKILT UT i to dedikerte sider 2026-07-25 (se eget punkt lenger
|
||||||
|
opp i denne filen: "Rundeliste + scorekort-redesign", "Den gamle vertikale
|
||||||
|
'Scorekort'-tabellen i round-stats.tsx FJERNET (erstattet med en lenke til
|
||||||
|
den nye siden)") — nøyaktig for å unngå at én side blir for lang/rotete.
|
||||||
|
Riktig, minimal fiks er heller den manglende lenken SCOREKORT → STATISTIKK
|
||||||
|
(symmetrisk med den allerede eksisterende STATISTIKK → SCOREKORT-lenken).
|
||||||
|
Anbefalte OGSÅ å IKKE erstatte selve live-registrerings-gridet med det
|
||||||
|
front9/back9-vertikalt-delte formatet — gridet ble nettopp bygget 2026-07-27
|
||||||
|
for å ligne et ekte scorekort (Ut/Inn/Sum, alle 18 hull som kolonner,
|
||||||
|
`§Golfscore-språket`), men er samtidig en AKTIV data-registreringsflate
|
||||||
|
(tapp en celle for å åpne veiviseren) — et vertikalt delt to-blokk-format
|
||||||
|
(designet for LESING) ville krevd smalere celler/mer scrolling og dermed
|
||||||
|
svekket selve registreringsergonomikken uten reell gevinst. "Prikker for
|
||||||
|
mottatte slag i stedet for tekst" ble vurdert som en god idé UAVHENGIG av
|
||||||
|
selve sammenslåings-spørsmålet — kan bygges som en liten, isolert
|
||||||
|
polish-oppgave på eksisterende `−N`-tekst-hint, i BEGGE grid-typer.
|
||||||
|
**De to avgrensede oppfølgerne bygget samme dag, sammenslåings-spørsmålet
|
||||||
|
bevisst utsatt** (brukeren: "Vi får senere gjøre en test med faktiske
|
||||||
|
brukere for å beslutte endelig om alt skal samles på en side"):
|
||||||
|
1. Ny "Se full rundestatistikk"-lenke nederst på `round-scorecard.tsx`
|
||||||
|
(symmetrisk med den eksisterende "Se scorekort"-lenken i motsatt
|
||||||
|
retning, `round-stats.tsx`).
|
||||||
|
2. `−N`-teksthintet for mottatte slag (FØR et hull er fylt ut) erstattet
|
||||||
|
med prikker (`StrokeDots`, ny liten komponent i `round-detail.tsx`) i
|
||||||
|
BÅDE `ScorecardGrid` (individuell-ball) og `SideScorecardGrid`
|
||||||
|
(delt-ball) — selve informasjonen bevart i knappens `aria-label`
|
||||||
|
("...mottar N slag"), siden prikkene alene er dekorative
|
||||||
|
(§tilgjengelighet: aldri kun et visuelt symbol for noe som betyr noe).
|
||||||
|
**Browserverifisert grundig i en isolert scratch-nettleserøkt** (samme
|
||||||
|
mønster som resten av uken -- fersk `teecup_scratch`-database + isolert
|
||||||
|
scratch-MinIO + engangs API-container + en isolert `next dev`-container
|
||||||
|
med kopiert kildekode): en HCP 28-spiller (course handicap 30, seedet via
|
||||||
|
API) bekreftet å vise nøyaktig to prikker på hull 1-12 (stroke index
|
||||||
|
1-12, riktig ut fra 30-18=12 ekstra slag) i live-registrerings-gridet;
|
||||||
|
den nye lenken på scorekortet bekreftet klikkbar og navigerte korrekt til
|
||||||
|
`/stats`-siden. Ekte typesjekket produksjonsbuild kompilerte rent. Ingen
|
||||||
|
konsollfeil (kun en godartet WebSocket-advarsel fra rask sidenavigasjon,
|
||||||
|
urelatert).
|
||||||
|
**Rullet ut live 2026-07-30**, bruker bekreftet eksplisitt: ingen
|
||||||
|
migrasjon, `docker compose up -d --build teecup_frontend` (gjenskapte
|
||||||
|
også `teecup_api` som vanlig bivirkning). Begge containere boot-et rent,
|
||||||
|
`/health`/`/dashboard`/scorekort-siden → 200, `teeoff.no` upåvirket.
|
||||||
|
**Selve sammenslåings-spørsmålet (scorekort+statistikk+live-registrering
|
||||||
|
til én visning) er bevisst IKKE avgjort** — venter på en fremtidig
|
||||||
|
brukertest før noen endelig beslutning tas.
|
||||||
|
|
||||||
## En tredje (informasjons-)farge til designet — ✅ BYGGET OG LIVE 2026-07-28
|
## En tredje (informasjons-)farge til designet — ✅ BYGGET OG LIVE 2026-07-28
|
||||||
|
|
||||||
Brukeren spurte om det ville vært en idé å introdusere én (eller kanskje
|
Brukeren spurte om det ville vært en idé å introdusere én (eller kanskje
|
||||||
|
|
@ -3508,6 +3603,101 @@ over ekte https, `teeoff.no` upåvirket.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Notat 2026-07-30: GIR burde utlede "innspill traff green" automatisk, ikke kreve manuell merking — ✅ BYGGET, BROWSERVERIFISERT OG LIVE
|
||||||
|
|
||||||
|
Reist av brukeren: får man birdie med kun én putt, MÅ innspillet ha truffet
|
||||||
|
greenen (par − 2 slag brukt før putting = GIR per definisjon) — da bør
|
||||||
|
appen ikke kreve at spilleren i tillegg manuelt krysser av "Innspill: Traff"
|
||||||
|
i `DirectionCross`-feltet i `ScoringWizard` sitt "flere detaljer"-steg
|
||||||
|
(`round-detail.tsx`, `stat.approach`).
|
||||||
|
|
||||||
|
**Presisering, funnet ved kodegjennomgang:** appen har ALLEREDE en generell
|
||||||
|
GIR-inferens fra slag+putt (`score − putts ≤ par − 2`, `round-stats.tsx` sin
|
||||||
|
`isGir`/`round-detail.tsx` sin `showGir`) — brukt til selve GIR-STATISTIKKEN
|
||||||
|
(greentreff-donut, GIR-avhengige snitt). Det som IKKE er koblet sammen er
|
||||||
|
den separate, manuelt utfylte `approach_result`-verdien ("Innspill: Traff/
|
||||||
|
Kort/Langt/Venstre/Høyre") — brukt til RETNINGS-statistikken (hvilken vei
|
||||||
|
man bommer greenen). Disse to feltene lever i dag adskilt: den utledede
|
||||||
|
GIR-verdien styrer ALDRI hva som vises/kreves i `DirectionCross`-feltet.
|
||||||
|
|
||||||
|
**Naturlig fiks (ikke bygget):** når `score − putts ≤ par − 2` er sann for
|
||||||
|
et hull (dvs. GIR er logisk garantert), bør `stat.approach` auto-settes til
|
||||||
|
`"hit"` i stedet for å kreve et eget klikk — analogt de andre auto-hopp-
|
||||||
|
mekanismene i samme wizard (`autoAdvanceFieldFor`). Motsatt gjelder IKKE:
|
||||||
|
GIR kan være usann uten at man vet retningen (f.eks. en birdie med to
|
||||||
|
putter er ikke GIR-garantert, og en scrambling-birdie fra utenfor green gir
|
||||||
|
identisk slag/putt-mønster som en ekte GIR — kun i RETNING vet vi ingenting
|
||||||
|
automatisk der). Trigger-tidspunktet er litt kinkig: putt-tallet kommer
|
||||||
|
FØR "flere detaljer"-steget i wizard-rekkefølgen (`strokes → putts →
|
||||||
|
puttDistance → details`), så inferensen kan skje idet man ANKOMMER
|
||||||
|
detalj-steget (begge tall er da kjent) — ikke midt i et tidligere steg.
|
||||||
|
|
||||||
|
**Bygget nøyaktig som beskrevet over, samme dag:** ny `useEffect` i
|
||||||
|
`ScoringWizard` (`round-detail.tsx`) som trigges idet "details"-steget
|
||||||
|
nås — setter `stat.approach = "hit"` KUN når `stat.approach` fortsatt er
|
||||||
|
`null` (rører aldri et allerede satt, manuelt ELLER tidligere auto-satt
|
||||||
|
valg) og `stat.strokes - stat.putts <= hole.par - 2`.
|
||||||
|
**Browserverifisert grundig i en isolert scratch-nettleserøkt** (fersk
|
||||||
|
`teecup_scratch`-database + isolert scratch-MinIO + engangs API-container
|
||||||
|
+ en isolert `next dev`-frontend-container med KOPIERT, ikke bind-mountet,
|
||||||
|
kildekode -- bind-mount ga gjentatte Turbopack-panics i denne økten):
|
||||||
|
positiv sjekk (birdie+1-putt på et par 4-hull, hull 17) bekreftet
|
||||||
|
"Innspill: Traff" auto-merket UMIDDELBART ved ankomst til detalj-steget,
|
||||||
|
UTEN noe klikk -- bekreftet BÅDE visuelt (skjermbilde) og direkte i
|
||||||
|
databasen (`approach_result='hit'`). Negativ sjekk (bogey+1-putt, hull 2,
|
||||||
|
satt via direkte API-kall FØR wizard åpnet) bekreftet "Traff" korrekt
|
||||||
|
IKKE forhåndsmerket ved ankomst til samme steg. Ingen konsollfeil.
|
||||||
|
**Rullet ut live 2026-07-30**, bruker bekreftet eksplisitt: ingen
|
||||||
|
migrasjon, `docker compose up -d --build teecup_frontend` (gjenskapte
|
||||||
|
også `teecup_api` som vanlig bivirkning). Begge containere boot-et rent,
|
||||||
|
`/health`/`/dashboard` → 200, `teeoff.no` upåvirket.
|
||||||
|
|
||||||
|
## Notat 2026-07-30: auto-prompt "Fullfør runde" når alle hull er ført for alle spillere — ✅ BYGGET, BROWSERVERIFISERT OG LIVE
|
||||||
|
|
||||||
|
Reist av brukeren rett etter scoring-autorisasjonsfiksen for ADR-037.
|
||||||
|
Gjelder "single-runder" i CLAUDE.md sin etablerte betydning — frittstående
|
||||||
|
runder (`/my-rounds/{id}`, ADR-033), ikke org-scopede individuelle
|
||||||
|
turneringer.
|
||||||
|
|
||||||
|
**Nå:** `round-detail.tsx` sin "Fullfør runde"-knapp finnes allerede (i den
|
||||||
|
tonet-ned Rediger/Fullfør/Slett-raden øverst, se 2026-07-25s UX-fiks), men
|
||||||
|
er en helt PASSIV knapp — ingen kode sjekker i dag om alle hull faktisk er
|
||||||
|
ferdig utfylt for alle deltakere, og ingenting fremhever/popper opp knappen
|
||||||
|
når det skjer. Brukeren må selv huske å scrolle opp og trykke, selv lenge
|
||||||
|
etter siste hull er registrert.
|
||||||
|
|
||||||
|
**Naturlig fiks (ikke bygget):** en klientside-sjekk (samme type
|
||||||
|
`isEntryComplete()`-logikk som allerede finnes for wizard-fremdrift, se
|
||||||
|
`advanceToNextPlayerOrHole()`-mønsteret fra 2026-07-26) som kjører etter
|
||||||
|
HVER hull-innsending — er `stat_level`-relevante felt fylt ut for SISTE
|
||||||
|
hull for ALLE deltakere (ikke bare aktiv spiller), vis en tydelig CTA/
|
||||||
|
banner ("Alle hull er ført — Fullfør runden?") i stedet for kun den passive
|
||||||
|
knappen i den tonede raden øverst. For delt-ball-formater (foursome/
|
||||||
|
greensome/scramble) gjelder samme sjekk på SIDE-nivå (`round_side`), ikke
|
||||||
|
per spiller. Ingen backend-endring nødvendig — dette er ren
|
||||||
|
frontend-tilstand avledet fra data som allerede lastes.
|
||||||
|
|
||||||
|
**Bygget nøyaktig som beskrevet over, samme dag:** ny
|
||||||
|
`allHolesEnteredForEveryone`-beregning i `RoundDetail` — individuelle
|
||||||
|
formater: ALLE spillere har `played=true` på ALLE hull i `holeOrder`;
|
||||||
|
delt-ball-formater: BEGGE sider har `played=true` på alle hull. Gatet på
|
||||||
|
`round.setup_complete` (samme gate som selve knappen) og `!completed`. Ny
|
||||||
|
`AllHolesEnteredBanner`-komponent (samme visuelle språk som
|
||||||
|
`CompletedBanner`) vist øverst på Score-fanen når betingelsen er sann, med
|
||||||
|
en direkte "Fullfør runden"-CTA (kaller samme `finishRound()` som den
|
||||||
|
eksisterende knappen, inkl. samme `confirm()`-bekreftelse).
|
||||||
|
**Browserverifisert grundig i samme isolerte scratch-økt:** fylte de to
|
||||||
|
siste hullene (owner hull 17+18 via wizard, gjest hull 18 via API) for en
|
||||||
|
18-hulls 2-spiller-runde, reloadet siden — banneret dukket opp automatisk
|
||||||
|
med korrekt tekst ("Alle hull er ført — Klar til å fullføre runden?").
|
||||||
|
Klikket "Fullfør runden" (håndterte den native `confirm()`-dialogen via
|
||||||
|
Chrome DevTools) — runden ble korrekt fullført (HCP-differensialer
|
||||||
|
beregnet og vist, banneret erstattet av det eksisterende
|
||||||
|
"Runde fullført"-banneret). Ingen konsollfeil gjennom hele flyten.
|
||||||
|
**Rullet ut live 2026-07-30**, samme utrulling som GIR-fiksen over (én
|
||||||
|
felles `docker compose up -d --build teecup_frontend`), bruker bekreftet
|
||||||
|
eksplisitt.
|
||||||
|
|
||||||
## Bevisst endret fra opprinnelige (Gemini-)råd
|
## Bevisst endret fra opprinnelige (Gemini-)råd
|
||||||
|
|
||||||
- 🔀 **Banedata:** API mot teeoff (ADR-004), IKKE direkte delt database. Direkte
|
- 🔀 **Banedata:** API mot teeoff (ADR-004), IKKE direkte delt database. Direkte
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,17 @@ SQL summerer bare det cachede ved lesing"-mønster som `match.status_text`/
|
||||||
`points_side_a/b`. Sammenlagt over flere runder summeres VED LESING i
|
`points_side_a/b`. Sammenlagt over flere runder summeres VED LESING i
|
||||||
leaderboardet, ikke i en egen tredje cache-tabell (ADR-037 Beslutning C).
|
leaderboardet, ikke i en egen tredje cache-tabell (ADR-037 Beslutning C).
|
||||||
|
|
||||||
Autorisasjon i denne runden: org-medlemskap (`get_authorized_org`) for ALT,
|
Autorisasjon (strammet inn 2026-07-30, se `user_is_own_tournament_
|
||||||
inkl. selve score-registreringen -- samme brede tillitsnivå lagturneringer
|
participant` i `app/team_authz.py`): selve SCORE-REGISTRERINGEN
|
||||||
hadde FØR ADR-023 (kaptein-only) ble bygget som en egen, senere runde. En
|
(`update_hole`) krever nå at brukeren ER deltakeren (via `player.user_id`)
|
||||||
tilsvarende innstramming for individuelle turneringer er en naturlig
|
ELLER org-eier/admin -- samme mønster som `user_is_match_participant`
|
||||||
oppfølger, ikke del av denne grunnstrukturen.
|
strammet inn lagturneringer i ADR-023. Runde-/deltaker-OPPSETT (opprett/
|
||||||
|
slett runde, legg til/fjern turnering-/rundedeltaker) forblir bevisst på
|
||||||
|
vanlig org-medlemsnivå (`get_authorized_org`) -- samme presedens som
|
||||||
|
`session`-opprettelse og `team_roster`-tilføyelse i tournaments.py, som
|
||||||
|
heller aldri har vært captain-/admin-gatet. Kun VALGET AV HVEM SOM SPILLER
|
||||||
|
og selve SCORINGEN er gatet strengere noe sted i appen, aldri det generelle
|
||||||
|
oppsettet.
|
||||||
|
|
||||||
Bevisst UTENFOR omfang (se ADR-037): Københavner/High-low-high/Robbins/
|
Bevisst UTENFOR omfang (se ADR-037): Københavner/High-low-high/Robbins/
|
||||||
Try all/Flaggturnering (krever paring PÅ TVERS av spillere per hull, ikke
|
Try all/Flaggturnering (krever paring PÅ TVERS av spillere per hull, ikke
|
||||||
|
|
@ -39,9 +45,10 @@ from handicap_engine import (
|
||||||
stroke_play_net_total,
|
stroke_play_net_total,
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..auth import get_authorized_org
|
from ..auth import CurrentUser, get_authorized_org, get_current_user
|
||||||
from ..db import org_connection
|
from ..db import org_connection
|
||||||
from ..errors import app_error, translate_db_errors
|
from ..errors import app_error, translate_db_errors
|
||||||
|
from ..team_authz import user_is_own_tournament_participant
|
||||||
from .scoring import played_hole_numbers
|
from .scoring import played_hole_numbers
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
@ -572,6 +579,7 @@ async def update_hole(
|
||||||
hole_number: int,
|
hole_number: int,
|
||||||
body: HoleUpdate,
|
body: HoleUpdate,
|
||||||
organization_id: str = Depends(get_authorized_org),
|
organization_id: str = Depends(get_authorized_org),
|
||||||
|
user: CurrentUser = Depends(get_current_user),
|
||||||
) -> RoundHoleOut:
|
) -> RoundHoleOut:
|
||||||
async with org_connection(organization_id) as conn, translate_db_errors():
|
async with org_connection(organization_id) as conn, translate_db_errors():
|
||||||
round_row = await conn.fetchrow(
|
round_row = await conn.fetchrow(
|
||||||
|
|
@ -583,14 +591,27 @@ async def update_hole(
|
||||||
if hole_number not in played_hole_numbers(round_row["hole_config"]):
|
if hole_number not in played_hole_numbers(round_row["hole_config"]):
|
||||||
raise app_error(400, "OUT_OF_SCOPE", "Hullnummeret er utenfor rundens hullomfang.")
|
raise app_error(400, "OUT_OF_SCOPE", "Hullnummeret er utenfor rundens hullomfang.")
|
||||||
|
|
||||||
rp = await conn.fetchval(
|
rp = await conn.fetchrow(
|
||||||
"SELECT id FROM tournament_round_participant WHERE id = $1 AND tournament_round_id = $2",
|
"""
|
||||||
|
SELECT trp.id, trp.tournament_participant_id::text AS tournament_participant_id
|
||||||
|
FROM tournament_round_participant trp
|
||||||
|
WHERE trp.id = $1 AND trp.tournament_round_id = $2
|
||||||
|
""",
|
||||||
round_participant_id,
|
round_participant_id,
|
||||||
round_id,
|
round_id,
|
||||||
)
|
)
|
||||||
if rp is None:
|
if rp is None:
|
||||||
raise app_error(404, "NOT_FOUND", "Rundedeltakeren finnes ikke.")
|
raise app_error(404, "NOT_FOUND", "Rundedeltakeren finnes ikke.")
|
||||||
|
|
||||||
|
if not await user_is_own_tournament_participant(
|
||||||
|
conn, organization_id, rp["tournament_participant_id"], user.user_id
|
||||||
|
):
|
||||||
|
raise app_error(
|
||||||
|
403,
|
||||||
|
"NOT_TOURNAMENT_PARTICIPANT",
|
||||||
|
"Du kan kun registrere score for deg selv (eller være organisasjonsadministrator).",
|
||||||
|
)
|
||||||
|
|
||||||
hole = await conn.fetchrow(
|
hole = await conn.fetchrow(
|
||||||
"SELECT par, stroke_index FROM hole WHERE course_id = $1 AND hole_number = $2",
|
"SELECT par, stroke_index FROM hole WHERE course_id = $1 AND hole_number = $2",
|
||||||
round_row["course_id"],
|
round_row["course_id"],
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,40 @@ async def user_is_tournament_participant(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def user_is_own_tournament_participant(
|
||||||
|
conn: Connection, organization_id: str, tournament_participant_id: str, user_id: str
|
||||||
|
) -> bool:
|
||||||
|
"""Individuelle turneringer (ADR-037): er brukeren SELV spilleren bak
|
||||||
|
denne `tournament_participant`-raden (via `player.user_id`)?
|
||||||
|
|
||||||
|
Mirror av `user_is_match_participant`, men uten `team_side` -- en
|
||||||
|
individuell turnering har ingen lag å begrense siden til. Brukt av
|
||||||
|
`individual_tournaments.py` sin `update_hole` (2026-07-30): før denne
|
||||||
|
kunne ETHVERT org-medlem skrive score for EN HVILKEN SOM HELST
|
||||||
|
deltaker, ikke bare sin egen -- samme klasse hull `user_is_match_
|
||||||
|
participant` lukket for lagturneringer i ADR-023. Kun scoring er
|
||||||
|
strammet inn her; runde-/deltaker-oppsett (opprett runde, legg til/
|
||||||
|
fjern turnering-/rundedeltaker) forblir bevisst på org-medlemsnivå,
|
||||||
|
samme presedens som `session`-opprettelse og `team_roster`-tilføyelse
|
||||||
|
i tournaments.py -- ingen av dem er captain-/admin-gatet heller, kun
|
||||||
|
selve VALGET AV HVEM SOM SPILLER EN GITT MATCH/RUNDE og selve
|
||||||
|
scoreregistreringen er det."""
|
||||||
|
is_self = await conn.fetchval(
|
||||||
|
"""
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT 1 FROM tournament_participant tp
|
||||||
|
JOIN player p ON p.id = tp.player_id
|
||||||
|
WHERE tp.id = $1 AND p.user_id = $2
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
tournament_participant_id,
|
||||||
|
user_id,
|
||||||
|
)
|
||||||
|
if is_self:
|
||||||
|
return True
|
||||||
|
return await is_org_admin(conn, organization_id, user_id)
|
||||||
|
|
||||||
|
|
||||||
async def user_is_rostered_on_team(conn: Connection, team_id: str, user_id: str) -> bool:
|
async def user_is_rostered_on_team(conn: Connection, team_id: str, user_id: str) -> bool:
|
||||||
"""Kun for lag-chat (ADR-025 Beslutning C) -- BEVISST INGEN org-admin-
|
"""Kun for lag-chat (ADR-025 Beslutning C) -- BEVISST INGEN org-admin-
|
||||||
fallback, ulikt de to funksjonene over. «Det hemmelige rommet» er ekte
|
fallback, ulikt de to funksjonene over. «Det hemmelige rommet» er ekte
|
||||||
|
|
|
||||||
|
|
@ -848,6 +848,29 @@ export function RoundDetail({ roundId }: { roundId: string }) {
|
||||||
return null
|
return null
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}))
|
||||||
|
|
||||||
// Begge feltene (score OG selected_participant_id) sendes ALLTID sammen
|
// Begge feltene (score OG selected_participant_id) sendes ALLTID sammen
|
||||||
// (samme "full overwrite hvert kall"-kontrakt som played/score alltid har
|
// (samme "full overwrite hvert kall"-kontrakt som played/score alltid har
|
||||||
// hatt) -- ellers ville en score-oppdatering stille nullstilt en allerede
|
// hatt) -- ellers ville en score-oppdatering stille nullstilt en allerede
|
||||||
|
|
@ -1290,6 +1313,11 @@ export function RoundDetail({ roundId }: { roundId: string }) {
|
||||||
Samme komponent, ikke duplisert logikk. */}
|
Samme komponent, ikke duplisert logikk. */}
|
||||||
<FormatResultPanel round={round} players={players} result={formatResult} />
|
<FormatResultPanel round={round} players={players} result={formatResult} />
|
||||||
|
|
||||||
|
{/* 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} />}
|
||||||
|
|
||||||
{/* Completed summary banner */}
|
{/* Completed summary banner */}
|
||||||
{completed && (
|
{completed && (
|
||||||
<CompletedBanner
|
<CompletedBanner
|
||||||
|
|
@ -1555,6 +1583,26 @@ function ScoringWizard({
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [stat.strokes, stat.putts, stat.firstPuttBucket])
|
}, [stat.strokes, stat.putts, stat.firstPuttBucket])
|
||||||
|
|
||||||
|
// 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])
|
||||||
|
|
||||||
function back() {
|
function back() {
|
||||||
if (isFirstStep) {
|
if (isFirstStep) {
|
||||||
onClose()
|
onClose()
|
||||||
|
|
@ -1796,6 +1844,30 @@ function ScoringWizard({
|
||||||
|
|
||||||
// --- Completed banner ------------------------------------------------------
|
// --- Completed banner ------------------------------------------------------
|
||||||
|
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function CompletedBanner({
|
function CompletedBanner({
|
||||||
players,
|
players,
|
||||||
duration,
|
duration,
|
||||||
|
|
@ -4233,7 +4305,7 @@ function ScorecardGrid({
|
||||||
aria-label={
|
aria-label={
|
||||||
hasScore
|
hasScore
|
||||||
? `${player.name}, hull ${n}, ${apiHole?.score} slag${diff !== null && meta ? `, ${golfTermForScore(apiHole!.score as number, meta.par) ?? ""}` : ""}${won ? ", vant hullet" : ""}`
|
? `${player.name}, hull ${n}, ${apiHole?.score} slag${diff !== null && meta ? `, ${golfTermForScore(apiHole!.score as number, meta.par) ?? ""}` : ""}${won ? ", vant hullet" : ""}`
|
||||||
: `Registrer ${player.name} sin score på hull ${n}`
|
: `Registrer ${player.name} sin score på hull ${n}${apiHole?.strokes_received ? `, mottar ${apiHole.strokes_received} slag` : ""}`
|
||||||
}
|
}
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
|
|
@ -4255,15 +4327,11 @@ function ScorecardGrid({
|
||||||
net !== null && <span className="text-[10px] font-semibold tabular-nums text-muted-foreground">{net}</span>
|
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,
|
// Mottatte slag vist FØR hullet er fylt ut (2026-07-29,
|
||||||
// etterspurt av bruker) -- samme verdi som ville blitt
|
// etterspurt av bruker) -- kun når spilleren faktisk
|
||||||
// trukket fra ved utfylling, kun når spilleren faktisk
|
|
||||||
// mottar minst ett slag her (ingen støy på de andre).
|
// mottar minst ett slag her (ingen støy på de andre).
|
||||||
|
// Prikker i stedet for "−N"-tekst (2026-07-30).
|
||||||
apiHole?.strokes_received != null &&
|
apiHole?.strokes_received != null &&
|
||||||
apiHole.strokes_received > 0 && (
|
apiHole.strokes_received > 0 && <StrokeDots count={apiHole.strokes_received} />
|
||||||
<span className="text-[10px] font-semibold tabular-nums text-info">
|
|
||||||
−{apiHole.strokes_received}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -4330,6 +4398,21 @@ function ScorecardGrid({
|
||||||
// prosjektets etablerte "lokale komponentmønstre per fil"-konvensjon, se
|
// prosjektets etablerte "lokale komponentmønstre per fil"-konvensjon, se
|
||||||
// DESIGN_SYSTEM.md). Sirkel under par, "firkant" (`rounded-[4px]`) over
|
// DESIGN_SYSTEM.md). Sirkel under par, "firkant" (`rounded-[4px]`) over
|
||||||
// par, ren tekst på par, fylt ved 2+ slag fra par.
|
// par, ren tekst på par, fylt ved 2+ slag fra par.
|
||||||
|
// 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function ScorecardCell({
|
function ScorecardCell({
|
||||||
value,
|
value,
|
||||||
diff,
|
diff,
|
||||||
|
|
@ -4624,7 +4707,7 @@ function SideScorecardGrid({
|
||||||
aria-label={
|
aria-label={
|
||||||
hasScore
|
hasScore
|
||||||
? `${side.label ?? "Side"}, hull ${n}, ${apiHole?.score} slag${won ? ", vant hullet" : ""}`
|
? `${side.label ?? "Side"}, hull ${n}, ${apiHole?.score} slag${won ? ", vant hullet" : ""}`
|
||||||
: `Registrer score for hull ${n}`
|
: `Registrer score for hull ${n}${apiHole?.strokes_received ? `, mottar ${apiHole.strokes_received} slag` : ""}`
|
||||||
}
|
}
|
||||||
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"
|
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"
|
||||||
>
|
>
|
||||||
|
|
@ -4640,11 +4723,9 @@ function SideScorecardGrid({
|
||||||
)}
|
)}
|
||||||
{/* Mottatte slag vist FØR hullet er fylt ut (2026-07-29) --
|
{/* Mottatte slag vist FØR hullet er fylt ut (2026-07-29) --
|
||||||
samme kombinerte side-verdi som ville blitt trukket fra
|
samme kombinerte side-verdi som ville blitt trukket fra
|
||||||
ved utfylling. */}
|
ved utfylling. Prikker i stedet for "−N"-tekst (2026-07-30). */}
|
||||||
{!hasScore && apiHole?.strokes_received != null && apiHole.strokes_received > 0 && (
|
{!hasScore && apiHole?.strokes_received != null && apiHole.strokes_received > 0 && (
|
||||||
<span className="text-[10px] font-semibold tabular-nums text-info">
|
<StrokeDots count={apiHole.strokes_received} />
|
||||||
−{apiHole.strokes_received}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
|
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { ArrowLeft } from "lucide-react"
|
import { ArrowLeft, ChevronRight } from "lucide-react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
// --- API-typer ---------------------------------------------------------------
|
// --- API-typer ---------------------------------------------------------------
|
||||||
|
|
@ -317,31 +317,6 @@ function ScoreBlock({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Legend ----------------------------------------------------------------
|
|
||||||
|
|
||||||
function Legend() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex flex-wrap items-center gap-x-4 gap-y-2 rounded-2xl border border-border bg-card px-4 py-3"
|
|
||||||
aria-label="Symbolforklaring"
|
|
||||||
>
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<span className="flex size-6 items-center justify-center rounded-full border-2 border-primary bg-primary/10 text-xs font-extrabold text-primary">
|
|
||||||
3
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-semibold text-foreground">Under par (sirkel)</span>
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<span className="flex size-6 items-center justify-center rounded-[4px] border-2 border-brand-orange bg-brand-orange/10 text-xs font-extrabold text-brand-orange">
|
|
||||||
5
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-semibold text-foreground">Over par (firkant)</span>
|
|
||||||
</span>
|
|
||||||
<span className="text-sm font-medium text-muted-foreground">Fylt symbol = 2 slag eller mer</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Combined total --------------------------------------------------------
|
// --- Combined total --------------------------------------------------------
|
||||||
|
|
||||||
function TotalTile({ label, value, tone }: { label: string; value: string; tone?: "orange" | "green" }) {
|
function TotalTile({ label, value, tone }: { label: string; value: string; tone?: "orange" | "green" }) {
|
||||||
|
|
@ -1077,9 +1052,6 @@ export function RoundScorecard({ roundId }: { roundId: string }) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Legend */}
|
|
||||||
<Legend />
|
|
||||||
|
|
||||||
{/* Scorecard blocks */}
|
{/* Scorecard blocks */}
|
||||||
<div className="flex flex-col gap-3 rounded-3xl border border-border bg-card p-3 shadow-md shadow-black/8 sm:p-4">
|
<div className="flex flex-col gap-3 rounded-3xl border border-border bg-card p-3 shadow-md shadow-black/8 sm:p-4">
|
||||||
{front.length > 0 && (
|
{front.length > 0 && (
|
||||||
|
|
@ -1106,6 +1078,17 @@ export function RoundScorecard({ roundId }: { roundId: string }) {
|
||||||
<MatchScorecardGrid round={round} roundId={roundId} viewerId={viewerId} refreshKey={refreshKey} />
|
<MatchScorecardGrid round={round} roundId={roundId} viewerId={viewerId} refreshKey={refreshKey} />
|
||||||
)}
|
)}
|
||||||
{isSkins && <SkinsProgressTable roundId={roundId} refreshKey={refreshKey} />}
|
{isSkins && <SkinsProgressTable roundId={roundId} refreshKey={refreshKey} />}
|
||||||
|
|
||||||
|
{/* Symmetrisk med round-stats.tsx sin "Se scorekort"-lenke tilbake
|
||||||
|
hit (2026-07-30, etterspurt av bruker -- manglet fra scorekortet
|
||||||
|
og andre veien). */}
|
||||||
|
<Link
|
||||||
|
href={`/my-rounds/${roundId}/stats`}
|
||||||
|
className="flex min-h-14 items-center justify-center gap-1.5 rounded-2xl border border-primary/40 bg-card px-5 text-base font-bold text-foreground shadow-sm transition-colors hover:bg-accent/50"
|
||||||
|
>
|
||||||
|
Se full rundestatistikk
|
||||||
|
<ChevronRight aria-hidden="true" className="size-5" />
|
||||||
|
</Link>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue