"use client" // Utslagsgrupper (2026-08-18) -- hvem spiller med hvem, klokka når, for // én runde i en individuell turnering. Bruker: "Jeg må jo kunne // bulksjekke hvem som skal spille fra hvor, med hvem, klokka når?" // Design bekreftet: TRADISJONELT utslag (samme starthull for alle, // rundens eget start_hole -- uendret her), staggerte klokkeslett per // gruppe med fast intervall, MANUELL gruppesammensetning med et forslag // å justere, FAST gruppestørrelse. // // Håndkodet direkte (ikke sendt via V0 først) -- bruker ba eksplisitt om // funksjonen NÅ ("Jeg vil ha den nå, takk"), samme avveining som // DuplicateStep i player-import-panel.tsx: en avgrenset, funksjonell // arbeidsflate satt inn i en eksisterende flyt, bygget med de samme // globale design-tokenene som resten av appen (border/bg-card/bg-primær // osv.), ikke en forsøkt "ferdig polert" ny flaggskip-skjerm. Kan // poleres videre via V0 senere om ønskelig. import { useEffect, useState } from "react" import { Clock, Plus, Trash2, X } from "lucide-react" import { cn } from "@/lib/utils" type ApiGroupParticipant = { round_participant_id: string player_name: string } type ApiRoundGroup = { id: string | null sequence: number tee_time: string | null tee_time_override: string | null participants: ApiGroupParticipant[] } type ApiRoundGroups = { groups: ApiRoundGroup[] ungrouped: ApiGroupParticipant[] } type ApiRoundInfo = { scheduled_at: string | null tee_interval_minutes: number | null start_hole: number } function formatTeeTime(iso: string | null) { if (!iso) return null const date = new Date(iso) if (Number.isNaN(date.getTime())) return null return date.toLocaleTimeString("no-NO", { hour: "2-digit", minute: "2-digit" }) } // datetime-local-inputet gir "YYYY-MM-DDTHH:mm" (ingen tidssone) -- server // (Pydantic datetime-felt) godtar dette direkte, tolkes som lokal tid. function toDatetimeLocalValue(iso: string | null) { if (!iso) return "" const date = new Date(iso) if (Number.isNaN(date.getTime())) return "" const pad = (n: number) => String(n).padStart(2, "0") return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}` } const CONTROL = "h-10 rounded-lg border border-border bg-background px-3 text-sm font-semibold text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" export function RoundGroupsPanel({ base, roundId, roundLabel, onClose, }: { base: string roundId: string roundLabel: string onClose: () => void }) { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [saving, setSaving] = useState(false) const [savedMessage, setSavedMessage] = useState(null) const [roundInfo, setRoundInfo] = useState(null) const [scheduledAtInput, setScheduledAtInput] = useState("") const [intervalInput, setIntervalInput] = useState("") const [groups, setGroups] = useState([]) const [ungrouped, setUngrouped] = useState([]) const [groupSize, setGroupSize] = useState("4") async function loadAll() { setLoading(true) setError(null) try { const [roundRes, groupsRes] = await Promise.all([ fetch(`${base}/rounds`, { credentials: "include" }), fetch(`${base}/rounds/${roundId}/groups`, { credentials: "include" }), ]) if (!roundRes.ok || !groupsRes.ok) throw new Error() const rounds: (ApiRoundInfo & { id: string })[] = await roundRes.json() const thisRound = rounds.find((r) => r.id === roundId) ?? null const groupsData: ApiRoundGroups = await groupsRes.json() setRoundInfo(thisRound) setScheduledAtInput(toDatetimeLocalValue(thisRound?.scheduled_at ?? null)) setIntervalInput(thisRound?.tee_interval_minutes?.toString() ?? "") setGroups(groupsData.groups) setUngrouped(groupsData.ungrouped) } catch { setError("Klarte ikke å laste utslagsgruppene. Prøv igjen.") } finally { setLoading(false) } } useEffect(() => { void loadAll() // eslint-disable-next-line react-hooks/exhaustive-deps }, [roundId]) async function saveSchedule() { setError(null) // gir en TIDSSONELØS streng som betyr // "organisatorens lokale klokkeslett" (f.eks. "10:00" i Norge). new // Date(...) på en slik streng tolkes av nettleseren som LOKAL tid -- // .toISOString() konverterer deretter til riktig UTC-tidspunkt FØR // sending. Uten dette steget ville "10:00" blitt lagret som 10:00 UTC // (feil med hele tidssoneavviket, f.eks. 2 timer for norsk sommertid) // -- funnet under egen scratch-verifisering av akkurat denne skjermen. const scheduledAtIso = scheduledAtInput ? new Date(scheduledAtInput).toISOString() : null const res = await fetch(`${base}/rounds/${roundId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ scheduled_at: scheduledAtIso, tee_interval_minutes: intervalInput.trim() ? Number(intervalInput) : null, }), }) if (!res.ok) { setError("Klarte ikke å lagre utslagstidspunktet.") return } await loadAll() } async function suggest() { const size = Number(groupSize) if (!size || size < 1) return const hasExistingWork = groups.some((g) => g.participants.length > 0) if (hasExistingWork && !confirm("Dette erstatter det du eventuelt allerede har justert her (ikke lagret ennå). Fortsette?")) { return } setError(null) const res = await fetch(`${base}/rounds/${roundId}/groups/suggest`, { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ group_size: size }), }) if (!res.ok) { setError("Klarte ikke å beregne et forslag.") return } const data: ApiRoundGroups = await res.json() setGroups(data.groups) setUngrouped(data.ungrouped) setSavedMessage(null) } function addEmptyGroup() { const nextSequence = (groups.reduce((m, g) => Math.max(m, g.sequence), 0) || 0) + 1 setGroups((prev) => [...prev, { id: null, sequence: nextSequence, tee_time: null, tee_time_override: null, participants: [] }]) } function removeGroup(sequence: number) { const group = groups.find((g) => g.sequence === sequence) if (!group) return setUngrouped((prev) => [...prev, ...group.participants]) setGroups((prev) => prev.filter((g) => g.sequence !== sequence)) } // VIKTIG: `moved` må leses SYNKRONT fra det gjeldende (allerede rendrede) // state-et, IKKE inne i en setState-oppdateringsfunksjon -- React kjører // ikke oppdateringsfunksjoner umiddelbart, så en `let moved` satt inni én // slik funksjon er fortsatt `null` når den leses rett etterpå i samme // kall. Funnet under egen scratch-verifisering: en flyttet spiller // forsvant sporløst (fjernet fra den gamle gruppen, aldri lagt til den // nye) fordi nettopp dette skjedde. function moveParticipant(participantId: string, toSequence: number | "ungrouped") { const moved = groups.flatMap((g) => g.participants).find((p) => p.round_participant_id === participantId) ?? ungrouped.find((p) => p.round_participant_id === participantId) if (!moved) return setGroups((prev) => prev.map((g) => { if (g.sequence === toSequence) { const withoutMoved = g.participants.filter((p) => p.round_participant_id !== participantId) return { ...g, participants: [...withoutMoved, moved] } } return { ...g, participants: g.participants.filter((p) => p.round_participant_id !== participantId) } }), ) setUngrouped((prev) => { const withoutMoved = prev.filter((p) => p.round_participant_id !== participantId) return toSequence === "ungrouped" ? [...withoutMoved, moved] : withoutMoved }) } async function save() { setSaving(true) setError(null) setSavedMessage(null) try { const res = await fetch(`${base}/rounds/${roundId}/groups`, { method: "PUT", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ groups: groups.map((g) => ({ sequence: g.sequence, tee_time_override: null, round_participant_ids: g.participants.map((p) => p.round_participant_id), })), }), }) if (!res.ok) throw new Error() const data: ApiRoundGroups = await res.json() setGroups(data.groups) setUngrouped(data.ungrouped) setSavedMessage("Lagret.") } catch { setError("Klarte ikke å lagre utslagsgruppene. Prøv igjen.") } finally { setSaving(false) } } const allSequences = groups.map((g) => g.sequence).sort((a, b) => a - b) function GroupSelect({ participantId, currentSequence }: { participantId: string; currentSequence: number | "ungrouped" }) { return ( ) } return (

Utslagsgrupper

{roundLabel}

{error && (
{error}
)} {loading ? (

Laster…

) : ( <>

Første utslag og intervall

Alle grupper starter fra samme hull (hull {roundInfo?.start_hole ?? 1}, satt for hele runden), med staggerte klokkeslett ut fra intervallet under. Gruppe 1 slår ut først, gruppe 2 dette antall minutter senere, og så videre.

Grupper

Foreslå en fordeling, juster hvem som spiller sammen, lagre når du er fornøyd.

{groups.length === 0 && (

Ingen grupper ennå -- bruk "Foreslå grupper" eller "Ny gruppe".

)} {groups .slice() .sort((a, b) => a.sequence - b.sequence) .map((g) => (
{g.sequence}
{g.participants.length === 0 ? (

Ingen spillere i denne gruppen ennå.

) : (
    {g.participants.map((p) => (
  • {p.player_name}
  • ))}
)}
))}

Ugrupperte ({ungrouped.length})

{ungrouped.length === 0 ? (

Alle deltakere er i en gruppe.

) : (
    {ungrouped.map((p) => (
  • {p.player_name} {allSequences.length > 0 ? ( ) : ( Opprett en gruppe først )}
  • ))}
)}
{savedMessage && {savedMessage}}
)}
) }