diff --git a/admin-panel/src/views/OrganizationEditor.tsx b/admin-panel/src/views/OrganizationEditor.tsx index 5dfa310..bdfbc32 100644 --- a/admin-panel/src/views/OrganizationEditor.tsx +++ b/admin-panel/src/views/OrganizationEditor.tsx @@ -374,8 +374,12 @@ export default function OrganizationEditor() { const handleAutoLayout = () => { const laneWidth = 420 const xMargin = 40 - const yStart = 80 const yGap = 110 + // Zweistufiges Top-Band: Direktion ganz oben, darunter weitere ROOT-Knoten + const topYDirector = 10 + const topYRootRow = topYDirector + yGap + // Spaltenstart mit Abstand unterhalb der Top-Row, um Überlappungen zu vermeiden + const yStart = topYRootRow + yGap const newNodes: Node[] = nodes.map(n => ({ ...n, position: { ...n.position } })) const byId: Record = {} @@ -442,16 +446,36 @@ export default function OrganizationEditor() { return va - vb } - // Position Direktion and root-like nodes at the top center + // Position Direktion und ROOT-ähnliche Knoten als Top-Zeilen const rootNodes = lanesMap.get('ROOT') || [] let globalMinX = xMargin - const totalLanes = laneOrder.filter(k => k !== 'ROOT' && k !== 'UNK').length - const rootX = Math.max(xMargin, (totalLanes * laneWidth) / 2 - 150) - let rootY = 10 - rootNodes.forEach(n => { - n.position = { x: rootX, y: rootY } - rootY += yGap - }) + const lanesForWidth = laneOrder.filter(k => k !== 'ROOT' && k !== 'UNK') + const laneCount = Math.max(1, lanesForWidth.length) + const totalWidth = (laneCount - 1) * laneWidth + const cardHalfWidth = 150 // angenommene halbe Kartenbreite + + // 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() + if (directorIndex >= 0) { + const centerX = xMargin + totalWidth / 2 - cardHalfWidth + const x = Math.max(xMargin, centerX) + rootNodes[directorIndex].position = { x, y: topYDirector } + placedIndices.add(directorIndex) + } + + // 2) Übrige ROOT-Knoten in einer zweiten Top-Reihe gleichmäßig verteilen + const others: Node[] = rootNodes.filter((_, idx) => !placedIndices.has(idx)) + if (others.length === 1) { + const x = Math.max(xMargin, xMargin + totalWidth / 2 - cardHalfWidth) + others[0].position = { x, y: topYRootRow } + } 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 } + }) + } // Arrange lanes let laneIndex = 0 diff --git a/backend/src/routes/organizationImport.ts b/backend/src/routes/organizationImport.ts index 0b73443..12b92eb 100644 --- a/backend/src/routes/organizationImport.ts +++ b/backend/src/routes/organizationImport.ts @@ -83,6 +83,7 @@ function parseOrganizationFromText(text: string) { continue } + // Abteilung (inkl. Zentralabteilung erkennen) const abtMatch = line.match(/Abteilung\s+(\d+|Zentralabteilung)/i) if (abtMatch) { const abtNum = abtMatch[1] === 'Zentralabteilung' ? 'ZA' : abtMatch[1] @@ -100,6 +101,32 @@ function parseOrganizationFromText(text: string) { continue } + // Zentralabteilung alleinstehend (ohne Präfix "Abteilung") + if (/^Zentralabteilung\b/i.test(line) || /^ZA\b/i.test(line)) { + currentAbteilung = ensure({ + code: 'Abt ZA', + name: 'Zentralabteilung', + type: 'abteilung', + level: 1, + parentId: 'DIR', + color: colors['ZA'] || '#6b7280', + hasFuehrungsstelle: false + }) + currentDezernat = null + // nicht continue; nachfolgende Muster können weitere Details liefern + } + + // Dezernat ZA N (z. B. "Dez ZA 1" bis "Dez ZA 5") + const dezZaMatch = line.match(/^(?:Dezernat|Dez)\s+ZA\s*(\d{1,2})/i) + if (dezZaMatch) { + const zaNum = dezZaMatch[1].trim() + const dezName = line.replace(/^(?:Dezernat|Dez)\s+ZA\s*\d{1,2}\s*-?\s*/i, '').trim() || `Dezernat ZA ${zaNum}` + // Ensure Abt ZA exists + ensure({ code: 'Abt ZA', name: 'Zentralabteilung', type: 'abteilung', level: 1, parentId: 'DIR', color: colors['ZA'] || '#6b7280', hasFuehrungsstelle: false }) + currentDezernat = ensure({ code: `Dez ZA ${zaNum}`, name: dezName, type: 'dezernat', level: 2, parentId: 'Abt ZA' }) + continue + } + const dezMatch = line.match(/^(?:Dezernat|Dez)\s+([\d]+)/i) if (dezMatch) { const dezNum = dezMatch[1].trim()