"use client" import { apiGenderOf, lookupKnownGuest, searchAccounts } from "@/lib/ny-runde/api" import type { Account, Course, Gender, KnownGuest, Player, StatLevel } from "@/lib/ny-runde/types" import { cn } from "@/lib/utils" import { ChevronDown, Search, Sparkles, UserPlus, X } from "lucide-react" import { useEffect, useMemo, useRef, useState } from "react" import { Avatar, Field, NativeSelect, Pill, TextInput, ToggleButton, useDebouncedValue } from "./primitives" import type { Tee } from "@/lib/ny-runde/types" import { useWizard } from "./wizard-context" const GENDERS: { value: Gender; label: string }[] = [ { value: "mann", label: "Mann" }, { value: "kvinne", label: "Kvinne" }, { value: "annet", label: "Annet" }, ] const STAT: { value: StatLevel; label: string }[] = [ { value: "score", label: "Kun slag" }, { value: "score_putts", label: "Slag + putter" }, { value: "full", label: "Alt" }, ] let keyCounter = 0 function nextKey() { keyCounter += 1 return `p-${Date.now()}-${keyCounter}` } function teeName(course: Course | undefined, teeId?: string) { if (!course || !teeId) return "Ikke valgt" return course.tees.find((t) => t.id === teeId)?.name ?? "Ikke valgt" } function compatibleTees(course: Course | undefined, gender?: Gender) { if (!course) return [] if (!gender) return course.tees // ukjent kjønn (nylig lenket konto): vis alle const g = apiGenderOf(gender) if (g === "x") return course.tees return course.tees.filter((t) => t.genders.includes(g)) } export function Step3Players() { const { state, patch } = useWizard() const [adding, setAdding] = useState(false) function updatePlayer(key: string, p: Partial) { patch({ players: state.players.map((pl) => (pl.key === key ? { ...pl, ...p } : pl)) }) } function removePlayer(key: string) { patch({ players: state.players.filter((pl) => pl.key !== key) }) } function addPlayer(p: Player) { patch({ players: [...state.players, p] }) setAdding(false) } return (

Spillere

{state.players.length} {state.players.length === 1 ? "spiller" : "spillere"} i runden.

{adding ? ( setAdding(false)} onAdd={addPlayer} /> ) : ( )}
) } /* ------------------------------------------------------------------ */ /* Player card */ function PlayerCard({ player, course, onChange, onRemove, }: { player: Player course?: Course onChange: (p: Partial) => void onRemove: () => void }) { const [open, setOpen] = useState(false) const fullName = `${player.firstName}${player.lastName ? ` ${player.lastName}` : ""}`.trim() const isOwner = player.kind === "owner" const hcpText = player.hcpFromProfile ? player.hcp != null ? String(player.hcp) : "fra profil" : player.hcp != null ? String(player.hcp) : "–" const tees = compatibleTees(course, player.gender) return (
{fullName || "Ny spiller"} {isOwner ? Deg · Eier : null} {player.isGuest ? Gjest : null}

Utslag: {teeName(course, player.teeId)} · HCP: {hcpText}

{!isOwner ? ( ) : null}
{open ? (
{player.isGuest ? ( <>
onChange({ firstName: e.target.value })} invalid={player.firstName.trim() === ""} /> onChange({ lastName: e.target.value })} />
{ const stillOk = compatibleTees(course, g).some((t) => t.id === player.teeId) onChange({ gender: g, teeId: stillOk ? player.teeId : compatibleTees(course, g)[0]?.id }) }} /> onChange({ email: e.target.value })} /> ) : null} onChange({ teeId: e.target.value })} > {tees.map((t) => ( ))} {player.isGuest ? ( { const v = e.target.value onChange({ hcp: v.trim() === "" ? null : Number.parseFloat(v.replace(",", ".")) }) }} className="max-w-[10rem]" /> ) : player.hcpFromProfile && !isOwner ? ( // Redigerbar fra FØRSTE stund (2026-08-20 brukerfunn) -- var // tidligere en read-only visning ("hentes fra profilen"), som // tvang organisatoren til å legge til spilleren FØRST og // overstyre HCP-en senere via "Runde og spillere" (der // overstyring allerede fungerte, samme PATCH-felt). Forhånds- // utfylt med profilverdien; urørt betyr fortsatt "bruk // profilen", akkurat som før. { const v = e.target.value onChange({ hcp: v.trim() === "" ? null : Number.parseFloat(v.replace(",", ".")) }) }} className="max-w-[10rem]" /> ) : null} onChange({ statLevel: s })} />
) : null}
) } /* ------------------------------------------------------------------ */ /* Legg til-panel (søk + gjest) */ function AddPlayerPanel({ course, defaultStat, existing, onCancel, onAdd, }: { course?: Course defaultStat: StatLevel existing: Player[] onCancel: () => void onAdd: (p: Player) => void }) { const [mode, setMode] = useState<"search" | "guest">("search") const [query, setQuery] = useState("") const debounced = useDebouncedValue(query, 250) const [results, setResults] = useState([]) // gjesteskjema const [first, setFirst] = useState("") const [last, setLast] = useState("") const [email, setEmail] = useState("") const [gender, setGender] = useState("mann") const [hcp, setHcp] = useState("") const [stat, setStat] = useState(defaultStat) const [teeId, setTeeId] = useState(() => compatibleTees(course, "mann")[0]?.id) const firstRef = useRef(null) useEffect(() => { if (mode === "guest") firstRef.current?.focus() }, [mode]) const existingAccountIds = new Set(existing.map((p) => p.accountId).filter(Boolean)) useEffect(() => { const q = debounced.trim() if (q.length < 2) { setResults([]) return } let cancelled = false void searchAccounts(q).then((r) => { if (!cancelled) setResults(r.filter((a) => !existingAccountIds.has(a.id))) }) return () => { cancelled = true } // eslint-disable-next-line react-hooks/exhaustive-deps }, [debounced]) function switchToGuest() { const parts = query.trim().split(/\s+/) setFirst(parts[0] ?? "") setLast(parts.slice(1).join(" ")) setMode("guest") } function addAccount(acc: Account) { onAdd({ key: nextKey(), kind: "account", accountId: acc.id, firstName: acc.firstName, lastName: acc.lastName, gender: undefined, // ukjent klient-side inntil profilen lastes isGuest: false, hcp: acc.handicap, hcpFromProfile: true, teeId: course?.tees[0]?.id, statLevel: defaultStat, }) } function addGuest() { if (first.trim() === "") return onAdd({ key: nextKey(), kind: "guest", firstName: first.trim(), lastName: last.trim(), gender, isGuest: true, email: email.trim() || undefined, hcp: hcp.trim() === "" ? null : Number.parseFloat(hcp.replace(",", ".")), teeId, statLevel: stat, }) } return (

Legg til medspiller

{mode === "search" ? (
{debounced.trim().length >= 2 ? ( results.length > 0 ? (
    {results.map((a) => (
  • ))}
) : (

Ingen kontoer matcher søket.

) ) : null}
) : ( setMode("search")} onSubmit={addGuest} /> )}
) } function GuestForm({ course, first, setFirst, last, setLast, email, setEmail, gender, setGender, hcp, setHcp, stat, setStat, teeId, setTeeId, firstRef, onBackToSearch, onSubmit, }: { course?: Course first: string setFirst: (v: string) => void last: string setLast: (v: string) => void email: string setEmail: (v: string) => void gender: Gender setGender: (v: Gender) => void hcp: string setHcp: (v: string) => void stat: StatLevel setStat: (v: StatLevel) => void teeId: string | undefined setTeeId: (v: string | undefined) => void firstRef: React.RefObject onBackToSearch: () => void onSubmit: () => void }) { const tees = compatibleTees(course, gender) // kjent-gjest-oppslag på e-post (scoped til DENNE brukeren server-side) const debouncedEmail = useDebouncedValue(email, 350) const [match, setMatch] = useState(null) useEffect(() => { const e = debouncedEmail.trim() if (e.length < 5 || !e.includes("@")) { setMatch(null) return } let cancelled = false void lookupKnownGuest(e).then((g) => { if (!cancelled) setMatch(g) }) return () => { cancelled = true } }, [debouncedEmail]) return (
setFirst(e.target.value)} invalid={first.trim() === ""} /> setLast(e.target.value)} />
setEmail(e.target.value)} placeholder="navn@example.com" /> {match ? (
) : null} { const stillOk = compatibleTees(course, g).some((t) => t.id === teeId) setGender(g) if (!stillOk) setTeeId(compatibleTees(course, g)[0]?.id) }} /> setTeeId(e.target.value || undefined)}> {tees.map((t: Tee) => ( ))} setHcp(e.target.value)} className="max-w-[10rem]" />
) } /* ------------------------------------------------------------------ */ function GenderChoice({ value, onChange }: { value: Gender; onChange: (v: Gender) => void }) { return (
{GENDERS.map((g) => ( onChange(g.value)} className="flex-1"> {g.label} ))}
) } function StatChoice({ value, onChange }: { value: StatLevel; onChange: (v: StatLevel) => void }) { return (
{STAT.map((s) => ( onChange(s.value)} className="flex-1"> {s.label} ))}
) }