"use client" // V0-eksportert visning (2026-08-17, "stor skjerm først"-prompt, se // ARCHITECTURE_DECISIONS.md ADR-082) -- ren kontrollert visning, ingen // egne API-kall. All logikk (CSV-parsing/kolonnegjetting/lagre- // orkestrering) bor i player-import-panel.tsx, som importerer OG // wrapper denne. IKKE endre denne filen for å tilpasse den til // backend-detaljer (f.eks. kjønn-verdier) -- oversett i orkestrerings- // laget i stedet, se player-import-panel.tsx sin handleSave. import type React from "react" import { useRef } from "react" import { AlertCircle, CheckCircle2, FileUp, Plus, Trash2, TriangleAlert, UploadCloud, X, } from "lucide-react" import { cn } from "@/lib/utils" export 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" export type DraftRow = { key: string } & Record export type Mode = | { kind: "team"; teams: { id: string; name: string }[] } | { kind: "individual"; classes: { id: string; name: string }[] } export type PlayerImportStep = "upload" | "map" | "edit" | "saving" | "done" export type PlayerImportViewProps = { mode: Mode step: PlayerImportStep csvHeaders: string[] columnMapping: Record rows: DraftRow[] errorMessage: string | null saveWarnings: string[] onFileSelected: (file: File) => void onColumnMappingChange: (columnIndex: number, field: FieldKey | "") => void onConfirmMapping: () => void onCellChange: (rowKey: string, field: FieldKey, value: string) => void onDeleteRow: (rowKey: string) => void onAddBlankRow: () => void onSave: () => void onClose: () => void } // Norwegian labels for every field. team_name / class_name are filtered by mode. 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", } // Field order for the mapping dropdown and edit table columns. const FIELD_ORDER: FieldKey[] = [ "display_name", "first_name", "last_name", "nickname", "email", "mobile", "handicap_index", "gender", "birth_date", "club", "club_member_number", "country", "team_name", "class_name", "comment", ] // Per-field column width for the edit table. Compact by default so a wide // screen shows many columns at once; wider for free-text-heavy fields. const FIELD_MIN_W: Partial> = { display_name: "min-w-44", first_name: "min-w-36", last_name: "min-w-36", nickname: "min-w-32", email: "min-w-52", mobile: "min-w-36", handicap_index: "min-w-20", gender: "min-w-28", birth_date: "min-w-36", club: "min-w-44", club_member_number: "min-w-28", country: "min-w-28", team_name: "min-w-40", class_name: "min-w-40", comment: "min-w-56", } const GENDER_OPTIONS: { value: string; label: string }[] = [ { value: "", label: "—" }, { value: "female", label: "Kvinne" }, { value: "male", label: "Mann" }, { value: "other", label: "Annet" }, ] // Shared control classes. Desktop-first: compact but comfortable (mouse is // precise) with a clear keyboard focus ring that works in light and dark mode. const CONTROL_BASE = "h-9 rounded-lg border border-border bg-card text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background" const PRIMARY_BTN = "inline-flex h-10 items-center justify-center gap-2 rounded-lg bg-primary px-5 font-bold text-primary-foreground shadow-sm transition-colors hover:bg-primary/90 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50" const SECONDARY_BTN = "inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-border bg-card px-4 font-bold text-foreground transition-colors hover:border-primary/50 hover:bg-accent/40 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50" export function PlayerImportView({ mode, step, csvHeaders, columnMapping, rows, errorMessage, saveWarnings, onFileSelected, onColumnMappingChange, onConfirmMapping, onCellChange, onDeleteRow, onAddBlankRow, onSave, onClose, }: PlayerImportViewProps) { // Fields selectable in the mapping step, filtered so mode-specific fields // only appear for the matching mode. const selectableFields = FIELD_ORDER.filter((f) => { if (f === "team_name") return mode.kind === "team" if (f === "class_name") return mode.kind === "individual" return true }) // In the edit step, only render columns that some row actually uses (i.e. // fields that were mapped), keeping mode-specific columns filtered too. const activeFields = selectableFields.filter((field) => Object.values(columnMapping).includes(field), ) // Guarantee at least the name column so a blank table is still usable. const editFields = activeFields.length > 0 ? activeFields : ["display_name" as FieldKey] const isSaving = step === "saving" const isEditLike = step === "edit" || step === "saving" return (
{/* Header: title left, large close button right */}

Importer spillere

{mode.kind === "team" ? "Lagturnering" : "Individuell turnering"}

{/* Scrollable body */}
{/* Global error box — shown in every step */} {errorMessage && (
)} {step === "upload" && } {step === "map" && ( )} {isEditLike && ( )} {step === "done" && }
{/* Sticky action bar — always visible for the edit/saving step so the organizer never has to scroll to reach "Lagre" with many rows. */} {isEditLike && (
{rows.length} {rows.length === 1 ? "rad" : "rader"}
)}
) } function UploadStep({ onFileSelected }: { onFileSelected: (file: File) => void }) { const inputRef = useRef(null) function handleFiles(files: FileList | null) { const file = files?.[0] if (file) onFileSelected(file) } return ( ) } function MapStep({ csvHeaders, columnMapping, selectableFields, rowCount, onColumnMappingChange, onConfirmMapping, }: { csvHeaders: string[] columnMapping: Record selectableFields: FieldKey[] rowCount: number onColumnMappingChange: (columnIndex: number, field: FieldKey | "") => void onConfirmMapping: () => void }) { return (

Koble kolonner

Velg hvilket felt hver kolonne i filen tilsvarer. Kolonner du ikke trenger settes til{" "} Ignorer.

{/* Multi-column grid so many mappings are visible at once on a wide screen without scrolling. CSV header left, field dropdown right. */}
    {csvHeaders.map((header, index) => { const selectId = `col-map-${index}` return (
  • Kolonne {index + 1}
  • ) })}
) } function EditTable({ mode, rows, editFields, onCellChange, onDeleteRow, }: { mode: Mode rows: DraftRow[] editFields: FieldKey[] onCellChange: (rowKey: string, field: FieldKey, value: string) => void onDeleteRow: (rowKey: string) => void }) { return (

Kontroller spillerne

Rediger cellene direkte før du lagrer. Du kan legge til eller slette rader.

{rows.length === 0 ? (

Ingen rader ennå

Legg til en rad for å komme i gang.

) : ( // Horizontal scroll is isolated to this wrapper as a fallback for // narrow screens; on a wide screen every column is visible. `relative` // gives absolutely-positioned descendants (sr-only) a clipped block.
{editFields.map((field) => ( ))} {rows.map((row, rowIndex) => ( {editFields.map((field) => ( ))} ))}
{FIELD_LABELS[field]} Handlinger
{field === "gender" ? ( ) : field === "team_name" && mode.kind === "team" ? ( ) : field === "class_name" && mode.kind === "individual" ? ( ) : ( onCellChange(row.key, field, e.target.value)} className={cn(CONTROL_BASE, FIELD_MIN_W[field], "w-full px-2.5 text-sm")} /> )}
)}
) } function DoneStep({ saveWarnings, onClose }: { saveWarnings: string[]; onClose: () => void }) { return (

Importen er fullført

Spillerne er lagret i turneringen.

{saveWarnings.length > 0 && ( // Warnings are informational, not errors — amber, not red.
    {saveWarnings.map((warning, i) => (
  • ))}
)}
) }