'use client'; import { useState, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { Upload, ArrowRight, ArrowLeft, CheckCircle, AlertTriangle, } from 'lucide-react'; import { useAction } from 'next-safe-action/hooks'; import Papa from 'papaparse'; import { Badge } from '@kit/ui/badge'; import { Button } from '@kit/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card'; import { toast } from '@kit/ui/sonner'; import { createMember } from '../server/actions/member-actions'; const MEMBER_FIELDS = [ { key: 'memberNumber', label: 'Mitgliedsnr.' }, { key: 'salutation', label: 'Anrede' }, { key: 'firstName', label: 'Vorname' }, { key: 'lastName', label: 'Nachname' }, { key: 'dateOfBirth', label: 'Geburtsdatum' }, { key: 'email', label: 'E-Mail' }, { key: 'phone', label: 'Telefon' }, { key: 'mobile', label: 'Mobil' }, { key: 'street', label: 'Straße' }, { key: 'houseNumber', label: 'Hausnummer' }, { key: 'postalCode', label: 'PLZ' }, { key: 'city', label: 'Ort' }, { key: 'entryDate', label: 'Eintrittsdatum' }, { key: 'iban', label: 'IBAN' }, { key: 'bic', label: 'BIC' }, { key: 'accountHolder', label: 'Kontoinhaber' }, { key: 'notes', label: 'Notizen' }, ] as const; interface Props { accountId: string; account: string; } type Step = 'upload' | 'mapping' | 'preview' | 'importing' | 'done'; export function MemberImportWizard({ accountId, account }: Props) { const router = useRouter(); const [step, setStep] = useState('upload'); const [rawData, setRawData] = useState([]); const [headers, setHeaders] = useState([]); const [mapping, setMapping] = useState>({}); const [importResults, setImportResults] = useState<{ success: number; errors: string[]; }>({ success: 0, errors: [] }); const { execute: executeCreate } = useAction(createMember); // Step 1: Parse file const handleFileUpload = useCallback( (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; Papa.parse(file, { delimiter: ';', encoding: 'UTF-8', complete: (result) => { const data = result.data as string[][]; if (data.length < 2) { toast.error('Datei enthält keine Daten'); return; } setHeaders(data[0]!); setRawData( data.slice(1).filter((row) => row.some((cell) => cell?.trim())), ); // Auto-map by header name similarity const autoMap: Record = {}; for (const field of MEMBER_FIELDS) { const match = data[0]!.findIndex( (h) => h .toLowerCase() .includes(field.label.toLowerCase().replace('.', '')) || h.toLowerCase().includes(field.key.toLowerCase()), ); if (match >= 0) autoMap[field.key] = String(match); } setMapping(autoMap); setStep('mapping'); toast.success(`${data.length - 1} Zeilen erkannt`); }, error: (err) => { toast.error(`Fehler beim Lesen: ${err.message}`); }, }); }, [], ); // Step 3: Execute import const executeImport = useCallback(async () => { setStep('importing'); let success = 0; const errors: string[] = []; for (let i = 0; i < rawData.length; i++) { const row = rawData[i]!; try { const memberData: Record = { accountId }; for (const field of MEMBER_FIELDS) { const colIdx = mapping[field.key]; if (colIdx !== undefined && row[Number(colIdx)]) { memberData[field.key] = row[Number(colIdx)]!.trim(); } } if (!memberData.firstName || !memberData.lastName) { errors.push(`Zeile ${i + 2}: Vor-/Nachname fehlt`); continue; } await executeCreate(memberData as any); success++; } catch (err) { errors.push( `Zeile ${i + 2}: ${err instanceof Error ? err.message : 'Unbekannter Fehler'}`, ); } } setImportResults({ success, errors }); setStep('done'); toast.success(`${success} Mitglieder importiert`); }, [rawData, mapping, accountId, executeCreate]); const getMappedValue = (rowIdx: number, fieldKey: string): string => { const colIdx = mapping[fieldKey]; if (colIdx === undefined) return ''; return rawData[rowIdx]?.[Number(colIdx)]?.trim() ?? ''; }; return (
{/* Step indicator */}
{(['upload', 'mapping', 'preview', 'done'] as const).map((s, i) => { const labels = [ 'Datei hochladen', 'Spalten zuordnen', 'Vorschau & Import', 'Fertig', ]; const isActive = ['upload', 'mapping', 'preview', 'importing', 'done'].indexOf( step, ) >= i; return (
{i + 1}
{labels[i]} {i < 3 && ( )}
); })}
{/* Step 1: Upload */} {step === 'upload' && ( Datei hochladen

CSV-Datei auswählen

Semikolon-getrennt (;), UTF-8

)} {/* Step 2: Column mapping */} {step === 'mapping' && ( Spalten zuordnen

{rawData.length} Zeilen erkannt. Ordnen Sie die CSV-Spalten den Mitgliedsfeldern zu.

{MEMBER_FIELDS.map((field) => (
{field.label} {mapping[field.key] !== undefined && rawData[0] && ( z.B. "{rawData[0][Number(mapping[field.key])]}" )}
))}
)} {/* Step 3: Preview + execute */} {step === 'preview' && ( Vorschau ({rawData.length} Einträge)
{MEMBER_FIELDS.filter( (f) => mapping[f.key] !== undefined, ).map((f) => ( ))} {rawData.slice(0, 20).map((_, i) => { const hasName = getMappedValue(i, 'firstName') && getMappedValue(i, 'lastName'); return ( {MEMBER_FIELDS.filter( (f) => mapping[f.key] !== undefined, ).map((f) => ( ))} ); })}
# {f.label}
{i + 1}{' '} {!hasName && ( )} {getMappedValue(i, f.key) || '—'}
{rawData.length > 20 && (

... und {rawData.length - 20} weitere Einträge

)}
)} {/* Importing */} {step === 'importing' && (

Importiere Mitglieder...

Bitte warten Sie, bis der Import abgeschlossen ist.

)} {/* Done */} {step === 'done' && (

Import abgeschlossen

{importResults.success} erfolgreich {importResults.errors.length > 0 && ( {importResults.errors.length} Fehler )}
{importResults.errors.length > 0 && (
{importResults.errors.map((err, i) => (

{err}

))}
)}
)}
); }