Bei Profil die Dienststelle spackt nicht mehr

Dieser Commit ist enthalten in:
Claude Project Manager
2025-09-29 22:16:00 +02:00
Ursprung e34424bf1d
Commit 5cdf492f1d
2 geänderte Dateien mit 161 neuen und 60 gelöschten Zeilen

Datei anzeigen

@ -74,6 +74,103 @@ router.get('/hierarchy', authenticate, async (req: AuthRequest, res, next) => {
}
})
// Sorting helpers to ensure intuitive order at the top level
const isLeitungsstab = (u: any) =>
(String(u.code || '').toLowerCase() === 'lstab') || /leitungsstab/i.test(String(u.name || ''))
const isZA = (u: any) => {
const code = String(u.code || '').toLowerCase()
const name = String(u.name || '').toLowerCase()
return code === 'za' || code === 'abt za' || /zentralabteilung/i.test(name)
}
const abteilungNumber = (u: any): number | null => {
// Match "Abt 1", "Abt 2", etc.
const m = String(u.code || '').match(/^\s*Abt\s+(\d+)\s*$/i)
if (m) return parseInt(m[1], 10)
// Also try name like "Abteilung 4 - ..."
const n = String(u.name || '').match(/^\s*Abteilung\s+(\d+)/i)
if (n) return parseInt(n[1], 10)
return null
}
const sortRootChildren = (children: any[]) => {
children.sort((a, b) => {
// 1) Leitungsstab first
const aL = isLeitungsstab(a) ? 0 : 1
const bL = isLeitungsstab(b) ? 0 : 1
if (aL !== bL) return aL - bL
// 2) Zentralabteilung second
const aZA = isZA(a) ? 0 : 1
const bZA = isZA(b) ? 0 : 1
if (aZA !== bZA) return aZA - bZA
// 3) Abteilungen numerisch
const aNum = abteilungNumber(a)
const bNum = abteilungNumber(b)
const aIsAbt = aNum !== null
const bIsAbt = bNum !== null
if (aIsAbt && bIsAbt) return (aNum as number) - (bNum as number)
if (aIsAbt) return -1
if (bIsAbt) return 1
// 4) Fallback: orderIndex, then name
const oi = (Number(a.orderIndex) || 0) - (Number(b.orderIndex) || 0)
if (oi !== 0) return oi
return String(a.name || '').localeCompare(String(b.name || ''))
})
}
const sortChildrenGeneric = (children: any[]) => {
children.sort((a, b) => {
// Prefer numeric sort by common prefixes (Dez, SG, TD)
const parseKey = (u: any): [number, number, string] => {
const code = String(u.code || '')
// e.g. "Dez 41" / "Dezernat 41" / "Dez ZA 3"
let primary = Number(u.orderIndex) || 0
let secondary = 0
const dez = code.match(/^\s*(Dez|Dezernat)\s*(?:ZA\s*)?(\d+)/i)
if (dez) {
primary = 10
secondary = parseInt(dez[2], 10)
} else {
const sg = code.match(/^\s*(SG|TD)\s*(?:ZA\s*)?(\d+)(?:\.(\d+))?/i)
if (sg) {
primary = 20 + (sg[1].toUpperCase() === 'TD' ? 1 : 0)
secondary = parseInt(sg[2], 10) * 100 + (sg[3] ? parseInt(sg[3], 10) : 0)
}
}
return [primary, secondary, String(u.name || '')]
}
const [ap, as, an] = parseKey(a)
const [bp, bs, bn] = parseKey(b)
if (ap !== bp) return ap - bp
if (as !== bs) return as - bs
return an.localeCompare(bn)
})
}
// Recursively sort tree
const sortTree = (node: any) => {
if (!node || !Array.isArray(node.children)) return
if (node.type === 'direktion') {
sortRootChildren(node.children)
} else {
sortChildrenGeneric(node.children)
}
node.children.forEach(sortTree)
}
// Ensure root order as well (Direktion first)
rootUnits.sort((a, b) => {
const aw = a.type === 'direktion' ? 0 : 1
const bw = b.type === 'direktion' ? 0 : 1
if (aw !== bw) return aw - bw
return String(a.name || '').localeCompare(String(b.name || ''))
})
// Apply sorting from the top
rootUnits.forEach(sortTree)
res.json({ success: true, data: rootUnits })
} catch (error) {
logger.error('Error building organizational hierarchy:', error)

Datei anzeigen

@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import type { OrganizationalUnit } from '@skillmate/shared'
import api from '../services/api'
import { ChevronRight, Building2, Search, X } from 'lucide-react'
@ -207,73 +208,76 @@ export default function OrganizationSelector({ value, onChange, disabled }: Orga
</button>
</div>
{showModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-3xl w-full max-h-[80vh] flex flex-col">
{/* Header */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold dark:text-white">Organisationseinheit auswählen</h2>
{showModal && createPortal(
(
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-3xl w-full max-h-[80vh] flex flex-col">
{/* Header */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold dark:text-white">Organisationseinheit auswählen</h2>
<button
onClick={() => setShowModal(false)}
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Search */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Suche nach Name oder Code..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-3 py-2 border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
</div>
</div>
{/* Tree View */}
<div className="flex-1 overflow-y-auto p-4">
{loading ? (
<div className="text-center text-gray-500 dark:text-gray-400">
Lade Organisationsstruktur...
</div>
) : hierarchy.length === 0 ? (
<div className="text-center text-gray-500 dark:text-gray-400">
Keine Organisationseinheiten vorhanden
</div>
) : (
<div className="space-y-1">
{hierarchy.map(unit => renderUnit(unit))}
</div>
)}
</div>
{/* Footer */}
<div className="p-4 border-t border-gray-200 dark:border-gray-700 flex justify-between">
<button
onClick={() => {
setSelectedUnit(null)
setCurrentUnitName('')
onChange(null, '')
setShowModal(false)
}}
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200"
>
Auswahl entfernen
</button>
<button
onClick={() => setShowModal(false)}
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
<X className="w-5 h-5" />
Schließen
</button>
</div>
{/* Search */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Suche nach Name oder Code..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-3 py-2 border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
</div>
</div>
{/* Tree View */}
<div className="flex-1 overflow-y-auto p-4">
{loading ? (
<div className="text-center text-gray-500 dark:text-gray-400">
Lade Organisationsstruktur...
</div>
) : hierarchy.length === 0 ? (
<div className="text-center text-gray-500 dark:text-gray-400">
Keine Organisationseinheiten vorhanden
</div>
) : (
<div className="space-y-1">
{hierarchy.map(unit => renderUnit(unit))}
</div>
)}
</div>
{/* Footer */}
<div className="p-4 border-t border-gray-200 dark:border-gray-700 flex justify-between">
<button
onClick={() => {
setSelectedUnit(null)
setCurrentUnitName('')
onChange(null, '')
setShowModal(false)
}}
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200"
>
Auswahl entfernen
</button>
<button
onClick={() => setShowModal(false)}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Schließen
</button>
</div>
</div>
</div>
),
document.body
)}
</>
)