teecup/frontend/lib/print-data.ts

269 lines
8.3 KiB
TypeScript
Raw Normal View History

2026-08-21 13:54:01 +02:00
// 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<T>(url: string): Promise<T> {
const res = await fetch(url, { credentials: "include" })
if (!res.ok) throw new Error(`${url}: ${res.status}`)
return res.json() as Promise<T>
}
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<string, number>
projected_points_by_team: Record<string, number>
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<ApiTournament | null> {
const list = await getJson<ApiTournament[]>(`/orgs/${org}/tournaments`)
return list.find((t) => t.id === tournamentId) ?? null
}
export function fetchRounds(org: string, tournamentId: string) {
return getJson<ApiRound[]>(`${tournamentBase(org, tournamentId)}/rounds`)
}
export function fetchRoundGroups(org: string, tournamentId: string, roundId: string) {
return getJson<ApiRoundGroups>(`${tournamentBase(org, tournamentId)}/rounds/${roundId}/groups`)
}
export function fetchRoundParticipants(org: string, tournamentId: string, roundId: string) {
return getJson<ApiRoundParticipant[]>(`${tournamentBase(org, tournamentId)}/rounds/${roundId}/participants`)
}
export function fetchTournamentParticipants(org: string, tournamentId: string) {
return getJson<ApiTournamentParticipant[]>(`${tournamentBase(org, tournamentId)}/participants`)
}
export function fetchCourseHoles(org: string, courseId: string) {
return getJson<ApiCourseHole[]>(`/orgs/${org}/courses/${courseId}/holes`)
}
export function fetchCourses(org: string) {
return getJson<ApiCourse[]>(`/orgs/${org}/courses`)
}
export function fetchSessions(org: string, tournamentId: string) {
return getJson<ApiSession[]>(`${tournamentBase(org, tournamentId)}/sessions`)
}
export function fetchMatches(org: string, sessionId: string) {
return getJson<ApiMatch[]>(`/orgs/${org}/sessions/${sessionId}/matches`)
}
export function fetchTeams(org: string, tournamentId: string) {
return getJson<ApiTeam[]>(`${tournamentBase(org, tournamentId)}/teams`)
}
export function fetchRoster(org: string, teamId: string) {
return getJson<ApiRosterEntry[]>(`/orgs/${org}/teams/${teamId}/roster`)
}
export function fetchIndividualLeaderboard(org: string, tournamentId: string) {
return getJson<ApiLeaderboardEntry[]>(`${tournamentBase(org, tournamentId)}/individual-leaderboard`)
}
export function fetchCupLeaderboard(org: string, tournamentId: string) {
return getJson<ApiCupLeaderboard>(`${tournamentBase(org, tournamentId)}/leaderboard`)
}
export function fetchPlayers(org: string) {
return getJson<ApiPlayer[]>(`/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<string, string> = {
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
}