import React, { useCallback, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { AlertCircle, CheckCircle, FileText, Loader2, Upload, RotateCcw, ArrowLeft, Eye, Play, } from 'lucide-react'; import clsx from 'clsx'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Select } from '@/components/ui/Select'; import { Badge } from '@/components/ui/Badge'; import { useToast } from '@/components/ui/Toast'; import { importCsv, type ImportResult } from '@/api/importExport'; // ─── Constants ────────────────────────────────────────────────────────────── const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB const ACCEPTED_EXTENSIONS = ['.csv']; const ENTITY_OPTIONS = [ { value: 'contacts', label: 'Kontakte' }, { value: 'companies', label: 'Firmen' }, ]; type WizardStep = 'upload' | 'preview' | 'review' | 'result'; // ─── Component ────────────────────────────────────────────────────────────── export function ImportWizard() { const { t } = useTranslation(); const toast = useToast(); const [step, setStep] = useState('upload'); const [file, setFile] = useState(null); const [entityType, setEntityType] = useState('contacts'); const [previewResult, setPreviewResult] = useState(null); const [importResult, setImportResult] = useState(null); const [previewConfirmed, setPreviewConfirmed] = useState(false); const [isDragOver, setIsDragOver] = useState(false); const [isLoadingPreview, setIsLoadingPreview] = useState(false); const [isLoadingImport, setIsLoadingImport] = useState(false); const [fileError, setFileError] = useState(null); const inputRef = useRef(null); // ─── File validation ──────────────────────────────────────────────────── const validateFile = useCallback((f: File): string | null => { const ext = f.name.toLowerCase().substring(f.name.lastIndexOf('.')); if (!ACCEPTED_EXTENSIONS.includes(ext)) { return t('importExport.onlyCsvAllowed', 'Nur CSV-Dateien sind erlaubt'); } if (f.size > MAX_FILE_SIZE) { return t('importExport.fileTooLarge', 'Datei ist größer als 10 MB'); } return null; }, [t]); const handleFileSelect = useCallback((f: File | null) => { if (!f) return; const error = validateFile(f); if (error) { setFileError(error); setFile(null); return; } setFileError(null); setFile(f); }, [validateFile]); const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); const droppedFile = e.dataTransfer.files?.[0]; if (droppedFile) handleFileSelect(droppedFile); }, [handleFileSelect]); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragOver(false); }, []); const handleFileInputChange = useCallback((e: React.ChangeEvent) => { const selected = e.target.files?.[0] ?? null; handleFileSelect(selected); }, [handleFileSelect]); // ─── Preview ──────────────────────────────────────────────────────────── const handlePreview = async () => { if (!file) return; setIsLoadingPreview(true); try { const result = await importCsv(file, entityType, true); setPreviewResult(result); setStep('preview'); } catch (err: any) { toast.error(err?.message || t('importExport.previewFailed', 'Vorschau fehlgeschlagen')); } finally { setIsLoadingPreview(false); } }; // ─── Import ───────────────────────────────────────────────────────────── const handleImport = async () => { if (!file) return; setIsLoadingImport(true); try { const result = await importCsv(file, entityType, false); setImportResult(result); setStep('result'); toast.success(t('importExport.importSuccess', 'Import erfolgreich abgeschlossen')); } catch (err: any) { setImportResult({ errors: [err?.message || t('importExport.importFailed', 'Import fehlgeschlagen')], }); setStep('result'); } finally { setIsLoadingImport(false); } }; // ─── Reset ────────────────────────────────────────────────────────────── const handleReset = () => { setStep('upload'); setFile(null); setPreviewResult(null); setImportResult(null); setPreviewConfirmed(false); setFileError(null); if (inputRef.current) inputRef.current.value = ''; }; const handleBack = () => { if (step === 'preview') setStep('upload'); else if (step === 'review') setStep('preview'); }; // ─── Step indicator ───────────────────────────────────────────────────── const steps: { key: WizardStep; label: string }[] = [ { key: 'upload', label: t('importExport.stepUpload', 'Datei hochladen') }, { key: 'preview', label: t('importExport.stepPreview', 'Vorschau') }, { key: 'review', label: t('importExport.stepReview', 'Überprüfung') }, { key: 'result', label: t('importExport.stepResult', 'Ergebnis') }, ]; const currentStepIndex = steps.findIndex((s) => s.key === step); // ─── Render ───────────────────────────────────────────────────────────── return (
{/* Step indicator */}
{steps.map((s, idx) => (
{idx < currentStepIndex ? ( ) : ( idx + 1 )}
{idx < steps.length - 1 && (
)} ))}
{/* Step: Upload */} {step === 'upload' && (
{/* Entity type */} {file ? (

{file.name}

{(file.size / 1024).toFixed(1)} KB

) : (

{t('importExport.dropFileHere', 'CSV-Datei hierher ziehen oder klicken zum Auswählen')}

{t('importExport.fileConstraints', 'Max. 10 MB, nur .csv')}

)}
{fileError && (
{fileError}
)} {/* Action */}
)} {/* Step: Preview */} {step === 'preview' && previewResult && ( }> {t('common.back', 'Zurück')} } >
{/* Summary stats */}
} /> } /> } />
{/* Errors */} {previewResult.errors && previewResult.errors.length > 0 && (

{t('importExport.errors', 'Fehler')} ({previewResult.errors.length})

    {previewResult.errors.map((err, idx) => (
  • {err}
  • ))}
)} {/* Warnings */} {previewResult.warnings && previewResult.warnings.length > 0 && (

{t('importExport.warnings', 'Warnungen')} ({previewResult.warnings.length})

    {previewResult.warnings.map((warn, idx) => (
  • {warn}
  • ))}
)} {/* Preview rows */} {previewResult.rows && previewResult.rows.length > 0 && (

{t('importExport.previewRows', 'Vorschau der ersten Zeilen')}

{Object.keys(previewResult.rows[0]).slice(0, 6).map((key) => ( ))} {previewResult.rows.slice(0, 5).map((row, idx) => ( {Object.values(row).slice(0, 6).map((val, vidx) => ( ))} ))}
{key}
{String(val ?? '')}
)} {/* Action */}
)} {/* Step: Review */} {step === 'review' && ( }> {t('common.back', 'Zurück')} } >
{/* Summary */}
{t('importExport.entityType', 'Entitätstyp')} {entityType === 'contacts' ? 'Kontakte' : 'Firmen'}
{t('importExport.file', 'Datei')} {file?.name}
{previewResult && ( <>
{t('importExport.totalRows', 'Gesamtzeilen')} {previewResult.total ?? 0}
{t('importExport.created', 'Neu')} {previewResult.created ?? 0}
{t('importExport.updated', 'Aktualisiert')} {previewResult.updated ?? 0}
{previewResult.errors && previewResult.errors.length > 0 && (
{t('importExport.errors', 'Fehler')} {previewResult.errors.length}
)} {previewResult.warnings && previewResult.warnings.length > 0 && (
{t('importExport.warnings', 'Warnungen')} {previewResult.warnings.length}
)} )}
{/* Confirmation checkbox */} {/* Actions */}
)} {/* Step: Result */} {step === 'result' && importResult && ( }> {t('importExport.newImport', 'Neuer Import')} } >
{/* Success / Error indicator */} {importResult.errors && importResult.errors.length > 0 && !importResult.created && !importResult.updated ? (
{t('importExport.importFailed', 'Import fehlgeschlagen')}
) : (
{t('importExport.importSuccess', 'Import erfolgreich abgeschlossen')}
)} {/* Stats */} {(importResult.created !== undefined || importResult.updated !== undefined || importResult.total !== undefined) && (
{importResult.total !== undefined && ( } /> )} {importResult.created !== undefined && ( } /> )} {importResult.updated !== undefined && ( } /> )}
)} {/* Errors */} {importResult.errors && importResult.errors.length > 0 && (

{t('importExport.errors', 'Fehler')} ({importResult.errors.length})

    {importResult.errors.map((err, idx) => (
  • {err}
  • ))}
)} {/* Warnings */} {importResult.warnings && importResult.warnings.length > 0 && (

{t('importExport.warnings', 'Warnungen')} ({importResult.warnings.length})

    {importResult.warnings.map((warn, idx) => (
  • {warn}
  • ))}
)}
)}
); } // ─── Helper sub-component ─────────────────────────────────────────────────── function StatBox({ label, value, icon }: { label: string; value: number; icon: React.ReactNode }) { return (
{icon}

{label}

{value}

); }