refactor(symbol-manager): remove template import, focus on library UX
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 12m43s

This commit is contained in:
Pepe Ziberi
2026-05-21 07:41:17 +02:00
parent 56895be16f
commit 40cea9a9be
3 changed files with 1728 additions and 161 deletions

View File

@@ -32,9 +32,7 @@ import {
LayoutGrid,
ImageIcon,
FolderOpen,
Download,
AlertCircle,
Package,
Library,
} from 'lucide-react'
@@ -63,14 +61,6 @@ interface SymbolGroup {
symbols: TenantSymbol[]
}
interface TemplatePackage {
packageId: string
packageName: string
categoryCount: number
symbolCount: number
previewSymbols: { name: string; svgPath: string }[]
}
/* ─── Component ─── */
export function SymbolManager() {
const { toast } = useToast()
@@ -80,12 +70,11 @@ export function SymbolManager() {
const [categories, setCategories] = useState<TenantCategory[]>([])
const [symbolGroups, setSymbolGroups] = useState<SymbolGroup[]>([])
const [flatSymbols, setFlatSymbols] = useState<TenantSymbol[]>([])
const [templates, setTemplates] = useState<TemplatePackage[]>([])
/* -- UI state -- */
const [search, setSearch] = useState('')
const [expandedCats, setExpandedCats] = useState<Set<string>>(new Set())
const [activeTab, setActiveTab] = useState<'symbols' | 'categories' | 'import' | 'library'>('symbols')
const [activeTab, setActiveTab] = useState<'symbols' | 'categories' | 'library'>('library')
/* -- Symbol editing -- */
const [editingSymbolId, setEditingSymbolId] = useState<string | null>(null)
@@ -103,10 +92,6 @@ export function SymbolManager() {
const [uploading, setUploading] = useState(false)
const fileInputRef = useRef<HTMLInputElement>(null)
/* -- Import dialog -- */
const [importOpen, setImportOpen] = useState(false)
const [importingPkg, setImportingPkg] = useState<string | null>(null)
/* -- Library -- */
const [libraryIcons, setLibraryIcons] = useState<any[]>([])
const [librarySearch, setLibrarySearch] = useState('')
@@ -117,10 +102,9 @@ export function SymbolManager() {
const fetchData = useCallback(async () => {
setLoading(true)
try {
const [catRes, symRes, tplRes] = await Promise.all([
const [catRes, symRes] = await Promise.all([
fetch('/api/tenant/categories'),
fetch('/api/tenant/symbols?grouped=true'),
fetch('/api/templates'),
])
if (catRes.ok) {
const c = await catRes.json()
@@ -133,10 +117,6 @@ export function SymbolManager() {
const all = (s.categories || []).flatMap((c: any) => c.symbols || [])
setFlatSymbols(all)
}
if (tplRes.ok) {
const t = await tplRes.json()
setTemplates(t.packages || [])
}
} catch {
toast({ title: 'Fehler beim Laden', variant: 'destructive' })
}
@@ -281,30 +261,6 @@ export function SymbolManager() {
toast({ title: `${success} Datei(en) hochgeladen` })
}
/* ─── Import ─── */
const importPackage = async (packageId: string) => {
setImportingPkg(packageId)
try {
const res = await fetch('/api/templates/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ packageId }),
})
const data = await res.json().catch(() => ({}))
if (!res.ok) {
toast({ title: data.error || 'Import fehlgeschlagen', variant: 'destructive' })
} else {
toast({ title: `${data.imported} Symbole importiert` })
await fetchData()
setImportOpen(false)
setActiveTab('symbols')
}
} catch {
toast({ title: 'Import fehlgeschlagen', variant: 'destructive' })
}
setImportingPkg(null)
}
/* ─── Library ─── */
const fetchLibrary = useCallback(async () => {
setLibraryLoading(true)
@@ -374,7 +330,6 @@ export function SymbolManager() {
{ key: 'symbols', label: 'Meine Symbole', icon: LayoutGrid },
{ key: 'categories', label: 'Kategorien', icon: FolderOpen },
{ key: 'library', label: 'Bibliothek', icon: Library },
{ key: 'import', label: 'Vorlagen importieren', icon: Download },
] as const).map(t => (
<button
key={t.key}
@@ -393,9 +348,6 @@ export function SymbolManager() {
<Button size="sm" variant="outline" onClick={() => setUploadOpen(true)}>
<Upload className="w-4 h-4 mr-1.5" /> Upload
</Button>
<Button size="sm" variant="outline" onClick={() => setImportOpen(true)}>
<Download className="w-4 h-4 mr-1.5" /> Import
</Button>
</div>
</div>
@@ -461,15 +413,15 @@ export function SymbolManager() {
<div className="text-center py-12 border rounded-lg">
<ImageIcon className="w-10 h-10 mx-auto text-muted-foreground/40 mb-3" />
<p className="text-sm text-muted-foreground">
{search ? 'Keine Symbole gefunden.' : 'Noch keine Symbole vorhanden.'}
{search ? 'Keine Symbole gefunden.' : 'Noch keine Symbole. Füge Symbole aus der Bibliothek hinzu oder lade eigene hoch.'}
</p>
<div className="flex justify-center gap-2 mt-3">
<Button size="sm" variant="outline" onClick={() => setActiveTab('library')}>
<Library className="w-4 h-4 mr-1" /> Bibliothek durchsuchen
</Button>
<Button size="sm" variant="outline" onClick={() => setUploadOpen(true)}>
<Upload className="w-4 h-4 mr-1" /> Hochladen
</Button>
<Button size="sm" variant="outline" onClick={() => setImportOpen(true)}>
<Download className="w-4 h-4 mr-1" /> Importieren
</Button>
</div>
</div>
)}
@@ -564,62 +516,6 @@ export function SymbolManager() {
</div>
)}
{/* ===== TAB: Import ===== */}
{activeTab === 'import' && (
<div className="space-y-4">
{templates.length === 0 ? (
<div className="text-center py-12 border rounded-lg text-muted-foreground">
<Package className="w-10 h-10 mx-auto mb-3 opacity-40" />
<p className="text-sm">Keine Vorlagen-Pakete verfügbar.</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{templates.map(pkg => (
<div key={pkg.packageId} className="border rounded-lg p-4 space-y-3 hover:border-primary/30 transition-colors">
<div className="flex items-start justify-between">
<div>
<h4 className="font-medium text-sm">{pkg.packageName}</h4>
<p className="text-xs text-muted-foreground mt-0.5">
{pkg.categoryCount} Kategorien · {pkg.symbolCount} Symbole
</p>
</div>
<Package className="w-5 h-5 text-muted-foreground" />
</div>
{/* Preview */}
<div className="flex gap-2">
{pkg.previewSymbols.slice(0, 4).map((p, i) => (
<div key={i} className="w-10 h-10 border rounded flex items-center justify-center bg-muted/30">
<img
src={`/signaturen/${p.svgPath.replace(/^.*[\\/]/, '')}`}
alt={p.name}
className="w-8 h-8 object-contain"
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
/>
</div>
))}
</div>
<Button
size="sm"
className="w-full"
onClick={() => importPackage(pkg.packageId)}
disabled={importingPkg === pkg.packageId}
>
{importingPkg === pkg.packageId ? (
<Loader2 className="w-4 h-4 animate-spin mr-1.5" />
) : (
<Download className="w-4 h-4 mr-1.5" />
)}
{importingPkg === pkg.packageId ? 'Importiere...' : 'Importieren'}
</Button>
</div>
))}
</div>
)}
</div>
)}
{/* ===== TAB: Bibliothek ===== */}
{activeTab === 'library' && (
<div className="space-y-4">
@@ -749,57 +645,6 @@ export function SymbolManager() {
</DialogContent>
</Dialog>
{/* ===== IMPORT DIALOG ===== */}
<Dialog open={importOpen} onOpenChange={setImportOpen}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Aus Vorlagen importieren</DialogTitle>
<DialogDescription>
Wähle ein Vorlagen-Paket aus, das als Mandanten-Symbole importiert werden soll.
</DialogDescription>
</DialogHeader>
<div className="space-y-3 max-h-[60vh] overflow-y-auto pr-1">
{templates.map(pkg => (
<div key={pkg.packageId} className="flex items-center gap-3 border rounded-lg p-3">
<div className="flex -space-x-2">
{pkg.previewSymbols.slice(0, 3).map((p, i) => (
<div key={i} className="w-9 h-9 border-2 border-background rounded bg-muted/30 flex items-center justify-center">
<img
src={`/signaturen/${p.svgPath.replace(/^.*[\\/]/, '')}`}
alt=""
className="w-7 h-7 object-contain"
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
/>
</div>
))}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{pkg.packageName}</p>
<p className="text-xs text-muted-foreground">{pkg.symbolCount} Symbole · {pkg.categoryCount} Kategorien</p>
</div>
<Button
size="sm"
onClick={() => importPackage(pkg.packageId)}
disabled={importingPkg === pkg.packageId}
>
{importingPkg === pkg.packageId ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Download className="w-4 h-4" />
)}
</Button>
</div>
))}
{templates.length === 0 && (
<div className="text-center py-6 text-sm text-muted-foreground">
Keine Vorlagen verfügbar.
</div>
)}
</div>
</DialogContent>
</Dialog>
</div>
)
}