diff --git a/ARCHITECTURE_DECISIONS.md b/ARCHITECTURE_DECISIONS.md index a085915..6fa037e 100644 --- a/ARCHITECTURE_DECISIONS.md +++ b/ARCHITECTURE_DECISIONS.md @@ -7663,6 +7663,73 @@ Containeren startet rent. --- +## ADR-078: Chip/Bunker/Straffeslag/Anywayslag som egen skjerm ("Steg 2" av tre forbedringer i score-registreringen) (2026-08-16) + +Foranledning: bruker observerte at Chip/Bunker/Straffeslag/Anywayslag ble +vist klemt inn under retningsvalget for utslag/innspill i begge +scoringsflytene -- ikke godt nok, burde være en egen skjerm. Bekreftet +(AskUserQuestion) at dette skal gjelde BEGGE flyter -- frittstående runder +OG org-turneringer -- "opplevelsen for brukeren skal være lik uansett". + +**Strukturell splitt, begge flyter:** +- Frittstående runder (`ScoringWizard`, `round-detail.tsx`): `WizardStep` + utvidet fra `"strokes" | "putts" | "puttDistance" | "details"` til + `"strokes" | "putts" | "puttDistance" | "direction" | "holeDetails"` -- + "details" splittet i to egne steg. GIR-auto-inferensen (2026-07-30) + flyttet fra å trigge på `step !== "details"` til `step !== "direction"`. +- Org-turneringer (`HoleStatsSheet`, `individual-tournament-detail.tsx`): + bygget om fra ÉN lang scrollbar skjerm til en LITEN intern steg-flyt + (samme stegrekkefølge som over), med samme auto-hopp og + GIR-auto-inferens som runde-siden. **Bevisst avgrensning**: lagringen + er UENDRET -- fortsatt ÉN batched `save()`-kall ved siste steg (nå + trigget fra "Lagre"-knappen i stedet for en alltid-synlig knapp), IKKE + konvertert til runde-sidens PATCH-per-felt-modell. De to flytene får nå + samme STRUKTUR/interaksjon, ikke delt kode eller delt lagrings- + mekanikk -- unngår å risikere en fungerende, annerledes arkitektur som + ikke var en del av forespørselen. + +**Visuell polering (V0-designet)**: siden det gjenstående "Detaljer per +hull"-steget nå har en hel skjerm for seg selv, ble den tidligere +1-kolonne-stabelen (mye tomrom under fire små tellere) erstattet med et +FAST 2x2-rutenett av teller-FLISER -- egne kort med etikett + stort +sentrert tall + -/+ nederst. Interaksjonskontrakten er uendret (minus +disabled på 0, pluss disabled på `max`, alle trykkflater >=44px), kun det +visuelle uttrykket er nytt. `Stepper`-komponenten (delt av begge flyter +via `hole-stat-inputs.tsx`) oppdatert i selve komponenten -- automatisk +identisk i begge flyter uten duplisert arbeid. `grid-cols-2` (ALDRI +`grid-cols-3`) fordi seksjonen er fast `max-w-sm` (384px): to fliser gir +~184px hver, nok til flisens to 44px-knapper side om side. + +V0-prompt skrevet av Claude (se `feedback_frontend_via_v0`), kjørt av +bruker, eksport (`tee-cup (12).zip`) integrert -- KUN den relevante +`Stepper`-omdesignet + grid-endringen ble portert inn (eksporten var en +full prosjekt-snapshot fra en delt V0-arbeidsflate, inkludert en eldre, +FØR-splitt kopi av hele `round-detail.tsx` -- ingen fullstendig +filerstatning, kun det faktisk endrede uttrykket for Stepper/rutenett +overført til den ekte, allerede steg-splittede filen). Zip slettet fra +`Temp-uploads/` etter integrering. + +**Verifisert:** `tsc --noEmit` rent + 45/45 vitest (begge ganger -- +strukturell splitt, og etter V0-integrering). Egen scratch-database + +scratch `teecup_api` + lokal `next dev`, to runder (port 18105/13105 for +strukturell verifisering, port 18106/13106 for V0-integrering): begge +flyter testet ende-til-ende gjennom ALLE steg (strokes → putts → +puttDistance → direction → holeDetails), GIR-auto-inferens bekreftet +fortsatt virker (trigget riktig på "direction"-steget), lagring bekreftet +i begge flyter (round-siden PATCH-per-felt, turnering-siden batched +"Lagre"), gjenåpning av et allerede utfylt hull viste korrekt lagrede +verdier. 2x2-rutenettet med teller-flisene bekreftet identisk i begge +flyter, interaksjon (øke/redusere, disabled ved 0/max) bekreftet +fungerende. Lys+mørk bekreftet i begge runder. Scratch-stackene +fullstendig revet ned -- ekte `teecup_db`/`teecup_api`/`teecup_frontend` +urørt. + +**Ingen migrasjon** -- ren frontend-endring. + +**Rullet ut**: ikke ennå -- venter på bekreftelse fra bruker. + +--- + ## Utviklingsplan (rekkefølge) 1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cff4fe..7792e22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11690,3 +11690,29 @@ Neste steg: **Rullet ut 2026-08-16** -- ingen migrasjon, `docker compose build teecup_frontend && up -d` etter eksplisitt bekreftelse fra bruker. Containeren startet rent. + +94. **Chip/Bunker/Straffeslag/Anywayslag som egen skjerm (ADR-078) — + 2026-08-16.** Andre av tre forbedringer i score-registreringen. Disse + fire tellerne var klemt inn under retningsvalget for utslag/innspill i + begge scoringsflytene (frittstående runder OG org-turneringer, etter + eksplisitt "opplevelsen skal være lik uansett"). + + `ScoringWizard`s "details"-steg splittet i "direction"/"holeDetails". + `HoleStatsSheet` (org-turneringer) bygget om fra ÉN lang skjerm til en + tilsvarende liten intern steg-flyt -- lagringen selv er BEVISST + uendret (fortsatt én batched "Lagre" der, ikke konvertert til runde- + sidens PATCH-per-felt). Visuell polering via V0 (Claude-skrevet + prompt): de fire tellerne fikk et 2x2-rutenett av flis-kort i stedet + for en 1-kolonne-stabel med mye tomrom -- `Stepper`-komponenten (delt + av begge flyter) oppdatert ett sted, automatisk identisk begge steder. + + **Verifisert:** `tsc --noEmit` rent + 45/45 vitest. To scratch-runder + (strukturell splitt, så V0-integrering): begge flyter testet + ende-til-ende gjennom alle steg, GIR-auto-inferens fortsatt virker, + lagring bekreftet i begge flyter, gjenåpning viste korrekt lagrede + verdier, 2x2-rutenettet identisk og fungerende i begge flyter. + Lys+mørk bekreftet. Scratch-stackene fullstendig revet ned. + + **Ingen migrasjon.** + + **Rullet ut**: ikke ennå. diff --git a/frontend/components/hole-stat-inputs.tsx b/frontend/components/hole-stat-inputs.tsx index a0ebd2d..8ea7364 100644 --- a/frontend/components/hole-stat-inputs.tsx +++ b/frontend/components/hole-stat-inputs.tsx @@ -316,26 +316,42 @@ export function Stepper({ max?: number }) { const atMax = max !== undefined && value >= max + const atMin = value <= 0 + // Teller-FLIS (ADR-078, 2026-08-16, V0-designet) -- selvstendig kort med + // etikett + stort tall + -/+ på bunnen, ment for et 2x2-rutenett (se + // holeDetails-steget i round-detail.tsx/individual-tournament-detail.tsx). + // Interaksjonskontrakten er uendret: -/tall/+, minus disabled på 0, pluss + // disabled på max, alle trykkflater >=44px. Kun app-tokens. return ( -
- {label} -
+
+ {label} + {/* Tallet bærer selve informasjonen (aldri farge alene); nullverdi + dempes kun visuelt via muted-foreground, som fortsatt er AA på + kort. */} + + {value} + +
- {value} diff --git a/frontend/components/individual-tournament-detail.tsx b/frontend/components/individual-tournament-detail.tsx index d0b62b4..3514c8b 100644 --- a/frontend/components/individual-tournament-detail.tsx +++ b/frontend/components/individual-tournament-detail.tsx @@ -3236,6 +3236,23 @@ type HolePatch = { expected_version?: number | null } +// ADR-078 (2026-08-16): HoleStatsSheet bygget om fra ÉN lang scrollbar +// skjerm til en liten intern steg-flyt -- samme visuelle språk (fulskjerm, +// header/footer) og samme stegrekkefølge som ScoringWizard (frittstående +// runder), etter eksplisitt ønske om at "opplevelsen for brukeren skal +// være lik uansett". IKKE en gjenbruk av ScoringWizard-koden -- lagringen +// er bevisst UENDRET (ÉN batched save() ved siste steg, ikke PATCH per +// felt slik runde-siden gjør) for å unngå å røre en fungerende, annerledes +// arkitektur mekanikk som ikke var en del av forespørselen. Se +// wizardStepsFor()/WizardStep i round-detail.tsx for det speilede +// mønsteret på runde-siden. +type SheetStep = "strokes" | "putts" | "puttDistance" | "direction" | "holeDetails" + +function sheetStepsFor(statLevel: "strokes_and_putts" | "full"): SheetStep[] { + if (statLevel === "full") return ["strokes", "putts", "puttDistance", "direction", "holeDetails"] + return ["strokes", "putts", "puttDistance"] +} + function HoleStatsSheet({ hole, playerName, @@ -3268,6 +3285,43 @@ function HoleStatsSheet({ const [saving, setSaving] = useState(false) const [error, setError] = useState(null) + const steps = sheetStepsFor(statLevel) + const [stepIndex, setStepIndex] = useState(0) + const step = steps[Math.min(stepIndex, steps.length - 1)] + const isFirstStep = stepIndex === 0 + const isLastStep = stepIndex === steps.length - 1 + const canProceed = step === "strokes" ? strokes !== null : true + + // Auto-hopp, samme mønster som ScoringWizard (round-detail.tsx) -- + // hopper videre av seg selv når steget sitt eget felt får en verdi. + const autoField = step === "strokes" ? "strokes" : step === "putts" ? "putts" : step === "puttDistance" ? "puttBucket" : null + const enteredWithValueRef = useRef(false) + const firedRef = useRef(false) + useEffect(() => { + firedRef.current = false + enteredWithValueRef.current = + autoField === "strokes" ? strokes !== null : autoField === "putts" ? putts !== null : autoField === "puttBucket" ? puttBucket !== null : false + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [step]) + useEffect(() => { + if (!autoField || firedRef.current || enteredWithValueRef.current) return + const value = autoField === "strokes" ? strokes : autoField === "putts" ? putts : puttBucket + if (value !== null) { + firedRef.current = true + forward() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [strokes, putts, puttBucket]) + + // GIR-auto-inferens, samme formel/begrunnelse som ScoringWizard. + useEffect(() => { + if (step !== "direction") return + if (approach !== null) return + if (strokes === null || putts === null) return + if (strokes - putts <= hole.par - 2) setApproach("hit") + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [step, strokes, putts, approach, hole.par]) + async function save() { if (strokes === null) { setError("Registrer antall slag først.") @@ -3296,6 +3350,21 @@ function HoleStatsSheet({ } } + function back() { + if (isFirstStep) { + onClose() + return + } + setStepIndex((i) => i - 1) + } + function forward() { + if (!isLastStep) { + setStepIndex((i) => i + 1) + return + } + void save() + } + return (
@@ -3318,52 +3387,61 @@ function HoleStatsSheet({
- + {step === "strokes" && ( + <> + - {/* Spillerens per-hull-historikk (ADR-072) -- skjuler seg selv når - banen er custom/håndlaget eller spilleren ikke har historikk - ennå (se HoleHistoryPanel/app/hole_history.py). */} - + {/* Spillerens per-hull-historikk (ADR-072) -- skjuler seg + selv når banen er custom/håndlaget eller spilleren ikke + har historikk ennå (se HoleHistoryPanel/app/hole_ + history.py). */} + + + )} - + {step === "putts" && ( + + )} - + {step === "puttDistance" && ( + + )} - {statLevel === "full" && ( + {step === "direction" && ( <> - - -
- - - - -
-
)} + {/* Teller-flis-rutenett (V0-designet, ADR-078, 2026-08-16) -- + samme 2x2-mønster som round-detail.tsx sitt speilede + holeDetails-steg, se kommentaren der for begrunnelsen. */} + {step === "holeDetails" && ( +
+ + + + +
+ )} + {error &&

{error}

}
-
+
+
diff --git a/frontend/components/round-detail.tsx b/frontend/components/round-detail.tsx index 13958e6..d0136d0 100644 --- a/frontend/components/round-detail.tsx +++ b/frontend/components/round-detail.tsx @@ -1921,12 +1921,17 @@ export function RoundDetail({ roundId }: { roundId: string }) { // 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" +type WizardStep = "strokes" | "putts" | "puttDistance" | "direction" | "holeDetails" function wizardStepsFor(statLevel: StatLevel): WizardStep[] { if (statLevel === "strokes_only") return ["strokes"] if (statLevel === "strokes_and_putts") return ["strokes", "putts", "puttDistance"] - return ["strokes", "putts", "puttDistance", "details"] + // ADR-078 (2026-08-16): "details" splittet i to egne skjermer -- retning + // (utslag/innspill, + kølle/slagmåling som hører til utslaget) og + // Chip/Bunker/Straffeslag/Anywayslag fikk hver sin egen skjerm i stedet + // for å dele én, etter tilbakemelding om at detaljene druknet under + // retningsvalget. + return ["strokes", "putts", "puttDistance", "direction", "holeDetails"] } // Hvilket HoleStat-felt (om noen) som "eier" auto-hopp for et gitt steg -- @@ -2032,10 +2037,11 @@ function ScoringWizard({ // 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). + // ekte GIR). Trigges når "direction"-steget nås (ADR-078: flyttet fra det + // tidligere "details"-steget, som ble splittet i to), siden slag+putt + // begge må være kjent (kommer tidligere i wizard-rekkefølgen). useEffect(() => { - if (step !== "details") return + if (step !== "direction") return if (stat.approach !== null) return if (stat.strokes === null || stat.putts === null) return if (stat.strokes - stat.putts <= hole.par - 2) { @@ -2206,7 +2212,12 @@ function ScoringWizard({ /> )} - {step === "details" && ( + {/* ADR-078 (2026-08-16): "Retning" er nå EGEN skjerm -- inkl. + kølle/slagmåling, som begge hører til utslaget. Splittet fra + det tidligere "details"-steget etter tilbakemelding om at + Chip/Bunker/Straffeslag/Anywayslag druknet under retnings- + valget. */} + {step === "direction" && ( <> - {/* Seksjon 1 -- retning (2026-08-11): retningskorsene utgjør nå - en tydelig egen gruppe med overskrift, adskilt fra tellerne - under, så det er visuelt klart at skjermen består av to - ulike typer inndata (brukertilbakemelding: umulig å vite at - man måtte skrolle for å finne resten). */} {hole.par !== 3 && ( - - {/* Seksjon 2 -- detaljer per hull (2026-08-11): egen overskrift - + divider fra "Retning" over. Alle FIRE tellerne (Chip/ - Bunker/Straffeslag/Anywayslag) er konseptuelt samme type - data (antall småhendelser per hull, typisk 0-3) og bruker nå - ETT konsistent +/--stepper-mønster -- Anywayslag var - tidligere det eneste tall-rutenettet, som brøt mønsteret - uten grunn. 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 stepperne - (hver ~190px minimum pga. faste 44px-knapper) til å - overlappe hverandre i stedet for å faktisk ha plass. */} - -
- onUpdate({ chip: v })} readOnly={readOnly} max={stat.strokes ?? undefined} /> - onUpdate({ bunker: v })} readOnly={readOnly} max={stat.strokes ?? undefined} /> - onUpdate({ penalty: v })} readOnly={readOnly} max={stat.strokes ?? undefined} /> - onUpdate({ anywayStrokes: v })} - readOnly={readOnly} - max={stat.strokes ?? undefined} - /> -
-
)} + + {/* ADR-078 (2026-08-16): Chip/Bunker/Straffeslag/Anywayslag har nå + EGEN skjerm, splittet fra "Retning" (se over). Alle FIRE + tellerne er konseptuelt samme type data (antall småhendelser + per hull, typisk 0-3) og bruker ETT konsistent + +/--teller-flis-mønster (Stepper, hole-stat-inputs.tsx). + + Layout (V0-designet, 2026-08-16): et FAST 2x2-rutenett av + teller-fliser i stedet for en høy stabel som etterlot mye + tomrom under nå som feltene har fått en egen skjerm. Fire + likeverdige felt leses da som ett samlet sett og fyller + kolonnen bevisst. grid-cols-2 er trygt -- og ALDRI + grid-cols-3 -- fordi seksjonen er fast max-w-sm (384px, se + containeren over): to fliser gir ~184px hver, rikelig til + flisens to 44px-knapper side om side, mens tre ville presset + hver flis under ~120px og fått knappene til å overlappe. + Viewport-uavhengig, så det holder på alle skjermbredder. */} + {step === "holeDetails" && ( +
+ onUpdate({ chip: v })} readOnly={readOnly} max={stat.strokes ?? undefined} /> + onUpdate({ bunker: v })} readOnly={readOnly} max={stat.strokes ?? undefined} /> + onUpdate({ penalty: v })} readOnly={readOnly} max={stat.strokes ?? undefined} /> + onUpdate({ anywayStrokes: v })} + readOnly={readOnly} + max={stat.strokes ?? undefined} + /> +
+ )}