From 42603d70c68cd9e2697e91ea5c4a61fee874cd4e Mon Sep 17 00:00:00 2001 From: Claude Project Manager Date: Thu, 25 Sep 2025 00:30:38 +0200 Subject: [PATCH] =?UTF-8?q?Gleiche=20L=C3=A4nge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin-panel/src/views/OrganizationEditor.tsx | 121 +++++-------------- 1 file changed, 32 insertions(+), 89 deletions(-) diff --git a/admin-panel/src/views/OrganizationEditor.tsx b/admin-panel/src/views/OrganizationEditor.tsx index bdfbc32..b3af480 100644 --- a/admin-panel/src/views/OrganizationEditor.tsx +++ b/admin-panel/src/views/OrganizationEditor.tsx @@ -47,7 +47,7 @@ const OrganizationNode = ({ data }: { data: any }) => { } return ( -
+
(null) const [clearExisting, setClearExisting] = useState(false) const [issues, setIssues] = useState<{ orphans: Set }>({ orphans: new Set() }) - const [showValidation, setShowValidation] = useState(false) - const [reparentMode, setReparentMode] = useState<{ source: Node | null }>({ source: null }) const [preview, setPreview] = useState(null) const [pendingOverrides, setPendingOverrides] = useState>({}) const [rememberRules, setRememberRules] = useState(true) @@ -212,13 +210,7 @@ export default function OrganizationEditor() { ) 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) => { @@ -379,12 +371,24 @@ export default function OrganizationEditor() { const topYDirector = 10 const topYRootRow = topYDirector + yGap // 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 byId: Record = {} 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 code: string = n.data?.code || '' const type: string = n.data?.type || '' @@ -457,26 +461,36 @@ export default function OrganizationEditor() { // 1) Direktion zentriert ganz oben const directorIndex = rootNodes.findIndex(n => (String(n.data?.type).toLowerCase() === 'direktion') || String(n.data?.code).toUpperCase() === 'DIR') let placedIndices = new Set() + let directorRowHeight = 0 if (directorIndex >= 0) { const centerX = xMargin + totalWidth / 2 - cardHalfWidth const x = Math.max(xMargin, centerX) rootNodes[directorIndex].position = { x, y: topYDirector } placedIndices.add(directorIndex) + directorRowHeight = getNodeHeight(rootNodes[directorIndex].id) } // 2) Übrige ROOT-Knoten in einer zweiten Top-Reihe gleichmäßig verteilen const others: Node[] = rootNodes.filter((_, idx) => !placedIndices.has(idx)) + let rootRowHeight = 0 if (others.length === 1) { const x = Math.max(xMargin, xMargin + totalWidth / 2 - cardHalfWidth) others[0].position = { x, y: topYRootRow } + rootRowHeight = getNodeHeight(others[0].id) } else if (others.length > 1) { const step = others.length > 1 ? (totalWidth / (others.length - 1)) : 0 others.forEach((n, i) => { const xi = xMargin + i * step - cardHalfWidth 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 let laneIndex = 0 laneOrder.forEach(key => { @@ -490,19 +504,19 @@ export default function OrganizationEditor() { let yCursor = yStart if (abteilung) { abteilung.position = { x: xBase, y: yCursor } - yCursor += yGap + yCursor += getNodeHeight(abteilung.id) + yGap } // Place Dezernate const dezernate = laneNodes.filter(n => n.data?.type === 'dezernat').sort(dezOrder) dezernate.forEach(dez => { dez.position = { x: xBase, y: yCursor } - yCursor += yGap + yCursor += getNodeHeight(dez.id) + yGap // 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) children.forEach((c) => { c.position = { x: xBase + 40, y: yCursor } - yCursor += yGap + yCursor += getNodeHeight(c.id) + yGap }) // small gap after each dezernat block yCursor += 12 @@ -511,7 +525,7 @@ export default function OrganizationEditor() { // Any remaining nodes (fallback) laneNodes.filter(n => !['abteilung','dezernat','sachgebiet','teildezernat'].includes(n.data?.type || '')).forEach(n => { n.position = { x: xBase, y: yCursor } - yCursor += yGap + yCursor += getNodeHeight(n.id) + yGap }) laneIndex += 1 @@ -521,35 +535,7 @@ export default function OrganizationEditor() { } // 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) { return ( @@ -604,25 +590,7 @@ export default function OrganizationEditor() { > Auto anordnen - - - +
@@ -894,32 +862,7 @@ export default function OrganizationEditor() {
)} - {/* Validation Dialog */} - {showValidation && ( -
-
-

Validierung

-
-
-

Waisen-Knoten (Parent fehlt)

- {issues.orphans.size === 0 ? ( -

Keine

- ) : ( -
    - {Array.from(issues.orphans).map(id => { - const n = nodes.find(n=>n.id===id) - return
  • {n?.data?.code || id} – {n?.data?.name}
  • - })} -
- )} -
-
-
- -
-
-
- )} +
) }