Full V0-tro omskriving av rundeleaderboard, tab-navigasjonsbug, formatbryter
Brukeren testet en Slagspill-runde etter forrige leaderboard-utrulling og så ingen visuell endring - forrige runde hadde gjenbrukt gamle håndkodede komponenter for individuell/to-sidet visning i stedet for å implementere V0s tegning fullt ut. Skrev om round-leaderboard.tsx fra bunnen mot V0s faktiske komponenter, portet manuelt mot ekte API-data. Fant og rettet fem reelle bugs underveis (courseHcp-matte, manglende hcpIndex, ødelagt TeamFlightBoard-pille, CSS-trunkering i SideName, feil kildetekst for matchoverskrift). Fikset også: "Spillere og runde"-snarveien som ikke virket fra Score-fanen (useEffect manglet på searchParams-endring), play-format-bryteren i Rediger runde-panelet utvidet fra 3 til 16 formater, 8 nye statistikkrader i scorekortet (Fairway/Putt/GIR/Innspill/Chip/Bunker/Straffeslag/Anyway). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
242c7a2bfc
commit
76fe6f6c2c
4 changed files with 1582 additions and 444 deletions
164
CHANGELOG.md
164
CHANGELOG.md
|
|
@ -6520,6 +6520,170 @@ Ferdig og verifisert:
|
|||
spiller-rad-tilfellet (`ScorecardGrid`), og det fantes ingen
|
||||
tilsvarende referanse å bygge en "side-kort"-variant mot. Fortsatt
|
||||
det brede gridet for de formatene, uendret.
|
||||
- **Fem separate brukerpunkter fra samme økt, ALLE BYGGET, SCRATCH-
|
||||
VERIFISERT OG LIVE (2026-08-03), samme dag som score-kortrunden
|
||||
over:**
|
||||
1. **Ekte bug funnet via opplastet skjermopptak, rettet:** brukeren
|
||||
rapporterte at "snarveien" (Administrer-dialogens "Spillere og
|
||||
runde"-lenke, `?tab=manage`) ikke virket fra Score-fanen, men
|
||||
virket fra Scorekort/Leaderboard. Video analysert bilde for bilde
|
||||
(ffmpeg) -- bekreftet presist: URL-en endret seg riktig til
|
||||
`?tab=manage`, men selve fanen ble stående på "Score". Rotårsak:
|
||||
`useState(initialTab)` i round-detail.tsx leste kun
|
||||
`useSearchParams()` ved FØRSTE mount -- en navigasjon FRA Score-
|
||||
siden til seg selv med kun en ny query-parameter er ingen ekte
|
||||
remount i Next.js App Router, så verdien ble aldri regnet på nytt.
|
||||
Fra Scorekort/Leaderboard virket det fordi det ER en remount
|
||||
(annen rute). Fikset med en `useEffect` som reagerer på selve
|
||||
parameter-ENDRINGEN (ensrettet -- hopper TIL "manage", tvinger
|
||||
aldri tilbake til "score"). Reprodusert i scratch FØR fiksen
|
||||
(bekreftet feilen), verifisert etterpå fra alle tre
|
||||
inngangspunkter (Score/Scorekort/Leaderboard) + at lokal
|
||||
"Score"-pille fortsatt virker uendret.
|
||||
2. **`EditRoundPanel` sin spilleform-bryter utvidet fra 3 til 16
|
||||
format:** var blitt hengende igjen på stroke/match/stableford fra
|
||||
FØR de åtte nye formatene (ADR-039) ble bygget -- en ren
|
||||
forglemmelse. Rettet BÅDE i backend (`RoundUpdate.play_format`
|
||||
i rounds.py hadde samme gamle 3-verdis-Literal) OG frontend (ny
|
||||
`EDITABLE_PLAY_FORMATS`-liste, samme etiketter som new-round.tsx).
|
||||
Verifisert live i scratch: byttet en pågående runde fra Slagspill
|
||||
til Fourball midt i runden, `SidesPanel` sin eksisterende "opprett
|
||||
sider først"-melding tok over korrekt uten noen krasj.
|
||||
3. **`completed_at`-feltet for å overstyre fullført-tidspunkt manuelt
|
||||
bekreftet FORTSATT TIL STEDE, uendret siden 2026-07-24** -- ren
|
||||
kodeverifisering, ingen endring nødvendig. Feltet vises (med
|
||||
hensikt) kun ETTER at "Fullfør runde" er trykket -- forvirringen
|
||||
var trolig et resultat av bug (1) over (kom seg aldri til
|
||||
"Spillere og runde" for å finne knappen).
|
||||
4. **`round-scorecard.tsx` sitt individuelle scorekort: kolonnetekst-
|
||||
overflow rettet + 8 nye detaljrader lagt til** (Fairway/Putt/
|
||||
GIR/Innspill/Chip/Bunkerslag/Straffeslag/Anywayslag), alle med
|
||||
forkortede radetiketter + kompakte lucide-ikoner (bøyde piler for
|
||||
Fairway, rette piler+blink for Innspill, sjekkmerke for GIR) --
|
||||
samme golfscore-språk som resten av appen. Data var allerede
|
||||
tilgjengelig i `RoundHoleOut` (bare ikke lest av denne siden før).
|
||||
Vises kun når minst ett hull faktisk har dataen (samme "vis kun
|
||||
det som finnes"-prinsipp). GIR-formelen gjenbruker EKSAKT samme
|
||||
`score - putts <= par - 2`-regel som round-stats.tsx/round-
|
||||
detail.tsx allerede bruker.
|
||||
5. **Rundeleaderboard (V0-eksport "zip 28") -- IKKE V0 sin skyld at
|
||||
forrige versjon "så rart sammenskrudd ut", presisert til bruker:
|
||||
forrige leaderboard var HÅNDKODET (ikke V0), bygget 2026-07-26 da
|
||||
brukeren gikk tom for V0-credits. Zip 28 dekket alle hullene i
|
||||
V0-prompten (skrevet samme økt) presist -- portet MANUELT mot ekte
|
||||
data, IKKE en full filerstatning: gjenbrukte eksisterende
|
||||
`rankEntries`/`ValueMark`/`RankBadge`/`ModeToggle`/`HoleStrip`/
|
||||
matchstatus-tug-of-war-baren (allerede nær identisk med V0s egen
|
||||
visuelle idé, lavere risiko å beholde enn å skrive om). Kun TRE
|
||||
genuint manglende/feil biter bygget:
|
||||
- **High-low-high** falt tidligere inn i den generiske to-sidede
|
||||
"X UP"-matchstatusen (bekreftet FEIL for dette formatet -- HLH
|
||||
er 0-2 poeng-per-hull, ikke hull-ledelse). Ny `HighLowSection`:
|
||||
lav/høy-duell-rollen per spiller per hull utledes fra
|
||||
`entries[].points` (allerede MIN/MAKS av lagkameratenes
|
||||
Stableford-poeng den hullet, samme regel som
|
||||
`handicap_engine.py` sin `high_low_high_points_for_hole` --
|
||||
ingen backend-endring trengtes). Verifisert tall-for-tall i
|
||||
scratch: per-hull lav/høy-dueller summerte EKSAKT til
|
||||
`hlh_points_a`/`hlh_points_b` sine totaler.
|
||||
- **Bingo Bango Bongo/Københavner** sitt "Poeng"-tall var
|
||||
`ApiLeaderboardEntry.total_points` -- ALLTID generisk Stableford
|
||||
fra backend, uansett format (bekreftet reell bug: disse to
|
||||
formatene har sine EGNE poengsystem, aldri vist noe sted).
|
||||
Erstattet med faktiske `bbb_points`/`copenhagen_points` fra
|
||||
format-result FØR rangeringen bygges. Fanget og rettet EN egen
|
||||
bug UNDER scratch-verifiseringen: en deltaker med 0 ekte
|
||||
BBB/Københavner-poeng manglet fra dict-en (ikke `0`, fraværende
|
||||
nøkkel) -- første versjon falt da feilaktig tilbake til generisk
|
||||
Stableford i stedet for `0`. Rettet, verifisert: Københavner-
|
||||
poeng summerte EKSAKT til 6 per hull (regelen), BBB viste riktig
|
||||
`0` for en spiller uten noen bingo/bango/bongo-vinst.
|
||||
- **Shamble/Money Ball** sitt offisielle LAGRESULTAT (hele runden
|
||||
= ett lag) lå tidligere IKKE synlig noe sted -- kun spillernes
|
||||
råtall. Ny `TeamResultSection`: lagets til-par (fra
|
||||
`shamble_team_score`/`money_ball_team_score` + hullenes par-sum,
|
||||
begge allerede i format-result), pluss en NY flight-gruppe-
|
||||
sammenligning (henter søsken-rundenes EGNE format-result via et
|
||||
nytt `useFlightTeamStandings`-kall) mot andre lag i samme utgang
|
||||
-- tidligere fantes INGEN slik sammenligning i det hele tatt.
|
||||
Verifisert tall-for-tall i scratch (Shamble beste-2-av-3, Money
|
||||
Ball sin rotasjon) mot to separate flight-koblede runder.
|
||||
`RoundLeaderboardMini` (forhåndsvisningen i "Spillere og runde")
|
||||
oppdatert til å skjule seg for Shamble/Money Ball også (samme
|
||||
"ikke vis noe fremfor å vise en misvisende rangering"-prinsipp den
|
||||
allerede fulgte for to-sidede format).
|
||||
Regresjonstestet: vanlig Slagspill og Fourball uendret (identisk
|
||||
visning/matchstatus som før refaktoreringen som lot
|
||||
`MatchStatusSection` motta `result` som prop i stedet for å hente
|
||||
selv).
|
||||
**Rullet ut sammen med de fire punktene over, 2026-08-03**: BÅDE
|
||||
`teecup_api` (RoundUpdate.play_format-utvidelsen) OG
|
||||
`teecup_frontend` bygget og restartet, begge boot-et rent,
|
||||
bekreftet via kompilert bundle-grep + en faktisk backend-
|
||||
spørring mot den nye Literal-en. Zip-en slettet etter merge.
|
||||
- **Rundeleaderboard (zip 28) — punkt 5 over ERSTATTET, full omskriving
|
||||
(2026-08-03, samme dag):** brukeren testet en vanlig Slagspill-runde
|
||||
(den klart vanligste stien) etter forrige punkts utrulling og så
|
||||
NULL visuell endring, sendte skjermdump og spurte hva som hadde
|
||||
skjedd. Årsak: forrige punkt gjenbrukte bevisst de gamle håndkodede
|
||||
`RankBadge`/`ValueMark`/`ModeToggle`/`HoleStrip`/matchstatus-baren for
|
||||
individuell/to-sidet visning fordi jeg vurderte dem som "strukturelt
|
||||
nære nok" V0s tegning — akkurat den stien brukeren testet var derfor
|
||||
uendret. Brukerens eksplisitte, ordrette svar (bevart for ettertiden):
|
||||
*"Ja, jeg vil ABSOLUTT at du implementerer V0 sin tolkning av hvordan
|
||||
Leaderboardet skal se ut. Jeg vil ikke at du skal gjøre personlige
|
||||
endringer eller holde tilbake på noe i det hele tatt når det gjelder
|
||||
dette."* Lagret som stående feedback-memory (`v0-full-fidelity`) —
|
||||
gjelder ALL fremtidig V0-integrering i dette prosjektet, ikke bare
|
||||
denne filen.
|
||||
Fulgte opp med en fullstendig omskriving av `round-leaderboard.tsx`,
|
||||
denne gangen med HVER V0-komponent portet manuelt mot ekte data (ikke
|
||||
gjenbrukt): `Badge`/`MetricPill`/`ScoreMark`+`classify`/
|
||||
`IndividualBoard`/`MetricToggle`/`HoleByHole`/`Legend` (individuelt),
|
||||
`TwoSidedBoard`/`SideName`/`HoleWinners` (to-sidet), `HighLowBoard`/
|
||||
`DuelRow` (High-low-high), `TeamFlightBoard` (Shamble/Money Ball) — de
|
||||
gamle håndkodede visningskomponentene slettet i sin helhet, ikke
|
||||
beholdt ved siden av.
|
||||
Fem reelle bugs funnet og rettet UNDER omskrivingen (ikke antatt
|
||||
riktig fra en ren typesjekk alene — funnet ved skjermdump-inspeksjon i
|
||||
scratch):
|
||||
1. `courseHcp` var meningsløs plassholder-matematikk
|
||||
(`strokes_received * 18` uansett hva slagindeksen faktisk var) —
|
||||
V0s mockup antok klientside-utregning av spillehandicap, men
|
||||
backend returnerer allerede korrekt per-hull `strokes_received`
|
||||
direkte. Rettet: `IndividualPlayer` fikk et ekte
|
||||
`strokesReceived`-array lest direkte derfra, netto/Stableford
|
||||
regnet fra DET, ikke gjenutledet.
|
||||
2. `hcpIndex` var hardkodet `null` alltid — feltet
|
||||
(`handicap_index_snapshot`) fantes allerede i det hentede
|
||||
`/rounds/{id}`-svaret, bare ikke lest av filens egen, snevrere
|
||||
lokale type. Utvidet typen, lest verdien.
|
||||
3. `TeamFlightBoard` sin råscore-pille hadde et komma-operator-uttrykk
|
||||
som alltid evaluerte til hardkodet `0` uansett faktisk score.
|
||||
Rettet med den allerede korrekte `grossToPar(m, holes)`.
|
||||
4. CSS-trunkeringsbug i `SideName`: det ledende laget sin flagg-
|
||||
ikonrad manglet `min-w-0` på det indre flex-elementet, så navnet
|
||||
("Lag Bjørk") trunkerte til nesten ingenting ("L..") mens det ikke-
|
||||
ledende laget viste seg fullt, selv i like brede `flex-1`-
|
||||
containere. Rettet.
|
||||
5. Rotårsaken til (4) var egentlig et feil datavalg: brukte
|
||||
`match_status_text` (en backend-streng som ALLEREDE inkluderer
|
||||
lagnavnet, designet for en annen UI-plassering) i V0s
|
||||
overskrifts-slot, som var designet for en mye kortere tekst siden
|
||||
lagnavn vises separat via `SideName`. Rettet ved å utlede V0s
|
||||
egen korte frase ("2 opp"/"Dormie 2 opp"/"Ferdig 2&1"/"Vant 2
|
||||
opp"/"Delt"/"Alt likt") direkte fra de rå numeriske feltene
|
||||
(`match_lead`/`match_holes_played`/`match_holes_remaining`/
|
||||
`match_is_dormie`/`match_is_closed`) — fortsatt serverens
|
||||
autoritative tall, bare riktig tekstformat.
|
||||
**Scratch-verifisert på alle åtte formater** (Slagspill, BBB,
|
||||
Københavner, High-low-high, Fourball, Shamble, Money Ball, pluss
|
||||
`RoundLeaderboardMini`-regresjon) med tall håndverifisert mot samme
|
||||
testdata som resten av ADR-039-arbeidet — se punkt 5 over for selve
|
||||
tallutregningen, uendret av denne omskrivingen. **Rullet ut live
|
||||
2026-08-03** (kun `teecup_frontend` bygget/restartet — backend-
|
||||
endringen fra punktene over var allerede live). Scratch-miljøet
|
||||
(DB/rolle/MinIO/API-/frontend-containere) ryddet opp etter bruk.
|
||||
|
||||
Neste steg:
|
||||
0a. **Spillerliste-redesign — nå FAKTISK nettleser-bekreftet
|
||||
|
|
|
|||
|
|
@ -400,9 +400,17 @@ export function RoundDetail({ roundId }: { roundId: string }) {
|
|||
const router = useRouter()
|
||||
// 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).
|
||||
// siden. Fungerte derfra fordi det er en navigasjon MELLOM to ulike
|
||||
// rute-komponenter (ekte remount, `initialTab` regnes ut på nytt) --
|
||||
// men fra selve Score-siden er `/my-rounds/{id}` -> `/my-rounds/{id}
|
||||
// ?tab=manage` KUN en søkeparameter-endring på SAMME rute, ingen
|
||||
// remount, så `useState(initialTab)` sitt opprinnelige mount-tidspunkt-
|
||||
// resultat ble stående uendret -- knappen så ut til å ikke gjøre noe
|
||||
// (rapportert av bruker via video 2026-08-03, bekreftet: URL-en endret
|
||||
// seg riktig til ?tab=manage, men selve fanen forble "Score"). Fikset
|
||||
// med en effekt som reagerer på selve søkeparameter-ENDRINGEN, ikke
|
||||
// bare mount -- ensrettet (kun til "manage"), rører aldri brukerens
|
||||
// egen lokale "Score"-valg via pillen etterpå.
|
||||
const searchParams = useSearchParams()
|
||||
const initialTab = searchParams.get("tab") === "manage" ? "manage" : "score"
|
||||
const [round, setRound] = useState<ApiRound | null>(null)
|
||||
|
|
@ -442,6 +450,15 @@ export function RoundDetail({ roundId }: { roundId: string }) {
|
|||
// når frem til oppgaven). "Score" er default -- den ENESTE fanen de
|
||||
// fleste trenger under selve spillingen.
|
||||
const [pageTab, setPageTab] = useState<"score" | "manage">(initialTab)
|
||||
// Fikser "Administrer"-snarveien fra selve Score-siden (se begrunnelse
|
||||
// ved searchParams over) -- reagerer på selve parameter-ENDRINGEN i
|
||||
// stedet for kun mount-tidspunktet. Ensrettet med vilje: hopper TIL
|
||||
// "manage" når URL-en ber om det, men tvinger aldri tilbake til "score"
|
||||
// om parameteren mangler/fjernes (det ville kunnet rive brukeren vekk
|
||||
// fra et bevisst lokalt "Score"-valg de allerede har gjort).
|
||||
useEffect(() => {
|
||||
if (searchParams.get("tab") === "manage") setPageTab("manage")
|
||||
}, [searchParams])
|
||||
const [completing, setCompleting] = useState(false)
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const [showEditRound, setShowEditRound] = useState(false)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -30,7 +30,7 @@
|
|||
|
||||
import { useEffect, useState } from "react"
|
||||
import Link from "next/link"
|
||||
import { ArrowLeft } from "lucide-react"
|
||||
import { ArrowLeft, ArrowUp, ArrowDown, ArrowRight, ArrowUpLeft, ArrowUpRight, Check, Target } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
// --- API-typer ---------------------------------------------------------------
|
||||
|
|
@ -71,6 +71,18 @@ type ApiHole = {
|
|||
score: number | null
|
||||
picked_up: boolean
|
||||
strokes_received: number | null
|
||||
// Detaljfelt (2026-08-03) -- allerede returnert av samme
|
||||
// RoundHoleOut/`/rounds/.../holes`-endepunkt denne siden lenge har brukt,
|
||||
// bare ikke tidligere lest her. Presentert som egne, forkortede rader i
|
||||
// ScoreBlock i stedet for kun å ligge tilgjengelig i round-detail.tsx sin
|
||||
// "Så langt i runden"-seksjon.
|
||||
putts: number | null
|
||||
tee_shot_result: "fairway" | "left" | "right" | null
|
||||
approach_result: "hit" | "short" | "long" | "left" | "right" | null
|
||||
chip_count: number | null
|
||||
bunker_shot_count: number | null
|
||||
penalty_strokes: number | null
|
||||
anyway_strokes: number | null
|
||||
}
|
||||
|
||||
const SHARED_BALL_FORMATS = ["foursome", "greensome", "scramble_2", "scramble_4", "chapman"]
|
||||
|
|
@ -115,6 +127,13 @@ function stablefordPoints(h: ApiHole): number | null {
|
|||
return Math.max(0, h.par - net + 2)
|
||||
}
|
||||
|
||||
// Samme GIR-formel som round-stats.tsx sin isGir/round-detail.tsx sin
|
||||
// GIR-auto-inferens: slag minus putt garanterer green i regulering når
|
||||
// resultatet er par-2 eller bedre. Krever kjent slag+putt-tall.
|
||||
function isGir(h: ApiHole): boolean {
|
||||
return h.score !== null && h.putts !== null && h.score - h.putts <= h.par - 2
|
||||
}
|
||||
|
||||
type ScoreClass = "eagle" | "birdie" | "par" | "bogey" | "double"
|
||||
|
||||
function classify(gross: number, par: number): ScoreClass {
|
||||
|
|
@ -193,14 +212,53 @@ function ScoreMark({ gross, par, pickedUp }: { gross: number | null; par: number
|
|||
|
||||
// --- One nine-hole block (a real table for row/column semantics) -----------
|
||||
|
||||
// Kompakte retnings-/treff-ikoner for de nye detaljradene (2026-08-03) --
|
||||
// samme ikonvalg som DirectionCross (round-detail.tsx sin registrerings-
|
||||
// veiviser) allerede bruker, kun skalert ned for en tett tabellcelle.
|
||||
// Bøyde piler for Fairway (utslaget DRIVER av til en side, ulikt et
|
||||
// innspill som bommer rett til siden -- derfor ArrowUpLeft/-Right her,
|
||||
// ikke de rette ArrowLeft/-Right Innspill-raden bruker).
|
||||
function FairwayIcon({ result }: { result: ApiHole["tee_shot_result"] }) {
|
||||
if (result === null) return null
|
||||
if (result === "fairway") return <Target aria-hidden="true" className="mx-auto size-3.5 text-primary" />
|
||||
if (result === "left") return <ArrowUpLeft aria-hidden="true" className="mx-auto size-3.5 text-brand-orange" />
|
||||
return <ArrowUpRight aria-hidden="true" className="mx-auto size-3.5 text-brand-orange" />
|
||||
}
|
||||
|
||||
function ApproachIcon({ result }: { result: ApiHole["approach_result"] }) {
|
||||
if (result === null) return null
|
||||
const cls = "mx-auto size-3.5"
|
||||
if (result === "hit") return <Target aria-hidden="true" className={cn(cls, "text-primary")} />
|
||||
if (result === "long") return <ArrowUp aria-hidden="true" className={cn(cls, "text-brand-orange")} />
|
||||
if (result === "short") return <ArrowDown aria-hidden="true" className={cn(cls, "text-brand-orange")} />
|
||||
if (result === "left") return <ArrowLeft aria-hidden="true" className={cn(cls, "text-brand-orange")} />
|
||||
return <ArrowRight aria-hidden="true" className={cn(cls, "text-brand-orange")} />
|
||||
}
|
||||
|
||||
function ScoreBlock({
|
||||
holes,
|
||||
summaryLabel,
|
||||
showStableford,
|
||||
showFairway,
|
||||
showPutts,
|
||||
showGir,
|
||||
showApproach,
|
||||
showChip,
|
||||
showBunker,
|
||||
showPenalty,
|
||||
showAnyway,
|
||||
}: {
|
||||
holes: ApiHole[]
|
||||
summaryLabel: string
|
||||
showStableford: boolean
|
||||
showFairway: boolean
|
||||
showPutts: boolean
|
||||
showGir: boolean
|
||||
showApproach: boolean
|
||||
showChip: boolean
|
||||
showBunker: boolean
|
||||
showPenalty: boolean
|
||||
showAnyway: boolean
|
||||
}) {
|
||||
const parSum = sum(holes.map((h) => h.par))
|
||||
const playedHoles = holes.filter((h) => h.played && h.score !== null)
|
||||
|
|
@ -210,10 +268,31 @@ function ScoreBlock({
|
|||
const pointsValues = holes.map(stablefordPoints).filter((n): n is number => n !== null)
|
||||
const pointsSum = pointsValues.length > 0 ? sum(pointsValues) : null
|
||||
|
||||
// Fairwaytreff spores kun på ikke-par-3-hull (samme regel som
|
||||
// round-detail.tsx sin "Så langt i runden"), summen teller kun "Blink".
|
||||
const fairwayEligible = holes.filter((h) => h.par !== 3 && h.tee_shot_result !== null)
|
||||
const fairwayHitSum = fairwayEligible.length > 0 ? fairwayEligible.filter((h) => h.tee_shot_result === "fairway").length : null
|
||||
const puttsValues = holes.filter((h) => h.putts !== null).map((h) => h.putts as number)
|
||||
const puttsSum = puttsValues.length > 0 ? sum(puttsValues) : null
|
||||
const girEligible = holes.filter((h) => h.score !== null && h.putts !== null)
|
||||
const girSum = girEligible.length > 0 ? girEligible.filter(isGir).length : null
|
||||
const chipValues = holes.filter((h) => h.chip_count !== null).map((h) => h.chip_count as number)
|
||||
const chipSum = chipValues.length > 0 ? sum(chipValues) : null
|
||||
const bunkerValues = holes.filter((h) => h.bunker_shot_count !== null).map((h) => h.bunker_shot_count as number)
|
||||
const bunkerSum = bunkerValues.length > 0 ? sum(bunkerValues) : null
|
||||
const penaltyValues = holes.filter((h) => h.penalty_strokes !== null).map((h) => h.penalty_strokes as number)
|
||||
const penaltySum = penaltyValues.length > 0 ? sum(penaltyValues) : null
|
||||
const anywayValues = holes.filter((h) => h.anyway_strokes !== null).map((h) => h.anyway_strokes as number)
|
||||
const anywaySum = anywayValues.length > 0 ? sum(anywayValues) : null
|
||||
|
||||
const cell = "border border-border text-center tabular-nums"
|
||||
const sumCell = "border border-border bg-muted text-center font-extrabold tabular-nums"
|
||||
// whitespace-nowrap + overflow-hidden (2026-08-03, rapportert av bruker --
|
||||
// "Score"/"Netto" bløuted ut over kolonnegrensen) -- w-11 gir litt mer
|
||||
// rom enn tidligere w-9, men de VIRKELIG korte etikettene (forkortelser)
|
||||
// under er den egentlige fiksen, ikke bare bredden alene.
|
||||
const rowLabel =
|
||||
"border border-border px-1 text-left text-xs font-bold text-muted-foreground"
|
||||
"border border-border px-1 text-left text-xs font-bold text-muted-foreground whitespace-nowrap overflow-hidden"
|
||||
|
||||
return (
|
||||
<table className="w-full table-fixed border-collapse">
|
||||
|
|
@ -221,7 +300,7 @@ function ScoreBlock({
|
|||
Scorekort hull {holes[0].hole_number} til {holes[holes.length - 1].hole_number}
|
||||
</caption>
|
||||
<colgroup>
|
||||
<col className="w-9" />
|
||||
<col className="w-11" />
|
||||
{holes.map((h) => (
|
||||
<col key={h.hole_number} />
|
||||
))}
|
||||
|
|
@ -319,6 +398,133 @@ function ScoreBlock({
|
|||
<td className={cn(sumCell, "py-1 text-xs text-foreground")}>{pointsSum ?? "–"}</td>
|
||||
</tr>
|
||||
)}
|
||||
{/* Fairway -- ikon per hull (bøyd pil = utslaget drev til siden,
|
||||
blink = fairwaytreff), sum = antall fairwaytreff. */}
|
||||
{showFairway && (
|
||||
<tr>
|
||||
<th scope="row" className={rowLabel} title="Fairwaytreff">
|
||||
Fw
|
||||
</th>
|
||||
{holes.map((h) => (
|
||||
<td key={h.hole_number} className={cn(cell, "py-1")}>
|
||||
{h.par !== 3 ? <FairwayIcon result={h.tee_shot_result} /> : <span aria-hidden="true">–</span>}
|
||||
</td>
|
||||
))}
|
||||
<td className={cn(sumCell, "py-1 text-xs text-foreground")}>{fairwayHitSum ?? "–"}</td>
|
||||
</tr>
|
||||
)}
|
||||
{/* Putter -- antall per hull, summert. */}
|
||||
{showPutts && (
|
||||
<tr>
|
||||
<th scope="row" className={rowLabel} title="Putter">
|
||||
Putt
|
||||
</th>
|
||||
{holes.map((h) => (
|
||||
<td key={h.hole_number} className={cn(cell, "py-1 text-xs font-semibold text-muted-foreground")}>
|
||||
{h.putts ?? "–"}
|
||||
</td>
|
||||
))}
|
||||
<td className={cn(sumCell, "py-1 text-xs text-foreground")}>{puttsSum ?? "–"}</td>
|
||||
</tr>
|
||||
)}
|
||||
{/* GIR -- sjekkmerke når hullet er nådd i regulering, summert. */}
|
||||
{showGir && (
|
||||
<tr>
|
||||
<th scope="row" className={rowLabel} title="Green i regulering">
|
||||
GIR
|
||||
</th>
|
||||
{holes.map((h) => (
|
||||
<td key={h.hole_number} className={cn(cell, "py-1")}>
|
||||
{h.score !== null && h.putts !== null ? (
|
||||
isGir(h) ? (
|
||||
<Check aria-hidden="true" className="mx-auto size-3.5 text-primary" />
|
||||
) : (
|
||||
<span aria-hidden="true" className="text-muted-foreground">
|
||||
–
|
||||
</span>
|
||||
)
|
||||
) : (
|
||||
<span aria-hidden="true">–</span>
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
<td className={cn(sumCell, "py-1 text-xs text-foreground")}>{girSum ?? "–"}</td>
|
||||
</tr>
|
||||
)}
|
||||
{/* Innspill -- 5-veis retning, ingen meningsfull sum (retning, ikke
|
||||
et tall). */}
|
||||
{showApproach && (
|
||||
<tr>
|
||||
<th scope="row" className={rowLabel} title="Innspill">
|
||||
Appr.
|
||||
</th>
|
||||
{holes.map((h) => (
|
||||
<td key={h.hole_number} className={cn(cell, "py-1")}>
|
||||
<ApproachIcon result={h.approach_result} />
|
||||
{h.approach_result === null && <span aria-hidden="true">–</span>}
|
||||
</td>
|
||||
))}
|
||||
<td className={cn(sumCell, "py-1 text-xs text-muted-foreground")} aria-hidden="true">
|
||||
–
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{/* Chip -- antall per hull, summert. */}
|
||||
{showChip && (
|
||||
<tr>
|
||||
<th scope="row" className={rowLabel} title="Chip">
|
||||
Chip
|
||||
</th>
|
||||
{holes.map((h) => (
|
||||
<td key={h.hole_number} className={cn(cell, "py-1 text-xs font-semibold text-muted-foreground")}>
|
||||
{h.chip_count ?? "–"}
|
||||
</td>
|
||||
))}
|
||||
<td className={cn(sumCell, "py-1 text-xs text-foreground")}>{chipSum ?? "–"}</td>
|
||||
</tr>
|
||||
)}
|
||||
{/* Bunkerslag -- antall per hull, summert. */}
|
||||
{showBunker && (
|
||||
<tr>
|
||||
<th scope="row" className={rowLabel} title="Bunkerslag">
|
||||
Bnk
|
||||
</th>
|
||||
{holes.map((h) => (
|
||||
<td key={h.hole_number} className={cn(cell, "py-1 text-xs font-semibold text-muted-foreground")}>
|
||||
{h.bunker_shot_count ?? "–"}
|
||||
</td>
|
||||
))}
|
||||
<td className={cn(sumCell, "py-1 text-xs text-foreground")}>{bunkerSum ?? "–"}</td>
|
||||
</tr>
|
||||
)}
|
||||
{/* Straffeslag -- antall per hull, summert. */}
|
||||
{showPenalty && (
|
||||
<tr>
|
||||
<th scope="row" className={rowLabel} title="Straffeslag">
|
||||
Str
|
||||
</th>
|
||||
{holes.map((h) => (
|
||||
<td key={h.hole_number} className={cn(cell, "py-1 text-xs font-semibold text-muted-foreground")}>
|
||||
{h.penalty_strokes ?? "–"}
|
||||
</td>
|
||||
))}
|
||||
<td className={cn(sumCell, "py-1 text-xs text-foreground")}>{penaltySum ?? "–"}</td>
|
||||
</tr>
|
||||
)}
|
||||
{/* Anywayslag -- antall per hull, summert. */}
|
||||
{showAnyway && (
|
||||
<tr>
|
||||
<th scope="row" className={rowLabel} title="Anywayslag">
|
||||
Any.
|
||||
</th>
|
||||
{holes.map((h) => (
|
||||
<td key={h.hole_number} className={cn(cell, "py-1 text-xs font-semibold text-muted-foreground")}>
|
||||
{h.anyway_strokes ?? "–"}
|
||||
</td>
|
||||
))}
|
||||
<td className={cn(sumCell, "py-1 text-xs text-foreground")}>{anywaySum ?? "–"}</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
|
|
@ -992,6 +1198,18 @@ export function RoundScorecard({
|
|||
const back = orderedHoles.slice(9, 18)
|
||||
|
||||
const showStableford = holes.some((h) => h.strokes_received !== null)
|
||||
// Detaljrader (2026-08-03) -- vis kun rader som faktisk har data et sted
|
||||
// i runden (samme "vis kun det som faktisk finnes"-prinsipp som resten
|
||||
// av appen), regnet ut fra HELE runden (ikke bare front/back for seg)
|
||||
// slik at begge blokkene konsekvent viser/skjuler de samme radene.
|
||||
const showFairway = holes.some((h) => h.par !== 3 && h.tee_shot_result !== null)
|
||||
const showPutts = holes.some((h) => h.putts !== null)
|
||||
const showGir = holes.some((h) => h.score !== null && h.putts !== null)
|
||||
const showApproach = holes.some((h) => h.approach_result !== null)
|
||||
const showChip = holes.some((h) => h.chip_count !== null)
|
||||
const showBunker = holes.some((h) => h.bunker_shot_count !== null)
|
||||
const showPenalty = holes.some((h) => h.penalty_strokes !== null)
|
||||
const showAnyway = holes.some((h) => h.anyway_strokes !== null)
|
||||
const playedHoles = orderedHoles.filter((h) => h.played && h.score !== null)
|
||||
// Full rundens par (statisk referanse, f.eks. "Par 72") -- ALDRI brukt til
|
||||
// en til-par-utregning, kun til "Par"-feltet nederst som viser hele
|
||||
|
|
@ -1082,11 +1300,37 @@ export function RoundScorecard({
|
|||
)}
|
||||
|
||||
{/* 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 overflow-x-auto rounded-3xl border border-border bg-card p-3 shadow-md shadow-black/8 sm:p-4">
|
||||
{front.length > 0 && (
|
||||
<ScoreBlock holes={front} summaryLabel={back.length === 0 ? "Tot" : "Ut"} showStableford={showStableford} />
|
||||
<ScoreBlock
|
||||
holes={front}
|
||||
summaryLabel={back.length === 0 ? "Tot" : "Ut"}
|
||||
showStableford={showStableford}
|
||||
showFairway={showFairway}
|
||||
showPutts={showPutts}
|
||||
showGir={showGir}
|
||||
showApproach={showApproach}
|
||||
showChip={showChip}
|
||||
showBunker={showBunker}
|
||||
showPenalty={showPenalty}
|
||||
showAnyway={showAnyway}
|
||||
/>
|
||||
)}
|
||||
{back.length > 0 && (
|
||||
<ScoreBlock
|
||||
holes={back}
|
||||
summaryLabel="Inn"
|
||||
showStableford={showStableford}
|
||||
showFairway={showFairway}
|
||||
showPutts={showPutts}
|
||||
showGir={showGir}
|
||||
showApproach={showApproach}
|
||||
showChip={showChip}
|
||||
showBunker={showBunker}
|
||||
showPenalty={showPenalty}
|
||||
showAnyway={showAnyway}
|
||||
/>
|
||||
)}
|
||||
{back.length > 0 && <ScoreBlock holes={back} summaryLabel="Inn" showStableford={showStableford} />}
|
||||
</div>
|
||||
|
||||
{/* Combined total */}
|
||||
|
|
|
|||
Loading…
Reference in a new issue