Update Todos
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.
This commit is contained in:
parent
a5de225afe
commit
16f3a163c1
7 changed files with 331 additions and 18 deletions
|
|
@ -326,7 +326,8 @@
|
||||||
"Bash(curl -s http://localhost:18800/health)",
|
"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_hcp_fix.py)",
|
||||||
"Bash(python3 /tmp/claude-1000/-opt-teecup/a8bd2fc3-4b9c-4682-a2be-cf36e143de78/scratchpad/test_tee_gender.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": [
|
"additionalDirectories": [
|
||||||
"/opt/teeoff/deploy",
|
"/opt/teeoff/deploy",
|
||||||
|
|
|
||||||
|
|
@ -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å)
|
## Åpne spørsmål (ikke besluttet ennå)
|
||||||
|
|
||||||
Disse må avklares før eller under de relevante fasene:
|
Disse må avklares før eller under de relevante fasene:
|
||||||
|
|
|
||||||
31
CLAUDE.md
31
CLAUDE.md
|
|
@ -1907,6 +1907,37 @@ Ferdig og verifisert:
|
||||||
utrulling. Begge containere boot-et rent, `/health`/`dashboard` → 200,
|
utrulling. Begge containere boot-et rent, `/health`/`dashboard` → 200,
|
||||||
`teeoff.no` upåvirket.
|
`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:
|
Neste steg:
|
||||||
1. **Oppfølgingspunkt fra PWA-runden, eksplisitt notert på brukerens
|
1. **Oppfølgingspunkt fra PWA-runden, eksplisitt notert på brukerens
|
||||||
forespørsel:** offline-scoreregistrering er ALDRI browser-testet i
|
forespørsel:** offline-scoreregistrering er ALDRI browser-testet i
|
||||||
|
|
|
||||||
|
|
@ -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
|
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.
|
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
|
1. **✅ FIKSET 2026-07-19/20 (ADR-030).** Dashboard-turneringskortet viste
|
||||||
(rundene) har dato/klokkeslett satt.** `components/tournament-card.tsx`
|
"Ingen datoer satt" selv om øktene (rundene) hadde dato/klokkeslett
|
||||||
leser `tournament.start_date`/`end_date` — et EGET, frittstående felt på
|
satt — bekreftet av brukeren med et faktisk skjermbilde (en økt tydelig
|
||||||
selve turneringen (ADR-015), atskilt fra `session.scheduled_at` (dato per
|
planlagt til "lør. 11. juli", men kortet viste fortsatt "Ingen datoer
|
||||||
økt/runde). Grep bekrefter: disse to tournament-feltene LESES tre steder
|
satt"). Root cause: `tournament.start_date`/`end_date` var et EGET,
|
||||||
i frontend (`dashboard.tsx`, `public-tournament.tsx`, `public-club.tsx`),
|
frittstående felt (ADR-015) som INGEN UI noensinne satte — helt atskilt
|
||||||
men skrives INGEN steder — det finnes ingen UI for å sette dem i det hele
|
fra `session.scheduled_at`. Valgte retning (b) fra de to opprinnelig
|
||||||
tatt. Kortet vil derfor alltid vise "ingen datoer satt", uansett hvor mange
|
skisserte alternativene: `GET /orgs/{id}/tournaments` utleder nå
|
||||||
økter som har fått en `scheduled_at`, fordi feltet det leser aldri kan bli
|
datospennet fra øktenes `scheduled_at` (COALESCE med et evt. eksplisitt
|
||||||
satt gjennom UI-et. To mulige retninger: (a) bygg et faktisk
|
satt `start_date`/`end_date`, som fortsatt vinner om det noensinne
|
||||||
`start_date`/`end_date`-skjemafelt på turneringen, eller (b) la kortet
|
settes). Se ADR-030 for full detalj og verifisering.
|
||||||
utlede visningsdatoen fra øktenes `scheduled_at`-spenn i stedet for et
|
|
||||||
eget, separat felt — sistnevnte er trolig det organisatoren faktisk
|
|
||||||
forventer.
|
|
||||||
|
|
||||||
2. **`/orgs/{id}/members`-siden gir en rå API-404 (`{"detail":"Not
|
2. **`/orgs/{id}/members`-siden gir en rå API-404 (`{"detail":"Not
|
||||||
Found"}`), ikke medlemssiden.** Bekreftet root cause, ikke bare
|
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å
|
**Ingenting av dette er fikset ennå** — kun diagnostisert og notert på
|
||||||
brukerens eksplisitte instruks, for å ikke gå i glemmeboken mens PWA-runden
|
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ from pydantic import BaseModel, EmailStr, Field
|
||||||
|
|
||||||
from ..auth import get_authorized_org
|
from ..auth import get_authorized_org
|
||||||
from ..db import org_connection
|
from ..db import org_connection
|
||||||
from ..errors import translate_db_errors
|
from ..errors import app_error, translate_db_errors
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
@ -93,3 +93,57 @@ async def create_player(
|
||||||
body.club_member_number,
|
body.club_member_number,
|
||||||
)
|
)
|
||||||
return Player(**dict(row))
|
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))
|
||||||
|
|
|
||||||
|
|
@ -72,9 +72,33 @@ class Tournament(BaseModel):
|
||||||
async def list_tournaments(
|
async def list_tournaments(
|
||||||
organization_id: str = Depends(get_authorized_org),
|
organization_id: str = Depends(get_authorized_org),
|
||||||
) -> list[Tournament]:
|
) -> 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:
|
async with org_connection(organization_id) as conn:
|
||||||
rows = await conn.fetch(
|
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]
|
return [Tournament(**dict(r)) for r in rows]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
KeyRound,
|
KeyRound,
|
||||||
MessageCircle,
|
MessageCircle,
|
||||||
MoreVertical,
|
MoreVertical,
|
||||||
|
Pencil,
|
||||||
Plus,
|
Plus,
|
||||||
Star,
|
Star,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
|
@ -36,6 +37,7 @@ type ApiPlayer = {
|
||||||
id: string
|
id: string
|
||||||
display_name: string
|
display_name: string
|
||||||
handicap_index: number | null
|
handicap_index: number | null
|
||||||
|
gender: "m" | "f" | "x" | null
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApiRosterEntry = {
|
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) {
|
async function toggleCaptain(teamId: string, rosterId: string, current: boolean) {
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
|
|
@ -374,6 +400,7 @@ export function TournamentDetail({
|
||||||
onRemovePlayer={removePlayer}
|
onRemovePlayer={removePlayer}
|
||||||
onAddExisting={addExistingPlayer}
|
onAddExisting={addExistingPlayer}
|
||||||
onAddNew={addNewPlayer}
|
onAddNew={addNewPlayer}
|
||||||
|
onUpdatePlayer={updatePlayer}
|
||||||
onConcedeTournament={concedeTournament}
|
onConcedeTournament={concedeTournament}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
@ -523,6 +550,7 @@ function TeamPanel({
|
||||||
onRemovePlayer,
|
onRemovePlayer,
|
||||||
onAddExisting,
|
onAddExisting,
|
||||||
onAddNew,
|
onAddNew,
|
||||||
|
onUpdatePlayer,
|
||||||
onConcedeTournament,
|
onConcedeTournament,
|
||||||
}: {
|
}: {
|
||||||
team: Team
|
team: Team
|
||||||
|
|
@ -534,9 +562,14 @@ function TeamPanel({
|
||||||
onRemovePlayer: (teamId: string, rosterId: string) => void
|
onRemovePlayer: (teamId: string, rosterId: string) => void
|
||||||
onAddExisting: (teamId: string, playerId: string) => void
|
onAddExisting: (teamId: string, playerId: string) => void
|
||||||
onAddNew: (teamId: string, name: string, handicap?: number) => 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<string | null>
|
onConcedeTournament: (teamId: string) => Promise<string | null>
|
||||||
}) {
|
}) {
|
||||||
const [confirmingId, setConfirmingId] = useState<string | null>(null)
|
const [confirmingId, setConfirmingId] = useState<string | null>(null)
|
||||||
|
const [editingPlayerId, setEditingPlayerId] = useState<string | null>(null)
|
||||||
const [confirmingGiveUp, setConfirmingGiveUp] = useState(false)
|
const [confirmingGiveUp, setConfirmingGiveUp] = useState(false)
|
||||||
const [giveUpError, setGiveUpError] = useState<string | null>(null)
|
const [giveUpError, setGiveUpError] = useState<string | null>(null)
|
||||||
const [givingUp, setGivingUp] = useState(false)
|
const [givingUp, setGivingUp] = useState(false)
|
||||||
|
|
@ -589,6 +622,25 @@ function TeamPanel({
|
||||||
)}
|
)}
|
||||||
{team.roster.map((entry) => {
|
{team.roster.map((entry) => {
|
||||||
const confirming = confirmingId === entry.id
|
const confirming = confirmingId === entry.id
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (confirming) {
|
if (confirming) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -653,6 +705,13 @@ function TeamPanel({
|
||||||
<MoreVertical aria-hidden="true" className="size-4" />
|
<MoreVertical aria-hidden="true" className="size-4" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="w-52 rounded-2xl p-1.5">
|
<DropdownMenuContent align="end" className="w-52 rounded-2xl p-1.5">
|
||||||
|
<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>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => onToggleCaptain(team.id, entry.id, entry.is_captain)}
|
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"
|
className="flex cursor-pointer items-center gap-2 rounded-xl px-3 py-2.5 text-sm font-semibold"
|
||||||
|
|
@ -956,6 +1015,80 @@ function AddPlayerControl({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// --- Helpers ---------------------------------------------------------------
|
// --- Helpers ---------------------------------------------------------------
|
||||||
|
|
||||||
function formatHandicap(handicap: number | null | undefined) {
|
function formatHandicap(handicap: number | null | undefined) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue