teecup/frontend/components/round-page-shell.tsx
Erol Haagenrud ebd60da746 Fiks: "Fullfør runde" i header-modalen svelget feil stille og lukket seg uansett utfall
Modalen ("Administrer runde", tilgjengelig fra alle tre rundefanene) hadde en
egen, uavhengig "Fullfør runde"-vei som aldri sjekket setup_complete og
lukket dialogen umiddelbart uten å vente på fetch-svaret -- så en ufullstendig
formatoppsett (f.eks. Københavner med for få spillere) så ut til å fullføres,
men gjorde det aldri, uten noen feilmelding. Backend-valideringen var korrekt
fra før; kun frontend fulgte aldri opp svaret.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 19:48:17 +02:00

103 lines
3.7 KiB
TypeScript

"use client"
// Delt datalag for RoundHeader (ADR-040 Beslutning C/D) -- brukt av alle
// tre faneskjermene (Score/Scorekort/Leaderboard). Henter kun de feltene
// selve headeren trenger (ikke hele RoundOut) -- de tre sidene under
// gjør fortsatt sin egen, uavhengige, mer komplette henting, akkurat som
// før denne runden. onFinishRound/onDeleteRound er egne, selvstendige
// implementasjoner (ikke delt med round-detail.tsx sine interne
// finishRound/deleteRound) -- samme to ekte endepunkter
// (`POST .../complete`, `DELETE ...`), men denne komponenten må fungere
// uavhengig av round-detail.tsx sitt state siden den også brukes fra
// Scorekort- og Leaderboard-siden.
import { useEffect, useState } from "react"
import { useRouter } from "next/navigation"
import { RoundHeader, type RoundTab } from "@/components/round-header"
type ApiRoundHeaderFields = {
name: string | null
course_name_snapshot: string
tee_name_snapshot: string
played_at: string
started_at: string | null
play_format: string
setup_complete: boolean
setup_message: string | null
}
const dateFormatter = new Intl.DateTimeFormat("no-NO", { day: "numeric", month: "long", year: "numeric" })
const timeFormatter = new Intl.DateTimeFormat("no-NO", { hour: "2-digit", minute: "2-digit" })
export function RoundPageShell({
roundId,
activeTab,
children,
}: {
roundId: string
activeTab: RoundTab
children: React.ReactNode
}) {
const router = useRouter()
const [round, setRound] = useState<ApiRoundHeaderFields | null>(null)
useEffect(() => {
let cancelled = false
fetch(`/rounds/${roundId}`, { credentials: "include" })
.then((res) => (res.ok ? res.json() : null))
.then((data: ApiRoundHeaderFields | null) => {
if (!cancelled && data) setRound(data)
})
.catch(() => {})
return () => {
cancelled = true
}
}, [roundId])
async function onFinishRound(): Promise<{ ok: boolean; message?: string } | null> {
if (!confirm("Fullføre runden? Du kan fortsatt se den, men ikke lenger endre registrerte hull.")) return null
const res = await fetch(`/rounds/${roundId}/complete`, { method: "POST", credentials: "include" })
if (res.ok) {
router.refresh()
return { ok: true }
}
const body = await res.json().catch(() => null)
return { ok: false, message: body?.detail?.message ?? "Klarte ikke å fullføre runden. Prøv igjen." }
}
async function onDeleteRound() {
const res = await fetch(`/rounds/${roundId}`, { method: "DELETE", credentials: "include" })
if (res.ok) router.replace("/my-rounds")
}
return (
<div className="flex min-h-[100dvh] flex-col bg-background">
{round && (
<RoundHeader
activeTab={activeTab}
round={{
courseName: round.course_name_snapshot,
customName: round.name?.trim() || undefined,
teeName: round.tee_name_snapshot,
date: dateFormatter.format(new Date(round.played_at)),
teeTime: round.started_at ? timeFormatter.format(new Date(round.started_at)) : undefined,
isMatchPlay: round.play_format === "match",
}}
backHref="/my-rounds"
tabHrefs={{
score: `/my-rounds/${roundId}`,
scorecard: `/my-rounds/${roundId}/scorecard`,
leaderboard: `/my-rounds/${roundId}/leaderboard`,
}}
manageHref={`/my-rounds/${roundId}?tab=manage`}
onFinishRound={onFinishRound}
finishDisabledReason={
!round.setup_complete ? round.setup_message ?? "Oppsettet er ikke komplett ennå." : undefined
}
onDeleteRound={onDeleteRound}
/>
)}
<div className="flex-1">{children}</div>
</div>
)
}