"use client" import type React from "react" import { useEffect, useId, useMemo, useRef, useState } from "react" import { Download, Printer, Star } from "lucide-react" import { cn } from "@/lib/utils" import { fetchMatches, fetchPlayers, fetchRoundGroups, fetchRoundParticipants, fetchRounds, fetchSessions, fetchTeams, fetchTournament, fetchTournamentParticipants, formatDate, formatTeeTime, type ApiMatch, type ApiPlayer, type ApiRound, type ApiRoundParticipant, type ApiSession, type ApiTeam, type ApiTournamentParticipant, } from "@/lib/print-data" /** * Print-friendly START-LISTE -- ekte data (ADR-097). Videreført fra * V0-mockupen: Variant A (individuell, gruppevis) / Variant B (Cup, * match-oppstilling m/ lagfarger). A3/A2-plakatformat kun for Cup. */ type SizeKey = "A4" | "Letter" | "A3" | "A2" type Orientation = "portrait" | "landscape" const PAPER: Record = { A4: { label: "A4", sub: "210 × 297 mm", w: 210, h: 297, large: false }, Letter: { label: "Letter", sub: "216 × 279 mm", w: 216, h: 279, large: false }, A3: { label: "A3 / Tabloid", sub: "297 × 420 mm", w: 297, h: 420, large: true }, A2: { label: "A2 / ANSI C", sub: "420 × 594 mm", w: 420, h: 594, large: true }, } const MM_TO_PX = 96 / 25.4 const INK = "#171717" const INK_MUTED = "#5b5b5b" const HAIRLINE = "#d9d9d9" const HAIRLINE_STRONG = "#a3a3a3" type GroupRow = { seq: number; time: string; players: { name: string; tee: string; hcp: string; klasse: string; gender: string; club: string }[] } type MatchRow = { no: number time: string points: string nord: { name: string; tee: string; hcp: string; klasse: string; captain?: boolean }[] sor: { name: string; tee: string; hcp: string; klasse: string; captain?: boolean }[] } function Segmented({ options, value, onChange, ariaLabel, }: { options: { key: T; label: string; sub?: string; disabled?: boolean }[] value: T onChange: (v: T) => void ariaLabel: string }) { return (
{options.map((o) => { const active = o.key === value return ( ) })}
) } function ToggleRow({ label, hint, checked, onCheckedChange }: { label: string; hint?: string; checked: boolean; onCheckedChange: (v: boolean) => void }) { return ( ) } function PanelSection({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
) } function TeamChip({ color, name }: { color: string; name: string }) { return ( {name} ) } function SheetIndividual({ tournamentName, round, groups, fields, }: { tournamentName: string round: ApiRound groups: GroupRow[] fields: { hcp: boolean; klasse: boolean; gender: boolean; club: boolean } }) { return (

{tournamentName}

{round.start_mode === "shotgun" ? "Startliste — shotgun" : "Startliste"}
{round.name} {round.course_name} {formatDate(round.scheduled_at)}
{groups.map((g) => (
Gruppe {g.seq} {round.start_mode === "shotgun" ? `Starthull ${g.time}` : `Utslag ${g.time}`}
{fields.hcp && } {fields.klasse && } {fields.gender && } {fields.club && } {g.players.map((p, i) => ( {fields.hcp && } {fields.klasse && } {fields.gender && } {fields.club && } ))}
Spiller TeeHCPKlasseKjønnHjemmeklubb
{p.name} {p.tee}{p.hcp}{p.klasse}{p.gender}{p.club}
))}
) } function SheetCup({ tournamentName, session, teamNord, teamSor, matches, fields, poster, }: { tournamentName: string session: ApiSession teamNord: ApiTeam teamSor: ApiTeam matches: MatchRow[] fields: { tee: boolean; hcp: boolean; klasse: boolean; points: boolean; captain: boolean } poster: boolean }) { const meta = [`${session.name} — ${session.format}`, formatDate(session.scheduled_at)] return (

{tournamentName}

Oppstilling
{meta.map((m, i) => ( {m} ))}
vs
{matches.map((m) => (
Match {m.no} {m.time} {fields.points && ( {m.points} )}
vs
))}
) } function MatchSide({ accent, players, fields, poster, align, }: { accent: string players: MatchRow["nord"] fields: { tee: boolean; hcp: boolean; klasse: boolean; points: boolean; captain: boolean } poster: boolean align: "left" | "right" }) { return (
{players.map((p, i) => (
{p.name} {fields.captain && p.captain && ( Kaptein )} {(fields.tee || fields.hcp || fields.klasse) && ( {[fields.tee && p.tee, fields.hcp && `HCP ${p.hcp}`, fields.klasse && `Kl. ${p.klasse}`].filter(Boolean).join(" · ")} )}
))}
) } function SheetFooter() { return (
Side 1 av 1 Generert {new Date().toLocaleString("no-NO")} TeeCup
) } export function PrintStartlist({ organizationId, tournamentId, roundId, sessionId, initialSize = "A4", initialOrientation = "portrait", }: { organizationId: string tournamentId: string roundId?: string sessionId?: string initialSize?: SizeKey initialOrientation?: Orientation }) { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [tournamentName, setTournamentName] = useState("Turnering") const [round, setRound] = useState(null) const [groups, setGroups] = useState([]) const [session, setSession] = useState(null) const [matches, setMatches] = useState([]) const [teamNord, setTeamNord] = useState(null) const [teamSor, setTeamSor] = useState(null) const [downloading, setDownloading] = useState(false) const [size, setSize] = useState(initialSize) const [orientation, setOrientation] = useState(initialOrientation) const [aFields, setAFields] = useState({ hcp: true, klasse: true, gender: false, club: true }) const [bFields, setBFields] = useState({ tee: true, hcp: false, klasse: false, points: true, captain: true }) useEffect(() => { let cancelled = false async function load() { setLoading(true) setError(null) try { const t = await fetchTournament(organizationId, tournamentId) if (cancelled) return setTournamentName(t?.name ?? "Turnering") if (roundId) { const [rounds, groupsRes, roundParticipants, tournamentParticipants, players] = await Promise.all([ fetchRounds(organizationId, tournamentId), fetchRoundGroups(organizationId, tournamentId, roundId), fetchRoundParticipants(organizationId, tournamentId, roundId), fetchTournamentParticipants(organizationId, tournamentId), fetchPlayers(organizationId), ]) const r = rounds.find((x: ApiRound) => x.id === roundId) ?? null if (!r) throw new Error("Fant ikke runden.") const rpById = new Map(roundParticipants.map((rp) => [rp.id, rp])) const tpById = new Map(tournamentParticipants.map((tp) => [tp.id, tp])) const playerById = new Map(players.map((p) => [p.id, p])) const genderLabel = (g: ApiPlayer["gender"]) => (g === "m" ? "M" : g === "f" ? "K" : g === "x" ? "A" : "–") const built: GroupRow[] = groupsRes.groups.map((g, i) => ({ seq: g.sequence ?? i + 1, time: r.start_mode === "shotgun" ? String(g.start_hole ?? "–") : formatTeeTime(g.tee_time), players: g.participants.map((gp) => { const rp = rpById.get(gp.round_participant_id) const tp = rp ? tpById.get(rp.tournament_participant_id) : undefined const player = tp ? playerById.get(tp.player_id) : undefined return { name: gp.player_name, tee: rp?.tee_name ?? "—", hcp: tp?.handicap_index_snapshot != null ? tp.handicap_index_snapshot.toFixed(1).replace(".", ",") : "–", klasse: tp?.class_name ?? "—", gender: genderLabel(player?.gender ?? null), club: player?.club ?? "—", } }), })) if (cancelled) return setRound(r) setGroups(built) } else if (sessionId) { const [sessions, matchesRaw, teams] = await Promise.all([ fetchSessions(organizationId, tournamentId), fetchMatches(organizationId, sessionId), fetchTeams(organizationId, tournamentId), ]) const s = sessions.find((x: ApiSession) => x.id === sessionId) ?? null if (!s) throw new Error("Fant ikke økten.") const [teamA, teamB] = teams const built: MatchRow[] = matchesRaw.map((m: ApiMatch) => { const side = (sideKey: "a" | "b") => m.participants .filter((p) => p.team_side === sideKey) .map((p) => ({ name: p.player_name, tee: p.tee_name ?? "—", hcp: p.playing_handicap != null ? String(p.playing_handicap) : "–", klasse: "—" })) return { no: m.sequence, time: s.start_mode === "shotgun" ? String(m.start_hole ?? "–") : formatTeeTime(m.tee_time), points: `${s.points_per_match.toLocaleString("nb-NO", { maximumFractionDigits: 1 })} poeng`, nord: side("a"), sor: side("b"), } }) if (cancelled) return setSession(s) setTeamNord(teamA ?? { id: "a", name: "Lag A", color: "#1f5fbf" }) setTeamSor(teamB ?? { id: "b", name: "Lag B", color: "#d1561b" }) setMatches(built) } else { throw new Error("Mangler runde- eller økt-ID.") } } catch { if (!cancelled) setError("Klarte ikke å laste data for startlisten. Prøv igjen.") } finally { if (!cancelled) setLoading(false) } } void load() return () => { cancelled = true } }, [organizationId, tournamentId, roundId, sessionId]) const paper = PAPER[size] const natW = (orientation === "portrait" ? paper.w : paper.h) * MM_TO_PX const natH = (orientation === "portrait" ? paper.h : paper.w) * MM_TO_PX const areaRef = useRef(null) const [scale, setScale] = useState(0.4) useEffect(() => { const el = areaRef.current if (!el) return const compute = () => { const pad = 56 const availW = el.clientWidth - pad const availH = el.clientHeight - pad if (availW <= 0 || availH <= 0) return setScale(Math.max(0.12, Math.min(availW / natW, availH / natH, 1.6))) } compute() const ro = new ResizeObserver(compute) ro.observe(el) return () => ro.disconnect() }, [natW, natH]) const poster = sessionId != null && paper.large const pageCss = useMemo( () => `@page { size: ${orientation === "portrait" ? `${paper.w}mm ${paper.h}mm` : `${paper.h}mm ${paper.w}mm`}; margin: 0; }`, [paper.w, paper.h, orientation], ) const sizeOptions = (Object.keys(PAPER) as SizeKey[]).map((k) => ({ key: k, label: PAPER[k].label, sub: PAPER[k].sub, disabled: PAPER[k].large && roundId != null, })) async function downloadPdf() { setDownloading(true) try { const res = await fetch(`/orgs/${organizationId}/print/pdf`, { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ kind: "startlist", tournament_id: tournamentId, round_id: roundId ?? null, session_id: sessionId ?? null, paper: size === "A3" || size === "A2" ? "A4" : size, orientation, }), }) if (!res.ok) throw new Error() const blob = await res.blob() const url = URL.createObjectURL(blob) const a = document.createElement("a") a.href = url a.download = "teecup-startliste.pdf" document.body.appendChild(a) a.click() a.remove() URL.revokeObjectURL(url) } catch { setError("Klarte ikke å generere PDF-en. Prøv igjen.") } finally { setDownloading(false) } } const ready = !loading const styleId = useId() return (

Skriv ut startliste

{error ?? (loading ? "Laster …" : `${groups.length + matches.length} rad(er) klare`)}

{loading ? (

Laster startliste …

) : round ? (
) : session && teamNord && teamSor ? (
) : (

{error ?? "Ingen data funnet."}

)}
) }