diff --git a/ARCHITECTURE_DECISIONS.md b/ARCHITECTURE_DECISIONS.md index a57b1e7..2179e13 100644 --- a/ARCHITECTURE_DECISIONS.md +++ b/ARCHITECTURE_DECISIONS.md @@ -6396,12 +6396,36 @@ on-demand-endepunktet fungerer også lenge etter rundefullføring. Deployet til ekte `teecup_db`/`teecup_api` sammen med migrasjon 067 (GolfAPI-tillegg, se ADR-064-tillegg over), 2026-08-13. -**Bevisst utenfor denne runden:** frontend-UI (`Player.completed`-felt, -"Avslutt for [navn]"/"Ferdig ✓"/"Angre"/"Send scorekort"-knapper i -`PlayerHoleCards` og "Spillere og runde"-fanen, `allHolesEnteredFor -Everyone`-justering, ScoringWizard-spillerkjede-hopp for fullførte -deltakere) — backend er bygget og verifisert, men ikke koblet inn i UI -ennå. +**Tillegg samme dag — frontend-UI bygget og rullet ut.** `Player. +completed` lagt til (avledet fra `ApiParticipant.completed`), delt +`ParticipantCompletionActions`-komponent gjenbrukt i BÅDE +`PlayerHoleCards` (Score-fanen) og `PlayerList` ("Spillere og +runde"-fanen) -- "Avslutt for [navn]" (skjult når runden selv er +fullført, siden backend uansett avviser da), "Ferdig ✓ (N hull)" + +"Angre" (kun når ikke rundefullført), "Send scorekort" (alltid synlig, +uansett fullføringsstatus -- dekker "send til enkeltspiller i etterkant, +lenge etter fullføring"-tillegget). `allHolesEnteredForEveryone` og +hurtig-hopp-stripens "registrert"-sjekk teller nå en `completed`-spiller +som ferdig uavhengig av faktisk hull-dekning. `advanceWizardPlayer` +hopper over fullførte spillere i samlebånds-kjeden. Et ikke-spilt hull +for en fullført spiller viser nå "Ferdig" i stedet for "Registrer" + +den stiplede pluss-boksen. + +**Verifisert:** `tsc --noEmit` rent, 45/45 vitest. Ekte nettleser- +verifisering (ekte innlogging inkl. reell TOTP-2FA-kode generert fra +kontoens lagrede secret, ekte runde opprettet på Tjøme -- cachet bane, +0 nye GolfAPI-kall -- med to deltakere): "Avslutt for X"/"Send +scorekort" synlig på begge kort i begge faner, avslutning av gjesten +trigget e-post umiddelbart (ingen feil i `teecup_api`-loggen, SMTP +konfigurert), UI oppdaterte seg til "Ferdig ✓"-merke + "Angre"-lenke + +"Ferdig"-tekst i score-kolonnen, "Send scorekort" for eieren (lenket +konto, andre kodesti enn gjesten) ga "Sendt ✓"-tilbakemelding, "Angre" +tilbakestilte korrekt og var persistent etter fane-bytte. Bekreftet +visuelt lys+mørk. Ingen konsollfeil gjennom hele forløpet. Testrunden +slettet etter verifisering (`DELETE /rounds/{id}`, bekreftet fjernet fra +`teecup_db`). **Rullet ut 2026-08-13** -- ren frontend-endring (ingen ny +migrasjon), `docker compose build teecup_frontend && up -d +teecup_frontend`. --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 20c0235..f3557f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10909,10 +10909,33 @@ Neste steg: **Rullet ut 2026-08-13**, samme økt, sammen med migrasjon 067 (punkt 79 under). `docker compose build teecup_api && up -d`. - **Bevisst utenfor denne runden:** frontend-UI (`Player.completed`, - "Avslutt for [navn]"/"Ferdig ✓"/"Angre"/"Send scorekort"-knapper, - nagging-justering, ScoringWizard-spillerkjede-hopp) -- backend - bygget og verifisert, ikke koblet inn i UI ennå. + **Tillegg samme dag -- frontend-UI bygget og rullet ut.** + `Player.completed` (fra `ApiParticipant.completed`), delt + `ParticipantCompletionActions`-komponent i BÅDE `PlayerHoleCards` + (Score-fanen) og `PlayerList` ("Spillere og runde"-fanen): + "Avslutt for [navn]" (skjult når runden er fullført), + "Ferdig ✓ (N hull)" + "Angre" (kun før rundefullføring), + "Send scorekort" (alltid synlig, uansett fullføringsstatus). + `allHolesEnteredForEveryone` + hurtig-hopp-stripen teller en + `completed`-spiller som ferdig uansett faktisk hull-dekning. + `advanceWizardPlayer` hopper over fullførte spillere. Et ikke-spilt + hull for en fullført spiller viser "Ferdig" i stedet for + "Registrer". + + **Verifisert:** `tsc --noEmit` rent, 45/45 vitest. Ekte + nettleserverifisering (ekte innlogging inkl. reell TOTP-2FA-kode + generert fra kontoens lagrede secret, ekte runde på Tjøme -- cachet + bane, 0 nye GolfAPI-kall -- to deltakere): avslutning av gjesten + trigget e-post umiddelbart (ingen feil i loggen), UI oppdaterte til + "Ferdig ✓"/"Angre"/"Ferdig"-tekst korrekt, "Send scorekort" for + eieren (lenket konto, annen kodesti enn gjesten) ga "Sendt ✓", + "Angre" tilbakestilte og var persistent etter fane-bytte. Lys+mørk + bekreftet visuelt, ingen konsollfeil. Testrunden slettet etter + verifisering, bekreftet fjernet fra `teecup_db`. + + **Rullet ut 2026-08-13**, samme økt. Ren frontend-endring (ingen ny + migrasjon) -- `docker compose build teecup_frontend && up -d + teecup_frontend`. 79. **To nye baner (Larvik/Seasidebanen, Nesbyen/Nesfjellet) via GolfAPI + en reell datakvalitetsbug funnet under importen — diff --git a/frontend/components/round-detail.tsx b/frontend/components/round-detail.tsx index f1d5941..f94bdd7 100644 --- a/frontend/components/round-detail.tsx +++ b/frontend/components/round-detail.tsx @@ -91,6 +91,8 @@ type Player = { playingHandicap: number | null // Money Ball (2026-07-30) -- se ApiParticipant.lineup_order. lineupOrder: number | null + // ADR-065 -- se ApiParticipant.completed. + completed: boolean } type HoleStat = { @@ -195,6 +197,10 @@ type ApiParticipant = { // Money Ball (2026-07-30) -- fast plass i rotasjonsrekkefølgen (0-3), // null inntil eieren tildeler den. KUN meningsfullt for play_format="money_ball". lineup_order: number | null + // Per-deltaker-fullføring (ADR-065, migrasjon 066) -- UAVHENGIG av + // round.completed_at, kun en nagging-brems + e-post-trigger for DENNE + // spilleren, ikke en cascade til hele rundens fullføring. + completed: boolean } type ApiSide = { @@ -880,6 +886,7 @@ export function RoundDetail({ roundId }: { roundId: string }) { roundSideId: p.round_side_id, playingHandicap: p.playing_handicap, lineupOrder: p.lineup_order, + completed: p.completed, })) ?? [] // Kun eieren kan forvalte runden (rediger/slett/legge til/fjerne @@ -1029,7 +1036,10 @@ export function RoundDetail({ roundId }: { roundId: string }) { // ALDRI via denne per-deltaker-veiviseren (se scrambleSoloTeamSideId), så // de skal aldri kunne bli et "neste"-mål her heller. function advanceWizardPlayer() { - const chain = scrambleSoloTeamSideId ? players.filter((p) => p.roundSideId !== scrambleSoloTeamSideId) : players + // Fullførte spillere (ADR-065) hopper over i samlebåndet -- de skal + // ikke bli "neste mål" for registrering, akkurat som scramble_solos + // lag-spillere allerede hoppes over. + const chain = players.filter((p) => (!scrambleSoloTeamSideId || p.roundSideId !== scrambleSoloTeamSideId) && !p.completed) const idx = chain.findIndex((p) => p.id === wizardPlayerId) const isLast = idx === -1 || idx === chain.length - 1 if (!isLast) { @@ -1062,6 +1072,9 @@ export function RoundDetail({ roundId }: { roundId: string }) { }) : players.length > 0 && players.every((p) => { + // ADR-065 -- en individuelt avsluttet spiller teller som "ferdig" + // for CTA-formål selv om ikke alle hull i holeOrder faktisk er ført. + if (p.completed) return true const list = holesByParticipant[p.id] return list !== undefined && holeOrder.every((n) => list.find((h) => h.hole_number === n)?.played === true) })) @@ -1223,6 +1236,26 @@ export function RoundDetail({ roundId }: { roundId: string }) { return { ok: true } } + // ADR-065 -- fungerer UANSETT fullføringsstatus, også lenge etter at + // runden (eller kun denne deltakeren) er fullført. Muterer ingen + // databasetilstand, kun les+send -- derfor ingen lokal round-oppdatering + // her, ulikt patchParticipant. + async function sendScorecard(participantId: string): Promise<{ ok: true } | { ok: false; message: string }> { + const res = await fetch(`/rounds/${roundId}/participants/${participantId}/send-scorecard`, { + method: "POST", + credentials: "include", + }) + if (!res.ok) { + const errBody = await res.json().catch(() => null) + const message = + errBody?.detail?.code === "NO_EMAIL_ADDRESS" + ? "Denne spilleren har ingen registrert e-postadresse." + : (errBody?.detail?.message ?? "Klarte ikke å sende scorekortet. Prøv igjen.") + return { ok: false, message } + } + return { ok: true } + } + async function finishRound() { if (!confirm("Fullføre runden? Du kan fortsatt se den, men ikke lenger endre registrerte hull.")) return setCompleting(true) @@ -1473,6 +1506,7 @@ export function RoundDetail({ roundId }: { roundId: string }) { roundId={roundId} players={players} sides={round.sides} + holesByParticipant={holesByParticipant} activePlayerId={activePlayer.id} onSelect={setActivePlayerId} onRemove={removeGuest} @@ -1483,6 +1517,7 @@ export function RoundDetail({ roundId }: { roundId: string }) { editingPlayerId={editingPlayerId} onToggleEdit={(id) => setEditingPlayerId((prev) => (prev === id ? null : id))} onPatchParticipant={patchParticipant} + onSendScorecard={sendScorecard} onAddGuest={addGuest} onAddSearched={addSearchedParticipant} /> @@ -1638,6 +1673,8 @@ export function RoundDetail({ roundId }: { roundId: string }) { onPrevHole={goPrev} onNextHole={goNext} onOpenEntry={openPlayerOrTeamSideEntry} + onPatchParticipant={patchParticipant} + onSendScorecard={sendScorecard} />
+ {busyError} +
+ )} + {sendState === "error" && sendError && ( ++ {sendError} +
+ )} +