import { useState, useEffect, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Database, Plus, Trash2, RotateCcw, Loader2, Pencil, Check, X, Search, Download, Upload } from 'lucide-react'; import { api } from '../api/client'; import type { SpoolCatalogEntry } from '../api/client'; import { useToast } from '../contexts/ToastContext'; import { Card, CardHeader, CardContent } from './Card'; import { ConfirmModal } from './ConfirmModal'; export function SpoolCatalogSettings() { const { t } = useTranslation(); const { showToast } = useToast(); const [catalog, setCatalog] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const fileInputRef = useRef(null); // Add/Edit form state const [showAddForm, setShowAddForm] = useState(false); const [editingId, setEditingId] = useState(null); const [formName, setFormName] = useState(''); const [formWeight, setFormWeight] = useState(''); const [saving, setSaving] = useState(false); // Confirmation modals const [deleteEntry, setDeleteEntry] = useState(null); const [showResetConfirm, setShowResetConfirm] = useState(false); const loadCatalog = useCallback(async () => { try { const entries = await api.getSpoolCatalog(); setCatalog(entries); } catch { showToast(t('settings.catalog.loadFailed'), 'error'); } finally { setLoading(false); } }, [showToast, t]); useEffect(() => { loadCatalog(); }, [loadCatalog]); const filteredCatalog = catalog.filter(entry => entry.name.toLowerCase().includes(search.toLowerCase()) ); const handleAdd = async () => { if (!formName.trim() || !formWeight) { showToast(t('settings.catalog.nameWeightRequired'), 'error'); return; } setSaving(true); try { const entry = await api.addCatalogEntry({ name: formName.trim(), weight: parseInt(formWeight) }); setCatalog(prev => [...prev, entry].sort((a, b) => a.name.localeCompare(b.name))); setShowAddForm(false); setFormName(''); setFormWeight(''); showToast(t('settings.catalog.entryAdded'), 'success'); } catch { showToast(t('settings.catalog.addFailed'), 'error'); } finally { setSaving(false); } }; const startEdit = (entry: SpoolCatalogEntry) => { setEditingId(entry.id); setFormName(entry.name); setFormWeight(entry.weight.toString()); }; const cancelEdit = () => { setEditingId(null); setFormName(''); setFormWeight(''); }; const handleUpdate = async (id: number) => { if (!formName.trim() || !formWeight) { showToast(t('settings.catalog.nameWeightRequired'), 'error'); return; } setSaving(true); try { const updated = await api.updateCatalogEntry(id, { name: formName.trim(), weight: parseInt(formWeight) }); setCatalog(prev => prev.map(e => e.id === id ? updated : e).sort((a, b) => a.name.localeCompare(b.name))); setEditingId(null); setFormName(''); setFormWeight(''); showToast(t('settings.catalog.entryUpdated'), 'success'); } catch { showToast(t('settings.catalog.updateFailed'), 'error'); } finally { setSaving(false); } }; const handleDelete = async () => { if (!deleteEntry) return; try { await api.deleteCatalogEntry(deleteEntry.id); setCatalog(prev => prev.filter(e => e.id !== deleteEntry.id)); showToast(t('settings.catalog.entryDeleted'), 'success'); } catch { showToast(t('settings.catalog.deleteFailed'), 'error'); } finally { setDeleteEntry(null); } }; const handleReset = async () => { setShowResetConfirm(false); setLoading(true); try { await api.resetSpoolCatalog(); await loadCatalog(); showToast(t('settings.catalog.resetSuccess'), 'success'); } catch { showToast(t('settings.catalog.resetFailed'), 'error'); setLoading(false); } }; const handleExport = () => { const exportData = catalog.map(({ name, weight }) => ({ name, weight })); const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'spool-catalog.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); showToast(t('settings.catalog.exported', { count: catalog.length }), 'success'); }; const handleImport = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; try { const text = await file.text(); const data = JSON.parse(text) as Array<{ name: string; weight: number }>; if (!Array.isArray(data)) throw new Error('Invalid format'); let added = 0; let skipped = 0; for (const item of data) { if (!item.name || typeof item.weight !== 'number') { skipped++; continue; } const exists = catalog.some(c => c.name.toLowerCase() === item.name.toLowerCase()); if (exists) { skipped++; continue; } try { const entry = await api.addCatalogEntry({ name: item.name, weight: item.weight }); setCatalog(prev => [...prev, entry].sort((a, b) => a.name.localeCompare(b.name))); added++; } catch { skipped++; } } showToast(t('settings.catalog.imported', { added, skipped }), 'success'); } catch { showToast(t('settings.catalog.importFailed'), 'error'); } if (fileInputRef.current) fileInputRef.current.value = ''; }; return (

{t('settings.catalog.spoolCatalog')}

({catalog.length})

{t('settings.catalog.spoolCatalogDescription')}

{/* Search */}
setSearch(e.target.value)} />
{/* Add form */} {showAddForm && (

{t('settings.catalog.addNewEntry')}

setFormName(e.target.value)} />
setFormWeight(e.target.value)} /> g
)} {/* Catalog list */} {loading ? (
{t('common.loading')}
) : (
{filteredCatalog.length === 0 ? ( ) : ( filteredCatalog.map(entry => ( {editingId === entry.id ? ( <> ) : ( <> )} )) )}
{t('common.name')} {t('settings.catalog.weight')} {t('settings.catalog.type')}
{search ? t('settings.catalog.noMatch') : t('settings.catalog.empty')}
setFormName(e.target.value)} /> setFormWeight(e.target.value)} /> -
{entry.name} {entry.weight}g {entry.is_default ? ( {t('settings.catalog.default')} ) : ( {t('settings.catalog.custom')} )}
)}
{/* Delete confirmation */} {deleteEntry && ( setDeleteEntry(null)} /> )} {/* Reset confirmation */} {showResetConfirm && ( setShowResetConfirm(false)} /> )}
); }