From 1867c65143c466583c6c7006c157c267069fcdc7 Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Tue, 18 Aug 2026 20:18:47 +0200 Subject: [PATCH] Turneringsoppsett som steg-wizard (ADR-092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Oppsett-fanen var én lang stablet side med alle kort etter hverandre. Omgjort til 6 klikkbare steg med fri navigasjon, strukturelt inspirert av Golf GameBooks steg-navigasjon (ikke visuelt -- egne clubhouse- tokens). Grupper og startliste er det eneste genuint nye steget, løfter Utslagsgrupper ut av å være gjemt inni hver enkelt runde. Co-Authored-By: Claude Sonnet 5 --- ARCHITECTURE_DECISIONS.md | 83 +++++ CHANGELOG.md | 36 +++ .../individual-tournament-detail.tsx | 297 ++++++++++-------- .../components/round-groups-step-list.tsx | 66 ++++ frontend/components/tournament-setup-nav.tsx | 71 +++++ 5 files changed, 425 insertions(+), 128 deletions(-) create mode 100644 frontend/components/round-groups-step-list.tsx create mode 100644 frontend/components/tournament-setup-nav.tsx diff --git a/ARCHITECTURE_DECISIONS.md b/ARCHITECTURE_DECISIONS.md index 496ebe2..217804b 100644 --- a/ARCHITECTURE_DECISIONS.md +++ b/ARCHITECTURE_DECISIONS.md @@ -9319,6 +9319,89 @@ allerede fungerer andre steder i appen). ekte `teecup_db`, deretter `docker compose build teecup_api teecup_frontend && up -d` for begge. Rene logger, 200 OK. +## ADR-092: Turneringsoppsett som steg-wizard, strukturelt inspirert av Golf GameBook (2026-08-18) + +Bruker viste skjermdumper av Golf GameBooks turneringsoppsett (steg- +navigasjon: Tournament Setup → Round Setup → Tournament Site → Branding +→ Entries → Players → Groups and Start List → Results and Options) og +spurte: "Jeg synes denne delen av TeeCup er veldig lite brukervennlig. +Kan du hjelpe meg å tenke gjennom dette?" -- TeeCups "Oppsett"-fane +(`SetupTab` i `individual-tournament-detail.tsx`) var én lang stablet +side med alle kort etter hverandre (`TournamentSettingsCard` → +Scoringsmetode → `RoundsCard` → `CutCard`/`TiebreakCard` → +`ClassesCard` → `ParticipantsCard`), i byggerekkefølge, ikke i en +rekkefølge en arrangør faktisk tenker gjennom oppsettet i. Bruker +svarte "Full wizard. Bring gjerne V0 inn", med ett eksplisitt forbehold +underveis: "Yes, men husk at vi ikke skal ende opp med et plagiat av +Golf GameBook." + +**Struktur, ikke visuell kopi.** GameBook-skjermdumpene ble brukt +UTELUKKENDE som referanse for selve IA-mønsteret (steg i stedet for én +lang side), ikke som visuell mal -- V0-prompten instruerte eksplisitt +om å bruke TeeCups egne clubhouse-fargetoken/`rounded-2xl`-kortstil fra +`DESIGN_SYSTEM.md`, ingen grønnfarge/ikonsett/spacing hentet fra +GameBook. Dette var et rent IA-/navigasjonsproblem, ikke et manglende- +funksjonalitet-problem -- praktisk talt alle felt GameBook viser fantes +allerede og fungerte. Ingen backend-endring, ingen migrasjon. + +**6 steg** (ikke GameBooks 8 -- "Tournament Site"/"Branding" dekkes +allerede av TeeCups egen frittstående Presentasjon-fane, "Entries" er +allerede en velfungerende inne-i-kortet-seksjon i `TournamentSettings +Card` som ikke var verdt å splitte ut av et nylig ferdigstilt kort), +hver drevet av et kort som allerede fantes og fungerte -- ingen av dem +trengte V0-rework: Turneringsoppsett (`TournamentSettingsCard`), Runder +og baner (`RoundsCard`), Klasser (`ClassesCard`), Spillere +(`ParticipantsCard`), Resultat og opsjoner (Scoringsmetode-seksjonen, +flyttet hit fra å stå løst øverst siden det er et resultatvalg ikke +identitet, + `CutCard` + `TiebreakCard`, samme betingede visning som +før). + +**Grupper og startliste er det eneste genuint nye steget.** +`RoundGroupsPanel` var tidligere gjemt bak en "Utslagsgrupper"-knapp +inni hver enkelt `RoundCard` (måtte åpne runden først) -- direkte i +ånden til brukerens opprinnelige klage om at rundene har mye å ta tak i +mens org-oppsettet har lite. Løftet til et eget toppnivå-steg: ny +`RoundGroupsStepList`-komponent (V0-generert) lister alle runder +(sammenhengende liste-mønster, `divide-y`, ikke separate kort) med én +"Rediger grupper"-knapp per runde som åpner samme `RoundGroupsPanel` +uendret. Den gamle knappen inni `RoundCard` ble FJERNET (ikke bare +duplisert) -- to veier til samme handling ville vært akkurat den typen +duplisering wizarden var ment å rydde opp i. + +**Fri navigasjon, ingen låsing.** GameBooks egne skjermdumper viser +faktisk grønne haker på ALLE 8 steg samtidig som "Tournament name" +sto tomt i opprettelses-flyten -- hakene er altså ikke ekte fullført- +validering, bare statisk wizard-pynt. Ny `TournamentSetupNav` +(V0-generert): alle 6 steg alltid klikkbare i vilkårlig rekkefølge, +ingen fullført-per-steg-logikk bygget (TeeCup er allerede fleksibelt på +oppsett-rekkefølge, og en slik validering ble aldri etterspurt). Steget +er ren `useState` inni `SetupTab`, ikke URL-drevet -- samme mønster som +de ytre fanene (Oppsett/Scorekort/Leaderboard/Presentasjon) allerede +bruker. + +**Bredde-regresjon funnet og rettet under verifisering:** Oppsett-fanen +delte tidligere en `max-w-4xl` (896px) sidewrapper med Scorekort/ +Presentasjon-fanene. Ved en lokal scratch-visning på 1440px viste det +seg at 6 steg-piller IKKE fikk plass side ved side i 896px -- akkurat +den "stor skjerm først"-egenskapen wizarden skulle innfri (jf. samme +mønster som leaderboardets `max-w-[110rem]`-unntak fra tidligere denne +dagen). Fikset med en egen `max-w-6xl`-unntaksbredde for `tab === +"setup"` i den delte ternæren i `individual-tournament-detail.tsx` +sin `
`. + +**Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55. Lokal scratch- +visning (`next dev` på port 3100, midlertidig `/setupwizardpreviewtmp`- +side, ALDRI committet, fjernet etter bruk): alle 6 steg klikkbare og +viser riktig innhold, Grupper-steget bekreftet å sende riktig runde-id +videre til panelet (tre varianter testet: dato+bane, kun bane, ingen av +delene), lys+mørk tema, smal skjerm (390px -- steg-raden scroller +sidelengs i stedet for å wrappe, som spesifisert, i stedet for feilende +layout). + +**Rullet ut 2026-08-18** -- bruker bekreftet. Ingen migrasjon (ren +frontend). `docker compose build teecup_frontend && up -d +teecup_frontend`, rene logger, 200 OK. + --- ## Utviklingsplan (rekkefølge) diff --git a/CHANGELOG.md b/CHANGELOG.md index efd5b7c..50c9914 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12878,3 +12878,39 @@ Neste steg: **Rullet ut 2026-08-18** -- bruker bekreftet. Migrasjon 087 kjørt mot ekte `teecup_db`, deretter `docker compose build teecup_api teecup_frontend && up -d` for begge, rene logger, 200 OK. + +123. **Turneringsoppsett som steg-wizard -- 2026-08-18 (ADR-092).** + Bruker viste GameBook-skjermdumper: "Jeg synes denne delen av + TeeCup er veldig lite brukervennlig ... Full wizard. Bring gjerne + V0 inn", med presisering underveis: "husk at vi ikke skal ende opp + med et plagiat av Golf GameBook." Oppsett-fanen (`SetupTab`) var én + lang stablet side -- omgjort til 6 klikkbare steg (Turnerings- + oppsett/Runder og baner/Klasser/Spillere/Grupper og startliste/ + Resultat og opsjoner), fri navigasjon uten låsing eller fullført- + haker (GameBooks egne skjermdumper viste haker på alle steg selv + med tomt navnefelt -- ikke ekte validering). Kun to nye V0- + komponenter, GameBook brukt UTELUKKENDE som strukturelt forbilde + (steg-mønsteret), ikke visuelt -- begge bruker TeeCups egne + clubhouse-tokens fra `DESIGN_SYSTEM.md`: `tournament-setup-nav.tsx` + (steg-raden) og `round-groups-step-list.tsx` (ny topplassering for + "Utslagsgrupper", tidligere gjemt bak en knapp inni hver enkelt + runde -- den gamle knappen fjernet, ikke duplisert). Alle 6 stegs + faktiske innhold er allerede-eksisterende kort (TournamentSettings + Card/RoundsCard/ClassesCard/ParticipantsCard/CutCard/TiebreakCard), + kun flyttet til riktig steg -- ingen ny logikk, ingen backend- + endring, ingen migrasjon. Fant og fikset en bredde-regresjon + underveis: Oppsett delte en `max-w-4xl`-wrapper med Scorekort/ + Presentasjon som gjorde at 6 steg-piller ikke fikk plass ved siden + av hverandre selv på en bred skjerm -- egen `max-w-6xl`-unntaksbredde + lagt til (samme mønster som leaderboardets `max-w-[110rem]`-unntak). + + **Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55. Lokal + scratch-visning (`next dev`, midlertidig previewside, ALDRI + committet, fjernet etter bruk): alle 6 steg klikkbare med riktig + innhold, Grupper-steget sender riktig runde-id videre (tre + varianter: dato+bane/kun bane/ingen av delene), lys+mørk, smal + skjerm (steg-raden scroller sidelengs i stedet for å wrappe/feile). + + **Rullet ut 2026-08-18** -- bruker bekreftet. Ingen migrasjon (ren + frontend). `docker compose build teecup_frontend && up -d + teecup_frontend`, rene logger, 200 OK. diff --git a/frontend/components/individual-tournament-detail.tsx b/frontend/components/individual-tournament-detail.tsx index 1ccccc9..72ad10a 100644 --- a/frontend/components/individual-tournament-detail.tsx +++ b/frontend/components/individual-tournament-detail.tsx @@ -29,9 +29,11 @@ import { Crosshair, Flag, KeyRound, + Layers, MapPin, Medal, Plus, + Settings2, Trash2, Trophy, Upload, @@ -44,7 +46,9 @@ import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { PlayerImportPanel } from "@/components/player-import-panel" import { RoundGroupsPanel } from "@/components/round-groups-panel" +import { RoundGroupsStepList, type RoundSummary } from "@/components/round-groups-step-list" import { TournamentSettingsCard, type TournamentSettingsValues } from "@/components/tournament-settings-card" +import { TournamentSetupNav, type SetupStep } from "@/components/tournament-setup-nav" import { DropdownMenu, DropdownMenuContent, @@ -991,11 +995,17 @@ export function IndividualTournamentDetail({ egen kommentar om "storskjerm uten skrolling"), men satt inni denne delte 4xl-wrapperen (896px, samme wrapper som skjema-tunge Oppsett/ Presentasjon-fanene bruker) fikk den aldri sjansen til det -- felles - med resten av siden siden komponenten ble bygget/testet isolert. */} + med resten av siden siden komponenten ble bygget/testet isolert. + + Oppsett-fanen fikk samme problem 2026-08-18 (ADR-092): 6 steg i + TournamentSetupNav får ikke plass side ved side i 896px, selv på en + bred skjerm -- akkurat den "stor skjerm først"-regelen wizarden + skulle innfri. Egen, litt smalere unntaksbredde enn leaderboardets + 110rem (dette er skjemainnhold, ikke en datatabell). */}
{error && ( @@ -1104,6 +1114,21 @@ function fromDatetimeLocalValue(value: string | null): string | null { return Number.isNaN(date.getTime()) ? null : date.toISOString() } +// Wizard-steg (ADR-092) -- GameBook-skjermdumper var strukturelt forbilde +// (steg i stedet for én lang stablet side), IKKE visuelt forbilde. +// Fri navigasjon -- ingen låsing/fullført-haker, se ADR-092: GameBooks +// egne skjermdumper viser haker på alle steg selv med tomt navnefelt, +// altså ikke ekte validering, og TeeCup er allerede fleksibelt på +// oppsett-rekkefølge. +const SETUP_STEPS: SetupStep[] = [ + { key: "basics", label: "Turneringsoppsett", icon: Settings2 }, + { key: "rounds", label: "Runder og baner", icon: Flag }, + { key: "classes", label: "Klasser", icon: Layers }, + { key: "players", label: "Spillere", icon: Users }, + { key: "groups", label: "Grupper og startliste", icon: Clock }, + { key: "results", label: "Resultat og opsjoner", icon: Trophy }, +] + function SetupTab({ base, organizationId, @@ -1171,6 +1196,8 @@ function SetupTab({ ) => Promise onError: (message: string) => void }) { + const [step, setStep] = useState("basics") + const [groupsOpenFor, setGroupsOpenFor] = useState(null) const [pool, setPool] = useState([]) async function refreshPool() { @@ -1202,116 +1229,151 @@ function SetupTab({ registrationRequiresApproval: tournament.registration_requires_approval, } + const roundSummaries: RoundSummary[] = rounds.map((r) => ({ + id: r.id, + name: r.name || `Runde ${r.sequence}`, + sequence: r.sequence, + courseName: r.course_name || null, + dateLabel: r.scheduled_at + ? new Date(r.scheduled_at).toLocaleDateString("nb-NO", { day: "numeric", month: "short", year: "numeric" }) + : null, + })) + const groupsOpenRound = rounds.find((r) => r.id === groupsOpenFor) ?? null + return ( -
- + + {step === "basics" && } -
-

Scoringsmetode

-

- Avgjør hvordan totalen på leaderboardet regnes ut, og om deltakere trenger - registrert handicap og kjønn. -

- - - {tournament.scoring_method ? SCORING_METHOD_LABELS[tournament.scoring_method] : "Ikke satt"} - - - {Object.entries(SCORING_METHOD_LABELS).map(([value, label]) => ( - onUpdateScoringMethod(value)} - className="cursor-pointer rounded-xl px-3 py-2.5 text-sm font-semibold" - > - {label} - - ))} - - - {tournament.scoring_method === "bingo_bango_bongo" && ( -
+ )} +
+ )} - + {step === "results" && ( +
+
+

Scoringsmetode

+

+ Avgjør hvordan totalen på leaderboardet regnes ut, og om deltakere trenger + registrert handicap og kjønn. +

+ + + {tournament.scoring_method ? SCORING_METHOD_LABELS[tournament.scoring_method] : "Ikke satt"} + + + {Object.entries(SCORING_METHOD_LABELS).map(([value, label]) => ( + onUpdateScoringMethod(value)} + className="cursor-pointer rounded-xl px-3 py-2.5 text-sm font-semibold" + > + {label} + + ))} + + + {tournament.scoring_method === "bingo_bango_bongo" && ( + + )} + {tournament.scoring_method === "flag" && ( + + )} +
- {tournament.scoring_method != null && - ["stroke_gross", "stroke_net", "stableford"].includes(tournament.scoring_method) && ( - <> - - - - )} - - - - { - await Promise.all([refreshPool(), onImportComplete()]) - }} - onRemoveParticipant={onRemoveParticipant} - onSetClass={onSetParticipantClass} - onSetStatLevel={onSetParticipantStatLevel} - onSetHandicap={onSetParticipantHandicap} - onSetStatus={onSetParticipantStatus} - /> -
+ {tournament.scoring_method != null && + ["stroke_gross", "stroke_net", "stableford"].includes(tournament.scoring_method) && ( + <> + + + + )} + + )} + ) } @@ -2603,7 +2665,6 @@ function RoundCard({ const [roundParticipants, setRoundParticipants] = useState(null) const [tees, setTees] = useState([]) const [confirmingDelete, setConfirmingDelete] = useState(false) - const [groupsOpen, setGroupsOpen] = useState(false) async function loadRoundParticipants() { const [rpData, teeData] = await Promise.all([ @@ -2758,30 +2819,10 @@ function RoundCard({ deltakere før det er på plass.

)} - {roundParticipants.length > 0 && ( - - )} )} )} - - {groupsOpen && ( - setGroupsOpen(false)} - /> - )} ) } diff --git a/frontend/components/round-groups-step-list.tsx b/frontend/components/round-groups-step-list.tsx new file mode 100644 index 0000000..89f3bcb --- /dev/null +++ b/frontend/components/round-groups-step-list.tsx @@ -0,0 +1,66 @@ +"use client" + +import { Clock } from "lucide-react" + +export type RoundSummary = { + id: string + name: string + sequence: number + courseName: string | null + dateLabel: string | null // already formatted, e.g. "20. sep 2026", or null if unscheduled +} + +export type RoundGroupsStepListProps = { + rounds: RoundSummary[] + onManageGroups: (roundId: string) => void +} + +export function RoundGroupsStepList({ rounds, onManageGroups }: RoundGroupsStepListProps) { + if (rounds.length === 0) { + return ( +
+

+ Ingen runder er lagt til ennå — legg til runder under «Runder og baner» først. +

+
+ ) + } + + return ( + // Connected list: one container, rows divided by hairlines — not separate + // shadowed cards with gaps between them. +
+
    + {rounds.map((round) => { + // Build the secondary "course · date" line from whatever is set. + const meta = [round.courseName, round.dateLabel].filter(Boolean).join(" · ") + return ( +
  • +
    + + Runde {round.sequence} + + {round.name} + + {meta === "" ? "Ingen bane eller dato satt ennå" : meta} + +
    + + +
  • + ) + })} +
+
+ ) +} diff --git a/frontend/components/tournament-setup-nav.tsx b/frontend/components/tournament-setup-nav.tsx new file mode 100644 index 0000000..bbcc8c3 --- /dev/null +++ b/frontend/components/tournament-setup-nav.tsx @@ -0,0 +1,71 @@ +"use client" + +import type { LucideIcon } from "lucide-react" +import type { ReactNode } from "react" +import { cn } from "@/lib/utils" + +export type SetupStep = { + key: string + label: string + icon: LucideIcon +} + +export type TournamentSetupNavProps = { + steps: SetupStep[] + activeStep: string + onChange: (key: string) => void + children?: ReactNode +} + +export function TournamentSetupNav({ + steps, + activeStep, + onChange, + children, +}: TournamentSetupNavProps) { + return ( +
+ + +
{children}
+
+ ) +}