teecup/frontend/lib/print-data.ts
Erol Haagenrud 546900c2d6
Some checks failed
Backend-tester / test (push) Successful in 1m8s
Frontend-tester / test (push) Failing after 20s
Før endringer i turneringsoppsettet
2026-08-21 13:54:01 +02:00

268 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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
}