From 16f3a163c185339cad9b734f33294dbca14a94cc Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Mon, 20 Jul 2026 10:01:44 +0200 Subject: [PATCH] Update Todos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Diagnostisere dashboard-dato-bug (allerede kjent, bekreftet på nytt) Fiks: utled turnering-datospenn fra øktenes scheduled_at i list_tournaments Bygg PATCH /orgs/{id}/players/{id} (rediger spillerpool) Bygg frontend: Rediger spiller-skjema i tournament-detail.tsx Scratch-verifisere begge fiksene Typesjekket frontend-build Oppdatere .md-filer + be om utrullingsbekreftelse Logikken bak "Ingen datoer satt": dashbord-kortet leser et helt eget felt på selve turneringen (start_date/end_date), atskilt fra øktenes scheduled_at som Program-fanen viser. Ingen UI har noensinne satt det feltet, så det var alltid tomt — uavhengig av at "11. juli" faktisk var satt på øktene. Fikset: kortet henter nå datospennet fra øktene i stedet (11.–12. juli her, siden du har to økter samme dag). Rediger spiller: bygget. Ny "Rediger spiller" i "⋮"-menyen på hver spiller — navn, HCP og kjønn kan endres. Én viktig presisering vist rett i skjemaet: dette endrer spilleren i poolen (gjelder fremtidig rostring), men endrer ikke HCP-tallet som allerede er frosset på et lag i en turnering (det er bevisst, for at et allerede spilt/pågående oppgjør ikke skal endre seg i etterkant). Skal du rette et tall som allerede vises på et lag, må du fortsatt fjerne og legge til spilleren på nytt — si fra om du vil at jeg bygger en direkte "rett dette laget sitt tall her og nå"-vei også. Begge deler er scratch-verifisert (9 automatiserte sjekker, inkludert at frysing-regelen fortsatt holder) og typesjekket bygget. Ingen migrasjon. --- .claude/settings.local.json | 3 +- ARCHITECTURE_DECISIONS.md | 42 +++++++ CLAUDE.md | 31 +++++ FEATURE_BACKLOG.md | 58 +++++++--- app/routers/players.py | 56 ++++++++- app/routers/tournaments.py | 26 ++++- frontend/components/tournament-detail.tsx | 133 ++++++++++++++++++++++ 7 files changed, 331 insertions(+), 18 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c4b7c5a..c96e9a1 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -326,7 +326,8 @@ "Bash(curl -s http://localhost:18800/health)", "Bash(python3 /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/test_hcp_fix.py)", "Bash(python3 /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/test_tee_gender.py)", - "Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" https://teeoff.no)" + "Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" https://teeoff.no)", + "Bash(python3 /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/test_player_edit_and_dates.py)" ], "additionalDirectories": [ "/opt/teeoff/deploy", diff --git a/ARCHITECTURE_DECISIONS.md b/ARCHITECTURE_DECISIONS.md index d6f9303..b0c125c 100644 --- a/ARCHITECTURE_DECISIONS.md +++ b/ARCHITECTURE_DECISIONS.md @@ -1406,6 +1406,48 @@ etter migrasjonen, bekreftet med en direkte spørring). Begge containere --- +## ADR-030: Dashbord-datoer utledes fra øktene, ikke et separat felt + +Reist av brukeren 2026-07-19/20, direkte oppfølging av det tidligere +dokumenterte UI-hullet ("Dashboard-turneringskortet viser 'Ingen datoer +satt'", notert 2026-07-19). Brukeren viste et faktisk skjermbilde: en +turnering med en økt tydelig planlagt til "lør. 11. juli, 10:50" på +Program-fanen, mens dashbord-kortet fortsatt viste "Ingen datoer satt". + +**Root cause, bekreftet ved kodegjennomgang (samme konklusjon som forrige +runde, nå faktisk fikset):** dashbord-kortet leser `tournament.start_date`/ +`end_date` — et eget felt på selve turneringen (ADR-015) — som INGEN UI +noensinne har hatt en vei til å sette. Fullstendig atskilt fra +`session.scheduled_at` (som Program-fanen korrekt viser). + +**Beslutning: utled datospennet fra øktenes `scheduled_at` i +`list_tournaments`, ikke bygg et manuelt datofelt-skjema.** To kilder til +sannhet (et manuelt "turnering-dato"-felt OG faktiske økt-tidspunkter) ville +uunngåelig kommet ut av synk — turneringens reelle datoer ER ganske enkelt +når rundene faktisk er satt til å spilles. `GET /orgs/{id}/tournaments` +gjør nå `COALESCE(t.start_date, MIN(økt.scheduled_at))` / +`COALESCE(t.end_date, MAX(økt.scheduled_at))` — et eksplisitt satt +`start_date`/`end_date` (om det noensinne blir gitt en skrivevei senere) +vinner fortsatt over det utledede spennet, men i praksis er det alltid det +utledede spennet som vises i dag. En turnering uten noen tidsplanlagte +økter viser fortsatt riktig "Ingen datoer satt" (ikke en feil). + +**Konsekvens:** kun `list_tournaments` sin spørring endret (ikke +`_TOURNAMENT_COLUMNS`, som fortsatt brukes uendret av opprett-/ +PATCH-endepunktene sine `RETURNING`-klausuler — de trenger ikke +sesjons-utledningen rett etter en skriveoperasjon). Ingen migrasjon. + +**Scratch-verifisert:** turnering uten økter → `null`/`null` (ikke feil); +turnering med to økter (11./12. juli) → riktig utledet spenn; en turnering +med et EKSPLISITT satt `start_date`/`end_date` ved opprettelse beholder +fortsatt sin egen verdi selv med økter senere lagt til (bekrefter +COALESCE-prioriteringen). + +**Status: ✅ BYGGET, SCRATCH-VERIFISERT.** Se CLAUDE.md-status for +utrulling. + +--- + ## Åpne spørsmål (ikke besluttet ennå) Disse må avklares før eller under de relevante fasene: diff --git a/CLAUDE.md b/CLAUDE.md index de37732..541b271 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1907,6 +1907,37 @@ Ferdig og verifisert: utrulling. Begge containere boot-et rent, `/health`/`dashboard` → 200, `teeoff.no` upåvirket. +- **Dashboard-dato-fiks (ADR-030) + rediger spiller, BYGGET OG SCRATCH- + VERIFISERT (2026-07-19/20):** brukeren viste et faktisk skjermbilde av + det tidligere dokumenterte datovisning-hullet (økt planlagt til "11. + juli" på Program-fanen, men dashbord-kortet viste fortsatt "Ingen + datoer satt") og ba samtidig om å kunne redigere spilleres HCP. + **Dato-fiks:** `list_tournaments` (`app/routers/tournaments.py`) utleder + nå datospennet fra øktenes `scheduled_at` (`COALESCE` med et evt. + eksplisitt satt `tournament.start_date`/`end_date`, som fortsatt vinner + om det noensinne settes — ingen UI gjør det i dag). Kun denne ene + spørringen endret, `_TOURNAMENT_COLUMNS` (brukt av opprett/PATCH sine + `RETURNING`-klausuler) urørt. Se ADR-030. + **Rediger spiller:** ny `PATCH /orgs/{id}/players/{id}` + (`app/routers/players.py`, vanlig `exclude_unset`-mønster, dekker alle + spillerfelt) + ny "Rediger spiller"-handling i rosterradens meny + (`tournament-detail.tsx`). **Bevisst grense, forklart i selve UI-et:** + endrer spillerpoolen, IKKE et lags allerede frosne + `handicap_index_snapshot` (ADR-007) — reproduserbarhet for allerede + opprettede lag er et bevisst, tidligere designvalg, ikke noe denne + fiksen skulle endre. Skjemaet sier dette rett ut i stedet for å late som + endringen slår inn overalt. + **Scratch-verifisert, 9 sjekker:** spiller-PATCH (hcp-endring, delvis + PATCH lar andre felt stå urørt, tomt PATCH avvist, ukjent id gir 404), + DEN KRITISKE sjekken (en spillers allerede frosne roster-snapshot for et + eksisterende lag forble UENDRET etter en påfølgende spiller-PATCH — + bekrefter ADR-007 fortsatt holder), dato-utledning (ingen økter → null, + to økter 11./12. juli → riktig utledet spenn, eksplisitt satt dato + vinner over utledet). Ekte typesjekket produksjonsbuild kjørt og + bekreftet. + **Ikke rullet ut ennå** — ingen migrasjon, venter på + utrullingsbekreftelse for `teecup_api`+`teecup_frontend`. + Neste steg: 1. **Oppfølgingspunkt fra PWA-runden, eksplisitt notert på brukerens forespørsel:** offline-scoreregistrering er ALDRI browser-testet i diff --git a/FEATURE_BACKLOG.md b/FEATURE_BACKLOG.md index a886e17..75ff5f8 100644 --- a/FEATURE_BACKLOG.md +++ b/FEATURE_BACKLOG.md @@ -920,20 +920,17 @@ Root cause funnet ved kodegjennomgang for de tre første (ikke bare gjettet); det fjerde er et reelt manglende UI-element, ingen kodefeil. Ingen av de fire er fikset i denne runden — kun dokumentert slik at de ikke går i glemmeboken. -1. **Dashboard-turneringskortet viser "Ingen datoer satt" selv om øktene - (rundene) har dato/klokkeslett satt.** `components/tournament-card.tsx` - leser `tournament.start_date`/`end_date` — et EGET, frittstående felt på - selve turneringen (ADR-015), atskilt fra `session.scheduled_at` (dato per - økt/runde). Grep bekrefter: disse to tournament-feltene LESES tre steder - i frontend (`dashboard.tsx`, `public-tournament.tsx`, `public-club.tsx`), - men skrives INGEN steder — det finnes ingen UI for å sette dem i det hele - tatt. Kortet vil derfor alltid vise "ingen datoer satt", uansett hvor mange - økter som har fått en `scheduled_at`, fordi feltet det leser aldri kan bli - satt gjennom UI-et. To mulige retninger: (a) bygg et faktisk - `start_date`/`end_date`-skjemafelt på turneringen, eller (b) la kortet - utlede visningsdatoen fra øktenes `scheduled_at`-spenn i stedet for et - eget, separat felt — sistnevnte er trolig det organisatoren faktisk - forventer. +1. **✅ FIKSET 2026-07-19/20 (ADR-030).** Dashboard-turneringskortet viste + "Ingen datoer satt" selv om øktene (rundene) hadde dato/klokkeslett + satt — bekreftet av brukeren med et faktisk skjermbilde (en økt tydelig + planlagt til "lør. 11. juli", men kortet viste fortsatt "Ingen datoer + satt"). Root cause: `tournament.start_date`/`end_date` var et EGET, + frittstående felt (ADR-015) som INGEN UI noensinne satte — helt atskilt + fra `session.scheduled_at`. Valgte retning (b) fra de to opprinnelig + skisserte alternativene: `GET /orgs/{id}/tournaments` utleder nå + datospennet fra øktenes `scheduled_at` (COALESCE med et evt. eksplisitt + satt `start_date`/`end_date`, som fortsatt vinner om det noensinne + settes). Se ADR-030 for full detalj og verifisering. 2. **`/orgs/{id}/members`-siden gir en rå API-404 (`{"detail":"Not Found"}`), ikke medlemssiden.** Bekreftet root cause, ikke bare @@ -979,7 +976,38 @@ er fikset i denne runden — kun dokumentert slik at de ikke går i glemmeboken. **Ingenting av dette er fikset ennå** — kun diagnostisert og notert på brukerens eksplisitte instruks, for å ikke gå i glemmeboken mens PWA-runden -prioriteres. +prioriteres. Punkt 1 (datovisning) senere ✅ fikset, se over (ADR-030). + +--- + +### Rediger spiller (spillerpool) — ✅ BYGGET OG SCRATCH-VERIFISERT 2026-07-19/20 + +Reist av brukeren samme runde som ADR-030: ingen vei fantes til å rette en +feilregistrert spiller (f.eks. en HCP-skrivefeil) etter opprettelse — kun +`POST` fantes for `player`. + +Ny `PATCH /orgs/{id}/players/{id}` (`app/routers/players.py`), vanlig +`exclude_unset`-PATCH-mønster, dekker alle spillerfelt (navn, HCP, kjønn, +mobil, e-post, fødselsdato, kallenavn, land, klubb, medlemsnummer). Frontend: +ny "Rediger spiller"-handling i rosterradens "⋮"-meny +(`tournament-detail.tsx`), inline skjema for navn/HCP/kjønn. + +**Viktig, bevisst grense — kommunisert i selve UI-et, ikke skjult:** dette +endrer spilleren i POOLEN (brukes ved fremtidig rostring), IKKE et lags +allerede FROSNE `team_roster.handicap_index_snapshot` (ADR-007 — reproduser- +barhet for allerede opprettede turneringer/lag er et bevisst designvalg, ikke +noe denne rundens fiks endrer på). Skjemaet viser en tydelig forklarende +tekst om dette; å oppdatere et allerede rostret lags viste HCP-tall krever +fortsatt å fjerne og legge til spilleren på nytt (eksisterende funksjon). + +**Scratch-verifisert:** HCP-endring lykkes, delvis PATCH (kun navn) lar HCP +stå urørt, tomt PATCH avvist (400), ukjent spiller-id gir 404, og — den +kritiske sjekken — en spillers allerede frosne roster-snapshot for et +EKSISTERENDE lag forble uendret etter en påfølgende spiller-PATCH (bekrefter +ADR-007 fortsatt holder). Ekte typesjekket produksjonsbuild kjørt og +bekreftet. + +**Ikke rullet ut ennå** — se CLAUDE.md-status. --- diff --git a/app/routers/players.py b/app/routers/players.py index 07ebb94..267359c 100644 --- a/app/routers/players.py +++ b/app/routers/players.py @@ -17,7 +17,7 @@ from pydantic import BaseModel, EmailStr, Field from ..auth import get_authorized_org from ..db import org_connection -from ..errors import translate_db_errors +from ..errors import app_error, translate_db_errors router = APIRouter() @@ -93,3 +93,57 @@ async def create_player( body.club_member_number, ) return Player(**dict(row)) + + +class PlayerUpdate(BaseModel): + """Alle felt valgfrie -- kun de som faktisk sendes med blir endret + (PATCH-semantikk via Pydantic sin exclude_unset, se update_player). + + Endrer KUN player-raden (spillerpoolen) -- rører ALDRI en allerede + frosset team_roster.handicap_index_snapshot (ADR-007): en HCP- + korrigering her slår derfor ikke inn på et lag spilleren allerede er + rostret på i en turnering, kun ved FREMTIDIG rostring. Fjern og legg + til spilleren på nytt på laget for å oppdatere en allerede frosset + snapshot-verdi. + """ + + display_name: str | None = Field(default=None, min_length=1, max_length=200) + handicap_index: float | None = None + gender: str | None = Field(default=None, pattern="^[mfx]$") + mobile: str | None = None + email: EmailStr | None = None + birth_date: date | None = None + nickname: str | None = None + country: str | None = None + club: str | None = None + club_member_number: str | None = None + + +@router.patch("/orgs/{organization_id}/players/{player_id}", response_model=Player) +async def update_player( + player_id: str, + body: PlayerUpdate, + organization_id: str = Depends(get_authorized_org), +) -> Player: + updates = body.model_dump(exclude_unset=True) + if not updates: + raise app_error(400, "VALIDATION_FAILED", "Ingen felt å oppdatere.") + if updates.get("email") is not None: + updates["email"] = updates["email"].lower() + + set_clauses = [f"{key} = ${i}" for i, key in enumerate(updates, start=1)] + values = list(updates.values()) + values.append(player_id) + + async with org_connection(organization_id) as conn, translate_db_errors(): + row = await conn.fetchrow( + f""" + UPDATE player SET {", ".join(set_clauses)} + WHERE id = ${len(values)} + RETURNING {_PLAYER_COLUMNS} + """, + *values, + ) + if row is None: + raise app_error(404, "NOT_FOUND", "Spilleren finnes ikke.") + return Player(**dict(row)) diff --git a/app/routers/tournaments.py b/app/routers/tournaments.py index c593a93..22e473c 100644 --- a/app/routers/tournaments.py +++ b/app/routers/tournaments.py @@ -72,9 +72,33 @@ class Tournament(BaseModel): async def list_tournaments( organization_id: str = Depends(get_authorized_org), ) -> list[Tournament]: + # ADR-030: dashbord-kortet skal vise turneringens FAKTISKE datospenn. + # `tournament.start_date`/`end_date` er egne, eksplisitte felt (ADR-015) + # som ingen UI noensinne har hatt en vei til å SETTE -- alltid NULL i + # praksis. Eksplisitt satt verdi vinner hvis den noensinne blir satt + # (fremtidig PATCH-vei), ellers utledes spennet fra øktenes + # `scheduled_at` -- informasjonen finnes jo allerede der. async with org_connection(organization_id) as conn: rows = await conn.fetch( - f"SELECT {_TOURNAMENT_COLUMNS} FROM tournament ORDER BY created_at DESC" + """ + SELECT t.id::text, t.name, t.status::text, + COALESCE(t.start_date, s.min_date) AS start_date, + COALESCE(t.end_date, s.max_date) AS end_date, + t.visibility, t.description, t.registration_deadline, + t.registration_capacity, t.registration_overflow_policy, + t.registration_requires_approval, t.hero_image_key, + t.join_code + FROM tournament t + LEFT JOIN ( + SELECT tournament_id, + min(scheduled_at)::date AS min_date, + max(scheduled_at)::date AS max_date + FROM session + WHERE scheduled_at IS NOT NULL + GROUP BY tournament_id + ) s ON s.tournament_id = t.id + ORDER BY t.created_at DESC + """ ) return [Tournament(**dict(r)) for r in rows] diff --git a/frontend/components/tournament-detail.tsx b/frontend/components/tournament-detail.tsx index 5ece325..d496218 100644 --- a/frontend/components/tournament-detail.tsx +++ b/frontend/components/tournament-detail.tsx @@ -11,6 +11,7 @@ import { KeyRound, MessageCircle, MoreVertical, + Pencil, Plus, Star, Trash2, @@ -36,6 +37,7 @@ type ApiPlayer = { id: string display_name: string handicap_index: number | null + gender: "m" | "f" | "x" | null } type ApiRosterEntry = { @@ -195,6 +197,30 @@ export function TournamentDetail({ } } + // 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.") + } + } + async function toggleCaptain(teamId: string, rosterId: string, current: boolean) { setError(null) try { @@ -374,6 +400,7 @@ export function TournamentDetail({ onRemovePlayer={removePlayer} onAddExisting={addExistingPlayer} onAddNew={addNewPlayer} + onUpdatePlayer={updatePlayer} onConcedeTournament={concedeTournament} /> ) @@ -523,6 +550,7 @@ function TeamPanel({ onRemovePlayer, onAddExisting, onAddNew, + onUpdatePlayer, onConcedeTournament, }: { team: Team @@ -534,9 +562,14 @@ function TeamPanel({ onRemovePlayer: (teamId: string, rosterId: string) => void onAddExisting: (teamId: string, playerId: string) => void onAddNew: (teamId: string, name: string, handicap?: number) => void + onUpdatePlayer: ( + playerId: string, + updates: { display_name?: string; handicap_index?: number | null; gender?: string | null }, + ) => void onConcedeTournament: (teamId: string) => Promise }) { const [confirmingId, setConfirmingId] = useState(null) + const [editingPlayerId, setEditingPlayerId] = useState(null) const [confirmingGiveUp, setConfirmingGiveUp] = useState(false) const [giveUpError, setGiveUpError] = useState(null) const [givingUp, setGivingUp] = useState(false) @@ -589,6 +622,25 @@ function TeamPanel({ )} {team.roster.map((entry) => { const confirming = confirmingId === entry.id + const editing = editingPlayerId === entry.player_id + + if (editing) { + const player = pool.find((p) => p.id === entry.player_id) + return ( +
  • + { + onUpdatePlayer(entry.player_id, updates) + setEditingPlayerId(null) + }} + onCancel={() => setEditingPlayerId(null)} + /> +
  • + ) + } if (confirming) { return ( @@ -653,6 +705,13 @@ function TeamPanel({