feat(1.4.3): add 'Bibliothek' tab to admin for single-icon selection
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 20m6s

This commit is contained in:
Pepe Ziberi
2026-05-20 23:36:00 +02:00
parent c8a94e1ea7
commit e9f66b2c3d
3 changed files with 123 additions and 2 deletions

View File

@@ -3,6 +3,11 @@
Alle nennenswerten Änderungen an diesem Projekt werden in dieser Datei dokumentiert.
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/).
## [1.4.3] 2026-05-20 — Feature: Einzelne Symbole aus Bibliothek wählen
### Neu
- **Admin → Symbol-Manager**: Neuer Tab „Bibliothek“ zeigt alle 117 globalen Symbole gruppiert nach Kategorie. Pro Symbol ein „+“-Button, um es mit einem Klick zu „Meinen Symbolen“ hinzuzufügen (via `POST /api/tenant/symbols` mit `iconId`).
## [1.4.2] 2026-05-20 — Hotfix: Admin leer & Legacy-Symbole
### Behoben

View File

@@ -1,6 +1,6 @@
{
"name": "lageplan",
"version": "1.4.2",
"version": "1.4.3",
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
"private": true,
"scripts": {

View File

@@ -35,6 +35,7 @@ import {
Download,
AlertCircle,
Package,
Library,
} from 'lucide-react'
/* ─── Types ─── */
@@ -84,7 +85,7 @@ export function SymbolManager() {
/* -- UI state -- */
const [search, setSearch] = useState('')
const [expandedCats, setExpandedCats] = useState<Set<string>>(new Set())
const [activeTab, setActiveTab] = useState<'symbols' | 'categories' | 'import'>('symbols')
const [activeTab, setActiveTab] = useState<'symbols' | 'categories' | 'import' | 'library'>('symbols')
/* -- Symbol editing -- */
const [editingSymbolId, setEditingSymbolId] = useState<string | null>(null)
@@ -106,6 +107,12 @@ export function SymbolManager() {
const [importOpen, setImportOpen] = useState(false)
const [importingPkg, setImportingPkg] = useState<string | null>(null)
/* -- Library -- */
const [libraryIcons, setLibraryIcons] = useState<any[]>([])
const [librarySearch, setLibrarySearch] = useState('')
const [libraryLoading, setLibraryLoading] = useState(false)
const [addingIconId, setAddingIconId] = useState<string | null>(null)
/* ─── Fetch ─── */
const fetchData = useCallback(async () => {
setLoading(true)
@@ -298,6 +305,46 @@ export function SymbolManager() {
setImportingPkg(null)
}
/* ─── Library ─── */
const fetchLibrary = useCallback(async () => {
setLibraryLoading(true)
try {
const res = await fetch('/api/icons')
if (res.ok) {
const data = await res.json()
setLibraryIcons(data.categories || [])
}
} catch {
toast({ title: 'Fehler beim Laden der Bibliothek', variant: 'destructive' })
}
setLibraryLoading(false)
}, [toast])
useEffect(() => {
if (activeTab === 'library') fetchLibrary()
}, [activeTab, fetchLibrary])
const addFromLibrary = async (iconId: string, name: string) => {
setAddingIconId(iconId)
try {
const res = await fetch('/api/tenant/symbols', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ iconId }),
})
if (!res.ok) {
const err = await res.json().catch(() => ({}))
toast({ title: err.error || 'Fehler', variant: 'destructive' })
} else {
toast({ title: `'${name}' hinzugefügt` })
await fetchData()
}
} catch {
toast({ title: 'Fehler beim Hinzufügen', variant: 'destructive' })
}
setAddingIconId(null)
}
/* ─── Derived data ─── */
const filteredGroups = symbolGroups.map(g => ({
...g,
@@ -326,6 +373,7 @@ 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
@@ -572,6 +620,74 @@ export function SymbolManager() {
</div>
)}
{/* ===== TAB: Bibliothek ===== */}
{activeTab === 'library' && (
<div className="space-y-4">
<div className="relative max-w-sm">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="In Bibliothek suchen..."
value={librarySearch}
onChange={e => setLibrarySearch(e.target.value)}
className="pl-9"
/>
</div>
{libraryLoading ? (
<div className="flex items-center gap-2 py-12 justify-center text-muted-foreground">
<Loader2 className="w-5 h-5 animate-spin" /> Bibliothek laden...
</div>
) : (
<div className="space-y-4">
{libraryIcons.map((cat: any) => {
const filtered = (cat.icons || []).filter((icon: any) =>
!librarySearch || (icon.name || '').toLowerCase().includes(librarySearch.toLowerCase())
)
if (filtered.length === 0) return null
return (
<div key={cat.id} className="border rounded-lg">
<div className="px-3 py-2 bg-muted/30 border-b">
<span className="font-medium text-sm">{cat.name}</span>
<span className="text-xs text-muted-foreground ml-2">({filtered.length})</span>
</div>
<div className="px-3 py-3">
<div className="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10 gap-3">
{filtered.map((icon: any) => (
<div key={icon.id} className="group relative border rounded-lg p-2 transition-all hover:shadow-md hover:border-primary/30">
<div className="aspect-square flex items-center justify-center mb-1.5 bg-muted/50 rounded">
<img
src={icon.url}
alt={icon.name}
className="w-10 h-10 object-contain"
draggable={false}
onError={(e) => { (e.target as HTMLImageElement).src = '/logo.svg' }}
/>
</div>
<p className="text-[11px] text-center truncate" title={icon.name}>{icon.name}</p>
<button
onClick={() => addFromLibrary(icon.id, icon.name)}
disabled={addingIconId === icon.id}
className="absolute top-1 right-1 w-6 h-6 rounded-full bg-primary text-primary-foreground flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-primary/90 disabled:opacity-50"
title="Zu Meinen Symbolen hinzufügen"
>
{addingIconId === icon.id ? (
<Loader2 className="w-3 h-3 animate-spin" />
) : (
<Plus className="w-3.5 h-3.5" />
)}
</button>
</div>
))}
</div>
</div>
</div>
)
})}
</div>
)}
</div>
)}
{/* ===== UPLOAD DIALOG ===== */}
<Dialog open={uploadOpen} onOpenChange={setUploadOpen}>
<DialogContent className="max-w-md">