"use client" // Massimport av spillere via CSV (2026-08-17) -- MIDLERTIDIG hånd-kodet // versjon, bygget for å verifisere hele kjeden (CSV -> kolonnetilknytning // -> redigerbar tabell -> POST .../players/bulk -> løkke over // participants/roster-endepunktene) ende-til-ende FØR V0-eksporten er // klar. Erstattes med V0-designet visning når den kommer tilbake (samme // rekkefølge som FlagPlantSheet, se ARCHITECTURE_DECISIONS.md). import { useRef, useState } from "react" import Papa from "papaparse" import { Upload, X } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" type FieldKey = | "display_name" | "email" | "handicap_index" | "gender" | "birth_date" | "mobile" | "club" | "nickname" | "country" | "club_member_number" | "first_name" | "last_name" | "comment" | "team_name" | "class_name" type DraftRow = { key: string } & Record const EMPTY_ROW: Omit = { display_name: "", email: "", handicap_index: "", gender: "", birth_date: "", mobile: "", club: "", nickname: "", country: "", club_member_number: "", first_name: "", last_name: "", comment: "", team_name: "", class_name: "", } const FIELD_LABELS: Record = { display_name: "Navn", email: "E-post", handicap_index: "HCP", gender: "Kjønn", birth_date: "Fødselsdato", mobile: "Mobil", club: "Klubb", nickname: "Kallenavn", country: "Land", club_member_number: "Medlemsnr.", first_name: "Fornavn", last_name: "Etternavn", comment: "Kommentar", team_name: "Lag", class_name: "Klasse", } // Fuzzy gjetting av kolonnetilknytning -- små bokstaver, uten mellomrom/ // bindestrek, sammenlignet mot vanlige norske/engelske varianter. const HEADER_GUESSES: Record = { navn: "display_name", name: "display_name", fulltnavn: "display_name", spillernavn: "display_name", epost: "email", email: "email", mail: "email", hcp: "handicap_index", handicap: "handicap_index", spillehandicap: "handicap_index", kjonn: "gender", gender: "gender", fodselsdato: "birth_date", fodtdato: "birth_date", birthdate: "birth_date", fodt: "birth_date", mobil: "mobile", mobile: "mobile", telefon: "mobile", phone: "mobile", klubb: "club", club: "club", medlemsnr: "club_member_number", medlemsnummer: "club_member_number", fornavn: "first_name", firstname: "first_name", etternavn: "last_name", lastname: "last_name", lag: "team_name", team: "team_name", klasse: "class_name", class: "class_name", kommentar: "comment", comment: "comment", land: "country", country: "country", kallenavn: "nickname", nickname: "nickname", } function normalizeHeader(h: string): string { return h.trim().toLowerCase().replace(/[\s_-]+/g, "") } function guessField(header: string): FieldKey | "" { return HEADER_GUESSES[normalizeHeader(header)] ?? "" } type ApiPlayer = { id: string display_name: string handicap_index: number | null gender: string | null mobile: string | null email: string | null birth_date: string | null nickname: string | null country: string | null club: string | null club_member_number: string | null first_name: string | null last_name: string | null paid: boolean comment: string | null } type Mode = | { kind: "team"; teams: { id: string; name: string }[] } | { kind: "individual"; classes: { id: string; name: string }[] } export function PlayerImportPanel({ organizationId, tournamentId, mode, onClose, onImportComplete, }: { organizationId: string tournamentId: string mode: Mode onClose: () => void onImportComplete: (createdOrUpdated: { player: ApiPlayer; status: string }[]) => void }) { const [step, setStep] = useState<"upload" | "map" | "edit" | "saving" | "done">("upload") const [csvHeaders, setCsvHeaders] = useState([]) const [csvRows, setCsvRows] = useState([]) const [mapping, setMapping] = useState>({}) const [rows, setRows] = useState([]) const [error, setError] = useState(null) const [saveErrors, setSaveErrors] = useState([]) const fileInputRef = useRef(null) function handleFile(file: File) { setError(null) Papa.parse(file, { skipEmptyLines: true, complete: (result) => { const data = result.data if (data.length === 0) { setError("Fant ingen rader i filen.") return } const headers = data[0] const body = data.slice(1) setCsvHeaders(headers) setCsvRows(body) const guessed: Record = {} headers.forEach((h, i) => { guessed[i] = guessField(h) }) setMapping(guessed) setStep("map") }, error: () => setError("Klarte ikke å lese filen. Er det en gyldig CSV?"), }) } function confirmMapping() { const mapped: DraftRow[] = csvRows.map((cells, i) => { const row: DraftRow = { key: `csv-${i}`, ...EMPTY_ROW } csvHeaders.forEach((_, colIdx) => { const field = mapping[colIdx] if (field) row[field] = (cells[colIdx] ?? "").trim() }) return row }) setRows(mapped) setStep("edit") } function updateCell(key: string, field: FieldKey, value: string) { setRows((prev) => prev.map((r) => (r.key === key ? { ...r, [field]: value } : r))) } function addBlankRow() { setRows((prev) => [...prev, { key: `manual-${Date.now()}`, ...EMPTY_ROW }]) } function removeRow(key: string) { setRows((prev) => prev.filter((r) => r.key !== key)) } function teamIdForName(name: string): string | null { if (mode.kind !== "team") return null const match = mode.teams.find((t) => t.name.trim().toLowerCase() === name.trim().toLowerCase()) return match?.id ?? null } function classIdForName(name: string): string | null { if (mode.kind !== "individual") return null const match = mode.classes.find((c) => c.name.trim().toLowerCase() === name.trim().toLowerCase()) return match?.id ?? null } async function handleSave() { const validRows = rows.filter((r) => r.display_name.trim().length > 0) if (validRows.length === 0) { setError("Ingen rader med navn å lagre.") return } setStep("saving") setError(null) setSaveErrors([]) try { const payload = validRows.map((r) => ({ display_name: r.display_name.trim(), email: r.email.trim() || null, handicap_index: r.handicap_index.trim() ? Number(r.handicap_index) : null, gender: r.gender.trim() || null, birth_date: r.birth_date.trim() || null, mobile: r.mobile.trim() || null, club: r.club.trim() || null, nickname: r.nickname.trim() || null, country: r.country.trim() || null, club_member_number: r.club_member_number.trim() || null, first_name: r.first_name.trim() || null, last_name: r.last_name.trim() || null, comment: r.comment.trim() || null, })) const bulkRes = await fetch(`/orgs/${organizationId}/players/bulk`, { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify(payload), }) if (!bulkRes.ok) throw new Error(`bulk: ${bulkRes.status}`) const results: { player: ApiPlayer; status: string }[] = await bulkRes.json() const errors: string[] = [] for (let i = 0; i < validRows.length; i++) { const row = validRows[i] const player = results[i]?.player if (!player) continue if (mode.kind === "team") { const teamId = teamIdForName(row.team_name) if (!teamId) { if (row.team_name.trim()) { errors.push(`${row.display_name}: fant ikke laget "${row.team_name}" -- lagt i spillerpoolen, ikke på lag.`) } continue } const res = await fetch(`/orgs/${organizationId}/teams/${teamId}/roster`, { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ player_id: player.id }), }) if (!res.ok && res.status !== 409) { errors.push(`${row.display_name}: klarte ikke å legge til på laget.`) } } else { const classId = row.class_name.trim() ? classIdForName(row.class_name) : null const res = await fetch(`/orgs/${organizationId}/tournaments/${tournamentId}/participants`, { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ player_id: player.id, class_id: classId }), }) if (!res.ok && res.status !== 409) { errors.push(`${row.display_name}: klarte ikke å melde på turneringen.`) } } } setSaveErrors(errors) setStep("done") onImportComplete(results) } catch { setError("Importen feilet. Prøv igjen.") setStep("edit") } } return (

Importer spillere fra CSV

{error && (
{error}
)} {step === "upload" && (
)} {step === "map" && (

Velg hvilket felt hver kolonne skal fylle. "Ignorer" hopper over kolonnen.

{csvHeaders.map((h, i) => (
{h}
))}
)} {(step === "edit" || step === "saving") && (
{(Object.keys(FIELD_LABELS) as FieldKey[]) .filter((f) => mode.kind === "team" || f !== "team_name") .filter((f) => mode.kind === "individual" || f !== "class_name") .map((f) => ( ))} {rows.map((row) => ( {(Object.keys(FIELD_LABELS) as FieldKey[]) .filter((f) => mode.kind === "team" || f !== "team_name") .filter((f) => mode.kind === "individual" || f !== "class_name") .map((f) => ( ))} ))}
{FIELD_LABELS[f]}
updateCell(row.key, f, e.target.value)} disabled={step === "saving"} className="min-w-24" />
)} {step === "done" && (

Importen er fullført.

{saveErrors.length > 0 && (

Noen rader trenger oppfølging:

    {saveErrors.map((e, i) => (
  • {e}
  • ))}
)}
)}
) }