Bei Profil die Dienststelle spackt nicht mehr
Dieser Commit ist enthalten in:
@ -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)
|
||||
|
||||
@ -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
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren