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å.",
|
||||
)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 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
|
||||
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">
|
||||
|
|
|
|||
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