import { useCallback, useEffect, useState, useRef } from 'react' import ReactFlow, { Node, Edge, Controls, Background, MiniMap, useNodesState, useEdgesState, addEdge, Connection, MarkerType, NodeTypes, Panel } from 'reactflow' import 'reactflow/dist/style.css' import { api } from '../services/api' import { OrganizationalUnit, OrganizationalUnitType } from '@skillmate/shared' import { Upload } from 'lucide-react' // Custom Node Component const OrganizationNode = ({ data }: { data: any }) => { const getTypeIcon = (type: OrganizationalUnitType) => { const icons = { direktion: '🏛️', abteilung: '🏢', dezernat: '📁', sachgebiet: '📋', teildezernat: '🔧', fuehrungsstelle: '⭐', stabsstelle: '🎯', sondereinheit: '🛡️' } return icons[type] || '📄' } const getGradient = (level: number) => { const gradients = [ 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)', 'linear-gradient(135deg, #fa709a 0%, #fee140 100%)', 'linear-gradient(135deg, #30cfd0 0%, #330867 100%)' ] return gradients[level % gradients.length] } return (
{getTypeIcon(data.type)} {data.code}

{data.name}

{data.employeeCount !== undefined && (

👥 {data.employeeCount} Mitarbeiter

)} {data.hasFuehrungsstelle && ( FüSt )}
) } const nodeTypes: NodeTypes = { organization: OrganizationNode } export default function OrganizationEditor() { const [nodes, setNodes, onNodesChange] = useNodesState([]) const [edges, setEdges, onEdgesChange] = useEdgesState([]) const [loading, setLoading] = useState(true) const [selectedNode, setSelectedNode] = useState(null) const [showAddDialog, setShowAddDialog] = useState(false) const [showImportDialog, setShowImportDialog] = useState(false) const [uploadProgress, setUploadProgress] = useState(null) const fileInputRef = useRef(null) const [formData, setFormData] = useState({ name: '', code: '', type: 'dezernat' as OrganizationalUnitType, level: 2, parentId: '', description: '' }) // Load organizational units useEffect(() => { loadOrganization() }, []) const loadOrganization = async () => { try { setLoading(true) const response = await api.get('/organization/hierarchy') if (response.data.success) { const units = response.data.data const { nodes: flowNodes, edges: flowEdges } = convertToFlowElements(units) setNodes(flowNodes) setEdges(flowEdges) } } catch (error) { console.error('Failed to load organization:', error) } finally { setLoading(false) } } const convertToFlowElements = (units: any[], parentPosition = { x: 0, y: 0 }, level = 0): { nodes: Node[], edges: Edge[] } => { const nodes: Node[] = [] const edges: Edge[] = [] const levelHeight = 150 const nodeWidth = 300 units.forEach((unit, index) => { const nodeId = unit.id const xPos = parentPosition.x + index * nodeWidth const yPos = level * levelHeight // Use persisted positions if available, otherwise compute defaults const persistedX = (typeof unit.positionX === 'number' ? unit.positionX : (unit.positionX != null && !isNaN(Number(unit.positionX)) ? Number(unit.positionX) : undefined)) const persistedY = (typeof unit.positionY === 'number' ? unit.positionY : (unit.positionY != null && !isNaN(Number(unit.positionY)) ? Number(unit.positionY) : undefined)) nodes.push({ id: nodeId, type: 'organization', position: { x: persistedX ?? xPos, y: persistedY ?? yPos }, data: { ...unit, level } }) // Create edge to parent if exists if (unit.parentId) { edges.push({ id: `e-${unit.parentId}-${nodeId}`, source: unit.parentId, target: nodeId, type: 'smoothstep', animated: false, markerEnd: { type: MarkerType.ArrowClosed, width: 20, height: 20, color: '#94a3b8' }, style: { strokeWidth: 2, stroke: '#94a3b8' } }) } // Process children recursively if (unit.children && unit.children.length > 0) { const childElements = convertToFlowElements( unit.children, { x: xPos, y: yPos }, level + 1 ) nodes.push(...childElements.nodes) edges.push(...childElements.edges) } }) return { nodes, edges } } const onConnect = useCallback( (params: Connection) => { setEdges((eds) => addEdge({ ...params, type: 'smoothstep', animated: false, markerEnd: { type: MarkerType.ArrowClosed, width: 20, height: 20, color: '#94a3b8' } }, eds)) }, [setEdges] ) const onNodeClick = useCallback((_: any, node: Node) => { setSelectedNode(node) }, []) const onNodeDragStop = useCallback(async (_: any, node: Node) => { try { await api.put(`/organization/units/${node.id}`, { positionX: node.position.x, positionY: node.position.y }) } catch (error) { console.error('Failed to update position:', error) } }, []) const handleAddUnit = async () => { try { // Build payload and attach selected node as parent if present const payload: any = { name: formData.name, code: formData.code || undefined, type: formData.type, level: selectedNode ? ((selectedNode.data?.level ?? 0) + 1) : formData.level, description: formData.description || undefined, } if (selectedNode?.id) payload.parentId = selectedNode.id const response = await api.post('/organization/units', payload) if (response.data.success) { await loadOrganization() setShowAddDialog(false) setFormData({ name: '', code: '', type: 'dezernat', level: 2, parentId: '', description: '' }) } } catch (error) { console.error('Failed to add unit:', error) } } const handleFileUpload = async (event: React.ChangeEvent) => { const file = event.target.files?.[0] if (!file) return if (file.type !== 'application/pdf') { alert('Bitte laden Sie nur PDF-Dateien hoch.') return } const formData = new FormData() formData.append('pdf', file) formData.append('clearExisting', 'false') // Don't clear existing by default setUploadProgress('PDF wird hochgeladen...') try { const response = await api.post('/organization/import-pdf', formData, { headers: { 'Content-Type': 'multipart/form-data' } }) if (response.data.success) { setUploadProgress(`Erfolgreich! ${response.data.data.unitsImported} Einheiten importiert.`) await loadOrganization() setTimeout(() => { setShowImportDialog(false) setUploadProgress(null) }, 2000) } } catch (error: any) { console.error('PDF import failed:', error) setUploadProgress(`Fehler: ${error.response?.data?.error?.message || 'Upload fehlgeschlagen'}`) } } if (loading) { return (
Lade Organigramm...
) } return (
{ const gradients = ['#667eea', '#f093fb', '#4facfe', '#43e97b', '#fa709a', '#30cfd0'] return gradients[node.data?.level % gradients.length] || '#94a3b8' }} />

LKA NRW Organigramm

{selectedNode ? ( <>

{selectedNode.data.name}

{selectedNode.data.code && (

Code: {selectedNode.data.code}

)}

Typ: {selectedNode.data.type}

Ebene: {selectedNode.data.level}

{selectedNode.data.description && (

{selectedNode.data.description}

)} ) : (

Klicken Sie auf eine Einheit für Details

)}
{/* Import PDF Dialog */} {showImportDialog && (

PDF Organigramm importieren

Laden Sie ein PDF-Dokument mit der Organisationsstruktur hoch. Das System extrahiert automatisch die Hierarchie.

{uploadProgress && (
{uploadProgress}
)}
)} {/* Add Unit Dialog */} {showAddDialog && (

Neue Einheit hinzufügen

setFormData({ ...formData, name: e.target.value })} className="w-full px-3 py-2 border rounded" required />
{selectedNode && (
Wird unterhalb von: {selectedNode.data?.name} eingefügt
)}
setFormData({ ...formData, code: e.target.value })} className="w-full px-3 py-2 border rounded" placeholder="z.B. ZA 1" required />
setFormData({ ...formData, level: parseInt(e.target.value) })} className="w-full px-3 py-2 border rounded" min="0" max="10" />