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