Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
"use client"
|
|
|
|
|
|
|
|
|
|
import type React from "react"
|
|
|
|
|
import { useEffect, useMemo, useState } from "react"
|
|
|
|
|
import Link from "next/link"
|
|
|
|
|
import {
|
|
|
|
|
ArrowLeft,
|
Update Todos
Undersøk kode-kontekst for alle tre byggeoppgavene
Avklar åpne designspørsmål med bruker
Bygg: flytte spiller mellom lag (backend + frontend)
Bygg: individuell rangering i turnering (backend + frontend)
Bygg: midlertidige spillere + etter-runde-e-post (backend + frontend)
Typesjekket produksjonsbuild av frontend
Scratch-verifiser alt tre + full regresjon
Browser-verifiser alt tre (Chrome DevTools)
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Rull ut mot ekte systemer (med bekreftelse)
Alle tre er live. Oppsummert:
Flytte spiller mellom lag — ny "Flytt til {annet lag}"-handling i lag-oversikten, atomisk, blokkert hvis spilleren allerede er lagt til i en match.
Individuell rangering per økt — ny brutto/netto/poeng-rangering for singles/fourball-slagspill-økter, ved siden av det eksisterende lag-poeng-leaderboardet, lenket fra blind draw-skjermen.
Midlertidige spillere: etter-runde-e-post — ny knapp som sender scorekort + ekte innloggingslenke til spillere uten konto, med dobbel-utsendingssperre.
Alt verifisert grundig: 54/54 nye scratch-sjekker + full regresjon (5+28+19 eksisterende sjekker), typesjekket produksjonsbuild, og en fullstendig gjennomgang i ekte nettleser (Chrome DevTools) av alle tre — ingen konsollfeil. Migrasjon 034 kjørt mot ekte database, begge containere redeployet, teeoff.no upåvirket. Samtidig rettet jeg opp noen steder der FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md hadde blitt hengende etter den faktiske, allerede leverte tilstanden (ADR-039-utrulling, PWA-offline-browsertesting).
2026-07-28 20:56:08 +02:00
|
|
|
ArrowLeftRight,
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
Check,
|
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
|
|
|
Copy,
|
2026-07-19 21:25:31 +02:00
|
|
|
Flag,
|
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
|
|
|
KeyRound,
|
2026-07-19 22:26:45 +02:00
|
|
|
MessageCircle,
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
MoreVertical,
|
2026-07-20 10:01:44 +02:00
|
|
|
Pencil,
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
Plus,
|
|
|
|
|
Star,
|
|
|
|
|
Trash2,
|
|
|
|
|
UserPlus,
|
|
|
|
|
Users,
|
|
|
|
|
X,
|
|
|
|
|
} from "lucide-react"
|
|
|
|
|
import { Button } from "@/components/ui/button"
|
|
|
|
|
import { Input } from "@/components/ui/input"
|
|
|
|
|
import { Label } from "@/components/ui/label"
|
|
|
|
|
import {
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
DropdownMenuItem,
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
} from "@/components/ui/dropdown-menu"
|
2026-07-19 21:51:05 +02:00
|
|
|
import { TournamentStatusPicker, type TournamentStatus } from "@/components/tournament-status-badge"
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
|
|
|
|
|
|
// --- Types (matcher API-kontrakten i app/routers/tournaments.py/players.py) -
|
|
|
|
|
|
|
|
|
|
type ApiPlayer = {
|
|
|
|
|
id: string
|
|
|
|
|
display_name: string
|
|
|
|
|
handicap_index: number | null
|
2026-07-20 10:01:44 +02:00
|
|
|
gender: "m" | "f" | "x" | null
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ApiRosterEntry = {
|
|
|
|
|
id: string
|
|
|
|
|
player_id: string
|
|
|
|
|
display_name: string
|
|
|
|
|
handicap_index_snapshot: number | null
|
|
|
|
|
is_captain: boolean
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
class_id: string | null
|
|
|
|
|
class_name: string | null
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Team = {
|
|
|
|
|
id: string
|
|
|
|
|
name: string
|
|
|
|
|
color: string
|
|
|
|
|
roster: ApiRosterEntry[]
|
|
|
|
|
}
|
|
|
|
|
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
// Konkurranseklasser (2026-08-03) -- fritt navngitte, med et valgfritt
|
|
|
|
|
// standardutslag brukt til å forhåndsutfylle riktig utslag når en spiller
|
|
|
|
|
// legges til en match (session-blind-draw.tsx). Ren utslag-bekvemmelighet
|
|
|
|
|
// her -- INGEN egen resultatliste for lagturneringer (bekreftet med
|
|
|
|
|
// bruker: poeng er knyttet til hele kamper, ikke enkeltspillere).
|
|
|
|
|
type ApiTournamentClass = {
|
|
|
|
|
id: string
|
|
|
|
|
name: string
|
|
|
|
|
default_tee_id: string | null
|
|
|
|
|
default_tee_name: string | null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ApiCourse = {
|
|
|
|
|
id: string
|
|
|
|
|
name: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type ApiTee = {
|
|
|
|
|
id: string
|
|
|
|
|
name: string
|
|
|
|
|
}
|
|
|
|
|
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
// Team colors er DATA brukt på scorekort senere -- bevisst atskilt fra
|
|
|
|
|
// app-ens merkevare-grønn/oransje.
|
|
|
|
|
const TEAM_COLORS: { value: string; label: string }[] = [
|
|
|
|
|
{ value: "#2563eb", label: "Blå" },
|
|
|
|
|
{ value: "#dc2626", label: "Rød" },
|
|
|
|
|
{ value: "#7c3aed", label: "Lilla" },
|
|
|
|
|
{ value: "#0891b2", label: "Turkis" },
|
|
|
|
|
{ value: "#db2777", label: "Rosa" },
|
|
|
|
|
{ value: "#475569", label: "Skifer" },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// --- Component -------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
export function TournamentDetail({
|
|
|
|
|
organizationId,
|
|
|
|
|
tournamentId,
|
|
|
|
|
tournamentName,
|
|
|
|
|
}: {
|
|
|
|
|
organizationId: string
|
|
|
|
|
tournamentId: string
|
|
|
|
|
tournamentName: string
|
|
|
|
|
}) {
|
|
|
|
|
const [teams, setTeams] = useState<[Team | null, Team | null]>([null, null])
|
|
|
|
|
const [pool, setPool] = useState<ApiPlayer[]>([])
|
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
|
|
|
const [joinCode, setJoinCode] = useState<string | null>(null)
|
2026-07-19 21:51:05 +02:00
|
|
|
const [status, setStatus] = useState<TournamentStatus | null>(null)
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
const [classes, setClasses] = useState<ApiTournamentClass[]>([])
|
|
|
|
|
const [courses, setCourses] = useState<ApiCourse[]>([])
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let cancelled = false
|
|
|
|
|
async function load() {
|
|
|
|
|
try {
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
const [teamsRes, poolRes, tournamentsRes, classesRes, coursesRes] = await Promise.all([
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
fetch(`/orgs/${organizationId}/tournaments/${tournamentId}/teams`, {
|
|
|
|
|
credentials: "include",
|
|
|
|
|
}),
|
|
|
|
|
fetch(`/orgs/${organizationId}/players`, { credentials: "include" }),
|
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
|
|
|
fetch(`/orgs/${organizationId}/tournaments`, { credentials: "include" }),
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
fetch(`/orgs/${organizationId}/tournaments/${tournamentId}/classes`, {
|
|
|
|
|
credentials: "include",
|
|
|
|
|
}),
|
|
|
|
|
fetch(`/orgs/${organizationId}/courses`, { credentials: "include" }),
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
])
|
|
|
|
|
if (!teamsRes.ok || !poolRes.ok) throw new Error("load failed")
|
|
|
|
|
const teamsData: { id: string; name: string; color: string | null }[] =
|
|
|
|
|
await teamsRes.json()
|
|
|
|
|
const poolData: ApiPlayer[] = await poolRes.json()
|
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
|
|
|
// Ingen enkelt-turnering-GET finnes ennå -- listen bærer allerede
|
|
|
|
|
// join_code (ADR-020), så vi finner raden herfra i stedet for å
|
|
|
|
|
// legge til et nytt endepunkt kun for dette.
|
|
|
|
|
if (tournamentsRes.ok) {
|
2026-07-19 21:51:05 +02:00
|
|
|
const tournamentsData: { id: string; join_code: string; status: TournamentStatus }[] =
|
|
|
|
|
await tournamentsRes.json()
|
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
|
|
|
const mine = tournamentsData.find((t) => t.id === tournamentId)
|
2026-07-19 21:51:05 +02:00
|
|
|
if (mine) {
|
|
|
|
|
setJoinCode(mine.join_code)
|
|
|
|
|
setStatus(mine.status)
|
|
|
|
|
}
|
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
|
|
|
}
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
if (classesRes.ok) setClasses(await classesRes.json())
|
|
|
|
|
if (coursesRes.ok) setCourses(await coursesRes.json())
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
|
|
|
|
|
const withRosters = await Promise.all(
|
|
|
|
|
teamsData.map(async (t) => {
|
|
|
|
|
const rosterRes = await fetch(`/orgs/${organizationId}/teams/${t.id}/roster`, {
|
|
|
|
|
credentials: "include",
|
|
|
|
|
})
|
|
|
|
|
const roster: ApiRosterEntry[] = rosterRes.ok ? await rosterRes.json() : []
|
|
|
|
|
return { id: t.id, name: t.name, color: t.color ?? TEAM_COLORS[0].value, roster }
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
if (cancelled) return
|
|
|
|
|
setTeams([withRosters[0] ?? null, withRosters[1] ?? null])
|
|
|
|
|
setPool(poolData)
|
|
|
|
|
} catch {
|
|
|
|
|
if (!cancelled) setError("Klarte ikke å laste lag og spillere. Prøv å laste siden på nytt.")
|
|
|
|
|
} finally {
|
|
|
|
|
if (!cancelled) setLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
void load()
|
|
|
|
|
return () => {
|
|
|
|
|
cancelled = true
|
|
|
|
|
}
|
|
|
|
|
}, [organizationId, tournamentId])
|
|
|
|
|
|
|
|
|
|
async function createTeam(slot: 0 | 1, name: string, color: string) {
|
|
|
|
|
setError(null)
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/orgs/${organizationId}/tournaments/${tournamentId}/teams`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
credentials: "include",
|
|
|
|
|
body: JSON.stringify({ name, color }),
|
|
|
|
|
})
|
|
|
|
|
if (!res.ok) throw new Error(`create team: ${res.status}`)
|
|
|
|
|
const created: { id: string; name: string; color: string | null } = await res.json()
|
|
|
|
|
setTeams((prev) => {
|
|
|
|
|
const next: [Team | null, Team | null] = [prev[0], prev[1]]
|
|
|
|
|
next[slot] = { id: created.id, name: created.name, color: created.color ?? color, roster: [] }
|
|
|
|
|
return next
|
|
|
|
|
})
|
|
|
|
|
} catch {
|
|
|
|
|
setError("Klarte ikke å opprette laget. Prøv igjen.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function addExistingPlayer(teamId: string, playerId: string) {
|
|
|
|
|
setError(null)
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/orgs/${organizationId}/teams/${teamId}/roster`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
credentials: "include",
|
|
|
|
|
body: JSON.stringify({ player_id: playerId, is_captain: false }),
|
|
|
|
|
})
|
|
|
|
|
if (!res.ok) throw new Error(`add roster: ${res.status}`)
|
|
|
|
|
const entry: ApiRosterEntry = await res.json()
|
|
|
|
|
setTeams((prev) =>
|
|
|
|
|
prev.map((t) => (t && t.id === teamId ? { ...t, roster: [...t.roster, entry] } : t)) as [
|
|
|
|
|
Team | null,
|
|
|
|
|
Team | null,
|
|
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
} catch {
|
|
|
|
|
setError("Klarte ikke å legge til spilleren. Prøv igjen.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function addNewPlayer(teamId: string, name: string, handicap?: number) {
|
|
|
|
|
setError(null)
|
|
|
|
|
try {
|
|
|
|
|
const playerRes = await fetch(`/orgs/${organizationId}/players`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
credentials: "include",
|
|
|
|
|
body: JSON.stringify({ display_name: name, handicap_index: handicap ?? null }),
|
|
|
|
|
})
|
|
|
|
|
if (!playerRes.ok) throw new Error(`create player: ${playerRes.status}`)
|
|
|
|
|
const player: ApiPlayer = await playerRes.json()
|
|
|
|
|
setPool((prev) => [...prev, player])
|
|
|
|
|
await addExistingPlayer(teamId, player.id)
|
|
|
|
|
} catch {
|
|
|
|
|
setError("Klarte ikke å opprette spilleren. Prøv igjen.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 10:01:44 +02:00
|
|
|
// Redigerer spillerpoolen (`player`), IKKE et lags frosne
|
|
|
|
|
// handicap_index_snapshot (ADR-007) -- se PlayerUpdate sin docstring i
|
|
|
|
|
// app/routers/players.py. Slår derfor ikke automatisk inn på tallet som
|
|
|
|
|
// allerede vises på et lag denne spilleren er rostret på.
|
|
|
|
|
async function updatePlayer(
|
|
|
|
|
playerId: string,
|
|
|
|
|
updates: { display_name?: string; handicap_index?: number | null; gender?: string | null },
|
|
|
|
|
) {
|
|
|
|
|
setError(null)
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/orgs/${organizationId}/players/${playerId}`, {
|
|
|
|
|
method: "PATCH",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
credentials: "include",
|
|
|
|
|
body: JSON.stringify(updates),
|
|
|
|
|
})
|
|
|
|
|
if (!res.ok) throw new Error(`update player: ${res.status}`)
|
|
|
|
|
const updated: ApiPlayer = await res.json()
|
|
|
|
|
setPool((prev) => prev.map((p) => (p.id === updated.id ? updated : p)))
|
|
|
|
|
} catch {
|
|
|
|
|
setError("Klarte ikke å oppdatere spilleren. Prøv igjen.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
async function toggleCaptain(teamId: string, rosterId: string, current: boolean) {
|
|
|
|
|
setError(null)
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/orgs/${organizationId}/teams/${teamId}/roster/${rosterId}`, {
|
|
|
|
|
method: "PATCH",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
credentials: "include",
|
|
|
|
|
body: JSON.stringify({ is_captain: !current }),
|
|
|
|
|
})
|
|
|
|
|
if (!res.ok) throw new Error(`toggle captain: ${res.status}`)
|
|
|
|
|
const updated: ApiRosterEntry = await res.json()
|
|
|
|
|
setTeams((prev) =>
|
|
|
|
|
prev.map((t) =>
|
|
|
|
|
t && t.id === teamId
|
|
|
|
|
? { ...t, roster: t.roster.map((r) => (r.id === updated.id ? updated : r)) }
|
|
|
|
|
: t,
|
|
|
|
|
) as [Team | null, Team | null],
|
|
|
|
|
)
|
|
|
|
|
} catch {
|
|
|
|
|
setError("Klarte ikke å endre kaptein. Prøv igjen.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
async function setRosterClass(teamId: string, rosterId: string, classId: string | null) {
|
|
|
|
|
setError(null)
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/orgs/${organizationId}/teams/${teamId}/roster/${rosterId}`, {
|
|
|
|
|
method: "PATCH",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
credentials: "include",
|
|
|
|
|
body: JSON.stringify({ class_id: classId }),
|
|
|
|
|
})
|
|
|
|
|
if (!res.ok) throw new Error(`set class: ${res.status}`)
|
|
|
|
|
const updated: ApiRosterEntry = await res.json()
|
|
|
|
|
setTeams((prev) =>
|
|
|
|
|
prev.map((t) =>
|
|
|
|
|
t && t.id === teamId
|
|
|
|
|
? { ...t, roster: t.roster.map((r) => (r.id === updated.id ? updated : r)) }
|
|
|
|
|
: t,
|
|
|
|
|
) as [Team | null, Team | null],
|
|
|
|
|
)
|
|
|
|
|
} catch {
|
|
|
|
|
setError("Klarte ikke å endre klasse. Prøv igjen.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createClass(name: string, defaultTeeId: string | null) {
|
|
|
|
|
setError(null)
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/orgs/${organizationId}/tournaments/${tournamentId}/classes`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
credentials: "include",
|
|
|
|
|
body: JSON.stringify({ name, default_tee_id: defaultTeeId }),
|
|
|
|
|
})
|
|
|
|
|
if (!res.ok) throw new Error(`create class: ${res.status}`)
|
|
|
|
|
const created: ApiTournamentClass = await res.json()
|
|
|
|
|
setClasses((prev) => [...prev, created])
|
|
|
|
|
} catch {
|
|
|
|
|
setError("Klarte ikke å opprette klassen. Prøv igjen.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deleteClass(classId: string) {
|
|
|
|
|
setError(null)
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(
|
|
|
|
|
`/orgs/${organizationId}/tournaments/${tournamentId}/classes/${classId}`,
|
|
|
|
|
{ method: "DELETE", credentials: "include" },
|
|
|
|
|
)
|
|
|
|
|
if (res.status !== 204) throw new Error(`delete class: ${res.status}`)
|
|
|
|
|
setClasses((prev) => prev.filter((c) => c.id !== classId))
|
|
|
|
|
// Roster-rader som pekte på klassen mister den lokalt også (backend
|
|
|
|
|
// satte allerede class_id=NULL via ON DELETE SET NULL) -- unngår en
|
|
|
|
|
// hengende, ugyldig klasse-referanse i UI-et inntil neste full reload.
|
|
|
|
|
setTeams(
|
|
|
|
|
(prev) =>
|
|
|
|
|
prev.map((t) =>
|
|
|
|
|
t
|
|
|
|
|
? {
|
|
|
|
|
...t,
|
|
|
|
|
roster: t.roster.map((r) =>
|
|
|
|
|
r.class_id === classId ? { ...r, class_id: null, class_name: null } : r,
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
: t,
|
|
|
|
|
) as [Team | null, Team | null],
|
|
|
|
|
)
|
|
|
|
|
} catch {
|
|
|
|
|
setError("Klarte ikke å fjerne klassen. Prøv igjen.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
async function removePlayer(teamId: string, rosterId: string) {
|
|
|
|
|
setError(null)
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/orgs/${organizationId}/teams/${teamId}/roster/${rosterId}`, {
|
|
|
|
|
method: "DELETE",
|
|
|
|
|
credentials: "include",
|
|
|
|
|
})
|
|
|
|
|
if (!res.ok && res.status !== 204) throw new Error(`remove roster: ${res.status}`)
|
|
|
|
|
setTeams((prev) =>
|
|
|
|
|
prev.map((t) =>
|
|
|
|
|
t && t.id === teamId ? { ...t, roster: t.roster.filter((r) => r.id !== rosterId) } : t,
|
|
|
|
|
) as [Team | null, Team | null],
|
|
|
|
|
)
|
|
|
|
|
} catch {
|
|
|
|
|
setError("Klarte ikke å fjerne spilleren. Prøv igjen.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Update Todos
Undersøk kode-kontekst for alle tre byggeoppgavene
Avklar åpne designspørsmål med bruker
Bygg: flytte spiller mellom lag (backend + frontend)
Bygg: individuell rangering i turnering (backend + frontend)
Bygg: midlertidige spillere + etter-runde-e-post (backend + frontend)
Typesjekket produksjonsbuild av frontend
Scratch-verifiser alt tre + full regresjon
Browser-verifiser alt tre (Chrome DevTools)
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Rull ut mot ekte systemer (med bekreftelse)
Alle tre er live. Oppsummert:
Flytte spiller mellom lag — ny "Flytt til {annet lag}"-handling i lag-oversikten, atomisk, blokkert hvis spilleren allerede er lagt til i en match.
Individuell rangering per økt — ny brutto/netto/poeng-rangering for singles/fourball-slagspill-økter, ved siden av det eksisterende lag-poeng-leaderboardet, lenket fra blind draw-skjermen.
Midlertidige spillere: etter-runde-e-post — ny knapp som sender scorekort + ekte innloggingslenke til spillere uten konto, med dobbel-utsendingssperre.
Alt verifisert grundig: 54/54 nye scratch-sjekker + full regresjon (5+28+19 eksisterende sjekker), typesjekket produksjonsbuild, og en fullstendig gjennomgang i ekte nettleser (Chrome DevTools) av alle tre — ingen konsollfeil. Migrasjon 034 kjørt mot ekte database, begge containere redeployet, teeoff.no upåvirket. Samtidig rettet jeg opp noen steder der FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md hadde blitt hengende etter den faktiske, allerede leverte tilstanden (ADR-039-utrulling, PWA-offline-browsertesting).
2026-07-28 20:56:08 +02:00
|
|
|
async function movePlayer(teamId: string, rosterId: string, targetTeamId: string) {
|
|
|
|
|
setError(null)
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(
|
|
|
|
|
`/orgs/${organizationId}/teams/${teamId}/roster/${rosterId}/move`,
|
|
|
|
|
{
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
credentials: "include",
|
|
|
|
|
body: JSON.stringify({ target_team_id: targetTeamId }),
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const body = await res.json().catch(() => null)
|
|
|
|
|
throw new Error(body?.detail?.message ?? `move roster: ${res.status}`)
|
|
|
|
|
}
|
|
|
|
|
const moved: ApiRosterEntry = await res.json()
|
|
|
|
|
setTeams(
|
|
|
|
|
(prev) =>
|
|
|
|
|
prev.map((t) => {
|
|
|
|
|
if (!t) return t
|
|
|
|
|
if (t.id === teamId) return { ...t, roster: t.roster.filter((r) => r.id !== rosterId) }
|
|
|
|
|
if (t.id === targetTeamId) return { ...t, roster: [...t.roster, moved] }
|
|
|
|
|
return t
|
|
|
|
|
}) as [Team | null, Team | null],
|
|
|
|
|
)
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err instanceof Error ? err.message : "Klarte ikke å flytte spilleren. Prøv igjen.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 21:51:05 +02:00
|
|
|
async function updateStatus(newStatus: TournamentStatus) {
|
|
|
|
|
setError(null)
|
|
|
|
|
const previous = status
|
|
|
|
|
setStatus(newStatus) // optimistisk -- rulles tilbake under ved feil
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/orgs/${organizationId}/tournaments/${tournamentId}`, {
|
|
|
|
|
method: "PATCH",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
credentials: "include",
|
|
|
|
|
body: JSON.stringify({ status: newStatus }),
|
|
|
|
|
})
|
|
|
|
|
if (!res.ok) throw new Error(`update status: ${res.status}`)
|
|
|
|
|
} catch {
|
|
|
|
|
setStatus(previous)
|
|
|
|
|
setError("Klarte ikke å endre turnering-status. Prøv igjen.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-19 21:25:31 +02:00
|
|
|
// Walkover/konsesjon på turnering-nivå (ADR-024): gir opp ALLE
|
|
|
|
|
// ikke-avgjorte matcher laget har i turneringen, i én operasjon. Kun
|
|
|
|
|
// kaptein for laget som gir seg (eller org-admin) får lov -- backend
|
|
|
|
|
// håndhever dette, en 403 vises bare som vanlig feiltekst.
|
|
|
|
|
async function concedeTournament(teamId: string): Promise<string | null> {
|
|
|
|
|
setError(null)
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/orgs/${organizationId}/tournaments/${tournamentId}/concede`, {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
credentials: "include",
|
|
|
|
|
body: JSON.stringify({ conceding_team_id: teamId }),
|
|
|
|
|
})
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const body = await res.json().catch(() => null)
|
|
|
|
|
const message: string = body?.detail?.message ?? "Klarte ikke å gi opp turneringen."
|
|
|
|
|
setError(message)
|
|
|
|
|
return message
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
} catch {
|
|
|
|
|
const message = "Klarte ikke å gi opp turneringen."
|
|
|
|
|
setError(message)
|
|
|
|
|
return message
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
// Hvilket lag er en gitt spiller (fra pool) allerede rostret på, om noen?
|
|
|
|
|
function findPlayerTeam(playerId: string): Team | null {
|
|
|
|
|
for (const t of teams) {
|
|
|
|
|
if (t && t.roster.some((r) => r.player_id === playerId)) return t
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex min-h-[100dvh] flex-col bg-background">
|
|
|
|
|
<header className="sticky top-0 z-10 border-b border-border bg-background/80 backdrop-blur">
|
|
|
|
|
<div className="mx-auto flex w-full max-w-4xl items-center gap-3 px-5 py-4">
|
|
|
|
|
<Link
|
|
|
|
|
href="/dashboard"
|
|
|
|
|
aria-label="Tilbake til dashbord"
|
|
|
|
|
className="flex size-10 shrink-0 items-center justify-center rounded-xl border border-border bg-card text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
|
|
|
|
|
>
|
|
|
|
|
<ArrowLeft aria-hidden="true" className="size-5" />
|
|
|
|
|
</Link>
|
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
|
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
<span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
|
|
|
Turnering
|
|
|
|
|
</span>
|
|
|
|
|
<h1 className="truncate text-xl font-extrabold tracking-tight text-foreground">
|
|
|
|
|
{tournamentName}
|
|
|
|
|
</h1>
|
|
|
|
|
</div>
|
2026-07-19 21:51:05 +02:00
|
|
|
{status && <TournamentStatusPicker status={status} onChange={updateStatus} />}
|
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
|
|
|
{joinCode && <JoinCodeChip code={joinCode} />}
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
</div>
|
Program-skjermen er bygget og grundig scratch-verifisert. Kort oppsummert:
Nytt:
app/routers/courses.py — enkel bane-CRUD (GET/POST /orgs/{id}/courses), fant og tettet et reelt hull: SessionCreate.course_id var påkrevd, men ingen vei fantes til å skaffe én
components/tournament-program.tsx + rute /tournaments/[id]/program — tidslinje over økter, opprett-skjema med bane-type-ahead og avanserte handicap-brytere
Fanerad lagt til i både roster- og program-skjermen så du kan bevege deg mellom dem
To reelle feil rettet før integrering:
V0-promptet mitt ba om ett generisk "Scramble"-format, men databasen/motoren krever scramble_2/scramble_4 som atskilte verdier — rettet til to segment-knapper
Verifiserte allowance_override-JSON-formen eksakt mot parse_allowance_config (typet combined/per_player + 0–1-brøk, ikke flat prosent) — bekreftet med en ekte rundtur i scratch, ikke bare lest fra koden
Verifisert: courses opprettet+listet, kryss-org-isolasjon, økt med klokkeslett, økt med scramble_4+full handicap-override-rundtur, gammel "scramble"-verdi korrekt avvist, test_isolation.sql 12/12, ekte typesjekket produksjonsbuild (samme Dockerfile som deployes).
2026-07-18 12:20:45 +02:00
|
|
|
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
<nav
|
|
|
|
|
className="mx-auto flex w-full max-w-4xl items-center gap-2 overflow-x-auto px-5 pb-3"
|
|
|
|
|
aria-label="Turneringsseksjoner"
|
|
|
|
|
>
|
Program-skjermen er bygget og grundig scratch-verifisert. Kort oppsummert:
Nytt:
app/routers/courses.py — enkel bane-CRUD (GET/POST /orgs/{id}/courses), fant og tettet et reelt hull: SessionCreate.course_id var påkrevd, men ingen vei fantes til å skaffe én
components/tournament-program.tsx + rute /tournaments/[id]/program — tidslinje over økter, opprett-skjema med bane-type-ahead og avanserte handicap-brytere
Fanerad lagt til i både roster- og program-skjermen så du kan bevege deg mellom dem
To reelle feil rettet før integrering:
V0-promptet mitt ba om ett generisk "Scramble"-format, men databasen/motoren krever scramble_2/scramble_4 som atskilte verdier — rettet til to segment-knapper
Verifiserte allowance_override-JSON-formen eksakt mot parse_allowance_config (typet combined/per_player + 0–1-brøk, ikke flat prosent) — bekreftet med en ekte rundtur i scratch, ikke bare lest fra koden
Verifisert: courses opprettet+listet, kryss-org-isolasjon, økt med klokkeslett, økt med scramble_4+full handicap-override-rundtur, gammel "scramble"-verdi korrekt avvist, test_isolation.sql 12/12, ekte typesjekket produksjonsbuild (samme Dockerfile som deployes).
2026-07-18 12:20:45 +02:00
|
|
|
<span
|
|
|
|
|
aria-current="page"
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
className="shrink-0 rounded-full bg-primary px-4 py-2 text-sm font-bold text-primary-foreground"
|
Program-skjermen er bygget og grundig scratch-verifisert. Kort oppsummert:
Nytt:
app/routers/courses.py — enkel bane-CRUD (GET/POST /orgs/{id}/courses), fant og tettet et reelt hull: SessionCreate.course_id var påkrevd, men ingen vei fantes til å skaffe én
components/tournament-program.tsx + rute /tournaments/[id]/program — tidslinje over økter, opprett-skjema med bane-type-ahead og avanserte handicap-brytere
Fanerad lagt til i både roster- og program-skjermen så du kan bevege deg mellom dem
To reelle feil rettet før integrering:
V0-promptet mitt ba om ett generisk "Scramble"-format, men databasen/motoren krever scramble_2/scramble_4 som atskilte verdier — rettet til to segment-knapper
Verifiserte allowance_override-JSON-formen eksakt mot parse_allowance_config (typet combined/per_player + 0–1-brøk, ikke flat prosent) — bekreftet med en ekte rundtur i scratch, ikke bare lest fra koden
Verifisert: courses opprettet+listet, kryss-org-isolasjon, økt med klokkeslett, økt med scramble_4+full handicap-override-rundtur, gammel "scramble"-verdi korrekt avvist, test_isolation.sql 12/12, ekte typesjekket produksjonsbuild (samme Dockerfile som deployes).
2026-07-18 12:20:45 +02:00
|
|
|
>
|
|
|
|
|
Lag og spillere
|
|
|
|
|
</span>
|
|
|
|
|
<Link
|
|
|
|
|
href={`/tournaments/${tournamentId}/program?org=${organizationId}&name=${encodeURIComponent(tournamentName)}`}
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
className="shrink-0 rounded-full px-4 py-2 text-sm font-semibold text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
Program-skjermen er bygget og grundig scratch-verifisert. Kort oppsummert:
Nytt:
app/routers/courses.py — enkel bane-CRUD (GET/POST /orgs/{id}/courses), fant og tettet et reelt hull: SessionCreate.course_id var påkrevd, men ingen vei fantes til å skaffe én
components/tournament-program.tsx + rute /tournaments/[id]/program — tidslinje over økter, opprett-skjema med bane-type-ahead og avanserte handicap-brytere
Fanerad lagt til i både roster- og program-skjermen så du kan bevege deg mellom dem
To reelle feil rettet før integrering:
V0-promptet mitt ba om ett generisk "Scramble"-format, men databasen/motoren krever scramble_2/scramble_4 som atskilte verdier — rettet til to segment-knapper
Verifiserte allowance_override-JSON-formen eksakt mot parse_allowance_config (typet combined/per_player + 0–1-brøk, ikke flat prosent) — bekreftet med en ekte rundtur i scratch, ikke bare lest fra koden
Verifisert: courses opprettet+listet, kryss-org-isolasjon, økt med klokkeslett, økt med scramble_4+full handicap-override-rundtur, gammel "scramble"-verdi korrekt avvist, test_isolation.sql 12/12, ekte typesjekket produksjonsbuild (samme Dockerfile som deployes).
2026-07-18 12:20:45 +02:00
|
|
|
>
|
|
|
|
|
Program
|
|
|
|
|
</Link>
|
2026-07-18 18:22:10 +02:00
|
|
|
<Link
|
|
|
|
|
href={`/tournaments/${tournamentId}/leaderboard?org=${organizationId}&name=${encodeURIComponent(tournamentName)}`}
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
className="shrink-0 rounded-full px-4 py-2 text-sm font-semibold text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
2026-07-18 18:22:10 +02:00
|
|
|
>
|
|
|
|
|
Leaderboard
|
|
|
|
|
</Link>
|
Legg til turnering-presentasjon: hero-bilde, sponsorer, synlighet, påmelding
Backend for dette (hero_image_key, sponsor-CRUD, visibility/description/
registrering) har vært klart og live siden ADR-018 (2026-07-18), men ingen
organisator-skjerm satte noensinne disse feltene - alt var 100% API-only.
Ny tournament-presentation.tsx dekker begge turneringstyper (delt
tournament-tabell, ikke format_type-spesifikt): full side for
lagturneringer (ny rute), embeddet som fjerde fane for individuelle
turneringer. Lagt til i alle fire eksisterende nav-rader.
To små backend-tillegg uten migrasjon: hero_image_url/logo_url som
beregnede felt (samme mønster som avatar_url), og en DELETE-endepunkt for
hero-bilde (fantes fra før kun opplasting).
Fant og rettet en ekte mobil-layoutbug under scratch-verifisering:
sponsor-raden klemte navnet til nesten ingenting på 390px viewport -
samme klasse trunkeringsdefekt som leaderboard-omskrivingen tidligere
denne uken, men på et annet sted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 19:03:53 +02:00
|
|
|
<Link
|
|
|
|
|
href={`/tournaments/${tournamentId}/presentation?org=${organizationId}&name=${encodeURIComponent(tournamentName)}`}
|
|
|
|
|
className="shrink-0 rounded-full px-4 py-2 text-sm font-semibold text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
|
|
|
|
>
|
|
|
|
|
Presentasjon
|
|
|
|
|
</Link>
|
Program-skjermen er bygget og grundig scratch-verifisert. Kort oppsummert:
Nytt:
app/routers/courses.py — enkel bane-CRUD (GET/POST /orgs/{id}/courses), fant og tettet et reelt hull: SessionCreate.course_id var påkrevd, men ingen vei fantes til å skaffe én
components/tournament-program.tsx + rute /tournaments/[id]/program — tidslinje over økter, opprett-skjema med bane-type-ahead og avanserte handicap-brytere
Fanerad lagt til i både roster- og program-skjermen så du kan bevege deg mellom dem
To reelle feil rettet før integrering:
V0-promptet mitt ba om ett generisk "Scramble"-format, men databasen/motoren krever scramble_2/scramble_4 som atskilte verdier — rettet til to segment-knapper
Verifiserte allowance_override-JSON-formen eksakt mot parse_allowance_config (typet combined/per_player + 0–1-brøk, ikke flat prosent) — bekreftet med en ekte rundtur i scratch, ikke bare lest fra koden
Verifisert: courses opprettet+listet, kryss-org-isolasjon, økt med klokkeslett, økt med scramble_4+full handicap-override-rundtur, gammel "scramble"-verdi korrekt avvist, test_isolation.sql 12/12, ekte typesjekket produksjonsbuild (samme Dockerfile som deployes).
2026-07-18 12:20:45 +02:00
|
|
|
</nav>
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<main className="mx-auto w-full max-w-4xl flex-1 px-5 py-6 sm:py-8">
|
|
|
|
|
<div className="mb-5 flex flex-col gap-1">
|
|
|
|
|
<h2 className="text-base font-bold text-foreground">Lag og spillere</h2>
|
|
|
|
|
<p className="text-sm leading-relaxed text-muted-foreground text-pretty">
|
|
|
|
|
Ryder Cup-format: nøyaktig to lag. Sett opp begge lagene og fyll troppene før
|
|
|
|
|
turneringen kan starte.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{error && (
|
|
|
|
|
<p role="alert" className="mb-4 text-sm font-medium text-destructive">
|
|
|
|
|
{error}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
<div className="flex justify-center py-16">
|
|
|
|
|
<div
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
className="size-10 animate-spin rounded-full border-4 border-primary/20 border-t-primary"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
<>
|
|
|
|
|
<ClassesCard
|
|
|
|
|
classes={classes}
|
|
|
|
|
courses={courses}
|
|
|
|
|
organizationId={organizationId}
|
|
|
|
|
onCreate={createClass}
|
|
|
|
|
onDelete={deleteClass}
|
|
|
|
|
/>
|
|
|
|
|
<div className="grid gap-4 md:grid-cols-2">
|
|
|
|
|
{([0, 1] as const).map((slot) => {
|
|
|
|
|
const team = teams[slot]
|
|
|
|
|
if (team) {
|
|
|
|
|
const otherTeam = teams[slot === 0 ? 1 : 0]
|
|
|
|
|
return (
|
|
|
|
|
<TeamPanel
|
|
|
|
|
key={team.id}
|
|
|
|
|
team={team}
|
|
|
|
|
otherTeam={otherTeam}
|
|
|
|
|
pool={pool}
|
|
|
|
|
classes={classes}
|
|
|
|
|
chatHref={`/tournaments/${tournamentId}/teams/${team.id}/chat?org=${organizationId}&name=${encodeURIComponent(tournamentName)}&team=${encodeURIComponent(team.name)}`}
|
|
|
|
|
findPlayerTeam={findPlayerTeam}
|
|
|
|
|
onToggleCaptain={toggleCaptain}
|
|
|
|
|
onRemovePlayer={removePlayer}
|
|
|
|
|
onMovePlayer={movePlayer}
|
|
|
|
|
onAddExisting={addExistingPlayer}
|
|
|
|
|
onAddNew={addNewPlayer}
|
|
|
|
|
onUpdatePlayer={updatePlayer}
|
|
|
|
|
onConcedeTournament={concedeTournament}
|
|
|
|
|
onSetClass={setRosterClass}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
return <CreateTeamCard key={`slot-${slot}`} slot={slot} onCreate={createTeam} />
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
)}
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
Alt er ferdig og live. Kort oppsummering av hele runden:
Backend (ADR-020):
tournament.join_code — kort, unik kode generert automatisk, overstyrer synlighet
match.leading_side — cachet ledende side, oppdateres ved hvert hull
Leaderboardet fikk projected_points — hva stillingen blir om pågående matcher holder seg
Ny migrasjon 011, kjørt mot ekte teecup_db, test_isolation.sql 12/12
Frontend:
Login-skjermen har nå et kode-felt som tar deg rett til turneringen, uten innlogging
Invitasjonskoden vises i turnering-detalj med kopier-knapp
Leaderboardet har to fargesegmenterte barer øverst — faktisk stilling og projisert stilling
Matchlisten i blind draw er fargekodet etter hvem som leder
Underveis oppsto en reell hendelse: en feilformulert kommando eksponerte teeoff_db sitt superbrukerpassord. Det ble flagget umiddelbart, du valgte å rotere det, og det ble gjort trygt uten at verdien noensinne ble vist på nytt — bekreftet med rene logger og 200 på både teeoff.no og teecup.teeoff.no etterpå.
Alt er verifisert i scratch før utrulling, typesjekket build kjørt, og .md-filene (CLAUDE.md, FEATURE_BACKLOG.md, ARCHITECTURE_DECISIONS.md) er oppdatert. Jeg kommuniserer på norsk videre i dette prosjektet.
2026-07-19 09:33:34 +02:00
|
|
|
// --- Invitasjonskode-chip (ADR-020) -----------------------------------------
|
|
|
|
|
// Vises til organisator slik at koden kan gis muntlig/på en lapp til
|
|
|
|
|
// spillere som ellers ikke ville funnet turneringen. Overstyrer
|
|
|
|
|
// tournament.visibility når den brukes -- se login-skjermets kode-felt.
|
|
|
|
|
|
|
|
|
|
function JoinCodeChip({ code }: { code: string }) {
|
|
|
|
|
const [copied, setCopied] = useState(false)
|
|
|
|
|
|
|
|
|
|
async function handleCopy() {
|
|
|
|
|
try {
|
|
|
|
|
await navigator.clipboard.writeText(code)
|
|
|
|
|
setCopied(true)
|
|
|
|
|
setTimeout(() => setCopied(false), 2000)
|
|
|
|
|
} catch {
|
|
|
|
|
// Utilgjengelig clipboard-API (f.eks. usikker kontekst) -- koden er
|
|
|
|
|
// uansett synlig i chipen, bare uten kopier-snarveien.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={handleCopy}
|
|
|
|
|
title="Del denne koden med spillere som skal finne turneringen uten lenke"
|
|
|
|
|
className="flex shrink-0 items-center gap-1.5 rounded-xl border border-border bg-secondary/60 px-3 py-2 text-sm font-bold tracking-widest text-foreground transition-colors hover:bg-secondary"
|
|
|
|
|
>
|
|
|
|
|
<KeyRound aria-hidden="true" className="size-3.5 text-muted-foreground" />
|
|
|
|
|
{code}
|
|
|
|
|
{copied ? (
|
|
|
|
|
<Check aria-hidden="true" className="size-3.5 text-primary" />
|
|
|
|
|
) : (
|
|
|
|
|
<Copy aria-hidden="true" className="size-3.5 text-muted-foreground" />
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
// --- Create team (State A) -------------------------------------------------
|
|
|
|
|
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
// --- Konkurranseklasser (2026-08-03) ----------------------------------------
|
|
|
|
|
// Fritt navngitte klasser med et valgfritt standardutslag -- se
|
|
|
|
|
// tournament_class-tabellen (053_tournament_classes.sql). Kun utslag-
|
|
|
|
|
// bekvemmelighet i lagturneringer (ingen egen resultatliste her, bekreftet
|
|
|
|
|
// med bruker -- se AssignRoundParticipantControl/LeaderboardTab i
|
|
|
|
|
// individual-tournament-detail.tsx for der klasse OGSÅ deler leaderboardet).
|
|
|
|
|
|
|
|
|
|
function ClassesCard({
|
|
|
|
|
classes,
|
|
|
|
|
courses,
|
|
|
|
|
organizationId,
|
|
|
|
|
onCreate,
|
|
|
|
|
onDelete,
|
|
|
|
|
}: {
|
|
|
|
|
classes: ApiTournamentClass[]
|
|
|
|
|
courses: ApiCourse[]
|
|
|
|
|
organizationId: string
|
|
|
|
|
onCreate: (name: string, defaultTeeId: string | null) => void
|
|
|
|
|
onDelete: (classId: string) => void
|
|
|
|
|
}) {
|
|
|
|
|
const [name, setName] = useState("")
|
|
|
|
|
const [courseId, setCourseId] = useState("")
|
|
|
|
|
const [teeId, setTeeId] = useState("")
|
|
|
|
|
const [tees, setTees] = useState<ApiTee[]>([])
|
|
|
|
|
const [adding, setAdding] = useState(false)
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!courseId) {
|
|
|
|
|
setTees([])
|
|
|
|
|
setTeeId("")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
let cancelled = false
|
|
|
|
|
fetch(`/orgs/${organizationId}/courses/${courseId}/tees`, { credentials: "include" })
|
|
|
|
|
.then((res) => (res.ok ? res.json() : []))
|
|
|
|
|
.then((data: ApiTee[]) => {
|
|
|
|
|
if (!cancelled) {
|
|
|
|
|
setTees(data)
|
|
|
|
|
setTeeId(data[0]?.id ?? "")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
return () => {
|
|
|
|
|
cancelled = true
|
|
|
|
|
}
|
|
|
|
|
}, [organizationId, courseId])
|
|
|
|
|
|
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
if (name.trim().length === 0 || adding) return
|
|
|
|
|
setAdding(true)
|
|
|
|
|
await onCreate(name.trim(), teeId || null)
|
|
|
|
|
setAdding(false)
|
|
|
|
|
setName("")
|
|
|
|
|
setCourseId("")
|
|
|
|
|
setTeeId("")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section className="mb-4 flex flex-col gap-4 rounded-3xl border border-border bg-card p-5 shadow-md shadow-black/8 sm:p-6">
|
|
|
|
|
<div className="flex items-center gap-2.5">
|
|
|
|
|
<div className="flex size-10 items-center justify-center rounded-xl bg-muted">
|
|
|
|
|
<Users aria-hidden="true" className="size-5 text-muted-foreground" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-col">
|
|
|
|
|
<h3 className="text-sm font-bold text-foreground">Klasser</h3>
|
|
|
|
|
<p className="text-xs text-muted-foreground text-pretty">
|
|
|
|
|
Foreslår riktig utslag når en spiller legges til en kamp -- ingen egen resultatliste
|
|
|
|
|
her (poeng er knyttet til hele kamper, ikke enkeltspillere).
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{classes.length > 0 && (
|
|
|
|
|
<ul className="flex flex-col divide-y divide-border rounded-2xl border border-border">
|
|
|
|
|
{classes.map((c) => (
|
|
|
|
|
<li key={c.id} className="flex items-center gap-3 px-3 py-2.5">
|
|
|
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
|
|
|
<span className="truncate text-sm font-semibold text-foreground">{c.name}</span>
|
|
|
|
|
{c.default_tee_name && (
|
|
|
|
|
<span className="text-xs text-muted-foreground">
|
|
|
|
|
Standardutslag: {c.default_tee_name}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => onDelete(c.id)}
|
|
|
|
|
aria-label={`Fjern klassen ${c.name}`}
|
|
|
|
|
className="flex size-11 shrink-0 items-center justify-center rounded-xl text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 aria-hidden="true" className="size-4" />
|
|
|
|
|
</button>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<form onSubmit={handleSubmit} className="flex flex-col gap-2 sm:flex-row sm:items-end">
|
|
|
|
|
<div className="flex flex-1 flex-col gap-1.5">
|
|
|
|
|
<Label htmlFor="class-name">Klassenavn</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="class-name"
|
|
|
|
|
value={name}
|
|
|
|
|
onChange={(e) => setName(e.target.value)}
|
|
|
|
|
placeholder="F.eks. Damer"
|
|
|
|
|
className="h-11"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-1 flex-col gap-1.5">
|
|
|
|
|
<Label htmlFor="class-course">Bane (for standardutslag)</Label>
|
|
|
|
|
<select
|
|
|
|
|
id="class-course"
|
|
|
|
|
value={courseId}
|
|
|
|
|
onChange={(e) => setCourseId(e.target.value)}
|
|
|
|
|
className="h-11 rounded-lg border border-border bg-card px-2.5 text-sm font-medium text-foreground outline-none"
|
|
|
|
|
>
|
|
|
|
|
<option value="">Ingen standardutslag</option>
|
|
|
|
|
{courses.map((c) => (
|
|
|
|
|
<option key={c.id} value={c.id}>
|
|
|
|
|
{c.name}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
{tees.length > 0 && (
|
|
|
|
|
<div className="flex flex-1 flex-col gap-1.5">
|
|
|
|
|
<Label htmlFor="class-tee">Standardutslag</Label>
|
|
|
|
|
<select
|
|
|
|
|
id="class-tee"
|
|
|
|
|
value={teeId}
|
|
|
|
|
onChange={(e) => setTeeId(e.target.value)}
|
|
|
|
|
className="h-11 rounded-lg border border-border bg-card px-2.5 text-sm font-medium text-foreground outline-none"
|
|
|
|
|
>
|
|
|
|
|
{tees.map((t) => (
|
|
|
|
|
<option key={t.id} value={t.id}>
|
|
|
|
|
{t.name}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<Button
|
|
|
|
|
type="submit"
|
|
|
|
|
variant="outline"
|
|
|
|
|
disabled={adding || name.trim().length === 0}
|
|
|
|
|
className="h-11 shrink-0 rounded-xl font-semibold"
|
|
|
|
|
>
|
|
|
|
|
<Plus aria-hidden="true" className="size-4" />
|
|
|
|
|
Legg til
|
|
|
|
|
</Button>
|
|
|
|
|
</form>
|
|
|
|
|
</section>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
function CreateTeamCard({
|
|
|
|
|
slot,
|
|
|
|
|
onCreate,
|
|
|
|
|
}: {
|
|
|
|
|
slot: 0 | 1
|
|
|
|
|
onCreate: (slot: 0 | 1, name: string, color: string) => void
|
|
|
|
|
}) {
|
|
|
|
|
const [name, setName] = useState("")
|
|
|
|
|
const [color, setColor] = useState(TEAM_COLORS[slot === 0 ? 0 : 1].value)
|
|
|
|
|
const valid = name.trim().length >= 2
|
|
|
|
|
|
|
|
|
|
function handleSubmit(e: React.FormEvent) {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
if (!valid) return
|
|
|
|
|
onCreate(slot, name.trim(), color)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<form
|
|
|
|
|
onSubmit={handleSubmit}
|
|
|
|
|
className="flex flex-col gap-5 rounded-3xl border border-dashed border-border bg-card/50 p-5 sm:p-6"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<div className="flex size-11 items-center justify-center rounded-xl bg-muted">
|
|
|
|
|
<Users aria-hidden="true" className="size-5 text-muted-foreground" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-col">
|
|
|
|
|
<span className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
|
|
|
Lag {slot + 1}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-base font-bold text-foreground">Opprett lag</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
<Label htmlFor={`team-name-${slot}`} className="text-sm font-semibold">
|
|
|
|
|
Lagnavn
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id={`team-name-${slot}`}
|
|
|
|
|
placeholder="F.eks. Lag Birdie"
|
|
|
|
|
value={name}
|
|
|
|
|
onChange={(e) => setName(e.target.value)}
|
|
|
|
|
className="h-12 rounded-2xl text-base"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
<Label className="text-sm font-semibold">Lagfarge</Label>
|
|
|
|
|
<div className="flex flex-wrap gap-2.5">
|
|
|
|
|
{TEAM_COLORS.map((c) => {
|
|
|
|
|
const selected = c.value === color
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={c.value}
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setColor(c.value)}
|
|
|
|
|
aria-label={c.label}
|
|
|
|
|
aria-pressed={selected}
|
|
|
|
|
title={c.label}
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex size-9 items-center justify-center rounded-full ring-2 ring-offset-2 ring-offset-card transition-transform hover:scale-105",
|
|
|
|
|
selected ? "ring-foreground" : "ring-transparent",
|
|
|
|
|
)}
|
|
|
|
|
style={{ backgroundColor: c.value }}
|
|
|
|
|
>
|
|
|
|
|
{selected && <Check aria-hidden="true" className="size-4 text-white" />}
|
|
|
|
|
</button>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Button
|
|
|
|
|
type="submit"
|
|
|
|
|
disabled={!valid}
|
|
|
|
|
className="h-12 rounded-2xl text-base font-bold shadow-sm"
|
|
|
|
|
>
|
|
|
|
|
<Plus aria-hidden="true" className="size-5" />
|
|
|
|
|
Opprett lag
|
|
|
|
|
</Button>
|
|
|
|
|
</form>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Team panel with roster (State B) --------------------------------------
|
|
|
|
|
|
|
|
|
|
function TeamPanel({
|
|
|
|
|
team,
|
|
|
|
|
otherTeam,
|
|
|
|
|
pool,
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
classes,
|
2026-07-19 22:26:45 +02:00
|
|
|
chatHref,
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
findPlayerTeam,
|
|
|
|
|
onToggleCaptain,
|
|
|
|
|
onRemovePlayer,
|
Update Todos
Undersøk kode-kontekst for alle tre byggeoppgavene
Avklar åpne designspørsmål med bruker
Bygg: flytte spiller mellom lag (backend + frontend)
Bygg: individuell rangering i turnering (backend + frontend)
Bygg: midlertidige spillere + etter-runde-e-post (backend + frontend)
Typesjekket produksjonsbuild av frontend
Scratch-verifiser alt tre + full regresjon
Browser-verifiser alt tre (Chrome DevTools)
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Rull ut mot ekte systemer (med bekreftelse)
Alle tre er live. Oppsummert:
Flytte spiller mellom lag — ny "Flytt til {annet lag}"-handling i lag-oversikten, atomisk, blokkert hvis spilleren allerede er lagt til i en match.
Individuell rangering per økt — ny brutto/netto/poeng-rangering for singles/fourball-slagspill-økter, ved siden av det eksisterende lag-poeng-leaderboardet, lenket fra blind draw-skjermen.
Midlertidige spillere: etter-runde-e-post — ny knapp som sender scorekort + ekte innloggingslenke til spillere uten konto, med dobbel-utsendingssperre.
Alt verifisert grundig: 54/54 nye scratch-sjekker + full regresjon (5+28+19 eksisterende sjekker), typesjekket produksjonsbuild, og en fullstendig gjennomgang i ekte nettleser (Chrome DevTools) av alle tre — ingen konsollfeil. Migrasjon 034 kjørt mot ekte database, begge containere redeployet, teeoff.no upåvirket. Samtidig rettet jeg opp noen steder der FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md hadde blitt hengende etter den faktiske, allerede leverte tilstanden (ADR-039-utrulling, PWA-offline-browsertesting).
2026-07-28 20:56:08 +02:00
|
|
|
onMovePlayer,
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
onAddExisting,
|
|
|
|
|
onAddNew,
|
2026-07-20 10:01:44 +02:00
|
|
|
onUpdatePlayer,
|
2026-07-19 21:25:31 +02:00
|
|
|
onConcedeTournament,
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
onSetClass,
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
}: {
|
|
|
|
|
team: Team
|
|
|
|
|
otherTeam: Team | null
|
|
|
|
|
pool: ApiPlayer[]
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
classes: ApiTournamentClass[]
|
2026-07-19 22:26:45 +02:00
|
|
|
chatHref: string
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
findPlayerTeam: (playerId: string) => Team | null
|
|
|
|
|
onToggleCaptain: (teamId: string, rosterId: string, current: boolean) => void
|
|
|
|
|
onRemovePlayer: (teamId: string, rosterId: string) => void
|
Update Todos
Undersøk kode-kontekst for alle tre byggeoppgavene
Avklar åpne designspørsmål med bruker
Bygg: flytte spiller mellom lag (backend + frontend)
Bygg: individuell rangering i turnering (backend + frontend)
Bygg: midlertidige spillere + etter-runde-e-post (backend + frontend)
Typesjekket produksjonsbuild av frontend
Scratch-verifiser alt tre + full regresjon
Browser-verifiser alt tre (Chrome DevTools)
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Rull ut mot ekte systemer (med bekreftelse)
Alle tre er live. Oppsummert:
Flytte spiller mellom lag — ny "Flytt til {annet lag}"-handling i lag-oversikten, atomisk, blokkert hvis spilleren allerede er lagt til i en match.
Individuell rangering per økt — ny brutto/netto/poeng-rangering for singles/fourball-slagspill-økter, ved siden av det eksisterende lag-poeng-leaderboardet, lenket fra blind draw-skjermen.
Midlertidige spillere: etter-runde-e-post — ny knapp som sender scorekort + ekte innloggingslenke til spillere uten konto, med dobbel-utsendingssperre.
Alt verifisert grundig: 54/54 nye scratch-sjekker + full regresjon (5+28+19 eksisterende sjekker), typesjekket produksjonsbuild, og en fullstendig gjennomgang i ekte nettleser (Chrome DevTools) av alle tre — ingen konsollfeil. Migrasjon 034 kjørt mot ekte database, begge containere redeployet, teeoff.no upåvirket. Samtidig rettet jeg opp noen steder der FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md hadde blitt hengende etter den faktiske, allerede leverte tilstanden (ADR-039-utrulling, PWA-offline-browsertesting).
2026-07-28 20:56:08 +02:00
|
|
|
onMovePlayer: (teamId: string, rosterId: string, targetTeamId: string) => void
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
onAddExisting: (teamId: string, playerId: string) => void
|
|
|
|
|
onAddNew: (teamId: string, name: string, handicap?: number) => void
|
2026-07-20 10:01:44 +02:00
|
|
|
onUpdatePlayer: (
|
|
|
|
|
playerId: string,
|
|
|
|
|
updates: { display_name?: string; handicap_index?: number | null; gender?: string | null },
|
|
|
|
|
) => void
|
2026-07-19 21:25:31 +02:00
|
|
|
onConcedeTournament: (teamId: string) => Promise<string | null>
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
onSetClass: (teamId: string, rosterId: string, classId: string | null) => void
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
}) {
|
|
|
|
|
const [confirmingId, setConfirmingId] = useState<string | null>(null)
|
2026-07-20 10:01:44 +02:00
|
|
|
const [editingPlayerId, setEditingPlayerId] = useState<string | null>(null)
|
2026-07-19 21:25:31 +02:00
|
|
|
const [confirmingGiveUp, setConfirmingGiveUp] = useState(false)
|
|
|
|
|
const [giveUpError, setGiveUpError] = useState<string | null>(null)
|
|
|
|
|
const [givingUp, setGivingUp] = useState(false)
|
|
|
|
|
|
|
|
|
|
async function handleGiveUp() {
|
|
|
|
|
setGivingUp(true)
|
|
|
|
|
const message = await onConcedeTournament(team.id)
|
|
|
|
|
setGivingUp(false)
|
|
|
|
|
if (message) {
|
|
|
|
|
setGiveUpError(message)
|
|
|
|
|
} else {
|
|
|
|
|
setConfirmingGiveUp(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section
|
2026-07-29 12:44:51 +02:00
|
|
|
className="flex flex-col overflow-hidden rounded-3xl border border-border bg-card shadow-md shadow-black/8"
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
style={{ borderLeftWidth: 6, borderLeftColor: team.color }}
|
|
|
|
|
aria-label={`Lag ${team.name}`}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-3 border-b border-border px-5 py-4">
|
|
|
|
|
<span
|
|
|
|
|
aria-hidden="true"
|
|
|
|
|
className="size-4 shrink-0 rounded-full"
|
|
|
|
|
style={{ backgroundColor: team.color }}
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
|
|
|
<h3 className="truncate text-lg font-extrabold tracking-tight text-foreground">
|
|
|
|
|
{team.name}
|
|
|
|
|
</h3>
|
|
|
|
|
<span className="text-sm text-muted-foreground">
|
|
|
|
|
{team.roster.length} {team.roster.length === 1 ? "spiller" : "spillere"}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2026-07-19 22:26:45 +02:00
|
|
|
<Link
|
|
|
|
|
href={chatHref}
|
|
|
|
|
title="Privat lagchat -- kun for spillere rostret på laget"
|
|
|
|
|
className="flex size-9 shrink-0 items-center justify-center rounded-xl border border-border text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
|
|
|
|
>
|
|
|
|
|
<MessageCircle aria-hidden="true" className="size-4" />
|
|
|
|
|
</Link>
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<ul className="flex flex-col">
|
|
|
|
|
{team.roster.length === 0 && (
|
|
|
|
|
<li className="px-5 py-6 text-center text-sm text-muted-foreground">
|
|
|
|
|
Ingen spillere ennå. Legg til nedenfor.
|
|
|
|
|
</li>
|
|
|
|
|
)}
|
|
|
|
|
{team.roster.map((entry) => {
|
|
|
|
|
const confirming = confirmingId === entry.id
|
2026-07-20 10:01:44 +02:00
|
|
|
const editing = editingPlayerId === entry.player_id
|
|
|
|
|
|
|
|
|
|
if (editing) {
|
|
|
|
|
const player = pool.find((p) => p.id === entry.player_id)
|
|
|
|
|
return (
|
|
|
|
|
<li key={entry.id} className="border-b border-border px-5 py-3 last:border-b-0">
|
|
|
|
|
<EditPlayerForm
|
|
|
|
|
displayName={player?.display_name ?? entry.display_name}
|
|
|
|
|
handicapIndex={player?.handicap_index ?? null}
|
|
|
|
|
gender={player?.gender ?? null}
|
|
|
|
|
onSave={(updates) => {
|
|
|
|
|
onUpdatePlayer(entry.player_id, updates)
|
|
|
|
|
setEditingPlayerId(null)
|
|
|
|
|
}}
|
|
|
|
|
onCancel={() => setEditingPlayerId(null)}
|
|
|
|
|
/>
|
|
|
|
|
</li>
|
|
|
|
|
)
|
|
|
|
|
}
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
|
|
|
|
|
if (confirming) {
|
|
|
|
|
return (
|
|
|
|
|
<li
|
|
|
|
|
key={entry.id}
|
|
|
|
|
className="flex items-center justify-between gap-3 border-b border-border bg-destructive/5 px-5 py-3 last:border-b-0"
|
|
|
|
|
>
|
|
|
|
|
<span className="text-sm text-foreground text-pretty">
|
|
|
|
|
Fjern <span className="font-semibold">{entry.display_name}</span> fra laget?
|
|
|
|
|
</span>
|
|
|
|
|
<div className="flex shrink-0 items-center gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="destructive"
|
|
|
|
|
className="h-9 rounded-xl"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
onRemovePlayer(team.id, entry.id)
|
|
|
|
|
setConfirmingId(null)
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Fjern
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
className="h-9 rounded-xl"
|
|
|
|
|
onClick={() => setConfirmingId(null)}
|
|
|
|
|
>
|
|
|
|
|
Avbryt
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</li>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<li
|
|
|
|
|
key={entry.id}
|
|
|
|
|
className="flex items-center gap-3 border-b border-border px-5 py-3 last:border-b-0"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
|
|
|
|
<span className="truncate font-semibold text-foreground">
|
|
|
|
|
{entry.display_name}
|
|
|
|
|
</span>
|
|
|
|
|
{entry.is_captain && (
|
|
|
|
|
<span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-primary/15 px-2 py-0.5 text-xs font-semibold text-foreground">
|
|
|
|
|
<Star aria-hidden="true" className="size-3 fill-primary text-primary" />
|
|
|
|
|
Kaptein
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
{entry.class_name && (
|
|
|
|
|
<span className="inline-flex shrink-0 items-center rounded-full bg-muted px-2 py-0.5 text-xs font-semibold text-muted-foreground">
|
|
|
|
|
{entry.class_name}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
</div>
|
|
|
|
|
<span className="shrink-0 tabular-nums text-sm font-medium text-muted-foreground">
|
|
|
|
|
{formatHandicap(entry.handicap_index_snapshot)}
|
|
|
|
|
</span>
|
|
|
|
|
<DropdownMenu>
|
|
|
|
|
<DropdownMenuTrigger
|
|
|
|
|
aria-label={`Handlinger for ${entry.display_name}`}
|
|
|
|
|
className="flex size-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground"
|
|
|
|
|
>
|
|
|
|
|
<MoreVertical aria-hidden="true" className="size-4" />
|
|
|
|
|
</DropdownMenuTrigger>
|
|
|
|
|
<DropdownMenuContent align="end" className="w-52 rounded-2xl p-1.5">
|
2026-07-20 10:01:44 +02:00
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={() => setEditingPlayerId(entry.player_id)}
|
|
|
|
|
className="flex cursor-pointer items-center gap-2 rounded-xl px-3 py-2.5 text-sm font-semibold"
|
|
|
|
|
>
|
|
|
|
|
<Pencil aria-hidden="true" className="size-4" />
|
|
|
|
|
Rediger spiller
|
|
|
|
|
</DropdownMenuItem>
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={() => onToggleCaptain(team.id, entry.id, entry.is_captain)}
|
|
|
|
|
className="flex cursor-pointer items-center gap-2 rounded-xl px-3 py-2.5 text-sm font-semibold"
|
|
|
|
|
>
|
|
|
|
|
<Star aria-hidden="true" className="size-4" />
|
|
|
|
|
{entry.is_captain ? "Fjern som kaptein" : "Gjør til kaptein"}
|
|
|
|
|
</DropdownMenuItem>
|
Alt er bygget og grundig scratch-verifisert — både API-nivå og ekte nettleser-klikk, på begge turneringstyper. Før jeg ruller ut mot ekte teecup_db, her er planen:
Hva som skjer:
Kjør migrasjon 053_tournament_classes.sql mot ekte teecup_db — ny tournament_class-tabell + nullable class_id-kolonne på team_roster og tournament_participant (rent additivt, ingen eksisterende data røres).
docker compose up -d --build teecup_api teecup_frontend — begge containere bygges og redeployes.
Hva som er verifisert:
Klasser kan opprettes/slettes for begge turneringstyper, med valgfritt standardutslag (kaskaderende bane→utslag-valg)
I lagturneringer: klasse foreslår riktig utslag automatisk når en spiller legges til en kamp (bekreftet: Kari→44, Ola→56), ingen endring i selve kampresultatet
I individuelle turneringer: samme utslag-forslag ved runde-oppsett, PLUSS leaderboardet deler seg nå i egne seksjoner per klasse med riktig rangering innad i hver (bekreftet: Damer viste Kari #1/Siv #2, Herrer viste Ola #1 — matcher håndregnet slagsum)
Sletter man en klasse, mister spillerne bare merkelappen (ikke fjernet fra laget/turneringen)
Ingen klasser opprettet = identisk visning som før (bakoverkompatibelt)
98/98 enhetstester og 12/12 isolasjonstester uendret, ekte typesjekket produksjonsbuild
2026-08-04 03:44:16 +02:00
|
|
|
{classes.map((c) => (
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
key={c.id}
|
|
|
|
|
onClick={() => onSetClass(team.id, entry.id, c.id)}
|
|
|
|
|
className="flex cursor-pointer items-center gap-2 rounded-xl px-3 py-2.5 text-sm font-semibold"
|
|
|
|
|
>
|
|
|
|
|
<Users aria-hidden="true" className="size-4" />
|
|
|
|
|
Sett klasse: {c.name}
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
))}
|
|
|
|
|
{entry.class_id && (
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={() => onSetClass(team.id, entry.id, null)}
|
|
|
|
|
className="flex cursor-pointer items-center gap-2 rounded-xl px-3 py-2.5 text-sm font-semibold"
|
|
|
|
|
>
|
|
|
|
|
<X aria-hidden="true" className="size-4" />
|
|
|
|
|
Fjern klasse
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
)}
|
Update Todos
Undersøk kode-kontekst for alle tre byggeoppgavene
Avklar åpne designspørsmål med bruker
Bygg: flytte spiller mellom lag (backend + frontend)
Bygg: individuell rangering i turnering (backend + frontend)
Bygg: midlertidige spillere + etter-runde-e-post (backend + frontend)
Typesjekket produksjonsbuild av frontend
Scratch-verifiser alt tre + full regresjon
Browser-verifiser alt tre (Chrome DevTools)
Oppdater CLAUDE.md/FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md
Rull ut mot ekte systemer (med bekreftelse)
Alle tre er live. Oppsummert:
Flytte spiller mellom lag — ny "Flytt til {annet lag}"-handling i lag-oversikten, atomisk, blokkert hvis spilleren allerede er lagt til i en match.
Individuell rangering per økt — ny brutto/netto/poeng-rangering for singles/fourball-slagspill-økter, ved siden av det eksisterende lag-poeng-leaderboardet, lenket fra blind draw-skjermen.
Midlertidige spillere: etter-runde-e-post — ny knapp som sender scorekort + ekte innloggingslenke til spillere uten konto, med dobbel-utsendingssperre.
Alt verifisert grundig: 54/54 nye scratch-sjekker + full regresjon (5+28+19 eksisterende sjekker), typesjekket produksjonsbuild, og en fullstendig gjennomgang i ekte nettleser (Chrome DevTools) av alle tre — ingen konsollfeil. Migrasjon 034 kjørt mot ekte database, begge containere redeployet, teeoff.no upåvirket. Samtidig rettet jeg opp noen steder der FEATURE_BACKLOG.md/ARCHITECTURE_DECISIONS.md hadde blitt hengende etter den faktiske, allerede leverte tilstanden (ADR-039-utrulling, PWA-offline-browsertesting).
2026-07-28 20:56:08 +02:00
|
|
|
{otherTeam && (
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={() => onMovePlayer(team.id, entry.id, otherTeam.id)}
|
|
|
|
|
className="flex cursor-pointer items-center gap-2 rounded-xl px-3 py-2.5 text-sm font-semibold"
|
|
|
|
|
>
|
|
|
|
|
<ArrowLeftRight aria-hidden="true" className="size-4" />
|
|
|
|
|
Flytt til {otherTeam.name}
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
)}
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={() => setConfirmingId(entry.id)}
|
|
|
|
|
className="flex cursor-pointer items-center gap-2 rounded-xl px-3 py-2.5 text-sm font-semibold text-destructive"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 aria-hidden="true" className="size-4" />
|
|
|
|
|
Fjern fra laget
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|
</li>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
|
|
<div className="border-t border-border p-4">
|
|
|
|
|
<AddPlayerControl
|
|
|
|
|
team={team}
|
|
|
|
|
otherTeam={otherTeam}
|
|
|
|
|
pool={pool}
|
|
|
|
|
findPlayerTeam={findPlayerTeam}
|
|
|
|
|
onAddExisting={onAddExisting}
|
|
|
|
|
onAddNew={onAddNew}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-07-19 21:25:31 +02:00
|
|
|
|
|
|
|
|
<div className="border-t border-border p-4">
|
|
|
|
|
{confirmingGiveUp ? (
|
|
|
|
|
<div className="flex flex-col gap-3 rounded-2xl bg-destructive/5 p-3">
|
|
|
|
|
{giveUpError && (
|
|
|
|
|
<p role="alert" className="text-xs font-medium text-destructive text-pretty">
|
|
|
|
|
{giveUpError}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
<p className="text-sm text-foreground text-pretty">
|
|
|
|
|
Er du sikker på at <span className="font-semibold">{team.name}</span> gir opp
|
|
|
|
|
resten av turneringen? Motstanderen får full poengsum for ALLE ikke-avgjorte
|
|
|
|
|
matcher, med én gang.
|
|
|
|
|
</p>
|
|
|
|
|
<div className="flex justify-end gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="destructive"
|
|
|
|
|
className="h-9 rounded-xl"
|
|
|
|
|
disabled={givingUp}
|
|
|
|
|
onClick={handleGiveUp}
|
|
|
|
|
>
|
|
|
|
|
Gi opp turneringen
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
className="h-9 rounded-xl"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setConfirmingGiveUp(false)
|
|
|
|
|
setGiveUpError(null)
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Avbryt
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
onClick={() => setConfirmingGiveUp(true)}
|
|
|
|
|
className="h-9 w-full justify-start rounded-xl text-xs font-semibold text-muted-foreground hover:text-destructive"
|
|
|
|
|
>
|
|
|
|
|
<Flag aria-hidden="true" className="size-3.5" />
|
|
|
|
|
Gi opp resten av turneringen for laget (walkover)
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
</section>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Add player type-ahead -------------------------------------------------
|
|
|
|
|
|
|
|
|
|
function AddPlayerControl({
|
|
|
|
|
team,
|
|
|
|
|
pool,
|
|
|
|
|
findPlayerTeam,
|
|
|
|
|
onAddExisting,
|
|
|
|
|
onAddNew,
|
|
|
|
|
}: {
|
|
|
|
|
team: Team
|
|
|
|
|
otherTeam: Team | null
|
|
|
|
|
pool: ApiPlayer[]
|
|
|
|
|
findPlayerTeam: (playerId: string) => Team | null
|
|
|
|
|
onAddExisting: (teamId: string, playerId: string) => void
|
|
|
|
|
onAddNew: (teamId: string, name: string, handicap?: number) => void
|
|
|
|
|
}) {
|
|
|
|
|
const [open, setOpen] = useState(false)
|
|
|
|
|
const [query, setQuery] = useState("")
|
|
|
|
|
const [creating, setCreating] = useState(false)
|
|
|
|
|
const [newHandicap, setNewHandicap] = useState("")
|
|
|
|
|
|
|
|
|
|
const trimmed = query.trim()
|
|
|
|
|
|
|
|
|
|
const matches = useMemo(() => {
|
|
|
|
|
if (!trimmed) return []
|
|
|
|
|
const q = trimmed.toLowerCase()
|
|
|
|
|
return pool
|
|
|
|
|
.filter((p) => p.display_name.toLowerCase().includes(q))
|
|
|
|
|
.map((p) => {
|
|
|
|
|
const onTeam = findPlayerTeam(p.id)
|
|
|
|
|
return {
|
|
|
|
|
player: p,
|
|
|
|
|
onThisTeam: onTeam?.id === team.id,
|
|
|
|
|
onOtherTeam: onTeam && onTeam.id !== team.id ? onTeam : null,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.filter((m) => !m.onThisTeam)
|
|
|
|
|
}, [pool, trimmed, findPlayerTeam, team.id])
|
|
|
|
|
|
|
|
|
|
const exactMatch = useMemo(
|
|
|
|
|
() => pool.some((p) => p.display_name.toLowerCase() === trimmed.toLowerCase()),
|
|
|
|
|
[pool, trimmed],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const showCreate = trimmed.length >= 2 && !exactMatch
|
|
|
|
|
|
|
|
|
|
function reset() {
|
|
|
|
|
setQuery("")
|
|
|
|
|
setCreating(false)
|
|
|
|
|
setNewHandicap("")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleAddExisting(playerId: string) {
|
|
|
|
|
onAddExisting(team.id, playerId)
|
|
|
|
|
reset()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleCreate() {
|
|
|
|
|
if (trimmed.length < 2) return
|
|
|
|
|
const hcpValue = newHandicap.trim() === "" ? undefined : Number(newHandicap.replace(",", "."))
|
|
|
|
|
const hcp = hcpValue !== undefined && !Number.isNaN(hcpValue) ? hcpValue : undefined
|
|
|
|
|
onAddNew(team.id, trimmed, hcp)
|
|
|
|
|
reset()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
return (
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="secondary"
|
|
|
|
|
onClick={() => setOpen(true)}
|
|
|
|
|
className="h-11 w-full rounded-2xl text-sm font-bold"
|
|
|
|
|
>
|
|
|
|
|
<UserPlus aria-hidden="true" className="size-4" />
|
|
|
|
|
Legg til spiller
|
|
|
|
|
</Button>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Input
|
|
|
|
|
autoFocus
|
|
|
|
|
placeholder="Søk eller skriv navn…"
|
|
|
|
|
value={query}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
setQuery(e.target.value)
|
|
|
|
|
setCreating(false)
|
|
|
|
|
}}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === "Escape") {
|
|
|
|
|
setOpen(false)
|
|
|
|
|
reset()
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
className="h-11 flex-1 rounded-2xl text-base"
|
|
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setOpen(false)
|
|
|
|
|
reset()
|
|
|
|
|
}}
|
|
|
|
|
className="size-11 shrink-0 rounded-2xl text-muted-foreground"
|
|
|
|
|
aria-label="Lukk"
|
|
|
|
|
>
|
|
|
|
|
<X aria-hidden="true" className="size-5" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{trimmed.length > 0 && (
|
|
|
|
|
<div className="overflow-hidden rounded-2xl border border-border bg-background">
|
|
|
|
|
<ul className="flex max-h-64 flex-col overflow-auto">
|
|
|
|
|
{matches.map(({ player, onOtherTeam }) => {
|
|
|
|
|
if (onOtherTeam) {
|
|
|
|
|
return (
|
|
|
|
|
<li
|
|
|
|
|
key={player.id}
|
|
|
|
|
className="flex cursor-not-allowed items-center justify-between gap-3 border-b border-border px-4 py-2.5 opacity-60 last:border-b-0"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex min-w-0 flex-col">
|
|
|
|
|
<span className="truncate text-sm font-medium text-muted-foreground line-through">
|
|
|
|
|
{player.display_name}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="truncate text-xs text-muted-foreground">
|
|
|
|
|
allerede på {onOtherTeam.name}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span className="shrink-0 tabular-nums text-xs text-muted-foreground">
|
|
|
|
|
{formatHandicap(player.handicap_index)}
|
|
|
|
|
</span>
|
|
|
|
|
</li>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
<li key={player.id} className="border-b border-border last:border-b-0">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => handleAddExisting(player.id)}
|
|
|
|
|
className="flex w-full items-center justify-between gap-3 px-4 py-2.5 text-left transition-colors hover:bg-accent/60"
|
|
|
|
|
>
|
|
|
|
|
<span className="truncate text-sm font-semibold text-foreground">
|
|
|
|
|
{player.display_name}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="shrink-0 tabular-nums text-sm text-muted-foreground">
|
|
|
|
|
{formatHandicap(player.handicap_index)}
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
</li>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
{matches.length === 0 && !showCreate && (
|
|
|
|
|
<li className="px-4 py-3 text-center text-sm text-muted-foreground">
|
|
|
|
|
Ingen treff.
|
|
|
|
|
</li>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{showCreate && !creating && (
|
|
|
|
|
<li className="border-t border-border">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setCreating(true)}
|
|
|
|
|
className="flex w-full items-center gap-2 px-4 py-3 text-left text-sm font-semibold text-primary transition-colors hover:bg-accent/60"
|
|
|
|
|
>
|
|
|
|
|
<Plus aria-hidden="true" className="size-4" />
|
|
|
|
|
Opprett ny spiller: «{trimmed}»
|
|
|
|
|
</button>
|
|
|
|
|
</li>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{showCreate && creating && (
|
|
|
|
|
<li className="flex flex-col gap-3 border-t border-border bg-accent/30 px-4 py-3">
|
|
|
|
|
<span className="text-sm font-semibold text-foreground">
|
|
|
|
|
Ny spiller: {trimmed}
|
|
|
|
|
</span>
|
|
|
|
|
<div className="flex items-end gap-2">
|
|
|
|
|
<div className="flex flex-1 flex-col gap-1.5">
|
|
|
|
|
<Label htmlFor="new-hcp" className="text-xs font-semibold text-muted-foreground">
|
|
|
|
|
Handicap-indeks (valgfritt)
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="new-hcp"
|
|
|
|
|
autoFocus
|
|
|
|
|
inputMode="decimal"
|
|
|
|
|
placeholder="F.eks. 12.5"
|
|
|
|
|
value={newHandicap}
|
|
|
|
|
onChange={(e) => setNewHandicap(e.target.value)}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === "Enter") {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
handleCreate()
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
className="h-11 rounded-xl text-base"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={handleCreate}
|
|
|
|
|
className="h-11 shrink-0 rounded-xl font-bold"
|
|
|
|
|
>
|
|
|
|
|
Legg til
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</li>
|
|
|
|
|
)}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-20 10:01:44 +02:00
|
|
|
// --- Rediger spiller (spillerpoolen, ikke et lags frosne snapshot) --------
|
|
|
|
|
|
|
|
|
|
function EditPlayerForm({
|
|
|
|
|
displayName,
|
|
|
|
|
handicapIndex,
|
|
|
|
|
gender,
|
|
|
|
|
onSave,
|
|
|
|
|
onCancel,
|
|
|
|
|
}: {
|
|
|
|
|
displayName: string
|
|
|
|
|
handicapIndex: number | null
|
|
|
|
|
gender: "m" | "f" | "x" | null
|
|
|
|
|
onSave: (updates: { display_name: string; handicap_index: number | null; gender: string | null }) => void
|
|
|
|
|
onCancel: () => void
|
|
|
|
|
}) {
|
|
|
|
|
const [name, setName] = useState(displayName)
|
|
|
|
|
const [hcp, setHcp] = useState(handicapIndex === null ? "" : String(handicapIndex))
|
|
|
|
|
const [genderValue, setGenderValue] = useState<string>(gender ?? "")
|
|
|
|
|
const valid = name.trim().length >= 1
|
|
|
|
|
|
|
|
|
|
function handleSubmit(e: React.FormEvent) {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
if (!valid) return
|
|
|
|
|
onSave({
|
|
|
|
|
display_name: name.trim(),
|
|
|
|
|
handicap_index: hcp.trim() === "" ? null : Number(hcp.replace(",", ".")),
|
|
|
|
|
gender: genderValue === "" ? null : genderValue,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
|
|
|
|
|
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
|
|
|
|
<Input
|
|
|
|
|
value={name}
|
|
|
|
|
onChange={(e) => setName(e.target.value)}
|
|
|
|
|
placeholder="Navn"
|
|
|
|
|
className="h-10 rounded-xl text-sm sm:col-span-1"
|
|
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
value={hcp}
|
|
|
|
|
onChange={(e) => setHcp(e.target.value)}
|
|
|
|
|
placeholder="HCP"
|
|
|
|
|
inputMode="decimal"
|
|
|
|
|
className="h-10 rounded-xl text-sm"
|
|
|
|
|
/>
|
|
|
|
|
<select
|
|
|
|
|
value={genderValue}
|
|
|
|
|
onChange={(e) => setGenderValue(e.target.value)}
|
|
|
|
|
className="h-10 rounded-xl border border-border bg-card px-2.5 text-sm font-medium text-foreground outline-none"
|
|
|
|
|
>
|
|
|
|
|
<option value="">Kjønn ikke satt</option>
|
|
|
|
|
<option value="f">Dame</option>
|
|
|
|
|
<option value="m">Herre</option>
|
|
|
|
|
<option value="x">Annet</option>
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-xs leading-relaxed text-muted-foreground text-pretty">
|
|
|
|
|
Endrer spilleren i poolen (brukes ved fremtidig rostring). Endrer IKKE HCP-tallet som
|
|
|
|
|
allerede er registrert på et lag i denne turneringen — fjern og legg til spilleren på
|
|
|
|
|
nytt på laget for å oppdatere det.
|
|
|
|
|
</p>
|
|
|
|
|
<div className="flex justify-end gap-2">
|
|
|
|
|
<Button type="submit" size="sm" disabled={!valid} className="h-9 rounded-xl">
|
|
|
|
|
Lagre
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="button" size="sm" variant="ghost" onClick={onCancel} className="h-9 rounded-xl">
|
|
|
|
|
Avbryt
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
Lag/roster-skjermen er live på https://teecup.teeoff.no (klikk et turneringskort på dashbordet). Oppsummert:
Bygget:
/tournaments/[id] — to lag side ved side, hvert med egen fargevelger, spillerliste og en type-ahead for "legg til spiller" (søker i organisasjonens spillerpool, tilbyr inline "opprett ny spiller" hvis ingen treff, markerer spillere allerede rostret på det andre laget som utilgjengelige).
Fant et reelt hull før integrering, ikke etter: V0-skjermen bygger inn "fjern spiller" og "gjør til kaptein" — backend hadde bare GET/POST på roster, ingen DELETE/PATCH. Spurte deg, du sa bygg dem nå — lagt til og scratch-verifisert (PATCH setter kaptein riktig, DELETE gir 204 og er idempotent, test_isolation.sql fortsatt 12/12).
Navigasjon fra dashbordet er kablet opp (turneringskort er nå en ekte lenke).
Bevisst forenkling notert i backloggen: ingen håndheving av "kun én kaptein per lag" ennå — flere kan merkes samtidig. Hører sammen med det uavklarte brukerroller-punktet, løses ikke isolert her.
Verifisert: ekte typesjekket build (5 ruter), begge containere redeployet (backend hadde nye endepunkter), teeoff.no upåvirket. Selve skrive-flyten (opprett lag/spiller) er ikke testet med ekte data — samme som sist, venter på deg. Du har allerede "De Gamle er Eldst" liggende i "Tjøme Gents" — vil du prøve å sette opp de to lagene der?
2026-07-18 07:15:38 +02:00
|
|
|
// --- Helpers ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
function formatHandicap(handicap: number | null | undefined) {
|
|
|
|
|
if (handicap === null || handicap === undefined) return "—"
|
|
|
|
|
return handicap.toFixed(1)
|
|
|
|
|
}
|