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 (
)
}
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
{uploadProgress && (
{uploadProgress}
)}
)}
{/* Add Unit Dialog */}
{showAddDialog && (
Neue Einheit hinzufügen
)}
)
}