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å.", "Spilleren du valgte har ikke fullført profilen sin (mangler kjønn) og kan ikke legges til ennå.",
) )
gender = target["gender"] 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_name = None
guest_first_name = None guest_first_name = None
guest_last_name = None guest_last_name = None

View file

@ -80,7 +80,13 @@ export function Step3Players() {
{adding ? ( {adding ? (
<AddPlayerPanel <AddPlayerPanel
course={state.course} 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} existing={state.players}
onCancel={() => setAdding(false)} onCancel={() => setAdding(false)}
onAdd={addPlayer} onAdd={addPlayer}
@ -230,10 +236,30 @@ function PlayerCard({
/> />
</Field> </Field>
) : player.hcpFromProfile && !isOwner ? ( ) : player.hcpFromProfile && !isOwner ? (
<Field label="HCP"> // Redigerbar fra FØRSTE stund (2026-08-20 brukerfunn) -- var
<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)]"> // tidligere en read-only visning ("hentes fra profilen"), som
{player.hcp != null ? `${player.hcp} · hentes fra profilen` : "Hentes fra profilen…"} // tvang organisatoren til å legge til spilleren FØRST og
</div> // 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> </Field>
) : null} ) : null}

View file

@ -424,6 +424,13 @@ export function WizardProvider({
} }
if (p.accountId) { if (p.accountId) {
participantBody.user_id = 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 { } else {
participantBody.guest_first_name = p.firstName.trim() participantBody.guest_first_name = p.firstName.trim()
participantBody.guest_last_name = p.lastName.trim() || null participantBody.guest_last_name = p.lastName.trim() || null

View file

@ -303,9 +303,14 @@ function ManageRoundDialog({
</button> </button>
</div> </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 <Link
href={manageHref} 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" 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"> <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