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
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 12m43s
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user