'use client' import { useState, useEffect } from 'react' import { useDrag } from 'react-dnd' import { getEmptyImage } from 'react-dnd-html5-backend' import { Input } from '@/components/ui/input' import { ScrollArea } from '@/components/ui/scroll-area' 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, FolderOpen, } from 'lucide-react' interface DisplaySymbol { id: string name: string imageUrl: string } interface DisplayCategory { id: string name: string symbols: DisplaySymbol[] } interface TenantSymbolGroup { category: { id: string; name: string } | null symbols: DisplaySymbol[] } interface RightSidebarProps { onSymbolDrop: (iconId: string, coordinates: [number, number], imageUrl?: string) => void onSymbolSelect?: (symbol: { id: string; imageUrl: string }) => void canEdit: boolean isOpen?: boolean onToggle?: () => void activeTab?: 'map' | 'journal' onTabChange?: (tab: 'map' | 'journal') => void isCollapsed?: boolean onToggleCollapse?: () => void tenantId?: string | null mode?: 'EINSATZ' | 'UEBUNG' } const categoryIcons: Record = { 'Feuer / Brand': Flame, 'Wasser': Droplets, 'Gefahren / Stoffe': AlertTriangle, 'Rettung / Personen': Heart, 'Leitern / Geräte': Wrench, 'Gebäude / Schäden': Building, 'Einsatzführung': Radio, 'Organisationen': Shield, 'Entwicklung / Taktik': Target, 'Verschiedenes': MoreHorizontal, 'Eigene': Upload, } function DraggableSymbol({ symbol, canEdit, onSelect }: { symbol: DisplaySymbol canEdit: boolean onSelect?: (symbol: DisplaySymbol) => void }) { const [{ isDragging }, drag, preview] = useDrag(() => ({ type: 'SYMBOL', item: { iconId: symbol.id, imageUrl: symbol.imageUrl }, canDrag: canEdit, collect: (monitor) => ({ isDragging: monitor.isDragging(), }), }), [symbol.id, symbol.imageUrl, canEdit]) // Suppress native HTML5 drag ghost — we use CustomDragLayer instead useEffect(() => { preview(getEmptyImage(), { captureDraggingState: true }) }, [preview]) return (
} onClick={() => { if (canEdit && onSelect) onSelect(symbol) }} className={` flex flex-col items-center gap-1 p-1.5 md:p-2 lg:p-2.5 rounded-lg border-2 transition-all border-transparent hover:border-border hover:bg-accent cursor-grab active:cursor-grabbing active:scale-95 ${isDragging ? 'opacity-0' : ''} ${!canEdit ? 'opacity-50 cursor-not-allowed' : ''} `} title={canEdit ? `${symbol.name} — antippen zum Platzieren, oder auf die Karte ziehen` : symbol.name} >
{symbol.name}
{symbol.name}
) } export function RightSidebar({ onSymbolDrop, onSymbolSelect, canEdit, isOpen, onToggle, activeTab, onTabChange, isCollapsed, onToggleCollapse, tenantId, mode }: RightSidebarProps) { const journalLabel = mode === 'UEBUNG' ? 'Auswertung' : 'Journal' const [searchQuery, setSearchQuery] = useState('') const [activeCategory, setActiveCategory] = useState('') const [categories, setCategories] = useState([]) const [tenantGroups, setTenantGroups] = useState([]) const [isLoading, setIsLoading] = useState(true) const [showTenantSection, setShowTenantSection] = useState(true) const [showLibrarySection, setShowLibrarySection] = useState(true) const [expandedTenantCats, setExpandedTenantCats] = useState>(new Set()) useEffect(() => { async function fetchIcons() { setIsLoading(true) try { 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) => ({ id: cat.id, name: cat.name, symbols: cat.icons.map((icon: any) => ({ id: icon.id, name: icon.name, imageUrl: icon.url || `/api/icons/${icon.id}/image`, })), })) // 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') setCategories(globalCats) if (globalCats.length > 0 && !activeCategory) { setActiveCategory(globalCats[0].id) } // ─── New tenant symbol groups (Phase 1) ─── const groups: TenantSymbolGroup[] = (data.tenantSymbolGroups || []).map((g: any) => ({ category: g.category, 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 = { category: { id: '__legacy__', name: '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.category?.id || '__none__'))) setShowLibrarySection(false) } } } catch (err) { console.error('Failed to load icons:', err) } finally { setIsLoading(false) } } 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 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) + tenantGroups.reduce((sum, g) => sum + g.symbols.length, 0) return ( <> {/* Backdrop on mobile */} {isOpen && onToggle && (
)} {/* Collapsed state: thin strip with toggle + tab icons */} {isCollapsed && ( )} ) }