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:
parent
6603e94fd3
commit
a0163eaa16
5 changed files with 144 additions and 7 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 på 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">
|
||||||
|
|
|
||||||
93
tests/test_add_participant_hcp_override.py
Normal file
93
tests/test_add_participant_hcp_override.py
Normal 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
|
||||||
Loading…
Reference in a new issue