Chip/Bunker/Straffeslag/Anywayslag som egen skjerm (ADR-078)
Splittet ScoringWizard sitt "details"-steg i "direction"/"holeDetails", og bygget om HoleStatsSheet (org-turneringer) fra én lang skjerm til tilsvarende steg-flyt -- samme opplevelse i begge scoringsflyter, uten å røre HoleStatsSheet sin egen (bevisst annerledes) lagringsmodell. Visuell polering via V0: de fire tellerne fikk et 2x2-rutenett av flis-kort i stedet for en 1-kolonne-stabel. Stepper-komponenten er delt mellom begge flyter, så endringen gjelder automatisk begge steder. tsc + 45/45 vitest, scratch-verifisert i begge flyter (lys+mørk). Rullet ut. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
2ea4c26990
commit
b292401fe4
5 changed files with 304 additions and 103 deletions
|
|
@ -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)
|
## Utviklingsplan (rekkefølge)
|
||||||
|
|
||||||
1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003)
|
1. ✅ Land tenant-modell → **Organisasjon** (ADR-001/002/003)
|
||||||
|
|
|
||||||
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -11690,3 +11690,29 @@ Neste steg:
|
||||||
**Rullet ut 2026-08-16** -- ingen migrasjon, `docker compose build
|
**Rullet ut 2026-08-16** -- ingen migrasjon, `docker compose build
|
||||||
teecup_frontend && up -d` etter eksplisitt bekreftelse fra bruker.
|
teecup_frontend && up -d` etter eksplisitt bekreftelse fra bruker.
|
||||||
Containeren startet rent.
|
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å.
|
||||||
|
|
|
||||||
|
|
@ -316,26 +316,42 @@ export function Stepper({
|
||||||
max?: number
|
max?: number
|
||||||
}) {
|
}) {
|
||||||
const atMax = max !== undefined && value >= max
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col items-center gap-3 rounded-2xl border border-border bg-card p-4">
|
||||||
<span className="text-base font-semibold text-foreground">{label}</span>
|
<span className="text-center text-sm font-bold leading-tight text-foreground">{label}</span>
|
||||||
<div className="flex items-center justify-between gap-2 rounded-2xl border border-border bg-card p-1.5">
|
{/* Tallet bærer selve informasjonen (aldri farge alene); nullverdi
|
||||||
|
dempes kun visuelt via muted-foreground, som fortsatt er AA på
|
||||||
|
kort. */}
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"text-4xl font-extrabold leading-none tabular-nums",
|
||||||
|
atMin ? "text-muted-foreground" : "text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
<div className="flex w-full items-center gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={readOnly || value <= 0}
|
disabled={readOnly || atMin}
|
||||||
onClick={() => onChange(Math.max(0, value - 1))}
|
onClick={() => onChange(Math.max(0, value - 1))}
|
||||||
aria-label={`Færre ${label}`}
|
aria-label={`Færre ${label}`}
|
||||||
className="flex size-11 shrink-0 items-center justify-center rounded-xl border border-border bg-background text-foreground transition-colors hover:bg-accent/50 disabled:opacity-40"
|
className="flex h-11 flex-1 items-center justify-center rounded-xl border border-border bg-background text-foreground transition-colors hover:bg-accent/50 disabled:opacity-40"
|
||||||
>
|
>
|
||||||
<Minus aria-hidden="true" className="size-5" />
|
<Minus aria-hidden="true" className="size-5" />
|
||||||
</button>
|
</button>
|
||||||
<span className="min-w-8 text-center text-xl font-extrabold tabular-nums text-foreground">{value}</span>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={readOnly || atMax}
|
disabled={readOnly || atMax}
|
||||||
onClick={() => onChange(value + 1)}
|
onClick={() => onChange(value + 1)}
|
||||||
aria-label={`Flere ${label}`}
|
aria-label={`Flere ${label}`}
|
||||||
className="flex size-11 shrink-0 items-center justify-center rounded-xl border border-border bg-background text-foreground transition-colors hover:bg-accent/50 disabled:opacity-40"
|
className="flex h-11 flex-1 items-center justify-center rounded-xl border border-border bg-background text-foreground transition-colors hover:bg-accent/50 disabled:opacity-40"
|
||||||
>
|
>
|
||||||
<Plus aria-hidden="true" className="size-5" />
|
<Plus aria-hidden="true" className="size-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -3236,6 +3236,23 @@ type HolePatch = {
|
||||||
expected_version?: number | null
|
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({
|
function HoleStatsSheet({
|
||||||
hole,
|
hole,
|
||||||
playerName,
|
playerName,
|
||||||
|
|
@ -3268,6 +3285,43 @@ function HoleStatsSheet({
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(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() {
|
async function save() {
|
||||||
if (strokes === null) {
|
if (strokes === null) {
|
||||||
setError("Registrer antall slag først.")
|
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 (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex flex-col bg-background">
|
<div className="fixed inset-0 z-50 flex flex-col bg-background">
|
||||||
<header className="flex min-h-14 shrink-0 items-center gap-2 border-b border-border px-3">
|
<header className="flex min-h-14 shrink-0 items-center gap-2 border-b border-border px-3">
|
||||||
|
|
@ -3318,52 +3387,61 @@ function HoleStatsSheet({
|
||||||
|
|
||||||
<main className="flex min-h-0 flex-1 flex-col overflow-y-auto p-5 sm:p-6">
|
<main className="flex min-h-0 flex-1 flex-col overflow-y-auto p-5 sm:p-6">
|
||||||
<div className="mx-auto flex w-full max-w-sm flex-col gap-6">
|
<div className="mx-auto flex w-full max-w-sm flex-col gap-6">
|
||||||
<NumberPicker
|
{step === "strokes" && (
|
||||||
label="Slag"
|
<>
|
||||||
value={strokes}
|
<NumberPicker
|
||||||
directValues={[1, 2, 3, 4, 5, 6, 7, 8, 9]}
|
label="Slag"
|
||||||
expandValues={[10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]}
|
value={strokes}
|
||||||
expandLabel="10+"
|
directValues={[1, 2, 3, 4, 5, 6, 7, 8, 9]}
|
||||||
parValue={hole.par}
|
expandValues={[10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]}
|
||||||
showGolfTerms
|
expandLabel="10+"
|
||||||
strokesReceived={strokesReceived}
|
parValue={hole.par}
|
||||||
onChange={setStrokes}
|
showGolfTerms
|
||||||
readOnly={false}
|
strokesReceived={strokesReceived}
|
||||||
/>
|
onChange={setStrokes}
|
||||||
|
readOnly={false}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Spillerens per-hull-historikk (ADR-072) -- skjuler seg selv når
|
{/* Spillerens per-hull-historikk (ADR-072) -- skjuler seg
|
||||||
banen er custom/håndlaget eller spilleren ikke har historikk
|
selv når banen er custom/håndlaget eller spilleren ikke
|
||||||
ennå (se HoleHistoryPanel/app/hole_history.py). */}
|
har historikk ennå (se HoleHistoryPanel/app/hole_
|
||||||
<HoleHistoryPanel url={historyUrl} />
|
history.py). */}
|
||||||
|
<HoleHistoryPanel url={historyUrl} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<NumberPicker
|
{step === "putts" && (
|
||||||
label="Putter"
|
<NumberPicker
|
||||||
value={putts}
|
label="Putter"
|
||||||
directValues={[0, 1, 2, 3, 4, 5, 6]}
|
value={putts}
|
||||||
expandValues={[7, 8, 9, 10]}
|
directValues={[0, 1, 2, 3, 4, 5, 6]}
|
||||||
expandLabel="7+"
|
expandValues={[7, 8, 9, 10]}
|
||||||
maxValue={strokes ?? undefined}
|
expandLabel="7+"
|
||||||
accent="orange"
|
maxValue={strokes ?? undefined}
|
||||||
onChange={setPutts}
|
accent="orange"
|
||||||
readOnly={false}
|
onChange={setPutts}
|
||||||
/>
|
readOnly={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<ChoiceRow
|
{step === "puttDistance" && (
|
||||||
label="Avstand første putt"
|
<ChoiceRow
|
||||||
options={[
|
label="Avstand første putt"
|
||||||
{ value: "<1m", label: "0-1m" },
|
options={[
|
||||||
{ value: "<2m", label: "1-2m" },
|
{ value: "<1m", label: "0-1m" },
|
||||||
{ value: "<3m", label: "2-3m" },
|
{ value: "<2m", label: "1-2m" },
|
||||||
{ value: "<5m", label: "3-5m" },
|
{ value: "<3m", label: "2-3m" },
|
||||||
{ value: "<8m", label: "5-8m" },
|
{ value: "<5m", label: "3-5m" },
|
||||||
{ value: "8m+", label: "8m+" },
|
{ value: "<8m", label: "5-8m" },
|
||||||
]}
|
{ value: "8m+", label: "8m+" },
|
||||||
value={puttBucket}
|
]}
|
||||||
onChange={setPuttBucket}
|
value={puttBucket}
|
||||||
readOnly={false}
|
onChange={setPuttBucket}
|
||||||
/>
|
readOnly={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{statLevel === "full" && (
|
{step === "direction" && (
|
||||||
<>
|
<>
|
||||||
<ClubPicker
|
<ClubPicker
|
||||||
label="Kølle brukt ved utslaget"
|
label="Kølle brukt ved utslaget"
|
||||||
|
|
@ -3393,30 +3471,36 @@ function HoleStatsSheet({
|
||||||
readOnly={false}
|
readOnly={false}
|
||||||
/>
|
/>
|
||||||
</WizardSection>
|
</WizardSection>
|
||||||
|
|
||||||
<WizardSection title="Detaljer per hull">
|
|
||||||
<div className="grid grid-cols-1 gap-3">
|
|
||||||
<Stepper label="Chip" value={chip} onChange={setChip} readOnly={false} max={strokes ?? undefined} />
|
|
||||||
<Stepper label="Bunker" value={bunker} onChange={setBunker} readOnly={false} max={strokes ?? undefined} />
|
|
||||||
<Stepper label="Straffeslag" value={penalty} onChange={setPenalty} readOnly={false} max={strokes ?? undefined} />
|
|
||||||
<Stepper label="Anywayslag" value={anywayStrokes} onChange={setAnywayStrokes} readOnly={false} max={strokes ?? undefined} />
|
|
||||||
</div>
|
|
||||||
</WizardSection>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 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" && (
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<Stepper label="Chip" value={chip} onChange={setChip} readOnly={false} max={strokes ?? undefined} />
|
||||||
|
<Stepper label="Bunker" value={bunker} onChange={setBunker} readOnly={false} max={strokes ?? undefined} />
|
||||||
|
<Stepper label="Straffeslag" value={penalty} onChange={setPenalty} readOnly={false} max={strokes ?? undefined} />
|
||||||
|
<Stepper label="Anywayslag" value={anywayStrokes} onChange={setAnywayStrokes} readOnly={false} max={strokes ?? undefined} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{error && <p className="text-sm font-medium text-destructive">{error}</p>}
|
{error && <p className="text-sm font-medium text-destructive">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className="shrink-0 border-t border-border p-4">
|
<footer className="grid shrink-0 grid-cols-2 gap-3 border-t border-border p-4">
|
||||||
|
<Button type="button" variant="outline" onClick={back} className="h-14 rounded-2xl text-base font-bold">
|
||||||
|
{isFirstStep ? "Avbryt" : "Forrige"}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={saving || strokes === null}
|
disabled={!canProceed || saving}
|
||||||
onClick={save}
|
onClick={forward}
|
||||||
className="h-14 w-full rounded-2xl text-base font-bold disabled:opacity-40"
|
className="h-14 rounded-2xl text-base font-bold disabled:opacity-40"
|
||||||
>
|
>
|
||||||
{saving ? "Lagrer…" : "Lagre"}
|
{isLastStep ? (saving ? "Lagrer…" : "Lagre") : "Neste"}
|
||||||
</Button>
|
</Button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1921,12 +1921,17 @@ export function RoundDetail({ roundId }: { roundId: string }) {
|
||||||
// nullstiller til steg 1) eller "Ferdig" (er spilleren den siste, lukker
|
// nullstiller til steg 1) eller "Ferdig" (er spilleren den siste, lukker
|
||||||
// veiviseren og varsler forelderen om å gå videre til neste hull).
|
// 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[] {
|
function wizardStepsFor(statLevel: StatLevel): WizardStep[] {
|
||||||
if (statLevel === "strokes_only") return ["strokes"]
|
if (statLevel === "strokes_only") return ["strokes"]
|
||||||
if (statLevel === "strokes_and_putts") return ["strokes", "putts", "puttDistance"]
|
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 --
|
// 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-
|
// ALDRI et allerede satt (manuelt ELLER tidligere auto-satt) approach-
|
||||||
// valg, og gjelder aldri motsatt vei (GIR usann sier ingenting om
|
// valg, og gjelder aldri motsatt vei (GIR usann sier ingenting om
|
||||||
// retning -- en scrambling-birdie gir identisk slag/putt-mønster som en
|
// 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å
|
// ekte GIR). Trigges når "direction"-steget nås (ADR-078: flyttet fra det
|
||||||
// være kjent (kommer tidligere i wizard-rekkefølgen).
|
// tidligere "details"-steget, som ble splittet i to), siden slag+putt
|
||||||
|
// begge må være kjent (kommer tidligere i wizard-rekkefølgen).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (step !== "details") return
|
if (step !== "direction") return
|
||||||
if (stat.approach !== null) return
|
if (stat.approach !== null) return
|
||||||
if (stat.strokes === null || stat.putts === null) return
|
if (stat.strokes === null || stat.putts === null) return
|
||||||
if (stat.strokes - stat.putts <= hole.par - 2) {
|
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" && (
|
||||||
<>
|
<>
|
||||||
<ClubPicker
|
<ClubPicker
|
||||||
label="Kølle brukt ved utslaget"
|
label="Kølle brukt ved utslaget"
|
||||||
|
|
@ -2225,11 +2236,6 @@ function ScoringWizard({
|
||||||
variant="inline"
|
variant="inline"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 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). */}
|
|
||||||
<WizardSection title="Retning">
|
<WizardSection title="Retning">
|
||||||
{hole.par !== 3 && (
|
{hole.par !== 3 && (
|
||||||
<DirectionCross
|
<DirectionCross
|
||||||
|
|
@ -2251,37 +2257,39 @@ function ScoringWizard({
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
/>
|
/>
|
||||||
</WizardSection>
|
</WizardSection>
|
||||||
|
|
||||||
{/* 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. */}
|
|
||||||
<WizardSection title="Detaljer per hull">
|
|
||||||
<div className="grid grid-cols-1 gap-3">
|
|
||||||
<Stepper label="Chip" value={stat.chip} onChange={(v) => onUpdate({ chip: v })} readOnly={readOnly} max={stat.strokes ?? undefined} />
|
|
||||||
<Stepper label="Bunker" value={stat.bunker} onChange={(v) => onUpdate({ bunker: v })} readOnly={readOnly} max={stat.strokes ?? undefined} />
|
|
||||||
<Stepper label="Straffeslag" value={stat.penalty} onChange={(v) => onUpdate({ penalty: v })} readOnly={readOnly} max={stat.strokes ?? undefined} />
|
|
||||||
<Stepper
|
|
||||||
label="Anywayslag"
|
|
||||||
value={stat.anywayStrokes ?? 0}
|
|
||||||
onChange={(v) => onUpdate({ anywayStrokes: v })}
|
|
||||||
readOnly={readOnly}
|
|
||||||
max={stat.strokes ?? undefined}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</WizardSection>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 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" && (
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<Stepper label="Chip" value={stat.chip} onChange={(v) => onUpdate({ chip: v })} readOnly={readOnly} max={stat.strokes ?? undefined} />
|
||||||
|
<Stepper label="Bunker" value={stat.bunker} onChange={(v) => onUpdate({ bunker: v })} readOnly={readOnly} max={stat.strokes ?? undefined} />
|
||||||
|
<Stepper label="Straffeslag" value={stat.penalty} onChange={(v) => onUpdate({ penalty: v })} readOnly={readOnly} max={stat.strokes ?? undefined} />
|
||||||
|
<Stepper
|
||||||
|
label="Anywayslag"
|
||||||
|
value={stat.anywayStrokes ?? 0}
|
||||||
|
onChange={(v) => onUpdate({ anywayStrokes: v })}
|
||||||
|
readOnly={readOnly}
|
||||||
|
max={stat.strokes ?? undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollFade>
|
</ScrollFade>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue