Dieser Commit ist enthalten in:
Claude Project Manager
2025-09-25 00:30:38 +02:00
Ursprung b0165a2712
Commit 42603d70c6

Datei anzeigen

@ -47,7 +47,7 @@ const OrganizationNode = ({ data }: { data: any }) => {
} }
return ( return (
<div className="bg-white rounded-lg shadow-lg border-2 border-gray-200 min-w-[250px]"> <div className="bg-white rounded-lg shadow-lg border-2 border-gray-200 w-[300px]">
<div <div
className="p-2 text-white rounded-t-md" className="p-2 text-white rounded-t-md"
style={{ background: getGradient(data.level || 0) }} style={{ background: getGradient(data.level || 0) }}
@ -89,8 +89,6 @@ export default function OrganizationEditor() {
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const [clearExisting, setClearExisting] = useState(false) const [clearExisting, setClearExisting] = useState(false)
const [issues, setIssues] = useState<{ orphans: Set<string> }>({ orphans: new Set() }) const [issues, setIssues] = useState<{ orphans: Set<string> }>({ orphans: new Set() })
const [showValidation, setShowValidation] = useState(false)
const [reparentMode, setReparentMode] = useState<{ source: Node | null }>({ source: null })
const [preview, setPreview] = useState<any | null>(null) const [preview, setPreview] = useState<any | null>(null)
const [pendingOverrides, setPendingOverrides] = useState<Record<string, { parentCode?: string; type?: string; name?: string }>>({}) const [pendingOverrides, setPendingOverrides] = useState<Record<string, { parentCode?: string; type?: string; name?: string }>>({})
const [rememberRules, setRememberRules] = useState(true) const [rememberRules, setRememberRules] = useState(true)
@ -212,13 +210,7 @@ export default function OrganizationEditor() {
) )
const onNodeClick = useCallback((_: any, node: Node) => { const onNodeClick = useCallback((_: any, node: Node) => {
if (reparentMode.source && reparentMode.source.id !== node.id) {
// Reparent action: set node as new parent
handleReparent(reparentMode.source, node)
setReparentMode({ source: null })
} else {
setSelectedNode(node) setSelectedNode(node)
}
}, []) }, [])
const onNodeDragStop = useCallback(async (_: any, node: Node) => { const onNodeDragStop = useCallback(async (_: any, node: Node) => {
@ -379,12 +371,24 @@ export default function OrganizationEditor() {
const topYDirector = 10 const topYDirector = 10
const topYRootRow = topYDirector + yGap const topYRootRow = topYDirector + yGap
// Spaltenstart mit Abstand unterhalb der Top-Row, um Überlappungen zu vermeiden // Spaltenstart mit Abstand unterhalb der Top-Row, um Überlappungen zu vermeiden
const yStart = topYRootRow + yGap // yStart wird nach Messung der tatsächlichen Kartenhöhen berechnet
let yStart = topYRootRow + yGap
const newNodes: Node[] = nodes.map(n => ({ ...n, position: { ...n.position } })) const newNodes: Node[] = nodes.map(n => ({ ...n, position: { ...n.position } }))
const byId: Record<string, Node> = {} const byId: Record<string, Node> = {}
newNodes.forEach(n => { byId[n.id] = n }) newNodes.forEach(n => { byId[n.id] = n })
// Hilfsfunktion: tatsächliche Höhe einer Node messen (Fallback 130px)
const getNodeHeight = (id: string): number => {
const el = document.querySelector(`[data-id="${id}"]`) as HTMLElement | null
if (el) {
const rect = el.getBoundingClientRect()
// Mindestens 100, damit sehr kleine Wrapper nicht zu nahe rücken
return Math.max(100, Math.ceil(rect.height))
}
return 130
}
const getLaneKey = (n: Node): string => { const getLaneKey = (n: Node): string => {
const code: string = n.data?.code || '' const code: string = n.data?.code || ''
const type: string = n.data?.type || '' const type: string = n.data?.type || ''
@ -457,26 +461,36 @@ export default function OrganizationEditor() {
// 1) Direktion zentriert ganz oben // 1) Direktion zentriert ganz oben
const directorIndex = rootNodes.findIndex(n => (String(n.data?.type).toLowerCase() === 'direktion') || String(n.data?.code).toUpperCase() === 'DIR') const directorIndex = rootNodes.findIndex(n => (String(n.data?.type).toLowerCase() === 'direktion') || String(n.data?.code).toUpperCase() === 'DIR')
let placedIndices = new Set<number>() let placedIndices = new Set<number>()
let directorRowHeight = 0
if (directorIndex >= 0) { if (directorIndex >= 0) {
const centerX = xMargin + totalWidth / 2 - cardHalfWidth const centerX = xMargin + totalWidth / 2 - cardHalfWidth
const x = Math.max(xMargin, centerX) const x = Math.max(xMargin, centerX)
rootNodes[directorIndex].position = { x, y: topYDirector } rootNodes[directorIndex].position = { x, y: topYDirector }
placedIndices.add(directorIndex) placedIndices.add(directorIndex)
directorRowHeight = getNodeHeight(rootNodes[directorIndex].id)
} }
// 2) Übrige ROOT-Knoten in einer zweiten Top-Reihe gleichmäßig verteilen // 2) Übrige ROOT-Knoten in einer zweiten Top-Reihe gleichmäßig verteilen
const others: Node[] = rootNodes.filter((_, idx) => !placedIndices.has(idx)) const others: Node[] = rootNodes.filter((_, idx) => !placedIndices.has(idx))
let rootRowHeight = 0
if (others.length === 1) { if (others.length === 1) {
const x = Math.max(xMargin, xMargin + totalWidth / 2 - cardHalfWidth) const x = Math.max(xMargin, xMargin + totalWidth / 2 - cardHalfWidth)
others[0].position = { x, y: topYRootRow } others[0].position = { x, y: topYRootRow }
rootRowHeight = getNodeHeight(others[0].id)
} else if (others.length > 1) { } else if (others.length > 1) {
const step = others.length > 1 ? (totalWidth / (others.length - 1)) : 0 const step = others.length > 1 ? (totalWidth / (others.length - 1)) : 0
others.forEach((n, i) => { others.forEach((n, i) => {
const xi = xMargin + i * step - cardHalfWidth const xi = xMargin + i * step - cardHalfWidth
n.position = { x: Math.max(xMargin, xi), y: topYRootRow } n.position = { x: Math.max(xMargin, xi), y: topYRootRow }
rootRowHeight = Math.max(rootRowHeight, getNodeHeight(n.id))
}) })
} }
// Spaltenstart abhängig von den tatsächlichen Höhen berechnen
if (directorRowHeight || rootRowHeight) {
yStart = topYDirector + (directorRowHeight || 0) + yGap + (rootRowHeight ? (rootRowHeight + yGap) : 0) + 16
}
// Arrange lanes // Arrange lanes
let laneIndex = 0 let laneIndex = 0
laneOrder.forEach(key => { laneOrder.forEach(key => {
@ -490,19 +504,19 @@ export default function OrganizationEditor() {
let yCursor = yStart let yCursor = yStart
if (abteilung) { if (abteilung) {
abteilung.position = { x: xBase, y: yCursor } abteilung.position = { x: xBase, y: yCursor }
yCursor += yGap yCursor += getNodeHeight(abteilung.id) + yGap
} }
// Place Dezernate // Place Dezernate
const dezernate = laneNodes.filter(n => n.data?.type === 'dezernat').sort(dezOrder) const dezernate = laneNodes.filter(n => n.data?.type === 'dezernat').sort(dezOrder)
dezernate.forEach(dez => { dezernate.forEach(dez => {
dez.position = { x: xBase, y: yCursor } dez.position = { x: xBase, y: yCursor }
yCursor += yGap yCursor += getNodeHeight(dez.id) + yGap
// children SG/TD under this dez vertically // children SG/TD under this dez vertically
const children = laneNodes.filter(n => (n.data?.type === 'sachgebiet' || n.data?.type === 'teildezernat') && n.data?.parentId === dez.id).sort(childOrder) const children = laneNodes.filter(n => (n.data?.type === 'sachgebiet' || n.data?.type === 'teildezernat') && n.data?.parentId === dez.id).sort(childOrder)
children.forEach((c) => { children.forEach((c) => {
c.position = { x: xBase + 40, y: yCursor } c.position = { x: xBase + 40, y: yCursor }
yCursor += yGap yCursor += getNodeHeight(c.id) + yGap
}) })
// small gap after each dezernat block // small gap after each dezernat block
yCursor += 12 yCursor += 12
@ -511,7 +525,7 @@ export default function OrganizationEditor() {
// Any remaining nodes (fallback) // Any remaining nodes (fallback)
laneNodes.filter(n => !['abteilung','dezernat','sachgebiet','teildezernat'].includes(n.data?.type || '')).forEach(n => { laneNodes.filter(n => !['abteilung','dezernat','sachgebiet','teildezernat'].includes(n.data?.type || '')).forEach(n => {
n.position = { x: xBase, y: yCursor } n.position = { x: xBase, y: yCursor }
yCursor += yGap yCursor += getNodeHeight(n.id) + yGap
}) })
laneIndex += 1 laneIndex += 1
@ -521,35 +535,7 @@ export default function OrganizationEditor() {
} }
// Reparenting: select source then target // Reparenting: select source then target
const handleStartReparent = () => {
setReparentMode({ source: selectedNode })
}
const handleReparent = async (child: Node, newParent: Node) => {
try {
await api.put(`/organization/units/${child.id}`, {
parentId: newParent.id,
level: (newParent.data?.level ?? 0) + 1
})
await loadOrganization()
} catch (error: any) {
alert(error?.response?.data?.error?.message || 'Konnte Parent nicht ändern')
console.error('Failed to reparent:', error)
}
}
const persistPositions = async () => {
try {
await Promise.all(nodes.map(n => api.put(`/organization/units/${n.id}`, {
positionX: Math.round(n.position.x),
positionY: Math.round(n.position.y)
})))
alert('Positionen gespeichert')
} catch (error) {
console.error('Failed to persist positions:', error)
alert('Fehler beim Speichern der Positionen')
}
}
if (loading) { if (loading) {
return ( return (
@ -604,25 +590,7 @@ export default function OrganizationEditor() {
> >
Auto anordnen Auto anordnen
</button> </button>
<button
onClick={persistPositions}
className="w-full px-4 py-2 bg-slate-600 text-white rounded hover:bg-slate-700"
>
Positionen speichern
</button>
<button
onClick={() => setShowValidation(true)}
className="w-full px-4 py-2 bg-amber-600 text-white rounded hover:bg-amber-700"
>
Validierung anzeigen
</button>
<button
onClick={handleStartReparent}
disabled={!selectedNode}
className={`w-full px-4 py-2 rounded text-white ${selectedNode ? 'bg-cyan-600 hover:bg-cyan-700' : 'bg-gray-400'}`}
>
Parent neu setzen {reparentMode.source ? '(Quelle gewählt)' : ''}
</button>
</div> </div>
</Panel> </Panel>
@ -894,32 +862,7 @@ export default function OrganizationEditor() {
</div> </div>
)} )}
{/* Validation Dialog */}
{showValidation && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-lg w-full">
<h2 className="text-xl font-bold mb-4">Validierung</h2>
<div className="space-y-3 max-h-[60vh] overflow-auto">
<div>
<h3 className="font-semibold mb-2">Waisen-Knoten (Parent fehlt)</h3>
{issues.orphans.size === 0 ? (
<p className="text-gray-600">Keine</p>
) : (
<ul className="list-disc ml-5 text-sm">
{Array.from(issues.orphans).map(id => {
const n = nodes.find(n=>n.id===id)
return <li key={id}>{n?.data?.code || id} {n?.data?.name}</li>
})}
</ul>
)}
</div>
</div>
<div className="flex justify-end gap-2 mt-6">
<button onClick={()=>setShowValidation(false)} className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Schließen</button>
</div>
</div>
</div>
)}
</div> </div>
) )
} }