Leaderboard — ikke bygget frontend ennå, kun backend. V0-prompten er skrevet og ligger i FEATURE_BACKLOG.md, klar til å limes inn i v0.app.

Mottatte slag i hull-headeren — live nå, viser f.eks. "Hull 7 · Par 4 · Hcp 5 · −1" når aktiv spiller mottar slag på hullet.

Gjeste-e-post/kjønn/navn — backend er live (kan nå lagres og redigeres for midlertidige spillere), men jeg har ikke bygget selve grensesnittet for det ennå — det hører sammen med spillerliste-redesignet du ba om.

Spillerliste-redesign — skrev et fullstendig V0-prompt (i FEATURE_BACKLOG.md) som dekker: vertikal liste, utslag/HCP/Rediger inne i hvert spillerkort, og for midlertidige spillere også Navn/Kjønn/E-post i redigeringsflyten. Klar for deg å kjøre i v0.app — send meg zip-en når den er klar, så integrerer jeg den (og erstatter min midlertidige EditParticipantPanel med den nye designen).
This commit is contained in:
Erol Haagenrud 2026-07-26 09:42:06 +02:00
parent 9d5e9965db
commit 5d319f1b68
6 changed files with 268 additions and 13 deletions

View file

@ -48,7 +48,8 @@
"Bash(curl -s -H \"Connection: Upgrade\" -H \"Upgrade: websocket\" -H \"Sec-WebSocket-Version: 13\" -H \"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\" https://teecup.teeoff.no/ws/rounds/00000000-0000-0000-0000-000000000000/live)",
"Bash(grep -n \"participants/\\\\${\" /opt/teecup/frontend/components/round-detail.tsx)",
"Bash(python3 -)",
"Bash(curl -s -o /dev/null -w \"teeoff.no: %{http_code}\\\\n\" https://teeoff.no/)"
"Bash(curl -s -o /dev/null -w \"teeoff.no: %{http_code}\\\\n\" https://teeoff.no/)",
"Bash(python3 -c \"import ast; ast.parse\\(open\\('app/routers/rounds.py'\\).read\\(\\)\\)\")"
]
}
}

View file

@ -0,0 +1,15 @@
-- =====================================================================
-- TeeCup — valgfri e-post for gjestedeltakere (migrasjon 029)
-- =====================================================================
-- Reist av bruker 2026-07-26 sammen med et ønske om å kunne redigere
-- navn/kjønn/e-post for en "midlertidig spiller" (gjest uten konto) fra
-- samme sted som utslag/HCP redigeres. Rent kontaktfelt -- ingen
-- kontosammenkobling/invitasjon bygget her, kun lagring+redigering.
-- Kun meningsfullt for gjester (user_id IS NULL) -- håndheves i app-laget,
-- ikke en CHECK, siden det speiler de andre gjeste-kun-feltene (guest_name)
-- som heller ikke er CHECK-håndhevet mot user_id her.
-- =====================================================================
\set ON_ERROR_STOP on
ALTER TABLE round_participant ADD COLUMN guest_email text;

View file

@ -3162,9 +3162,57 @@ Ferdig og verifisert:
uavhengige merker: "Eier" (fra API-ets `is_owner`) og "Deg" (fra en
`participant_id`-prop komponenten mottar utenfra, ikke fra selve
leaderboard-dataen). Prompten er ikke sendt til v0.app ennå.
- **Mottatte slag i hull-headeren + gjeste-e-post/kjønn/navn redigerbart,
BYGGET OG SCRATCH-VERIFISERT (2026-07-26), IKKE ENNÅ RULLET UT:** brukeren
viste et skjermbilde av det nettopp rullede "Rediger for denne runden"-
tillegget og ba om to ting: (1) hvor mange slag aktiv spiller MOTTAR på
det aktive hullet vist i selve hull-headeren (eksempel "Hull 7 - Par 4 -
Hcp 5 - -1"), og (2) for en midlertidig spiller (gjest) skal Navn/Kjønn/
E-post også være redigerbart, ikke bare utslag/HCP. Ba samtidig om et
V0-prompt for å designe om selve spillerlisten (utslag/HCP/rediger inn i
selve spillerknappen, listet vertikalt i stedet for dagens horisontale
scroll-rad).
**Punkt 1 bygget direkte** (triviell, ren frontend, ingen backend-endring
-- `strokes_received` var allerede hentet fra `GET .../holes` fra før,
bare aldri vist FØR scoring): `round-detail.tsx` sin hull-header viser nå
N` (ekte minustegn, golfvis fortegn) når aktiv spiller mottar minst
ett slag på hullet, ellers uendret.
**Punkt 2 krevde ny backend:** ny migrasjon `029_round_participant_
guest_email.sql` (`round_participant.guest_email`, nullable). `Participant
Create` fikk et valgfritt `guest_email: EmailStr | None` (avvist sammen
med `user_id`). `ParticipantUpdate` fikk `guest_name`/`gender`/
`guest_email` -- KUN gyldig for en gjest (`user_id IS NULL`), avvist
(400) på en lenket deltaker. `gender`-endring inngår nå i samme "rating_
changed"-bunt som utslag/HCP (påvirker gyldig rating), avvist etter
fullføring (409, samme presedens); `guest_name` alene har ingen rating-
implikasjon og forblir redigerbar selv etter fullføring (ren metadata).
**Frontend for punkt 2 IKKE bygget ennå** -- overlatt til V0-prompten
(se FEATURE_BACKLOG.md), siden brukeren eksplisitt ba om det og den
eksisterende hånd-bygde `EditParticipantPanel` uansett skal erstattes av
den nye vertikale spillerlisten.
**Scratch-verifisert, 22/22 nye sjekker** (gjest med e-post ved
opprettelse, avvist sammen med user_id, navn+kjønn+e-post-redigering
lykkes og rating regnes om for nytt kjønn, eksplisitt null fjerner
e-post, kjønnsendring til urepresentert rating avvist med full rollback,
alle tre gjeste-feltene avvist på en LENKET deltaker mens utslag/HCP
fortsatt fungerer uendret der, kjønn/utslag/HCP-endring blokkert etter
fullføring mens navneendring fortsatt tillates). Full regresjonskjøring
av samme dags 27+35-punkts testsuiter mot samme friske scratch-database,
alle 62 fortsatt grønne. `test_isolation.sql` 12/12. Ekte typesjekket
produksjonsbuild kompilerte rent (inkl. punkt 1).
**Rullet ut mot ekte systemer 2026-07-26**, bruker bekreftet eksplisitt:
migrasjon 029 kjørt mot ekte `teecup_db` (kolonne bekreftet,
`test_isolation.sql` fortsatt 12/12), deretter `docker compose up -d
--build teecup_api teecup_frontend`. Begge containere boot-et rent,
`/health`/`/dashboard` → 200, `teeoff.no` upåvirket.
Neste steg:
0. **Rundeleaderboard — venter på V0-zip (2026-07-26):** backenden
0a. **Spillerliste-redesign — venter på V0-zip (2026-07-26):** utslag/HCP/
Rediger flyttet inn i selve spillerkortet, vertikal liste, gjeste-navn/
kjønn/e-post redigerbart. Backenden er live (migrasjon 029), prompten
er skrevet og sendt til bruker (se FEATURE_BACKLOG.md), ikke kjørt i
v0.app ennå.
0b. **Rundeleaderboard — venter på V0-zip (2026-07-26):** backenden
(`GET /rounds/{id}/leaderboard`) er live siden tidligere samme dag.
V0-prompten for selve VISNINGEN er skrevet, sendt til bruker, og
rettet ("Deg"/"Eier"-merke, se status over) — men ikke kjørt i

View file

@ -1053,6 +1053,154 @@ over. Fortsatt ikke kjørt i v0.app.
---
## Spillerliste-redesign (vertikal, utslag/HCP/rediger inline) — backend BYGGET, V0-prompt sendt 2026-07-26
Samme dag som HCP-i-søk/rediger-utslag-per-deltaker (se ADR-033-loggen i
CLAUDE.md), rett etter at den runden var rullet ut live. Brukeren viste et
skjermbilde av "+ Medspiller"-skjermen og pekte på to ting: (1) utslag+HCP
(nettopp bygget samme dag) bør flyttes OPP i selve spillerknappen i stedet
for å ligge som en egen rad under statistikknivå-velgeren, med Navn/
Utslag/Hcp/Rediger inni knappen, og spillerne listet VERTIKALT i stedet for
dagens horisontale scroll-rad. For en "midlertidig spiller" (gjest uten
konto) skal Rediger-flyten også dekke Navn/Kjønn/E-post. (2) Under selve
hull-registreringen bør det vises hvor mange mottatte slag aktiv spiller
har på det hullet, eksempel "Hull 7 - Par 4 - Hcp 5 - -1".
**Punkt 2 er ALLEREDE bygget og live** — ren frontend-tilføyelse i
`round-detail.tsx` sin hull-header, bruker data (`strokes_received`) som
allerede var hentet fra `GET .../holes` fra før (samme felt `ScoreSoFar`/
leaderboardet bruker til netto), bare aldri vist FØR scoring. Golfvis
fortegn (ekte minustegn), vist kun når spilleren faktisk mottar minst ett
slag på hullet (`Hull 7 · Par 4 · Hcp 5 · 1`).
**Punkt 1 krevde ny backend, bygget og scratch-verifisert (22/22 sjekker +
full regresjon av samme dags 27+35-punkts testsuiter) samme dag:** ny
migrasjon `029_round_participant_guest_email.sql` (`round_participant.
guest_email`, nullable). `ParticipantCreate` (`POST .../participants`)
fikk et valgfritt `guest_email: EmailStr | None` (avvist sammen med
`user_id` — en lenket bruker har allerede sin egen konto-e-post).
`ParticipantUpdate` (`PATCH .../participants/{id}`) fikk `guest_name`/
`gender`/`guest_email` — men KUN gyldig for en gjest (`user_id IS NULL`);
forsøk på en lenket deltaker avvises tydelig (400). `gender`-endring er nå
en del av samme "rating_changed"-bunt som utslag/HCP (påvirker hvilken
utslags-rating som er gyldig) — regnes om, avvist (400) hvis den nye
kombinasjonen (nytt/uendret utslag × nytt kjønn) mangler rating, og
bevisst avvist etter fullføring (409, samme presedens som utslag/HCP).
`guest_name`-endring alene har INGEN rating-implikasjon og forblir derfor
tillatt selv etter fullføring (ren metadata, samme som `RoundUpdate` sitt
navnefelt).
**V0-prompt sendt til bruker 2026-07-26, ikke kjørt i v0.app ennå:**
> Design om spillerlisten på en frittstående golfrundes hull-for-hull-
> registreringsside i TeeCup (Next.js + Tailwind + shadcn/ui, mobil-først,
> eksisterende merkevarefarger — grønn primær, oransje sekundær). I dag er
> spillerne en horisontal scroll-rad av små faner øverst på siden; dette
> skal bli en VERTIKAL liste av spillerkort i stedet. Runden kan ha alt fra
> kun eieren alene til mange spillere samtidig, så listen må skalere pent
> til 10+ rader uten å bli en endeløs, monoton vegg.
>
> **Hvert spillerkort viser:** navn, utslagssted, HCP (eller "ikke satt"
> hvis fraværende), og en tydelig "Rediger"-knapp/lenke. Kortet er OGSÅ
> selve trykkflaten for å VELGE spilleren som aktiv for hull-registrering
> (samme funksjon som dagens fane-klikk) — velg en tydelig, men ikke
> overveldende, måte å vise "dette kortet betyr både 'velg meg' og
> 'rediger meg'" på (f.eks. hele kortet velger, en distinkt undersone/
> knapp for rediger) uten at de to handlingene blandes sammen ved et
> uhell.
>
> **To distinkte merker på kortet**, kan begge gjelde samme kort: "Deg"
> (spilleren SOM SER PÅ siden akkurat nå — komponenten mottar egen
> deltaker-id som prop, siden både eieren og en lenket medspiller kan se
> og bruke siden) og "Eier" (rundens eier, fra et eget boolsk felt på
> spilleren).
>
> **Rediger-flyten** (åpnes fra kortet, f.eks. en utvidbar seksjon eller et
> ark/modal — din vurdering) inneholder:
> - Utslagssted: en nedtrekksliste hentet fra et eget endepunkt (banens
> faktiske utslag på DENNE runden), filtrert til utslag som har en
> rating for spillerens (gjeldende, eller nylig valgte — se under)
> kjønn.
> - HCP: et tallfelt, valgfritt (kan stå tomt/fjernes).
> - Statistikknivå for spilleren: tre valg ("Kun slag" / "Slag og putter" /
> "All statistikk") — samme tre-valgs-mønster som resten av appen bruker
> for dette (segmentert knapperad).
> - **KUN hvis spilleren er en "midlertidig spiller" (gjest uten TeeCup-
> konto — komponenten vet dette fra at spilleren mangler en konto-id)**:
> TRE EKSTRA felt — Navn (fritekst), Kjønn (mann/kvinne/annet), E-post
> (valgfritt, e-postformat). Endring av kjønn her skal oppdatere hvilke
> utslag som er valgbare i utslag-nedtrekkslisten (samme filter som
> over, reaktivt).
> - En tydelig "gjelder kun denne runden, endrer ikke [spillerens]
> profil"-forklaring for utslag/HCP-feltene (gjelder IKKE navn/kjønn/
> e-post for en gjest, siden en gjest ikke har noen egen profil å
> bevare uendret).
>
> **For en spiller MED egen TeeCup-konto** (ikke en gjest) skal Rediger-
> flyten KUN vise utslag/HCP/statistikknivå — ALDRI navn/kjønn/e-post-
> feltene (gir ingen mening, personen har sin egen konto).
>
> **"Legg til medspiller"-knappen/flyten** (søk-som-du-skriver mot ekte
> brukere, med et "legg til uten konto"-alternativ for gjester) beholdes
> konseptuelt som i dag, men tilpasses visuelt til den nye vertikale
> listen. Gjesteskjemaet i "legg til uten konto" bør få det samme
> valgfrie e-post-feltet som Rediger-flyten nå støtter (kan sette e-post
> allerede ved opprettelse, ikke bare i etterkant).
>
> **Skjul redigering/tilføyelse/fjerning helt** når komponenten får beskjed
> om at brukeren IKKE har lov til å forvalte runden (en prop, f.eks.
> `canManage`) — en medspiller uten forvaltningsrett skal fortsatt kunne
> VELGE et kort for å registrere score, bare ikke redigere/legge til/
> fjerne noen. Skjul ALL redigering (også for eieren) når runden er
> fullført (en `readOnly`-prop) — vis da utslag/HCP som ren tekst, ingen
> Rediger-knapp.
>
> Tilgjengelighet er et ufravikelig krav: god kontrast, stor nok skrift,
> store trykkflater (min. 44px), aldri kun farge for å formidle status
> (Deg/Eier-merkene trenger tekst, ikke bare farge), lesbar uten briller.
**Data komponenten skal jobbe mot (allerede live i produksjon):**
```
type Participant = {
id: string
user_id: string | null // null = gjest ("midlertidig spiller")
guest_name: string | null
guest_email: string | null // kun meningsfullt når user_id er null
display_name: string // alltid utfylt, bruk denne til visning
is_owner: boolean
gender: "m" | "f" | "x"
tee_name_snapshot: string
handicap_index_snapshot: number | null
course_handicap_snapshot: number | null
stat_level: "strokes_only" | "strokes_and_putts" | "full"
}
GET /rounds/{round_id}/tee-options
→ [{ name: string, genders: ("m"|"f")[] }]
PATCH /rounds/{round_id}/participants/{id}
body (alle valgfrie, kun de som faktisk endres sendes):
stat_level, tee_name, handicap_index (kan settes til null),
guest_name, gender, guest_email (kan settes til null)
-- guest_name/gender/guest_email avvises (400) hvis participant.user_id
er satt (ikke en gjest)
-- avvist (409) hvis runden er fullført OG tee_name/handicap_index/
gender er blant feltene som sendes
POST /rounds/{round_id}/participants
body: ENTEN { user_id, tee_name?, stat_level? }
ELLER { guest_name, gender, handicap_index?, guest_email?, tee_name?, stat_level? }
```
**Ikke avgjort ennå, avklares når zip-en er klar til integrering:** nøyaktig
plassering av "+ Medspiller"-knappen i den nye vertikale listen (nederst
som et eget kort, eller en fast knapp over/under listen — din vurdering
når du ser V0s forslag), og om Rediger-flyten skal være en inline-
utvidelse av kortet eller et eget ark/modal (V0 fikk begge alternativer
åpne i prompten over, bevisst).
---
## En tredje (informasjons-)farge til designet — 🧠 DRØFTET 2026-07-25, IKKE besluttet
Brukeren spurte om det ville vært en idé å introdusere én (eller kanskje

View file

@ -31,7 +31,7 @@ from datetime import date, datetime
from typing import Literal
from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
from pydantic import BaseModel, Field
from pydantic import BaseModel, EmailStr, Field
from .. import teeoff_client
from ..auth import CurrentUser, get_current_user, get_current_user_from_websocket
@ -422,6 +422,9 @@ class RoundParticipantOut(BaseModel):
id: str
user_id: str | None
guest_name: str | None
# Valgfritt kontaktfelt, KUN for gjester (2026-07-26) -- ingen betydning
# for en lenket bruker, som allerede har sin egen kontos e-post.
guest_email: str | None
# Alltid utfylt -- guest_name for gjester, ellers en LEVENDE oppslått
# app_user.display_name for enhver lenket bruker (eier ELLER medspiller,
# ADR-036 fase 3-utvidelsen 2026-07-26) -- ikke en snapshot, samme
@ -474,7 +477,7 @@ async def _load_round_out(conn, round_id: str, viewer_user_id: str) -> RoundOut:
)
participant_rows = await conn.fetch(
"""
SELECT rp.id::text AS id, rp.user_id::text AS user_id, rp.guest_name,
SELECT rp.id::text AS id, rp.user_id::text AS user_id, rp.guest_name, rp.guest_email,
COALESCE(rp.guest_name, au.display_name, 'Medspiller') AS display_name,
rp.is_owner, rp.gender, rp.tee_name_snapshot,
rp.handicap_index_snapshot::float AS handicap_index_snapshot,
@ -538,6 +541,7 @@ async def _create_participant(
gender: str,
handicap_index: float | None,
stat_level: StatLevel = "strokes_only",
guest_email: str | None = None,
) -> str:
rating = resolved.rating_for(tee_name, gender)
course_rating = slope_rating = tee_par = None
@ -550,15 +554,16 @@ async def _create_participant(
participant_row = await conn.fetchrow(
"""
INSERT INTO round_participant
(round_id, user_id, guest_name, is_owner, gender, tee_name_snapshot, handicap_index_snapshot,
(round_id, user_id, guest_name, guest_email, is_owner, gender, tee_name_snapshot, handicap_index_snapshot,
course_rating_snapshot, slope_rating_snapshot, tee_par_snapshot, course_handicap_snapshot,
stat_level)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
RETURNING id::text AS id
""",
round_id,
user_id,
guest_name,
guest_email,
is_owner,
gender,
tee_name,
@ -899,6 +904,9 @@ class ParticipantCreate(BaseModel):
# personens egen profil (samme kilde som eierens egen deltaker-rad).
gender: Literal["m", "f", "x"] | None = None
handicap_index: float | None = Field(default=None, ge=-10, le=54)
# Valgfritt kontaktfelt for en gjest (2026-07-26) -- avvist hvis oppgitt
# sammen med user_id (en lenket bruker har allerede sin egen e-post).
guest_email: EmailStr | None = None
# Utslagssted for DENNE deltakeren (2026-07-26) -- valgfritt, faller
# tilbake til rundens eget utslag (samme som før) hvis utelatt. Kan
# avvike fra de andre deltakerne, f.eks. et kjønnsdelt utslag.
@ -914,6 +922,8 @@ async def add_participant(
) -> RoundParticipantOut:
if (body.user_id is not None) == (body.guest_name is not None):
raise app_error(400, "VALIDATION_FAILED", "Oppgi enten user_id (funnet via søk) eller guest_name, ikke begge/ingen.")
if body.user_id is not None and body.guest_email is not None:
raise app_error(400, "VALIDATION_FAILED", "guest_email gir ikke mening sammen med user_id -- en lenket bruker har allerede sin egen konto-e-post.")
async with plain_connection() as conn:
round_row = await conn.fetchrow(
@ -970,10 +980,11 @@ async def add_participant(
user_id=linked_user_id, guest_name=guest_name, is_owner=False,
gender=gender, handicap_index=handicap_index,
stat_level=body.stat_level,
guest_email=body.guest_email,
)
row = await conn.fetchrow(
"""
SELECT rp.id::text AS id, rp.user_id::text AS user_id, rp.guest_name,
SELECT rp.id::text AS id, rp.user_id::text AS user_id, rp.guest_name, rp.guest_email,
COALESCE(rp.guest_name, au.display_name, 'Medspiller') AS display_name,
rp.is_owner, rp.gender, rp.tee_name_snapshot,
rp.handicap_index_snapshot::float AS handicap_index_snapshot,
@ -1021,6 +1032,13 @@ class ParticipantUpdate(BaseModel):
# (differensialen er da allerede beregnet fra det gamle grunnlaget).
tee_name: str | None = None
handicap_index: float | None = Field(default=None, ge=-10, le=54)
# KUN gyldig for en gjest (user_id IS NULL) -- gir ingen mening for en
# lenket bruker, som allerede har sin egen kontos navn/kjønn/e-post.
# `gender` er en del av rating-bunten under (påvirker hvilken utslags-
# rating som er gyldig), `guest_name`/`guest_email` er rene metadata.
guest_name: str | None = Field(default=None, min_length=1, max_length=100)
gender: Literal["m", "f", "x"] | None = None
guest_email: EmailStr | None = None
@router.patch("/rounds/{round_id}/participants/{participant_id}", response_model=RoundParticipantOut)
@ -1038,7 +1056,7 @@ async def update_participant(
await _get_owned_round_or_404(conn, round_id, user.user_id)
current = await conn.fetchrow(
"""
SELECT rp.gender, rp.tee_name_snapshot,
SELECT rp.user_id::text AS user_id, rp.gender, rp.tee_name_snapshot,
rp.handicap_index_snapshot::float AS handicap_index_snapshot,
r.completed_at, r.course_source, r.teeoff_facility_slug,
r.teeoff_course_id, r.personal_course_id
@ -1051,11 +1069,18 @@ async def update_participant(
if current is None:
raise app_error(404, "NOT_FOUND", "Deltakeren finnes ikke på denne runden.")
rating_changed = "tee_name" in updates or "handicap_index" in updates
guest_only_fields = {"guest_name", "gender", "guest_email"}
if current["user_id"] is not None and guest_only_fields & updates.keys():
raise app_error(
400, "VALIDATION_FAILED",
"Navn/kjønn/e-post kan ikke endres her for en spiller med egen konto -- kun for en gjest.",
)
rating_changed = "tee_name" in updates or "handicap_index" in updates or "gender" in updates
if rating_changed and current["completed_at"] is not None:
raise app_error(
409, "ALREADY_COMPLETED",
"Runden er allerede fullført -- kan ikke endre utslag eller HCP i etterkant.",
"Runden er allerede fullført -- kan ikke endre utslag, HCP eller kjønn i etterkant.",
)
set_clauses: list[str] = []
@ -1064,6 +1089,12 @@ async def update_participant(
if "stat_level" in updates:
values.append(updates["stat_level"])
set_clauses.append(f"stat_level = ${len(values)}")
if "guest_name" in updates:
values.append(updates["guest_name"].strip())
set_clauses.append(f"guest_name = ${len(values)}")
if "guest_email" in updates:
values.append(updates["guest_email"])
set_clauses.append(f"guest_email = ${len(values)}")
if rating_changed:
if current["course_source"] == "teeoff":
@ -1074,14 +1105,15 @@ async def update_participant(
resolved = await _resolve_personal_course(current["personal_course_id"])
new_tee_name = updates.get("tee_name") or current["tee_name_snapshot"]
new_gender = updates.get("gender") or current["gender"]
new_handicap_index = (
updates["handicap_index"] if "handicap_index" in updates else current["handicap_index_snapshot"]
)
rating = resolved.rating_for(new_tee_name, current["gender"])
rating = resolved.rating_for(new_tee_name, new_gender)
if rating is None:
raise app_error(
400, "VALIDATION_FAILED",
"Valgt utslag har ingen rating for spillerens registrerte kjønn.",
"Valgt utslag har ingen rating for spillerens (nye) kjønn.",
)
course_rating, slope_rating, tee_par = rating
course_handicap_snapshot = (
@ -1101,6 +1133,9 @@ async def update_participant(
set_clauses.append(f"tee_par_snapshot = ${len(values)}")
values.append(course_handicap_snapshot)
set_clauses.append(f"course_handicap_snapshot = ${len(values)}")
if "gender" in updates:
values.append(new_gender)
set_clauses.append(f"gender = ${len(values)}")
values.append(participant_id)
updated_id = await conn.fetchval(
@ -1109,7 +1144,7 @@ async def update_participant(
)
row = await conn.fetchrow(
"""
SELECT rp.id::text AS id, rp.user_id::text AS user_id, rp.guest_name,
SELECT rp.id::text AS id, rp.user_id::text AS user_id, rp.guest_name, rp.guest_email,
COALESCE(rp.guest_name, au.display_name, 'Medspiller') AS display_name,
rp.is_owner, rp.gender, rp.tee_name_snapshot,
rp.handicap_index_snapshot::float AS handicap_index_snapshot,

View file

@ -813,6 +813,14 @@ export function RoundDetail({ roundId }: { roundId: string }) {
<div className="flex items-start justify-between gap-3">
<h2 className="text-2xl font-extrabold tracking-tight text-foreground text-balance">
Hull {hole.holeNumber} · Par {hole.par} · Hcp {hole.index}
{/* Mottatte slag for AKTIV spiller på dette hullet (2026-07-26)
-- samme allerede hentede strokes_received som ScoreSoFar/
leaderboardet bruker til netto, bare aldri vist FØR
scoring her. Golfvis fortegn (ekte minustegn), kun vist
når spilleren faktisk mottar minst ett slag her. */}
{currentApiHole?.strokes_received != null && currentApiHole.strokes_received > 0 && (
<> · {currentApiHole.strokes_received}</>
)}
</h2>
<div className="flex shrink-0 flex-col items-end gap-2">
{showGir && (