import { useState, useRef, useEffect } from 'react'; import { Upload, X, AlertTriangle, CheckCircle, SkipForward, RefreshCw, Loader2, ChevronDown, ChevronUp } from 'lucide-react'; import { Card, CardContent } from './Card'; import { Button } from './Button'; import { Toggle } from './Toggle'; interface RestoreResult { success: boolean; message: string; restored?: Record; skipped?: Record; skipped_details?: Record; files_restored?: number; total_skipped?: number; } interface RestoreModalProps { onClose: () => void; onRestore: (file: File, overwrite: boolean) => Promise; onSuccess: () => void; } type ModalState = 'options' | 'restoring' | 'result'; const CATEGORY_LABELS: Record = { settings: 'Settings', notification_providers: 'Notification Providers', notification_templates: 'Notification Templates', smart_plugs: 'Smart Plugs', printers: 'Printers', filaments: 'Filaments', maintenance_types: 'Maintenance Types', archives: 'Archives', projects: 'Projects', pending_uploads: 'Pending Uploads', external_links: 'External Links', }; export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProps) { const [state, setState] = useState('options'); const [overwrite, setOverwrite] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [result, setResult] = useState(null); const [expandedCategories, setExpandedCategories] = useState>(new Set()); const fileInputRef = useRef(null); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' && state !== 'restoring') onClose(); }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [onClose, state]); const handleFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { setSelectedFile(file); } }; const handleRestore = async () => { if (!selectedFile) return; setState('restoring'); try { const restoreResult = await onRestore(selectedFile, overwrite); setResult(restoreResult); setState('result'); if (restoreResult.success) { onSuccess(); } } catch { setResult({ success: false, message: 'Failed to restore backup. Please check the file format.', }); setState('result'); } }; const toggleCategory = (category: string) => { setExpandedCategories(prev => { const next = new Set(prev); if (next.has(category)) { next.delete(category); } else { next.add(category); } return next; }); }; const totalRestored = result?.restored ? Object.values(result.restored).reduce((a, b) => a + b, 0) + (result.files_restored || 0) : 0; const totalSkipped = result?.total_skipped || 0; return (
{ // Only close if clicking directly on the backdrop, not on children if (e.target === e.currentTarget && state !== 'restoring') { onClose(); } }} > {/* Header */}
{state === 'result' && result?.success ? ( ) : state === 'result' && !result?.success ? ( ) : ( )}

{state === 'options' && 'Restore Backup'} {state === 'restoring' && 'Restoring...'} {state === 'result' && (result?.success ? 'Restore Complete' : 'Restore Failed')}

{state === 'options' && 'Import settings from a backup file'} {state === 'restoring' && 'Please wait while your data is being restored'} {state === 'result' && result?.message}

{state !== 'restoring' && ( )}
{/* Options State */} {state === 'options' && ( <>
{/* File Selection */}
{/* Info Box */}

How duplicate handling works:

  • Printers - matched by serial number
  • Smart Plugs - matched by IP address
  • Notification Providers - matched by name
  • Filaments - matched by name + type + brand
  • Archives - matched by content hash (always skipped)
  • Pending Uploads - matched by filename
  • Settings & Templates - always overwritten
{/* Overwrite Toggle */}

{overwrite ? ( ) : ( )} {overwrite ? 'Replace existing data' : 'Keep existing data'}

{overwrite ? 'Overwrite items that already exist with backup data' : 'Only restore items that don\'t already exist'}

{overwrite && (
Caution: Overwriting will replace your current configurations with data from the backup. Printer access codes are never overwritten for security.
)}
{/* Footer */}
)} {/* Restoring State */} {state === 'restoring' && (

Processing backup file...

)} {/* Result State */} {state === 'result' && result && ( <>
{/* Summary */}
{totalRestored}
Items Restored
{totalSkipped}
Items Skipped
{/* Restored Details */} {result.restored && Object.entries(result.restored).some(([, count]) => count > 0) && (

Restored

{Object.entries(result.restored) .filter(([, count]) => count > 0) .map(([key, count]) => (
{CATEGORY_LABELS[key] || key} {count}
))} {(result.files_restored || 0) > 0 && (
Files (3MF, thumbnails, etc.) {result.files_restored}
)}
)} {/* Skipped Details */} {result.skipped && Object.entries(result.skipped).some(([, count]) => count > 0) && (

Skipped (already exist)

{Object.entries(result.skipped) .filter(([, count]) => count > 0) .map(([key, count]) => { const details = result.skipped_details?.[key] || []; const isExpanded = expandedCategories.has(key); return (
{isExpanded && details.length > 0 && (
{details.slice(0, 10).map((item, i) => (
{item}
))} {details.length > 10 && (
...and {details.length - 10} more
)}
)}
); })}
)} {totalRestored === 0 && totalSkipped === 0 && (
No data was found to restore in the backup file.
)}
{/* Footer */}
)}
); }