"use client" import type React from "react" import { useEffect, useId, useRef, useState } from "react" import { Download, Printer } from "lucide-react" import { cn } from "@/lib/utils" import { fetchCourseHoles, fetchMatches, fetchRoundGroups, fetchRoundParticipants, fetchRounds, fetchSessions, fetchTeams, fetchTournament, fetchTournamentParticipants, mockHoleLength, type ApiCourseHole, type ApiMatch, type ApiRound, type ApiRoundGroup, type ApiRoundParticipant, type ApiSession, type ApiTeam, type ApiTournament, type ApiTournamentParticipant, } from "@/lib/print-data" /** * Print-friendly, tradisjonelt golf-SCOREKORT -- ekte data (ADR-097). * * Videreført fra V0-mockupen (samme sheet-mekanikk: A4/Letter/A5, * portrett/landskap, per-felt-toggles, én sheet PER UTSLAGSGRUPPE * (individuell) eller PER MATCH (Cup) -- en organisator skriver ut kort for * HELE feltet på én gang, ikke ett enkelt eksempelkort. * * Hullengde finnes ikke i datamodellen ennå (bevisst utsatt, se * FEATURE_BACKLOG.md) -- mockHoleLength() brukes akkurat som i selve * V0-utskriftspromptene, ikke lenger her. */ const PAPER = { A4: { w: 210, h: 297 }, Letter: { w: 216, h: 279 }, A5: { w: 148, h: 210 }, } as const type PaperKey = keyof typeof PAPER type Orientation = "portrait" | "landscape" type PerSheet = 1 | 2 type SheetPlayer = { name: string hcpLabel: string phcp: number | null teeName: string | null className: string | null teamName?: string teamColor?: string | null } type Sheet = { title: string; sublabel: string; players: SheetPlayer[] } type FieldOpts = { netto: boolean; strokes: boolean; putts: boolean; stableford: boolean; matchplay: boolean } function strokesOnHole(phcp: number, idx: number): number { const base = Math.floor(phcp / 18) const extra = phcp % 18 return base + (idx <= extra ? 1 : 0) } function ReceivedDots({ count }: { count: number }) { if (count <= 0) return null if (count >= 3) return {count}• return ( {Array.from({ length: count }).map((_, i) => ( ))} ) } function GridCell({ children, wide, shaded, header, className, }: { children?: React.ReactNode wide?: boolean shaded?: boolean header?: boolean className?: string }) { return ( {children} ) } function LabelCell({ children, shaded }: { children: React.ReactNode; shaded?: boolean }) { return ( {children} ) } // -------------------------- selve sheet-rendering -------------------------- function ScorecardSheet({ sheet, holes, fields, dense }: { sheet: Sheet; holes: ApiCourseHole[]; fields: FieldOpts; dense: boolean }) { const splitInOut = holes.length > 9 const out = splitInOut ? holes.slice(0, 9) : holes const inn = splitInOut ? holes.slice(9) : [] const outPar = out.reduce((s, h) => s + h.par, 0) const inPar = inn.reduce((s, h) => s + h.par, 0) const totalPar = outPar + inPar const W_LABEL = 150 const W_HOLE = 42 const W_SUM = 50 const sumCols = splitInOut ? 3 : 1 const colUnits = W_LABEL + holes.length * W_HOLE + sumCols * W_SUM const pctLabel = `${(W_LABEL / colUnits) * 100}%` const pctHole = `${(W_HOLE / colUnits) * 100}%` const pctSum = `${(W_SUM / colUnits) * 100}%` function HoleCells({ render }: { render: (kind: "hole" | "out" | "in" | "total", hole?: ApiCourseHole) => React.ReactNode }) { if (!splitInOut) { return ( <> {holes.map((h) => ( {render("hole", h)} ))} {render("total")} ) } return ( <> {out.map((h) => ( {render("hole", h)} ))} {render("out")} {inn.map((h) => ( {render("hole", h)} ))} {render("in")} {render("total")} ) } function PlayerRow({ player }: { player: SheetPlayer }) { const sub = ( <> HCP {player.hcpLabel} · {player.teeName ?? "—"} {player.className ? <> · Kl. {player.className} : null} {player.teamName ? ( <> {" · "} {player.teamName} ) : null} ) return ( <>
{player.name} {sub}
{ if (kind !== "hole") return null if (fields.strokes && hole && player.phcp != null) { const s = strokesOnHole(player.phcp, hole.stroke_index) return (
) } return
}} /> {fields.netto ? ( Netto
} /> ) : null} {fields.putts ? ( Putts
} /> ) : null} {fields.stableford ? ( Poeng
} /> ) : null} ) } return (
{sheet.title} {sheet.sublabel}
Offisielt scorekort — fylles ut for hånd
{holes.map((h) => ( ))} {splitInOut ? : null} {splitInOut ? : null} Hull { if (kind === "hole") return {hole!.hole_number} if (kind === "out") return Ut if (kind === "in") return Inn return {splitInOut ? "Tot" : "Sum"} }} /> Par { if (kind === "hole") return hole!.par if (kind === "out") return {outPar} if (kind === "in") return {inPar} return {totalPar} }} /> Indeks (kind === "hole" ? {hole!.stroke_index} : null)} /> Meter (kind === "hole" ? {mockHoleLength(hole!.par, hole!.hole_number)} : null)} /> {sheet.players.map((p, i) => ( ))} {fields.matchplay ? ( Match-status
f.eks. «2 opp», «AS»
} />
) : null}
{fields.strokes ? (

Mottatte slag: = ett slag,{" "} = to slag (tall ved 3+).

) : null}
{sheet.players.map((p, i) => (
Spillerens signatur — {p.name}
Dato
))}
Markørens signatur
Dato
) } function ScaledSheet({ paper, orientation, children }: { paper: PaperKey; orientation: Orientation; children: React.ReactNode }) { const containerRef = useRef(null) const [scale, setScale] = useState(1) const base = PAPER[paper] const wMm = orientation === "landscape" ? base.h : base.w const hMm = orientation === "landscape" ? base.w : base.h const PX_PER_MM = 3.7795 const sheetWpx = wMm * PX_PER_MM const sheetHpx = hMm * PX_PER_MM useEffect(() => { const el = containerRef.current if (!el) return const ro = new ResizeObserver(() => setScale(Math.min(1, el.clientWidth / sheetWpx))) ro.observe(el) return () => ro.disconnect() }, [sheetWpx]) return (
{children}
{children}
) } function PanelSection({ title, children }: { title: string; children: React.ReactNode }) { return (

{title}

{children}
) } function Segmented({ label, value, onChange, options, }: { label: string value: T onChange: (v: T) => void options: { value: T; label: string; disabled?: boolean }[] }) { return (
{options.map((o) => { const active = o.value === value return ( ) })}
) } function Toggle({ label, hint, checked, onChange }: { label: string; hint?: string; checked: boolean; onChange: (v: boolean) => void }) { return ( ) } // -------------------------- data-lasting + hovedkomponent -------------------------- export function PrintScorecard({ organizationId, tournamentId, roundId, sessionId, initialPaper = "A4", initialOrientation = "landscape", }: { organizationId: string tournamentId: string roundId?: string sessionId?: string initialPaper?: PaperKey initialOrientation?: Orientation }) { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [tournament, setTournament] = useState(null) const [holes, setHoles] = useState([]) const [sheets, setSheets] = useState([]) const [contextLabel, setContextLabel] = useState("") const [paper, setPaper] = useState(initialPaper) const [orientation, setOrientation] = useState(initialOrientation) const [perSheet, setPerSheet] = useState(1) const [fields, setFields] = useState({ netto: false, strokes: true, putts: false, stableford: false, matchplay: sessionId != null, }) const [downloading, setDownloading] = useState(false) const styleId = useId() useEffect(() => { let cancelled = false async function load() { setLoading(true) setError(null) try { const t = await fetchTournament(organizationId, tournamentId) if (cancelled) return setTournament(t) if (roundId) { const [rounds, groupsRes, roundParticipants, tournamentParticipants] = await Promise.all([ fetchRounds(organizationId, tournamentId), fetchRoundGroups(organizationId, tournamentId, roundId), fetchRoundParticipants(organizationId, tournamentId, roundId), fetchTournamentParticipants(organizationId, tournamentId), ]) const round = rounds.find((r: ApiRound) => r.id === roundId) ?? null if (!round) throw new Error("Fant ikke runden.") const courseHoles = await fetchCourseHoles(organizationId, round.course_id) const playedHoles = filterHolesByConfig(courseHoles, round.hole_config) const rpById = new Map(roundParticipants.map((rp) => [rp.id, rp])) const tpById = new Map(tournamentParticipants.map((tp) => [tp.id, tp])) const builtSheets: Sheet[] = groupsRes.groups.map((g: ApiRoundGroup, i: number) => ({ title: t?.name ?? "Turnering", sublabel: `${round.name} · ${round.course_name} · Gruppe ${g.sequence ?? i + 1}`, players: g.participants.map((gp) => { const rp = rpById.get(gp.round_participant_id) const tp = rp ? tpById.get(rp.tournament_participant_id) : undefined return { name: gp.player_name, hcpLabel: tp?.handicap_index_snapshot != null ? tp.handicap_index_snapshot.toFixed(1).replace(".", ",") : "–", phcp: rp?.playing_handicap ?? null, teeName: rp?.tee_name ?? null, className: tp?.class_name ?? null, } }), })) if (cancelled) return setHoles(playedHoles) setContextLabel(`${round.name} · Individuell slagspill`) setSheets(builtSheets) } else if (sessionId) { const [sessions, matches, teams] = await Promise.all([ fetchSessions(organizationId, tournamentId), fetchMatches(organizationId, sessionId), fetchTeams(organizationId, tournamentId), ]) const session = sessions.find((s: ApiSession) => s.id === sessionId) ?? null if (!session) throw new Error("Fant ikke økten.") const courseHoles = await fetchCourseHoles(organizationId, session.course_id) const playedHoles = filterHolesByConfig(courseHoles, session.hole_config) const teamById = new Map(teams.map((tm) => [tm.id, tm])) const builtSheets: Sheet[] = matches.map((m: ApiMatch) => { const teamA = teamById.get(m.team_a_id) const teamB = teamById.get(m.team_b_id) const players: SheetPlayer[] = m.participants.map((mp) => ({ name: mp.player_name, hcpLabel: mp.playing_handicap != null ? String(mp.playing_handicap) : "–", phcp: mp.playing_handicap, teeName: mp.tee_name, className: null, teamName: mp.team_side === "a" ? teamA?.name : teamB?.name, teamColor: mp.team_side === "a" ? teamA?.color : teamB?.color, })) return { title: t?.name ?? "Turnering", sublabel: `${session.name} · ${session.format} · Match ${m.sequence}`, players, } }) if (cancelled) return setHoles(playedHoles) setContextLabel(`${session.name} · Cup-format · Matchplay`) setSheets(builtSheets) } else { throw new Error("Mangler runde- eller økt-ID.") } } catch { if (!cancelled) setError("Klarte ikke å laste data for scorekortet. Prøv igjen.") } finally { if (!cancelled) setLoading(false) } } void load() return () => { cancelled = true } }, [organizationId, tournamentId, roundId, sessionId]) function choosePaper(p: PaperKey) { setPaper(p) if (p === "A5") { setPerSheet(1) setOrientation("landscape") } } function choosePerSheet(n: PerSheet) { setPerSheet(n) if (n === 2) setOrientation("portrait") } const twoUp = perSheet === 2 const dense = twoUp || paper === "A5" const base = PAPER[paper] const pageSize = `${base.w}mm ${base.h}mm ${orientation}` 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: "scorecard", tournament_id: tournamentId, round_id: roundId ?? null, session_id: sessionId ?? null, paper, 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-scorekort.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 return (
{loading ? (

Laster scorekort …

) : sheets.length === 0 ? (

Ingen grupper eller matcher funnet for denne runden/økten ennå.

) : twoUp ? ( chunk(sheets, 2).map((pair, i) => (
{pair[1] ? :
}
)) ) : ( sheets.map((sheet, i) => (
)) )}
) } function TwoUp({ orientation, children }: { orientation: Orientation; children: React.ReactNode[] | React.ReactNode }) { const stackVertical = orientation === "portrait" const kids = Array.isArray(children) ? children : [children] return (
{kids[0]}
{"✂"} klipp/brett her
{kids[1]}
) } function chunk(arr: T[], size: number): T[][] { const out: T[][] = [] for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size)) return out } function filterHolesByConfig(holes: ApiCourseHole[], config: "full_18" | "front_9" | "back_9"): ApiCourseHole[] { const sorted = [...holes].sort((a, b) => a.hole_number - b.hole_number) if (config === "front_9") return sorted.filter((h) => h.hole_number <= 9) if (config === "back_9") return sorted.filter((h) => h.hole_number > 9) return sorted }