"use client" // Individuell rangering PER ØKT i en org-turnering (FEATURE_BACKLOG.md // "Leaderboard for runder og turneringer", avklart 2026-07-28 -- omfang // bevisst AVGRENSET til én økt, ikke summert på tvers av turneringen, se // CLAUDE.md for begrunnelse). Ved siden av, ikke i stedet for, det // eksisterende lag-poeng-leaderboardet (tournament-leaderboard.tsx) -- KUN // meningsfullt for scoring_mode='stroke'-økter i format singles/fourball, // backend avviser (400) ellers. // // Gjenbruker samme rangering/mode-toggle/verdi-merke-mønster som // round-leaderboard.tsx (brutto/netto/poeng, "form + farge"-språket, delt // plassering "T-N") -- egen, enklere lokal versjon her siden denne // leaderboardet ikke har noen hull-for-hull-data å utvide til (backend // returnerer kun aggregerte tall per deltaker, ikke per hull). import { useEffect, useState } from "react" import Link from "next/link" import { ArrowLeft, Trophy } from "lucide-react" import { cn } from "@/lib/utils" type ApiEntry = { participant_id: string display_name: string team_id: string team_name: string team_color: string | null thru: number gross_score: number | null gross_to_par: number | null net_score: number | null net_to_par: number | null total_points: number | null } type ApiLeaderboard = { session_id: string format: string entries: ApiEntry[] } const FORMAT_LABELS: Record = { singles: "Singles", fourball: "Fourball", } type Mode = "gross" | "net" | "points" type RankedEntry = ApiEntry & { rank: number | null; tied: boolean } function valueFor(entry: ApiEntry, mode: Mode): number | null { if (mode === "gross") return entry.gross_to_par if (mode === "net") return entry.net_to_par return entry.total_points } function rankEntries(entries: ApiEntry[], mode: Mode): RankedEntry[] { const withValue = entries.filter((e) => valueFor(e, mode) !== null) const withoutValue = entries.filter((e) => valueFor(e, mode) === null) const direction = mode === "points" ? -1 : 1 withValue.sort((a, b) => direction * ((valueFor(a, mode) as number) - (valueFor(b, mode) as number))) const ranked: RankedEntry[] = [] let rank = 0 let prevValue: number | null = null for (let i = 0; i < withValue.length; i++) { const e = withValue[i] const v = valueFor(e, mode) as number if (v !== prevValue) { rank = i + 1 prevValue = v } const tied = withValue.filter((o) => valueFor(o, mode) === v).length > 1 ranked.push({ ...e, rank, tied }) } for (const e of withoutValue) { ranked.push({ ...e, rank: null, tied: false }) } return ranked } function formatToPar(value: number): string { if (value === 0) return "E" return value > 0 ? `+${value}` : `−${Math.abs(value)}` } function ToParMark({ value, emphasize }: { value: number; emphasize: boolean }) { if (value === 0) { return E } const under = value < 0 return ( {formatToPar(value)} ) } function PointsMark({ value, emphasize }: { value: number; emphasize: boolean }) { return ( {value} ) } function ValueMark({ mode, value, emphasize }: { mode: Mode; value: number; emphasize: boolean }) { return mode === "points" ? : } function RankBadge({ rank, tied, isLeader }: { rank: number | null; tied: boolean; isLeader: boolean }) { return ( ) } function ModeToggle({ mode, onChange }: { mode: Mode; onChange: (m: Mode) => void }) { const options: { value: Mode; label: string }[] = [ { value: "gross", label: "Brutto" }, { value: "net", label: "Netto" }, { value: "points", label: "Poeng" }, ] return (
{options.map((opt) => ( ))}
) } export function SessionIndividualLeaderboard({ organizationId, tournamentId, sessionId, tournamentName, }: { organizationId: string tournamentId: string sessionId: string tournamentName: string }) { const [data, setData] = useState(null) const [error, setError] = useState(null) const [mode, setMode] = useState("gross") useEffect(() => { let cancelled = false async function load() { const res = await fetch(`/orgs/${organizationId}/sessions/${sessionId}/individual-leaderboard`, { credentials: "include", }) if (res.status === 400) { if (!cancelled) setError("Denne økten støtter ikke individuell rangering -- kun slagspill i format singles eller fourball.") return } if (res.status === 404) { if (!cancelled) setError("Økten finnes ikke.") return } if (!res.ok) { if (!cancelled) setError("Klarte ikke å hente rangeringen. Prøv igjen om litt.") return } const parsed: ApiLeaderboard = await res.json() if (!cancelled) setData(parsed) } void load() return () => { cancelled = true } }, [organizationId, sessionId]) const backHref = `/tournaments/${tournamentId}/sessions/${sessionId}?org=${organizationId}&name=${encodeURIComponent(tournamentName)}` if (error) { return (

{error}

Tilbake til økten
) } if (!data) { return (
) } const ranked = rankEntries(data.entries, mode) return (
{tournamentName} {FORMAT_LABELS[data.format] ?? data.format}
{data.entries.length === 0 ? (

Ingen deltakere å rangere ennå -- enten er ingen matcher satt opp, eller oppstillingen er ikke avslørt.

) : ( <>

Stilling

    {ranked.map((entry) => { const value = valueFor(entry, mode) const isLeader = entry.rank === 1 return (
  • ) })}
{mode !== "gross" && ranked.some((e) => e.rank === null) && (

Spillere uten {mode === "net" ? "netto-tall" : "poengsum"} (ingen handicap beregnet ennå) vises uten rangering.

)} )}
) }