Ny runde-veiviseren: tre fikser (overleggsmeny, HCP-overstyring, statistikk-default)

- "Administrer runde"-dialogen lukker seg nå automatisk ved navigasjon
  til "Spillere og runde" (var stående åpen ved samme-rute-navigasjon).
- HCP kan nå overstyres for en kontokoblet medspiller fra første stund
  i veiviseren, ikke bare senere via "Runde og runde". Backend sitt
  add_participant æret aldri en eksplisitt overstyring for user_id,
  ulikt PATCH-endepunktet (ADR-038-avvik, nå rettet).
- Standard statistikknivå for en NY medspiller er nå "Kun slag" i
  stedet for å arve scoreførerens eget valg.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Erol Haagenrud 2026-08-21 08:05:05 +02:00
parent 6603e94fd3
commit a0163eaa16
5 changed files with 144 additions and 7 deletions

View file

@ -2955,7 +2955,13 @@ async def add_participant(
"Spilleren du valgte har ikke fullført profilen sin (mangler kjønn) og kan ikke legges til ennå.",
)
gender = target["gender"]
handicap_index = target["handicap_index"]
# Overstyring fra opprettelsesøyeblikket (2026-08-20, ADR-038
# sier allerede "brukes til Course Handicap i enhver runde med
# mindre eksplisitt overstyrt" -- PATCH-en (update_participant
# under) har alltid tillatt dette for en lenket bruker, POST
# gjorde det uventet IKKE). Faller tilbake til profilens egen
# verdi når organisatoren ikke har oppgitt noe eksplisitt.
handicap_index = body.handicap_index if body.handicap_index is not None else target["handicap_index"]
guest_name = None
guest_first_name = None
guest_last_name = None

View file

@ -80,7 +80,13 @@ export function Step3Players() {
{adding ? (
<AddPlayerPanel
course={state.course}
defaultStat={state.statLevel}
// Standard for EN ANNEN spiller enn den som fører score er "Kun
// slag", uansett hva scorefører selv har valgt for seg selv
// (2026-08-20 brukerfunn -- speilet tidligere feilaktig
// state.statLevel, som normalt er scoreførerens EGET, ofte
// høyere nivå). Organisatoren kan fortsatt sette et høyere nivå
// manuelt per medspiller.
defaultStat="score"
existing={state.players}
onCancel={() => setAdding(false)}
onAdd={addPlayer}
@ -230,10 +236,30 @@ function PlayerCard({
/>
</Field>
) : player.hcpFromProfile && !isOwner ? (
<Field label="HCP">
<div className="rounded-lg border border-[var(--nr-border)] bg-[var(--nr-surface-2)] px-3 py-2.5 text-sm text-[var(--nr-muted)]">
{player.hcp != null ? `${player.hcp} · hentes fra profilen` : "Hentes fra profilen…"}
</div>
// Redigerbar fra FØRSTE stund (2026-08-20 brukerfunn) -- var
// tidligere en read-only visning ("hentes fra profilen"), som
// tvang organisatoren til å legge til spilleren FØRST og
// overstyre HCP-en senere via "Runde og spillere" (der
// overstyring allerede fungerte, samme PATCH-felt). Forhånds-
// utfylt med profilverdien; urørt betyr fortsatt "bruk
// profilen", akkurat som før.
<Field
label="HCP"
optional
htmlFor={`${player.key}-hcp`}
hint="Hentes fra spillerens profil dersom du lar feltet stå urørt."
>
<TextInput
id={`${player.key}-hcp`}
inputMode="decimal"
placeholder={player.hcp == null ? "Henter fra profil…" : undefined}
value={player.hcp != null ? String(player.hcp) : ""}
onChange={(e) => {
const v = e.target.value
onChange({ hcp: v.trim() === "" ? null : Number.parseFloat(v.replace(",", ".")) })
}}
className="max-w-[10rem]"
/>
</Field>
) : null}

View file

@ -424,6 +424,13 @@ export function WizardProvider({
}
if (p.accountId) {
participantBody.user_id = p.accountId
// Overstyring fra FØRSTE stund (2026-08-20 brukerfunn) -- backend
// sin add_participant faller tilbake til spillerens profil-HCP
// når dette utelates/er null, akkurat som PATCH-en "Runde og
// runde"-panelet allerede bruker for samme overstyring senere.
// Uendret (`p.hcp` = profilverdien) sendes derfor trygt som
// no-op når organisatoren ikke har rørt feltet.
participantBody.handicap_index = p.hcp ?? null
} else {
participantBody.guest_first_name = p.firstName.trim()
participantBody.guest_last_name = p.lastName.trim() || null

View file

@ -303,9 +303,14 @@ function ManageRoundDialog({
</button>
</div>
{/* Reach the roster / team setup screen */}
{/* Reach the roster / team setup screen. `onClick={onClose}` (2026-08-20
brukerfunn): denne lenken navigerer ofte til KUN en søkeparameter-
endring samme rute (?tab=manage, se round-detail.tsx), som ikke
remounter RoundHeader -- uten dette ble dialogen stående åpen over
innholdet under, og måtte lukkes manuelt. */}
<Link
href={manageHref}
onClick={onClose}
className="flex min-h-14 items-center gap-3 rounded-2xl border border-border bg-background px-4 py-3 text-left transition-colors hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<span className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-muted text-muted-foreground">

View file

@ -0,0 +1,93 @@
"""
Overstyring av HCP for en KONTOKOBLET medspiller ved selve opprettelsen
(2026-08-20, brukerfunn: "Ny runde"-veiviseren kunne ikke overstyre HCP
for en nylig lagt til medspiller fra første stund -- kun i etterkant via
PATCH .../participants/{id}, som allerede støttet det, jf. ADR-038: "HCP
... brukes til Course Handicap i enhver runde med mindre eksplisitt
overstyrt"). Kaller add_participant direkte, samme mønster som
test_flag_plant.py.
"""
import uuid
from app.auth import CurrentUser
from app.routers.rounds import ParticipantCreate, add_participant
from tests.conftest import create_user
import app.db as app_db
async def _setup_round_with_custom_course(owner_user_id: str) -> str:
"""course_source='custom' (DB-only banedata, ingen nettverkskall mot
teeoff, ulikt 'teeoff'-fixturen conftest.create_round bruker) -- add_
participant slår alltid opp banens rating når handicap_index er satt."""
async with app_db.plain_connection() as conn:
personal_course_id = str(uuid.uuid4())
await conn.execute(
"INSERT INTO personal_course (id, created_by_user_id, name) VALUES ($1, $2, 'Hjemmebanen')",
personal_course_id, owner_user_id,
)
for n in range(1, 19):
await conn.execute(
"INSERT INTO personal_course_hole (personal_course_id, hole_number, par, stroke_index) "
"VALUES ($1, $2, 4, $3)",
personal_course_id, n, n,
)
tee_id = str(uuid.uuid4())
await conn.execute(
"INSERT INTO personal_course_tee (id, personal_course_id, name) VALUES ($1, $2, 'Gul')",
tee_id, personal_course_id,
)
for gender in ("m", "f"):
await conn.execute(
"INSERT INTO personal_course_tee_rating "
"(personal_course_tee_id, gender, course_rating, slope_rating, par) "
"VALUES ($1, $2, 71.5, 125, 72)",
tee_id, gender,
)
round_id = str(uuid.uuid4())
await conn.execute(
"""
INSERT INTO round (id, owner_user_id, course_source, personal_course_id,
course_name_snapshot, tee_name_snapshot, played_at)
VALUES ($1, $2, 'custom', $3, 'Hjemmebanen', 'Gul', CURRENT_DATE)
""",
round_id, owner_user_id, personal_course_id,
)
return round_id
async def test_add_participant_honors_explicit_handicap_override_for_linked_account(pool):
owner_id = await create_user()
target_id = await create_user(display_name="Medspiller")
async with app_db.plain_connection() as conn:
await conn.execute("UPDATE app_user SET gender = 'm', handicap_index = 18.4 WHERE id = $1", target_id)
round_id = await _setup_round_with_custom_course(owner_id)
user = CurrentUser(user_id=owner_id)
result = await add_participant(
round_id,
ParticipantCreate(user_id=target_id, handicap_index=9.1),
user=user,
)
assert result.handicap_index_snapshot == 9.1
assert result.course_handicap_snapshot is not None
async def test_add_participant_falls_back_to_profile_handicap_when_no_override_given(pool):
"""Regresjonsvern -- uendret oppførsel når organisatoren IKKE oppgir
noen overstyring (feltet stått urørt i veiviseren)."""
owner_id = await create_user()
target_id = await create_user(display_name="Medspiller")
async with app_db.plain_connection() as conn:
await conn.execute("UPDATE app_user SET gender = 'm', handicap_index = 18.4 WHERE id = $1", target_id)
round_id = await _setup_round_with_custom_course(owner_id)
user = CurrentUser(user_id=owner_id)
result = await add_participant(
round_id,
ParticipantCreate(user_id=target_id),
user=user,
)
assert result.handicap_index_snapshot == 18.4