"use client" // Horisontalt scorekort for en frittstående runde (ADR-033) -- egen, // dedikert side (ikke en del av round-stats.tsx sin aggregerte statistikk). // Presentasjon fra V0 (zip 17), datalag skrevet om fra mock til ekte fetch // mot samme `/rounds/{id}/participants/{id}/holes`-endepunkt som // round-detail.tsx og round-stats.tsx allerede bruker. // // Bevisst forskjell fra V0s eksport: V0s egen `strokesReceived()` var en // generisk modulo-formel -- byttet ut med backend sin ALLEREDE beregnede // `strokes_received` per hull (samme `allocate_strokes_by_index()`- // algoritme som resten av appen), for å unngå to ulike implementasjoner av // samme HCP-slagfordeling. // // Format-bevissthet lagt til 2026-07-28: brukeren rapporterte at siden kun // noensinne viste brutto/netto/stableford, uansett spilleform -- bekreftet // ved simulering at (a) foursome/greensome/scramble var HELT BLANKE her // (delt-ball-formater lagrer score PER SIDE, ikke per deltaker -- se // ADR-039 -- og denne siden spurte kun mot deltaker-endepunktet), og (b) // match/fourball/skins viste riktige rå tall, men aldri HVORFOR et hull // ble avgjort som det ble (brutto vs. netto, hvem sitt netto talte for // siden i fourball, hvem som vant/hvilket hull som rullet videre i skins). // Fikset ved å: (1) la delt-ball-formater bruke SIDER som "enhet" i stedet // for deltakere (samme visuelle ScoreBlock-tabell, bare på // `/rounds/{id}/sides/{id}/holes`, som fikk et nytt `strokes_received`-felt // samtidig), og (2) legge til en egen "Matchforløp"/"Skins hull for hull"- // seksjon drevet av `/rounds/{id}/format-result` sitt nye `holes`-felt // (brutto/netto/slag mottatt PER enhet PER hull, pluss hvem som vant og // hvorfor). import { useEffect, useState } from "react" import Link from "next/link" import { ArrowLeft, ArrowUp, ArrowDown, ArrowRight, ArrowUpLeft, ArrowUpRight, Check, Target } from "lucide-react" import { cn } from "@/lib/utils" // --- API-typer --------------------------------------------------------------- type ApiParticipant = { id: string user_id: string | null guest_name: string | null display_name: string is_owner: boolean round_side_id: string | null playing_handicap: number | null } type ApiSide = { id: string label: string | null } type ApiRound = { name: string | null id: string course_name_snapshot: string tee_name_snapshot: string played_at: string start_hole: number holes_planned: number play_format: string participants: ApiParticipant[] sides: ApiSide[] } type ApiHole = { hole_number: number par: number stroke_index: number played: boolean score: number | null picked_up: boolean strokes_received: number | null // Detaljfelt (2026-08-03) -- allerede returnert av samme // RoundHoleOut/`/rounds/.../holes`-endepunkt denne siden lenge har brukt, // bare ikke tidligere lest her. Presentert som egne, forkortede rader i // ScoreBlock i stedet for kun å ligge tilgjengelig i round-detail.tsx sin // "Så langt i runden"-seksjon. putts: number | null tee_shot_result: "fairway" | "left" | "right" | null approach_result: "hit" | "short" | "long" | "left" | "right" | null chip_count: number | null bunker_shot_count: number | null penalty_strokes: number | null anyway_strokes: number | null } const SHARED_BALL_FORMATS = ["foursome", "greensome", "scramble_2", "scramble_4", "chapman"] const TWO_SIDED_FORMATS = ["match", "fourball", "high_low_high", ...SHARED_BALL_FORMATS] const FORMAT_LABELS: Record = { stroke: "Slagspill", stableford: "Stableford", match: "Match", skins: "Skins", fourball: "Fourball", foursome: "Foursome", greensome: "Greensome", scramble_2: "Scramble (2)", scramble_4: "Scramble (4)", chapman: "Chapman", copenhagen: "Københavner", bbb: "Bingo Bango Bongo", flag: "Flaggturnering", shamble: "Shamble", money_ball: "Money Ball", high_low_high: "High-low-high", } const dateFormatter = new Intl.DateTimeFormat("no-NO", { day: "numeric", month: "long", year: "numeric" }) // Viewer-relativt (ADR-036 fase 3-utvidelsen, 2026-07-26) -- se samme // begrunnelse i round-detail.tsx sin playerLabel(). function participantLabel(p: ApiParticipant, viewerId: string | null): string { return viewerId !== null && p.user_id === viewerId ? "Deg" : p.display_name } // --- Scoring helpers ------------------------------------------------------- function netScore(h: ApiHole): number | null { return h.played && h.score !== null && h.strokes_received !== null ? h.score - h.strokes_received : null } // Stableford: 2 pts for net par, +/- 1 per stroke, floored at 0. function stablefordPoints(h: ApiHole): number | null { const net = netScore(h) if (net === null) return null return Math.max(0, h.par - net + 2) } // Samme GIR-formel som round-stats.tsx sin isGir/round-detail.tsx sin // GIR-auto-inferens: slag minus putt garanterer green i regulering når // resultatet er par-2 eller bedre. Krever kjent slag+putt-tall. function isGir(h: ApiHole): boolean { return h.score !== null && h.putts !== null && h.score - h.putts <= h.par - 2 } type ScoreClass = "eagle" | "birdie" | "par" | "bogey" | "double" function classify(gross: number, par: number): ScoreClass { const diff = gross - par if (diff <= -2) return "eagle" if (diff === -1) return "birdie" if (diff === 0) return "par" if (diff === 1) return "bogey" return "double" } function sum(values: number[]): number { return values.reduce((a, b) => a + b, 0) } function signedToPar(score: number, par: number): string { const diff = score - par if (diff === 0) return "E" return diff > 0 ? `+${diff}` : `−${Math.abs(diff)}` } // --- Score mark (the visual language for the Score row) -------------------- // Shape encodes direction (circle = under par, square = over par) so meaning // survives without color; fill encodes magnitude (filled = 2+ strokes off). function ScoreMark({ gross, par, pickedUp }: { gross: number | null; par: number; pickedUp?: boolean }) { const shared = "flex size-6 items-center justify-center text-sm font-extrabold tabular-nums sm:size-7" // "Plukket opp" (migrasjon 038, Stableford) -- 0 poeng er impliserte, // vises som "PU" i stedet for det underliggende Net Double Bogey-tallet. if (pickedUp) { return ( PU ) } if (gross === null) { return {"–"} } const kind = classify(gross, par) if (kind === "eagle") { return ( {gross} ) } if (kind === "birdie") { return ( {gross} ) } if (kind === "bogey") { return ( {gross} ) } if (kind === "double") { return ( {gross} ) } // par return {gross} } // --- One nine-hole block (a real table for row/column semantics) ----------- // Kompakte retnings-/treff-ikoner for de nye detaljradene (2026-08-03) -- // samme ikonvalg som DirectionCross (round-detail.tsx sin registrerings- // veiviser) allerede bruker, kun skalert ned for en tett tabellcelle. // Bøyde piler for Fairway (utslaget DRIVER av til en side, ulikt et // innspill som bommer rett til siden -- derfor ArrowUpLeft/-Right her, // ikke de rette ArrowLeft/-Right Innspill-raden bruker). function FairwayIcon({ result }: { result: ApiHole["tee_shot_result"] }) { if (result === null) return null if (result === "fairway") return