// Delte typer + fetch-funksjoner for de fire utskriftssidene // (print-scorecard/print-startlist/print-cart-tags/print-resultlist). // Ekte reell gjenbruk på tvers av alle fire (samme runde-/økt-/deltaker- // endepunkter trengs flere steder), ikke en tidlig abstraksjon -- se // ARCHITECTURE_DECISIONS.md ADR-097. async function getJson(url: string): Promise { const res = await fetch(url, { credentials: "include" }) if (!res.ok) throw new Error(`${url}: ${res.status}`) return res.json() as Promise } export function tournamentBase(org: string, tournamentId: string) { return `/orgs/${org}/tournaments/${tournamentId}` } export type ApiTournament = { id: string name: string status: string format_type: "team" | "individual" scoring_method: string | null cut_after_round: number | null cut_size: number | null } export type ApiRound = { id: string tournament_id: string sequence: number name: string hole_config: "full_18" | "front_9" | "back_9" course_id: string course_name: string scheduled_at: string | null tee_interval_minutes: number | null start_hole: number start_mode: "consecutive" | "shotgun" } export type ApiRoundGroupParticipant = { round_participant_id: string; player_name: string } export type ApiRoundGroup = { id: string | null sequence: number tee_time: string | null tee_time_override: string | null start_hole: number | null participants: ApiRoundGroupParticipant[] } export type ApiRoundGroups = { groups: ApiRoundGroup[]; ungrouped: ApiRoundGroupParticipant[] } export type ApiRoundParticipant = { id: string tournament_participant_id: string player_name: string tee_id: string | null tee_name: string | null course_handicap: number | null playing_handicap: number | null stat_level: "strokes_only" | "strokes_and_putts" | "full" } export type ApiTournamentParticipant = { id: string player_id: string player_name: string handicap_index_snapshot: number | null class_id: string | null class_name: string | null stat_level: string status: "active" | "dsq" | "rtd" | "dnf" | "dns" } export type ApiCourseHole = { hole_number: number; par: number; stroke_index: number } export type ApiCourse = { id: string; name: string; source: string } export type ApiSession = { id: string sequence: number name: string format: string hole_config: "full_18" | "front_9" | "back_9" course_id: string points_per_match: number scoring_mode: "stroke" | "hole_result" scheduled_at: string | null tee_interval_minutes: number | null start_hole: number start_mode: "consecutive" | "shotgun" locked_team_ids: string[] revealed: boolean } export type ApiMatchParticipant = { id: string team_side: "a" | "b" team_roster_id: string player_name: string tee_id: string | null tee_name: string | null playing_handicap: number | null lineup_order: number | null } export type ApiMatch = { id: string sequence: number team_a_id: string team_b_id: string status_text: string | null points_side_a: number | null points_side_b: number | null leading_side: "a" | "b" | null tee_time: string | null start_hole: number | null participants: ApiMatchParticipant[] } export type ApiTeam = { id: string; name: string; color: string | null } export type ApiRosterEntry = { id: string player_id: string display_name: string handicap_index_snapshot: number | null is_captain: boolean class_id: string | null class_name: string | null } export type ApiLeaderboardEntry = { tournament_participant_id: string player_id: string player_name: string rounds_played: number gross_total: number | null net_total: number | null stableford_total: number | null class_id: string | null class_name: string | null position: string | null total_label: string | null rank: number | null cut: boolean status: "active" | "dsq" | "rtd" | "dnf" | "dns" rounds: { round_number: number; label: string | null; tone: "under" | "even" | "over" | null }[] } export type ApiCupLeaderboard = { teams: { team_id: string; team_name: string; color: string | null; points: number; projected_points: number }[] matches_total: number matches_decided: number sessions: { session_id: string sequence: number name: string | null points_by_team: Record projected_points_by_team: Record matches_total: number matches_decided: number }[] } export type ApiPlayer = { id: string display_name: string handicap_index: number | null gender: "m" | "f" | "x" | null club: string | null } export async function fetchTournament(org: string, tournamentId: string): Promise { const list = await getJson(`/orgs/${org}/tournaments`) return list.find((t) => t.id === tournamentId) ?? null } export function fetchRounds(org: string, tournamentId: string) { return getJson(`${tournamentBase(org, tournamentId)}/rounds`) } export function fetchRoundGroups(org: string, tournamentId: string, roundId: string) { return getJson(`${tournamentBase(org, tournamentId)}/rounds/${roundId}/groups`) } export function fetchRoundParticipants(org: string, tournamentId: string, roundId: string) { return getJson(`${tournamentBase(org, tournamentId)}/rounds/${roundId}/participants`) } export function fetchTournamentParticipants(org: string, tournamentId: string) { return getJson(`${tournamentBase(org, tournamentId)}/participants`) } export function fetchCourseHoles(org: string, courseId: string) { return getJson(`/orgs/${org}/courses/${courseId}/holes`) } export function fetchCourses(org: string) { return getJson(`/orgs/${org}/courses`) } export function fetchSessions(org: string, tournamentId: string) { return getJson(`${tournamentBase(org, tournamentId)}/sessions`) } export function fetchMatches(org: string, sessionId: string) { return getJson(`/orgs/${org}/sessions/${sessionId}/matches`) } export function fetchTeams(org: string, tournamentId: string) { return getJson(`${tournamentBase(org, tournamentId)}/teams`) } export function fetchRoster(org: string, teamId: string) { return getJson(`/orgs/${org}/teams/${teamId}/roster`) } export function fetchIndividualLeaderboard(org: string, tournamentId: string) { return getJson(`${tournamentBase(org, tournamentId)}/individual-leaderboard`) } export function fetchCupLeaderboard(org: string, tournamentId: string) { return getJson(`${tournamentBase(org, tournamentId)}/leaderboard`) } export function fetchPlayers(org: string) { return getJson(`/orgs/${org}/players`) } // Ingen `color`-kolonne finnes på `tee` (kun `name`, verifisert mot skjema) // -- norsk klubbkonvensjon er at navnet ER fargen. Kun til en liten // fargeprikk i utskrift, faller tilbake til nøytral grå for ukjente navn. const TEE_COLOR_BY_NAME: Record = { gul: "#eab308", rød: "#dc2626", hvit: "#f5f5f4", blå: "#2563eb", sølv: "#94a3b8", oransje: "#f97316", svart: "#171717", grønn: "#16a34a", } export function teeColor(teeName: string | null | undefined): string { if (!teeName) return "#a3a3a3" const key = teeName.trim().toLowerCase().split(/\s+/)[0] return TEE_COLOR_BY_NAME[key] ?? "#a3a3a3" } export function formatTeeTime(iso: string | null): string { if (!iso) return "–" const d = new Date(iso) if (Number.isNaN(d.getTime())) return "–" return d.toLocaleTimeString("no-NO", { hour: "2-digit", minute: "2-digit" }) } export function formatDate(iso: string | null): string { if (!iso) return "–" const d = new Date(iso) if (Number.isNaN(d.getTime())) return "–" return d.toLocaleDateString("no-NO", { day: "numeric", month: "long", year: "numeric" }) } // Realistisk hull-lengde finnes ikke i datamodellen ennå (bevisst utsatt, // se FEATURE_BACKLOG.md "utskrift av turneringsdokumenter"-planlegging // 2026-08-21) -- mockes med plausible tall her, akkurat som i selve // V0-utskriftspromptene. Deterministisk fra par + hullnummer, ikke // tilfeldig, så tallet er stabilt mellom rendringer av samme hull. export function mockHoleLength(par: number, holeNumber: number): number { const base = par === 3 ? 150 : par === 5 ? 480 : 350 return base + ((holeNumber * 17) % 40) - 20 }