Phase 1 Sprint C+D: Admin UI + Frontend Sidebar
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
||||
Search, Flame, Droplets, AlertTriangle, Car, Users,
|
||||
Truck, Building, Target, Upload, Loader2, X, LayoutGrid,
|
||||
ChevronLeft, ChevronRight, Map, ClipboardList, PanelRightClose, PanelRightOpen,
|
||||
Shield, Wrench, Radio, MoreHorizontal, Heart,
|
||||
Shield, Wrench, Radio, MoreHorizontal, Heart, FolderOpen,
|
||||
} from 'lucide-react'
|
||||
|
||||
interface DisplaySymbol {
|
||||
@@ -24,6 +24,12 @@ interface DisplayCategory {
|
||||
symbols: DisplaySymbol[]
|
||||
}
|
||||
|
||||
interface TenantSymbolGroup {
|
||||
categoryId: string | null
|
||||
categoryName: string
|
||||
symbols: DisplaySymbol[]
|
||||
}
|
||||
|
||||
interface RightSidebarProps {
|
||||
onSymbolDrop: (iconId: string, coordinates: [number, number], imageUrl?: string) => void
|
||||
canEdit: boolean
|
||||
@@ -99,10 +105,11 @@ export function RightSidebar({ onSymbolDrop, canEdit, isOpen, onToggle, activeTa
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [activeCategory, setActiveCategory] = useState<string>('')
|
||||
const [categories, setCategories] = useState<DisplayCategory[]>([])
|
||||
const [tenantIcons, setTenantIcons] = useState<DisplaySymbol[]>([])
|
||||
const [tenantGroups, setTenantGroups] = useState<TenantSymbolGroup[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [showTenantSection, setShowTenantSection] = useState(true)
|
||||
const [showLibrarySection, setShowLibrarySection] = useState(true)
|
||||
const [expandedTenantCats, setExpandedTenantCats] = useState<Set<string>>(new Set())
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchIcons() {
|
||||
@@ -111,6 +118,8 @@ export function RightSidebar({ onSymbolDrop, canEdit, isOpen, onToggle, activeTa
|
||||
const res = await fetch('/api/icons', { cache: 'no-store' })
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
|
||||
// ─── Global library ───
|
||||
const allCats: DisplayCategory[] = (data.categories || [])
|
||||
.filter((cat: any) => cat.icons && cat.icons.length > 0)
|
||||
.map((cat: any) => ({
|
||||
@@ -123,27 +132,41 @@ export function RightSidebar({ onSymbolDrop, canEdit, isOpen, onToggle, activeTa
|
||||
})),
|
||||
}))
|
||||
|
||||
// Separate tenant-specific icons ("Eigene" category) from global library
|
||||
// Separate tenant-specific legacy "Eigene" category from global library
|
||||
const eigene = allCats.find(c => c.name === 'Eigene')
|
||||
const globalCats = allCats.filter(c => c.name !== 'Eigene')
|
||||
|
||||
// Merge: mySymbols (custom collection) + legacy "Eigene" category uploads
|
||||
const mySymbols: DisplaySymbol[] = (data.mySymbols || []).map((s: any) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
imageUrl: s.url || `/api/icons/${s.id}/image`,
|
||||
}))
|
||||
const legacyOwn = eigene?.symbols || []
|
||||
// Deduplicate: mySymbols takes priority over legacy
|
||||
const mySymbolIds = new Set(mySymbols.map(s => s.id))
|
||||
const mergedTenant = [...mySymbols, ...legacyOwn.filter(s => !mySymbolIds.has(s.id))]
|
||||
|
||||
setTenantIcons(mergedTenant)
|
||||
setCategories(globalCats)
|
||||
if (globalCats.length > 0) setActiveCategory(globalCats[0].id)
|
||||
if (globalCats.length > 0 && !activeCategory) {
|
||||
setActiveCategory(globalCats[0].id)
|
||||
}
|
||||
|
||||
// Auto-collapse library if tenant has own symbols
|
||||
if (mergedTenant.length > 0) {
|
||||
// ─── New tenant symbol groups (Phase 1) ───
|
||||
const groups: TenantSymbolGroup[] = (data.tenantSymbolGroups || []).map((g: any) => ({
|
||||
categoryId: g.categoryId,
|
||||
categoryName: g.categoryName,
|
||||
symbols: g.symbols.map((s: any) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
imageUrl: s.imageUrl || `/api/icons/${s.id}/image`,
|
||||
})),
|
||||
}))
|
||||
|
||||
// Merge legacy "Eigene" into tenant groups if present
|
||||
if (eigene && eigene.symbols.length > 0) {
|
||||
const legacyGroup: TenantSymbolGroup = {
|
||||
categoryId: '__legacy__',
|
||||
categoryName: 'Eigene',
|
||||
symbols: eigene.symbols,
|
||||
}
|
||||
groups.unshift(legacyGroup)
|
||||
}
|
||||
|
||||
setTenantGroups(groups)
|
||||
|
||||
// Auto-expand all tenant groups, auto-collapse library if tenant has symbols
|
||||
if (groups.length > 0 && groups.some(g => g.symbols.length > 0)) {
|
||||
setExpandedTenantCats(new Set(groups.map(g => g.categoryId || '__none__')))
|
||||
setShowLibrarySection(false)
|
||||
}
|
||||
}
|
||||
@@ -156,18 +179,31 @@ export function RightSidebar({ onSymbolDrop, canEdit, isOpen, onToggle, activeTa
|
||||
fetchIcons()
|
||||
}, [tenantId])
|
||||
|
||||
const toggleTenantCat = (catId: string | null) => {
|
||||
const key = catId || '__none__'
|
||||
setExpandedTenantCats(prev => {
|
||||
const n = new Set(prev)
|
||||
n.has(key) ? n.delete(key) : n.add(key)
|
||||
return n
|
||||
})
|
||||
}
|
||||
|
||||
const filteredCategories = categories.map((cat) => ({
|
||||
...cat,
|
||||
symbols: cat.symbols.filter((s) =>
|
||||
s.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
),
|
||||
}))
|
||||
const filteredTenantIcons = tenantIcons.filter(s =>
|
||||
s.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
const filteredTenantGroups = tenantGroups.map(g => ({
|
||||
...g,
|
||||
symbols: g.symbols.filter(s =>
|
||||
s.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
),
|
||||
})).filter(g => g.symbols.length > 0)
|
||||
|
||||
const currentCategory = filteredCategories.find((c) => c.id === activeCategory)
|
||||
const totalSymbols = categories.reduce((sum, c) => sum + c.symbols.length, 0) + tenantIcons.length
|
||||
const totalSymbols = categories.reduce((sum, c) => sum + c.symbols.length, 0) +
|
||||
tenantGroups.reduce((sum, g) => sum + g.symbols.length, 0)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -298,7 +334,7 @@ export function RightSidebar({ onSymbolDrop, canEdit, isOpen, onToggle, activeTa
|
||||
)}
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
{/* ─── Section 1: Meine Symbole (Tenant-specific) ─── */}
|
||||
{/* ─── Section 1: Meine Symbole (Tenant Symbol Groups) ─── */}
|
||||
{tenantId && (
|
||||
<div className="border-b border-border">
|
||||
<button
|
||||
@@ -307,24 +343,47 @@ export function RightSidebar({ onSymbolDrop, canEdit, isOpen, onToggle, activeTa
|
||||
>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Upload className="w-3.5 h-3.5" />
|
||||
Meine Symbole ({filteredTenantIcons.length})
|
||||
Meine Symbole ({tenantGroups.reduce((s, g) => s + g.symbols.length, 0)})
|
||||
</span>
|
||||
<ChevronRight className={`w-3.5 h-3.5 transition-transform ${showTenantSection ? 'rotate-90' : ''}`} />
|
||||
</button>
|
||||
{showTenantSection && (
|
||||
<div className="p-2 pt-0">
|
||||
{filteredTenantIcons.length === 0 ? (
|
||||
<div className="p-2 pt-0 space-y-1">
|
||||
{filteredTenantGroups.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-4 text-xs">
|
||||
Keine eigenen Symbole vorhanden.
|
||||
<br />
|
||||
<span className="text-[10px]">Symbole können unter Einstellungen → Symbole hochgeladen werden.</span>
|
||||
<span className="text-[10px]">Symbole können unter Admin → Symbole verwaltet werden.</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 md:grid-cols-2 lg:grid-cols-3 gap-1">
|
||||
{filteredTenantIcons.map((symbol) => (
|
||||
<DraggableSymbol key={symbol.id} symbol={symbol} canEdit={canEdit} />
|
||||
))}
|
||||
</div>
|
||||
filteredTenantGroups.map(g => {
|
||||
const key = g.categoryId || '__none__'
|
||||
const expanded = expandedTenantCats.has(key)
|
||||
return (
|
||||
<div key={key} className="border rounded-md">
|
||||
<button
|
||||
onClick={() => toggleTenantCat(g.categoryId)}
|
||||
className="w-full flex items-center justify-between px-2 py-1 text-[11px] font-medium hover:bg-muted/40 transition-colors"
|
||||
>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<FolderOpen className="w-3 h-3 text-muted-foreground" />
|
||||
{g.categoryName}
|
||||
<span className="text-[10px] text-muted-foreground">({g.symbols.length})</span>
|
||||
</span>
|
||||
<ChevronRight className={`w-3 h-3 transition-transform ${expanded ? 'rotate-90' : ''}`} />
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="px-1.5 pb-1.5">
|
||||
<div className="grid grid-cols-3 md:grid-cols-2 lg:grid-cols-3 gap-1">
|
||||
{g.symbols.map(symbol => (
|
||||
<DraggableSymbol key={symbol.id} symbol={symbol} canEdit={canEdit} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user