Roll Back Punkt - Ansicht klappt so semi

Dieser Commit ist enthalten in:
Claude Project Manager
2025-09-24 00:28:00 +02:00
Ursprung 2cabd4c0c6
Commit 1176db42e8
6 geänderte Dateien mit 1080 neuen und 166 gelöschten Zeilen

Datei anzeigen

@ -87,6 +87,15 @@ export default function OrganizationEditor() {
const [showImportDialog, setShowImportDialog] = useState(false) const [showImportDialog, setShowImportDialog] = useState(false)
const [uploadProgress, setUploadProgress] = useState<string | null>(null) const [uploadProgress, setUploadProgress] = useState<string | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null) const fileInputRef = useRef<HTMLInputElement>(null)
const [clearExisting, setClearExisting] = useState(false)
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 [pendingOverrides, setPendingOverrides] = useState<Record<string, { parentCode?: string; type?: string; name?: string }>>({})
const [rememberRules, setRememberRules] = useState(true)
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const [parentPicker, setParentPicker] = useState<{ open: boolean; target?: string; suggestions: any[] }>({ open: false, target: undefined, suggestions: [] })
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
code: '', code: '',
@ -111,6 +120,7 @@ export default function OrganizationEditor() {
const { nodes: flowNodes, edges: flowEdges } = convertToFlowElements(units) const { nodes: flowNodes, edges: flowEdges } = convertToFlowElements(units)
setNodes(flowNodes) setNodes(flowNodes)
setEdges(flowEdges) setEdges(flowEdges)
computeIssues(flowNodes)
} }
} catch (error) { } catch (error) {
console.error('Failed to load organization:', error) console.error('Failed to load organization:', error)
@ -202,7 +212,13 @@ export default function OrganizationEditor() {
) )
const onNodeClick = useCallback((_: any, node: Node) => { const onNodeClick = useCallback((_: any, node: Node) => {
setSelectedNode(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)
}
}, []) }, [])
const onNodeDragStop = useCallback(async (_: any, node: Node) => { const onNodeDragStop = useCallback(async (_: any, node: Node) => {
@ -249,6 +265,7 @@ export default function OrganizationEditor() {
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => { const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0] const file = event.target.files?.[0]
if (!file) return if (!file) return
setSelectedFile(file)
if (file.type !== 'application/pdf') { if (file.type !== 'application/pdf') {
alert('Bitte laden Sie nur PDF-Dateien hoch.') alert('Bitte laden Sie nur PDF-Dateien hoch.')
@ -257,24 +274,21 @@ export default function OrganizationEditor() {
const formData = new FormData() const formData = new FormData()
formData.append('pdf', file) formData.append('pdf', file)
formData.append('clearExisting', 'false') // Don't clear existing by default formData.append('clearExisting', String(clearExisting))
setUploadProgress('PDF wird hochgeladen...') setUploadProgress('PDF wird analysiert (Vorschau)...')
try { try {
const response = await api.post('/organization/import-pdf', formData, { const response = await api.post('/organization/import-pdf', formData, {
headers: { headers: {
'Content-Type': 'multipart/form-data' 'Content-Type': 'multipart/form-data'
} },
params: { previewOnly: 'true' }
}) })
if (response.data.success) { if (response.data.success) {
setUploadProgress(`Erfolgreich! ${response.data.data.unitsImported} Einheiten importiert.`) setPreview(response.data.data)
await loadOrganization() setUploadProgress(null)
setTimeout(() => {
setShowImportDialog(false)
setUploadProgress(null)
}, 2000)
} }
} catch (error: any) { } catch (error: any) {
console.error('PDF import failed:', error) console.error('PDF import failed:', error)
@ -282,6 +296,237 @@ export default function OrganizationEditor() {
} }
} }
const applyImport = async () => {
try {
setUploadProgress('Import wird angewendet...')
const overrides = Object.entries(pendingOverrides).map(([code, v]) => ({ code, ...v }))
const fd = new FormData()
if (selectedFile) fd.append('pdf', selectedFile)
fd.append('clearExisting', String(clearExisting))
fd.append('overrides', JSON.stringify(overrides))
fd.append('previewOnly', 'false')
fd.append('rememberRules', String(rememberRules))
const response = await api.post('/organization/import-pdf', fd, {
headers: { 'Content-Type': 'multipart/form-data' }
})
if (response.data.success) {
setUploadProgress('Erfolgreich importiert.')
await loadOrganization()
setTimeout(() => {
setShowImportDialog(false)
setUploadProgress(null)
setPreview(null)
setPendingOverrides({})
}, 1200)
}
} catch (error: any) {
console.error('Import apply failed:', error)
setUploadProgress(`Fehler: ${error.response?.data?.error?.message || 'Import fehlgeschlagen'}`)
}
}
const openParentPicker = async (targetCode: string) => {
try {
// Build suggestions from preview (preferred) + existing units
const suggestions: any[] = []
if (preview?.diff) {
const push = (u: any) => { if (u && (u.type === 'dezernat' || u.type === 'abteilung' || u.type === 'direktion')) suggestions.push({ code: u.code, name: u.name, type: u.type }) }
(preview.diff.create || []).forEach(push)
;(preview.diff.update || []).forEach((d: any) => { push(d.incoming); push(d.existing) })
}
try {
const res = await api.get('/organization/units')
if (res.data.success) {
res.data.data.forEach((u: any) => {
if (u && (u.type === 'dezernat' || u.type === 'abteilung' || u.type === 'direktion')) suggestions.push({ code: u.code, name: u.name, type: u.type })
})
}
} catch {}
// de-duplicate by code
const uniq: Record<string, any> = {}
suggestions.forEach(s => { uniq[s.code] = uniq[s.code] || s })
const list = Object.values(uniq)
setParentPicker({ open: true, target: targetCode, suggestions: list as any[] })
} catch (e) {
setParentPicker({ open: true, target: targetCode, suggestions: [] })
}
}
// Compute simple validation issues (orphans)
const computeIssues = (flowNodes: Node[]) => {
const ids = new Set(flowNodes.map(n => n.id))
const orphanIds = new Set<string>()
flowNodes.forEach(n => {
const parentId = (n.data?.parentId as string | undefined)
if (parentId && !ids.has(parentId)) {
orphanIds.add(n.id)
}
})
setIssues({ orphans: orphanIds })
// Apply visual styles
setNodes(ns => ns.map(n => ({
...n,
style: orphanIds.has(n.id) ? { ...(n.style||{}), border: '2px solid #ef4444' } : { ...(n.style||{}), border: '2px solid #e5e7eb' }
})))
}
// Auto layout: Swimlanes by Abteilung (columns), vertical stacking within lanes
const handleAutoLayout = () => {
const laneWidth = 420
const xMargin = 40
const yStart = 80
const yGap = 110
const newNodes: Node[] = nodes.map(n => ({ ...n, position: { ...n.position } }))
const byId: Record<string, Node> = {}
newNodes.forEach(n => { byId[n.id] = n })
const getLaneKey = (n: Node): string => {
const code: string = n.data?.code || ''
const type: string = n.data?.type || ''
if (type === 'direktion') return 'ROOT'
if (type === 'abteilung') {
const m = code.match(/Abt\s+(ZA|\d)/i)
return m ? m[1].toUpperCase() : 'UNK'
}
if (type === 'dezernat') {
if (/ZA/i.test(code)) return 'ZA'
const m = code.match(/Dez\s+(\d+)/i)
return m ? m[1].charAt(0) : 'UNK'
}
if (type === 'sachgebiet' || type === 'teildezernat') {
if (/ZA/i.test(code)) return 'ZA'
const m = code.match(/\s(\d+)\./)
return m ? m[1].charAt(0) : 'UNK'
}
if (type === 'stabsstelle' || type === 'sondereinheit' || type === 'fuehrungsstelle') {
return 'ROOT'
}
// fallback: derive from parent's lane
const parentId = n.data?.parentId
if (parentId && byId[parentId]) return getLaneKey(byId[parentId])
return 'UNK'
}
const lanesMap = new Map<string, Node[]>()
newNodes.forEach(n => {
const lane = getLaneKey(n)
if (!lanesMap.has(lane)) lanesMap.set(lane, [])
lanesMap.get(lane)!.push(n)
})
// Sort lanes: 1..6, ZA, ROOT, UNK
const laneOrder = Array.from(lanesMap.keys()).sort((a, b) => {
const rank = (k: string) => (k === 'ROOT' ? 99 : (k === 'UNK' ? 98 : (k === 'ZA' ? 7 : parseInt(k, 10))))
return rank(a) - rank(b)
})
// Helper to sort Dez/SG numerically within lane
const dezOrder = (a: Node, b: Node) => {
const parseDez = (n: Node) => {
const c: string = n.data?.code || ''
if (/ZA/i.test(c)) {
const m = c.match(/Dez\s+ZA\s*(\d+)/i)
return 100 + (m ? parseInt(m[1] || '0', 10) : 0)
}
const m = c.match(/Dez\s+(\d+)/i)
return m ? parseInt(m[1], 10) : 0
}
return parseDez(a) - parseDez(b)
}
const childOrder = (a: Node, b: Node) => {
const pa = (a.data?.code || '').match(/(\d+)\.(\d+)/)
const pb = (b.data?.code || '').match(/(\d+)\.(\d+)/)
const va = pa ? (parseInt(pa[1],10)*100 + parseInt(pa[2],10)) : 0
const vb = pb ? (parseInt(pb[1],10)*100 + parseInt(pb[2],10)) : 0
return va - vb
}
// Position Direktion and root-like nodes at the top center
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
})
// Arrange lanes
let laneIndex = 0
laneOrder.forEach(key => {
if (key === 'ROOT' || key === 'UNK') return
const laneNodes = lanesMap.get(key) || []
const xBase = xMargin + laneIndex * laneWidth
globalMinX = Math.min(globalMinX, xBase)
// Find the Abteilung node for this lane (if any)
const abteilung = laneNodes.find(n => n.data?.type === 'abteilung')
let yCursor = yStart
if (abteilung) {
abteilung.position = { x: xBase, y: yCursor }
yCursor += 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
// 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
})
// small gap after each dezernat block
yCursor += 12
})
// 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
})
laneIndex += 1
})
setNodes(newNodes)
}
// 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 (
<div className="flex items-center justify-center h-screen"> <div className="flex items-center justify-center h-screen">
@ -329,6 +574,31 @@ export default function OrganizationEditor() {
<Upload size={16} /> <Upload size={16} />
PDF importieren PDF importieren
</button> </button>
<button
onClick={handleAutoLayout}
className="w-full px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700"
>
Auto anordnen
</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>
@ -341,6 +611,9 @@ export default function OrganizationEditor() {
)} )}
<p className="text-sm text-gray-600">Typ: {selectedNode.data.type}</p> <p className="text-sm text-gray-600">Typ: {selectedNode.data.type}</p>
<p className="text-sm text-gray-600">Ebene: {selectedNode.data.level}</p> <p className="text-sm text-gray-600">Ebene: {selectedNode.data.level}</p>
{issues.orphans.has(selectedNode.id) && (
<p className="text-xs text-red-600 mt-1">Warnung: Parent fehlt (Waisen-Knoten)</p>
)}
{selectedNode.data.description && ( {selectedNode.data.description && (
<p className="text-sm mt-2">{selectedNode.data.description}</p> <p className="text-sm mt-2">{selectedNode.data.description}</p>
)} )}
@ -354,51 +627,149 @@ export default function OrganizationEditor() {
{/* Import PDF Dialog */} {/* Import PDF Dialog */}
{showImportDialog && ( {showImportDialog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <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-md w-full"> <div className="bg-white rounded-lg p-6 max-w-2xl w-full">
<h2 className="text-xl font-bold mb-4">PDF Organigramm importieren</h2> <h2 className="text-xl font-bold mb-4">PDF Organigramm importieren</h2>
<div className="space-y-4"> {!preview ? (
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center"> <div className="space-y-4">
<Upload className="mx-auto h-12 w-12 text-gray-400 mb-4" /> <div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center">
<p className="text-sm text-gray-600 mb-4"> <Upload className="mx-auto h-12 w-12 text-gray-400 mb-4" />
Laden Sie ein PDF-Dokument mit der Organisationsstruktur hoch. <p className="text-sm text-gray-600 mb-4">
Das System extrahiert automatisch die Hierarchie. Laden Sie ein PDF hoch. Es wird zunächst nur eine Vorschau (Diff) berechnet.
</p> </p>
<label className="flex items-center justify-center gap-2 text-sm mb-4">
<input <input type="checkbox" checked={clearExisting} onChange={(e)=>setClearExisting(e.target.checked)} />
ref={fileInputRef} Bestehende Einheiten löschen
type="file" </label>
accept=".pdf"
onChange={handleFileUpload} <input
className="hidden" ref={fileInputRef}
/> type="file"
accept=".pdf,application/pdf"
<button onChange={handleFileUpload}
onClick={() => fileInputRef.current?.click()} className="hidden"
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700" />
disabled={!!uploadProgress}
> <button
PDF auswählen onClick={() => fileInputRef.current?.click()}
</button> className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
</div> disabled={!!uploadProgress}
>
{uploadProgress && ( PDF auswählen
<div className="p-3 bg-blue-50 text-blue-700 rounded"> </button>
{uploadProgress}
</div> </div>
)}
</div> {uploadProgress && (
<div className="p-3 bg-blue-50 text-blue-700 rounded">
{uploadProgress}
</div>
)}
</div>
) : (
<div className="space-y-4">
<div className="grid grid-cols-3 gap-4">
<div className="p-3 border rounded">
<h3 className="font-semibold mb-2">Neu</h3>
<p className="text-sm text-gray-500 mb-2">{preview.counts?.create} Einheiten</p>
<div className="max-h-40 overflow-auto text-sm">
{preview.diff?.create?.slice(0,50).map((u: any)=> (
<div key={u.code}>{u.code} {u.name}</div>
))}
</div>
</div>
<div className="p-3 border rounded">
<h3 className="font-semibold mb-2">Änderungen</h3>
<p className="text-sm text-gray-500 mb-2">{preview.counts?.update} Einheiten</p>
<div className="max-h-40 overflow-auto text-sm">
{preview.diff?.update?.slice(0,50).map((d: any)=> (
<div key={d.incoming.code}>{d.incoming.code} {d.existing.name} {d.incoming.name}</div>
))}
</div>
</div>
<div className="p-3 border rounded">
<h3 className="font-semibold mb-2">Waisen (Parent fehlt)</h3>
<p className="text-sm text-gray-500 mb-2">{preview.counts?.orphans} Einheiten</p>
<div className="max-h-40 overflow-auto text-sm space-y-2">
{preview.diff?.orphans?.map((u: any)=> (
<div key={u.code} className="flex items-center gap-2">
<div className="flex-1 truncate" title={`${u.code}${u.name}`}>{u.code} {u.name}</div>
<input
placeholder="Parent-Code (z.B. Dez 11 / Abt 1)"
className="border rounded px-2 py-1 text-xs w-48"
value={pendingOverrides[u.code]?.parentCode || ''}
onChange={(e)=> setPendingOverrides(prev => ({...prev, [u.code]: { ...(prev[u.code]||{}), parentCode: e.target.value }}))}
/>
<button
onClick={() => openParentPicker(u.code)}
className="px-2 py-1 text-xs bg-slate-100 rounded border"
title="Parent auswählen"
>
</button>
</div>
))}
</div>
</div>
</div>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={rememberRules} onChange={(e)=>setRememberRules(e.target.checked)} />
Zuordnungen als Regeln speichern (für künftige Importe)
</label>
{uploadProgress && (
<div className="p-3 bg-blue-50 text-blue-700 rounded">{uploadProgress}</div>
)}
</div>
)}
<div className="flex justify-end gap-2 mt-6"> <div className="flex justify-end gap-2 mt-6">
<button <button
onClick={() => { onClick={() => {
setShowImportDialog(false) setShowImportDialog(false)
setUploadProgress(null) setUploadProgress(null)
setPreview(null)
setPendingOverrides({})
}} }}
className="px-4 py-2 text-gray-600 hover:text-gray-800" className="px-4 py-2 text-gray-600 hover:text-gray-800"
> >
Schließen Schließen
</button> </button>
{preview && (
<button
onClick={applyImport}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
>
Import anwenden
</button>
)}
</div>
</div>
</div>
)}
{/* Parent Picker Modal */}
{parentPicker.open && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-4 max-w-lg w-full">
<h3 className="font-semibold mb-3">Parent auswählen für {parentPicker.target}</h3>
<div className="max-h-80 overflow-auto border rounded">
{(parentPicker.suggestions || []).map((s: any) => (
<button
key={s.code}
onClick={() => {
if (parentPicker.target) {
setPendingOverrides(prev => ({ ...prev, [parentPicker.target!]: { ...(prev[parentPicker.target!]||{}), parentCode: s.code } }))
}
setParentPicker({ open: false, target: undefined, suggestions: [] })
}}
className="w-full text-left px-3 py-2 hover:bg-slate-100 border-b last:border-b-0"
>
<span className="font-mono text-xs mr-2">{s.code}</span>
<span>{s.name}</span>
</button>
))}
</div>
<div className="flex justify-end mt-3">
<button onClick={() => setParentPicker({ open: false, target: undefined, suggestions: [] })} className="px-3 py-1 text-slate-600 hover:text-slate-800">Abbrechen</button>
</div> </div>
</div> </div>
</div> </div>
@ -498,6 +869,33 @@ export default function OrganizationEditor() {
</div> </div>
</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>
) )
} }

Datei anzeigen

@ -0,0 +1,25 @@
// Simple helper to extract text from the root PDF for inspection
const fs = require('fs')
const path = require('path')
const pdfParse = require('pdf-parse')
async function main() {
const pdfPath = path.resolve(__dirname, '..', '..', 'Organigramm_ohne_Namen.pdf')
if (!fs.existsSync(pdfPath)) {
console.error('PDF not found at', pdfPath)
process.exit(1)
}
const buf = fs.readFileSync(pdfPath)
const res = await pdfParse(buf)
const outPath = path.resolve(__dirname, '..', '..', 'organigramm_text.txt')
fs.writeFileSync(outPath, res.text, 'utf8')
console.log('Extracted text length:', res.text.length)
console.log('Pages:', res.numpages)
console.log('Saved to:', outPath)
// Print first 200 lines to stdout for quick view
const lines = res.text.split('\n').map(s => s.trim()).filter(Boolean)
console.log('--- First lines ---')
console.log(lines.slice(0, 200).join('\n'))
}
main().catch(err => { console.error(err); process.exit(1) })

Datei anzeigen

@ -403,6 +403,19 @@ export function initializeDatabase() {
CREATE INDEX IF NOT EXISTS idx_org_units_level ON organizational_units(level); CREATE INDEX IF NOT EXISTS idx_org_units_level ON organizational_units(level);
`) `)
// Import rules for organization parsing (persist admin overrides)
db.exec(`
CREATE TABLE IF NOT EXISTS organization_import_rules (
id TEXT PRIMARY KEY,
target_code TEXT UNIQUE NOT NULL,
parent_code TEXT,
type_override TEXT,
name_override TEXT,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_org_rules_target ON organization_import_rules(target_code);
`)
// Employee Unit Assignments // Employee Unit Assignments
db.exec(` db.exec(`
CREATE TABLE IF NOT EXISTS employee_unit_assignments ( CREATE TABLE IF NOT EXISTS employee_unit_assignments (
@ -554,4 +567,4 @@ export function initializeDatabase() {
CREATE INDEX IF NOT EXISTS idx_bookings_status ON bookings(status); CREATE INDEX IF NOT EXISTS idx_bookings_status ON bookings(status);
CREATE INDEX IF NOT EXISTS idx_workspace_analytics_date ON workspace_analytics(date); CREATE INDEX IF NOT EXISTS idx_workspace_analytics_date ON workspace_analytics(date);
`) `)
} }

Datei anzeigen

@ -200,6 +200,8 @@ router.put('/units/:id',
body('name').optional().notEmpty().trim(), body('name').optional().notEmpty().trim(),
body('description').optional(), body('description').optional(),
body('color').optional(), body('color').optional(),
body('parentId').optional({ checkFalsy: true }).isUUID(),
body('level').optional().isInt({ min: 0, max: 10 }),
// allow updating persisted canvas positions from admin editor // allow updating persisted canvas positions from admin editor
body('positionX').optional().isNumeric(), body('positionX').optional().isNumeric(),
body('positionY').optional().isNumeric() body('positionY').optional().isNumeric()
@ -210,9 +212,32 @@ router.put('/units/:id',
return res.status(403).json({ success: false, error: { message: 'Admin access required' } }) return res.status(403).json({ success: false, error: { message: 'Admin access required' } })
} }
const { name, description, color, hasFuehrungsstelle, fuehrungsstelleName, positionX, positionY } = req.body const { name, description, color, hasFuehrungsstelle, fuehrungsstelleName, positionX, positionY, parentId, level } = req.body
const now = new Date().toISOString() const now = new Date().toISOString()
// Optional: validate parentId and avoid cycles
let newParentId: string | null | undefined = undefined
if (parentId !== undefined) {
if (parentId === null || parentId === '' ) {
newParentId = null
} else {
const parent = db.prepare('SELECT id, parent_id as parentId FROM organizational_units WHERE id = ?').get(parentId)
if (!parent) {
return res.status(404).json({ success: false, error: { message: 'Parent unit not found' } })
}
// cycle check: walk up from parent to root; id must not appear
const targetId = req.params.id
let cursor: any = parent
while (cursor && cursor.parentId) {
if (cursor.parentId === targetId) {
return res.status(400).json({ success: false, error: { message: 'Cyclic parent assignment is not allowed' } })
}
cursor = db.prepare('SELECT id, parent_id as parentId FROM organizational_units WHERE id = ?').get(cursor.parentId)
}
newParentId = parentId
}
}
const result = db.prepare(` const result = db.prepare(`
UPDATE organizational_units UPDATE organizational_units
SET name = COALESCE(?, name), SET name = COALESCE(?, name),
@ -220,6 +245,8 @@ router.put('/units/:id',
color = COALESCE(?, color), color = COALESCE(?, color),
has_fuehrungsstelle = COALESCE(?, has_fuehrungsstelle), has_fuehrungsstelle = COALESCE(?, has_fuehrungsstelle),
fuehrungsstelle_name = COALESCE(?, fuehrungsstelle_name), fuehrungsstelle_name = COALESCE(?, fuehrungsstelle_name),
parent_id = COALESCE(?, parent_id),
level = COALESCE(?, level),
position_x = COALESCE(?, position_x), position_x = COALESCE(?, position_x),
position_y = COALESCE(?, position_y), position_y = COALESCE(?, position_y),
updated_at = ? updated_at = ?
@ -230,6 +257,8 @@ router.put('/units/:id',
color || null, color || null,
hasFuehrungsstelle !== undefined ? (hasFuehrungsstelle ? 1 : 0) : null, hasFuehrungsstelle !== undefined ? (hasFuehrungsstelle ? 1 : 0) : null,
fuehrungsstelleName || null, fuehrungsstelleName || null,
newParentId !== undefined ? newParentId : null,
level !== undefined ? Number(level) : null,
positionX !== undefined ? Math.round(Number(positionX)) : null, positionX !== undefined ? Math.round(Number(positionX)) : null,
positionY !== undefined ? Math.round(Number(positionY)) : null, positionY !== undefined ? Math.round(Number(positionY)) : null,
now, now,

Datei anzeigen

@ -28,153 +28,179 @@ const upload = multer({
// Helper to parse organizational structure from PDF text // Helper to parse organizational structure from PDF text
function parseOrganizationFromText(text: string) { function parseOrganizationFromText(text: string) {
const units: any[] = [] const units: any[] = []
const byCode = new Map<string, any>()
const lines = text.split('\n').map(line => line.trim()).filter(line => line && line.length > 2) const lines = text.split('\n').map(line => line.trim()).filter(line => line && line.length > 2)
// Pattern matching for different organizational levels
const patterns = { const patterns = {
direktor: /(Direktor|Director)\s*(LKA)?/i, direktor: /(Direktor|Director)\s*(LKA)?/i,
abteilung: /^Abteilung\s+(\d+)/i, abteilung: /^Abteilung\s+(\d+|Zentralabteilung)/i,
dezernat: /^Dezernat\s+([\d]+)/i, dezernat: /^(?:Dezernat|Dez)\s+([\d]+)/i,
sachgebiet: /^SG\s+([\d\.]+)/i, sachgebiet: /^SG\s+([\d\.]+)/i,
teildezernat: /^TD\s+([\d\.]+)/i, teildezernat: /^TD\s+([\d\.]+)/i,
stabsstelle: /(Leitungsstab|LStab|Führungsgruppe)/i, stabsstelle: /(Leitungsstab|LStab|Führungsgruppe)/i,
fahndung: /Fahndungsgruppe/i, sondereinheit: /(Personalrat|Schwerbehindertenvertretung|Datenschutzbeauftrag|Gleichstellungsbeauftrag|Innenrevision|IUK-Lage)/i
sondereinheit: /(Personalrat|Schwerbehindertenvertretung|beauftragt|Innenrevision|IUK-Lage)/i
} }
// Color mapping for departments
const colors: Record<string, string> = { const colors: Record<string, string> = {
'1': '#dc2626', '1': '#dc2626', '2': '#ea580c', '3': '#0891b2', '4': '#7c3aed', '5': '#0d9488', '6': '#be185d', 'ZA': '#6b7280'
'2': '#ea580c',
'3': '#0891b2',
'4': '#7c3aed',
'5': '#0d9488',
'6': '#be185d',
'ZA': '#6b7280'
} }
const ensure = (u: any) => {
const existing = byCode.get(u.code)
if (existing) {
// Update minimal fields if missing
existing.name = existing.name || u.name
existing.type = existing.type || u.type
existing.level = existing.level ?? u.level
if (!existing.parentId && u.parentId) existing.parentId = u.parentId
if (!existing.color && u.color) existing.color = u.color
return existing
}
byCode.set(u.code, u)
units.push(u)
return u
}
const inferAbteilungCode = (dezNum: string) => {
const digits = (dezNum || '').replace(/\D/g, '')
const first = digits.charAt(0)
if (['1','2','3','4','5','6'].includes(first)) return `Abt ${first}`
if (dezNum.toUpperCase().includes('ZA') || first === '0') return 'Abt ZA'
return null
}
let currentAbteilung: any = null let currentAbteilung: any = null
let currentDezernat: any = null let currentDezernat: any = null
lines.forEach(line => { // Always ensure root
// Check for Direktor ensure({ code: 'DIR', name: 'Direktor LKA NRW', type: 'direktion', level: 0, parentId: null, color: '#1e3a8a' })
for (const raw of lines) {
const line = raw.replace(/[–—]/g, '-')
if (patterns.direktor.test(line)) { if (patterns.direktor.test(line)) {
units.push({ ensure({ code: 'DIR', name: 'Direktor LKA NRW', type: 'direktion', level: 0, parentId: null, color: '#1e3a8a' })
code: 'DIR', continue
name: 'Direktor LKA NRW',
type: 'direktion',
level: 0,
parentId: null,
color: '#1e3a8a'
})
} }
// Check for Abteilung
const abtMatch = line.match(/Abteilung\s+(\d+|Zentralabteilung)/i) const abtMatch = line.match(/Abteilung\s+(\d+|Zentralabteilung)/i)
if (abtMatch) { if (abtMatch) {
const abtNum = abtMatch[1] === 'Zentralabteilung' ? 'ZA' : abtMatch[1] const abtNum = abtMatch[1] === 'Zentralabteilung' ? 'ZA' : abtMatch[1]
const abtName = line.replace(/^Abteilung\s+\d+\s*[-–]\s*/, '') const abtName = line.replace(/^Abteilung\s+(?:\d+|Zentralabteilung)\s*-?\s*/i, '').trim() || `Abteilung ${abtNum}`
currentAbteilung = { currentAbteilung = ensure({
code: `Abt ${abtNum}`, code: `Abt ${abtNum}`,
name: abtName || `Abteilung ${abtNum}`, name: abtName,
type: 'abteilung', type: 'abteilung',
level: 1, level: 1,
parentId: 'DIR', parentId: 'DIR',
color: colors[abtNum] || '#6b7280', color: colors[abtNum] || '#6b7280',
hasFuehrungsstelle: abtNum !== 'ZA' hasFuehrungsstelle: abtNum !== 'ZA'
} })
units.push(currentAbteilung)
currentDezernat = null currentDezernat = null
continue
} }
// Check for Dezernat const dezMatch = line.match(/^(?:Dezernat|Dez)\s+([\d]+)/i)
const dezMatch = line.match(/(?:Dezernat|Dez)\s+([\d\s]+)/i) if (dezMatch) {
if (dezMatch && currentAbteilung) {
const dezNum = dezMatch[1].trim() const dezNum = dezMatch[1].trim()
const dezName = line.replace(/^(?:Dezernat|Dez)\s+[\d\s]+\s*[-–]?\s*/, '').trim() const dezName = line.replace(/^(?:Dezernat|Dez)\s+[\d]+\s*-?\s*/i, '').trim() || `Dezernat ${dezNum}`
currentDezernat = { // Strict mapping: first digit of dezNum determines Abteilung
code: `Dez ${dezNum}`, const inferredAbt = inferAbteilungCode(dezNum)
name: dezName || `Dezernat ${dezNum}`, let parentCode: string | null = null
type: 'dezernat', if (inferredAbt) {
level: 2, const abtNum = inferredAbt.split(' ')[1]
parentId: currentAbteilung.code ensure({ code: inferredAbt, name: inferredAbt.replace('Abt', 'Abteilung'), type: 'abteilung', level: 1, parentId: 'DIR', color: colors[abtNum] || '#6b7280', hasFuehrungsstelle: abtNum !== 'ZA' })
parentCode = inferredAbt
} }
units.push(currentDezernat) currentDezernat = ensure({ code: `Dez ${dezNum}`, name: dezName, type: 'dezernat', level: 2, parentId: parentCode })
continue
} }
// Check for Sachgebiet const sgMatch = line.match(/^SG\s+([\d\.]+)/i)
const sgMatch = line.match(/SG\s+([\d\.]+)/i) if (sgMatch) {
if (sgMatch && currentDezernat) { const sgNum = sgMatch[1].trim()
const sgNum = sgMatch[1] const sgName = line.replace(/^SG\s+[\d\.]+\s*-?\s*/i, '').trim() || `Sachgebiet ${sgNum}`
const sgName = line.replace(/^SG\s+[\d\.]+\s*[-–]?\s*/, '').trim() // Strict mapping: SG NN.X -> Dez NN under Abt N
units.push({ const prefix = sgNum.split('.')[0]
code: `SG ${sgNum}`, let dezCode: string | null = null
name: sgName || `Sachgebiet ${sgNum}`, if (prefix) {
type: 'sachgebiet', dezCode = `Dez ${prefix}`
level: 3, const inferredAbt = inferAbteilungCode(prefix)
parentId: currentDezernat.code if (inferredAbt) {
}) const abtNum = inferredAbt.split(' ')[1]
ensure({ code: inferredAbt, name: inferredAbt.replace('Abt', 'Abteilung'), type: 'abteilung', level: 1, parentId: 'DIR', color: colors[abtNum] || '#6b7280', hasFuehrungsstelle: abtNum !== 'ZA' })
}
ensure({ code: dezCode, name: `Dezernat ${prefix}`, type: 'dezernat', level: 2, parentId: inferredAbt || null })
}
ensure({ code: `SG ${sgNum}`, name: sgName, type: 'sachgebiet', level: 3, parentId: dezCode })
continue
} }
// Check for Teildezernat const tdMatch = line.match(/^TD\s+([\d\.]+)/i)
const tdMatch = line.match(/TD\s+([\d\.]+)/i) if (tdMatch) {
if (tdMatch && currentDezernat) { const tdNum = tdMatch[1].trim()
const tdNum = tdMatch[1] const tdName = line.replace(/^TD\s+[\d\.]+\s*-?\s*/i, '').trim() || `Teildezernat ${tdNum}`
const tdName = line.replace(/^TD\s+[\d\.]+\s*[-–]?\s*/, '').trim() // Strict mapping: TD NN.X -> Dez NN under Abt N
units.push({ const prefix = tdNum.split('.')[0]
code: `TD ${tdNum}`, let dezCode: string | null = null
name: tdName || `Teildezernat ${tdNum}`, if (prefix) {
type: 'teildezernat', dezCode = `Dez ${prefix}`
level: 3, const inferredAbt = inferAbteilungCode(prefix)
parentId: currentDezernat.code if (inferredAbt) {
}) const abtNum = inferredAbt.split(' ')[1]
ensure({ code: inferredAbt, name: inferredAbt.replace('Abt', 'Abteilung'), type: 'abteilung', level: 1, parentId: 'DIR', color: colors[abtNum] || '#6b7280', hasFuehrungsstelle: abtNum !== 'ZA' })
}
ensure({ code: dezCode, name: `Dezernat ${prefix}`, type: 'dezernat', level: 2, parentId: inferredAbt || null })
}
ensure({ code: `TD ${tdNum}`, name: tdName, type: 'teildezernat', level: 3, parentId: dezCode })
continue
} }
// Check for Stabsstelle
if (patterns.stabsstelle.test(line)) { if (patterns.stabsstelle.test(line)) {
units.push({ ensure({ code: 'LStab', name: 'Leitungsstab', type: 'stabsstelle', level: 1, parentId: 'DIR', color: '#6b7280' })
code: 'LStab', continue
name: 'Leitungsstab',
type: 'stabsstelle',
level: 1,
parentId: 'DIR',
color: '#6b7280'
})
} }
// Check for Sondereinheiten (non-hierarchical)
if (patterns.sondereinheit.test(line)) { if (patterns.sondereinheit.test(line)) {
let code = 'SE' let code = 'SE'
let name = line let name = line
if (/Personalrat/i.test(line)) { code = 'PR'; name = 'Personalrat' }
if (line.includes('Personalrat')) { else if (/Schwerbehindertenvertretung/i.test(line)) { code = 'SBV'; name = 'Schwerbehindertenvertretung' }
code = 'PR' else if (/Datenschutzbeauftrag/i.test(line)) { code = 'DSB'; name = 'Datenschutzbeauftragter' }
name = 'Personalrat' else if (/Gleichstellungsbeauftrag/i.test(line)) { code = 'GSB'; name = 'Gleichstellungsbeauftragte' }
} else if (line.includes('Schwerbehindertenvertretung')) { else if (/Innenrevision/i.test(line)) { code = 'IR'; name = 'Innenrevision' }
code = 'SBV' ensure({ code, name, type: 'sondereinheit', level: 1, parentId: null, color: '#059669' })
name = 'Schwerbehindertenvertretung' continue
} else if (line.includes('Datenschutzbeauftragter')) {
code = 'DSB'
name = 'Datenschutzbeauftragter'
} else if (line.includes('Gleichstellungsbeauftragte')) {
code = 'GSB'
name = 'Gleichstellungsbeauftragte'
} else if (line.includes('Innenrevision')) {
code = 'IR'
name = 'Innenrevision'
}
units.push({
code,
name,
type: 'sondereinheit',
level: 1,
parentId: null, // Non-hierarchical
color: '#059669'
})
} }
}) }
// Post-processing: infer missing parents
const has = (code: string) => byCode.has(code)
const get = (code: string) => byCode.get(code)
for (const u of units) {
if (u.type === 'dezernat' && !u.parentId) {
const inferred = inferAbteilungCode(String(u.code).replace(/^Dez\s+/i,''))
if (inferred) {
if (!has(inferred)) {
const abtNum = inferred.split(' ')[1]
ensure({ code: inferred, name: inferred.replace('Abt', 'Abteilung'), type: 'abteilung', level: 1, parentId: 'DIR', color: colors[abtNum] || '#6b7280', hasFuehrungsstelle: abtNum !== 'ZA' })
}
u.parentId = inferred
}
}
if ((u.type === 'sachgebiet' || u.type === 'teildezernat') && !u.parentId) {
const num = String(u.code).replace(/^(SG|TD)\s+/i,'')
const prefix = num.split('.')[0]
if (prefix) {
const dezCode = `Dez ${prefix}`
if (!has(dezCode)) {
ensure({ code: dezCode, name: `Dezernat ${prefix}`, type: 'dezernat', level: 2, parentId: inferAbteilungCode(prefix) })
}
u.parentId = dezCode
}
}
}
return units return units
} }
@ -229,7 +255,24 @@ router.post('/import-pdf',
} }
// Parse the organizational structure // Parse the organizational structure
const parsedUnits = parseOrganizationFromText(extractedText) let parsedUnits = parseOrganizationFromText(extractedText)
// Apply saved import rules (overrides)
try {
const rules = db.prepare('SELECT target_code, parent_code, type_override, name_override FROM organization_import_rules').all() as any[]
if (rules && rules.length) {
const byCode: Record<string, any> = {}
parsedUnits.forEach(u => { byCode[u.code] = u })
rules.forEach(r => {
const u = byCode[r.target_code]
if (u) {
if (r.parent_code) u.parentId = r.parent_code
if (r.type_override) u.type = r.type_override
if (r.name_override) u.name = r.name_override
}
})
}
} catch {}
if (parsedUnits.length === 0) { if (parsedUnits.length === 0) {
return res.status(400).json({ return res.status(400).json({
@ -238,29 +281,84 @@ router.post('/import-pdf',
}) })
} }
// Clear existing organization (optional - could be a parameter) // PREVIEW MODE: if requested, compute diff and return without writing
if (req.body.clearExisting === 'true') { const previewOnly = String((req.body as any).previewOnly ?? (req.query as any).previewOnly) === 'true'
const overridesRaw = (req.body as any).overrides ?? (req.query as any).overrides
let overrides: Array<{ code: string; parentCode?: string; type?: string; name?: string }> = []
try {
if (overridesRaw) overrides = typeof overridesRaw === 'string' ? JSON.parse(overridesRaw) : overridesRaw
} catch {}
if (overrides.length) {
const byCode: Record<string, any> = {}
parsedUnits.forEach(u => { byCode[u.code] = u })
overrides.forEach(o => {
const u = byCode[o.code]
if (u) {
if (o.parentCode) u.parentId = o.parentCode
if (o.type) u.type = o.type
if (o.name) u.name = o.name
}
})
}
const now = new Date().toISOString()
const existingMap = new Map<string, any>()
const existingRows = db.prepare(`SELECT id, code, name, type, level, parent_id as parentId FROM organizational_units`).all() as any[]
existingRows.forEach(r => existingMap.set(r.code, r))
// Diff classification
const orphans: any[] = []
const toCreate: any[] = []
const toUpdate: any[] = []
parsedUnits.forEach(u => {
const parentCode = u.parentId || null
const exists = existingMap.get(u.code)
if (!parentCode) {
orphans.push(u)
}
if (!exists) {
toCreate.push(u)
} else {
const diff = (exists.name !== u.name) || (exists.type !== u.type) || (exists.level !== u.level)
// Parent diff: compare codes by looking up parent id -> code
let existingParentCode: string | null = null
if (exists.parentId) {
const parentRow = db.prepare('SELECT code FROM organizational_units WHERE id = ?').get(exists.parentId) as any
existingParentCode = parentRow?.code || null
}
const parentDiff = existingParentCode !== parentCode
if (diff || parentDiff) {
toUpdate.push({ existing: exists, incoming: u })
}
}
})
if (previewOnly) {
return res.json({ success: true, data: {
message: 'Preview generated',
counts: { create: toCreate.length, update: toUpdate.length, orphans: orphans.length },
diff: { create: toCreate, update: toUpdate, orphans }
}})
}
// Clear existing organization (optional)
if (((req.body as any).clearExisting ?? (req.query as any).clearExisting) === 'true') {
db.prepare('DELETE FROM organizational_units').run() db.prepare('DELETE FROM organizational_units').run()
} }
// Prepare insert/update with stable parent references and FK-safe order // Prepare insert/update with stable parent references and FK-safe order
const now = new Date().toISOString()
const unitIdMap: Record<string, string> = {} const unitIdMap: Record<string, string> = {}
// Preload existing IDs by code
for (const unit of parsedUnits) { for (const unit of parsedUnits) {
const existing = db.prepare('SELECT id FROM organizational_units WHERE code = ?').get(unit.code) as any const existing = db.prepare('SELECT id FROM organizational_units WHERE code = ?').get(unit.code) as any
unitIdMap[unit.code] = existing?.id || uuidv4() unitIdMap[unit.code] = existing?.id || uuidv4()
} }
// Sort by level ascending so parents are processed first
const sorted = [...parsedUnits].sort((a, b) => (a.level ?? 0) - (b.level ?? 0)) const sorted = [...parsedUnits].sort((a, b) => (a.level ?? 0) - (b.level ?? 0))
const tx = db.transaction(() => { const tx = db.transaction(() => {
sorted.forEach((unit, index) => { sorted.forEach((unit, index) => {
const parentId = unit.parentId ? unitIdMap[unit.parentId] : null const parentId = unit.parentId ? unitIdMap[unit.parentId] : null
const existing = db.prepare('SELECT id FROM organizational_units WHERE code = ?').get(unit.code) as any const existing = db.prepare('SELECT id FROM organizational_units WHERE code = ?').get(unit.code) as any
if (existing) { if (existing) {
db.prepare(` db.prepare(`
UPDATE organizational_units UPDATE organizational_units
@ -303,8 +401,20 @@ router.post('/import-pdf',
} }
}) })
}) })
tx() tx()
// Remember rules (optional)
const rememberRules = String((req.body as any).rememberRules ?? (req.query as any).rememberRules) === 'true'
if (rememberRules && overrides.length) {
const upsert = db.prepare(`
INSERT INTO organization_import_rules (id, target_code, parent_code, type_override, name_override, created_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(target_code) DO UPDATE SET parent_code=excluded.parent_code, type_override=excluded.type_override, name_override=excluded.name_override
`)
overrides.forEach(o => {
upsert.run(uuidv4(), o.code, o.parentCode || null, o.type || null, o.name || null, now)
})
}
res.json({ res.json({
success: true, success: true,

339
organigramm_text.txt Normale Datei
Datei anzeigen

@ -0,0 +1,339 @@
Stand: 08/2025
Dezernat 62
Fahndungsgruppe Staatsschutz
Führungsgruppe
Fahndungsgruppen 1 - 8
SG 62.1
SG 62.2
SG 62.3
SG 62.4
SG 62.5
SG 62.6
SG 62.7
SG 62.8
SG 62.9 - Technische Gruppe
Dezernat 42
Cyber-Recherche- und Fahndungszentrum,
Ermittlungen Cybercrime
SG 42.1 - Personenorientierte Recherche in
Datennetzen
SG 42.2 - Sachorientierte Recherche in
Datennetzen
TD 42.3 Interventionsteams Digitale Tatorte
IUK-Lageunterstützung
Ermittlungskommissionen
Dezernat 33
Fahndung, Datenaustausch Polizei/Justiz,
Kriminalaktenhaltung, Internationale Rechtshilfe
SG 33.1 - Datenstation, Polizeiliche Beobachtung,
Grundsatz Fahndung, Fahndungsportal
SG 33.2 - Datenaustausch Polizei/Justiz,
Kriminalaktenhaltung
SG 33.3 - Rechtshilfe, PNR, internationale
Fahndung, Interpol- und Europolangelegenheiten,
Vermisste
Dezernat 32
Kriminalprävention, Kriminalistisch-
Kriminologische Forschungsstelle, Evaluation
SG 32.1 - Kriminalprävention und Opferschutz
TD 32.2 - Kriminalistisch-Kriminologische
Forschungsstelle (KKF)
SG 32.3 - Zentralstelle Evaluation (ZEVA)
Dezernat 31
Kriminalitätsauswertung und Analyse,
Polizeiliche Kriminalitätsstatistik
SG 31.1 - Grundsatzangelegenheiten / KoST
MAfEx / KoSt MOTIV/MIT/aMIT
SG 31.2 - Auswertung und Analyse 1, Eigentums-
und Vermögensdelikte, SÄM-ÜT, KoSt RTE
SG 31.3 - Auswertung/Analyse 2
Rauschgift-, Arzneimittel-, Menschenhandels-,
Schleusungskriminalität, Dokumentenfäschungen,
Gewaltdelikte
SG 31.4 Polizeiliche Kriminalstatistik (PKS)
Dezernat 64
Mobiles Einsatzkommando, Technische
Einsatzgruppe, Zielfahndung
FüGr
SG 64.1 - MEK 1
SG 64.2 - MEK 2
SG 64.3 - Technische Einsatzgruppe
SG 64.4 - Zielfahndung
Dezernat 63
Verdeckte Ermittlungen, Zeugenschutz
SG 63.1 - Einsatz VE 1
SG 63.2 - Einsatz VE 2
SG 63.3 - Scheinkäufer, Logistik
SG 63.4 - VP-Führung, Koordinierung
VP
TD 63.5 - Zeugenschutz, OpOS
Dezernat 34
Digitalstrategie, Polizeifachliche IT,
Landeszentrale Qualitätssicherung
TD 34.1 - Grundsatz , Gremien, Fachliche
IT-Projekte
TD 34.2 - Zentralstelle Polizei 2020, PIAV, QS
Verbundanwendungen, Europäisches
Informationssystem (EIS)
TD 34.3 - IT FaKo Fachbereich Kriminalität,
Zentralstelle ViVA, QS, INPOL-Z, ViVA-Büro LKA
Dezernat 51
Chemie, Physik
TD 51.1 - Schussspuren, Explosivstoffe, Brand,
Elektrotechnik
TD 51.2 - Betäubungsmittel
Dezernat 61
Kriminalitätsangelegenheiten der KPB,
Fachcontrolling, Koordination PUA,
Lagedienst
TD 61.1 - Kriminalitätsangelegenheiten
der KPB, Fachcontrolling, Koordination
PUA
SG 61.2 - Lagedienst
Dezernat 55
Waffen und Werkzeug
DNA-Analyse-Datei
SG 55.1 - Waffen und Munition
SG 55.2 - Werkzeug-, Form-, Passspuren,
Schließ- und Sicherungseinrichtungen
SG 55.3 - DNA-Analyse-Datei
Dezernat 52
Serologie, DNA-Analytik
TD 52.1 - DNA-Probenbearbeitung,
DNA-Spurenbearbeitung I
TD 52.2 DNA-Spurenbearbeitung II
TD 52.3 DNA-Spurenbearbeitung III
TD 52.4 - DNA-Spurenbearbeitung IV
DNA-Fremdvergabe
Dezernat 41
Zentrale Ansprechstelle Cybercrime, Grundsatz,
Digitale Forensik, IT-Entwicklung
SG 41.1 - Grundsatzangelegenheiten, Prävention,
Auswertung Cybercrime
TD 41.2 - Software und KI-Entwicklung,
IT-Verfahrensbetreuung, Marktschau
SG 41.3 -Forensik Desktop
Strategie und Entwicklung - Hinweisportal NRW
SG 41.4 Forensik Desktop
Datenaufbereitung und Automatisierung
TD 41.5 Forensik Desktop
Betrieb
Dezernat 53
Biologie, Materialspuren, Urkunden, Handschriften
TD 53.1 - forensische Textilkunde, Botanik,
Material-, Haar- und Erdspuren
SG 53.2 - Urkunden, Handschriften
Dezernat 54
Zentralstelle Kriminaltechnik, Forensische
Medientechnik,
USBV Entschärfung
SG 54.1 - Zentralstelle Kriminaltechnik
SG 54.2 -Tatortvermessung, Rekonstruktion und
XR-Lab, Phantombilderstellung
und visuelle Fahndungshilfe, Bild- und
Videotechnik
SG 54.3 - Entschärfung von unkonventionellen
Spreng- und Brandvorrichtungen
(USBV) – Einsatz- und Ermittlungsunterstützung
Explosivstoffe
Völklinger Straße 49, 40221 Düsseldorf
Telefon +49 211 939 - 0
Telefax +49 211 939 - 6599
E-Mail Poststelle.LKA@polizei.nrw.de
Internet https://lka.polizei.nrw.de
Dezernat 16
Finanzierung Organisierter Kriminalität und
Terrorismus
SG 16.1 - Grundsatzfragen/Auswertung/
Analyse
Ermittlungskommissionen
Dezernat 25
PMK Links, Ausländische Ideologie, ZSÜ
SG 25.1 - KoSt Gefährder Links, GETZ-Links
NRW, Landesvertreter GETZ-Links Bund,
Prüffallsachbearbeitung, Gefahrensachverhalte
SG 25.2 - KoSt Gefährder Ausländische Ideologie
(AI), Landesvertreter GETZ-AI Bund,
Prüffallsachbearbeitung, Gefahrensachverhalte
SG 25.3 - Zentrale Stelle NRW für ZSÜ
Dezernat 24
PMK Religiöse Ideologie
SG 24.1 - KoST Gefährder, SiKo
TD 24.2 - Gemeinsames
Terrorismusabwehrzentrum
(GTAZ) NRW, Landesvertreter GTAZ Bund,
ATD/RED
SG 24.3 - Prüffallbearbeitung,
Gefahrensachverhalte, islamistisch-terroristisches
Personenpotential
Dezernat 14
Auswerte- und Analysestelle OK
TD 14.1 - Operative Auswertung und Analyse,
kryptierte Täterkommunikation
SG 14.2 - Strategische Auswertung und Analyse,
Informationssteuerung
SG 14.3 - Technische Informationssysteme,
Unterstützungsgruppe CASE/DAR
SG 14.4 - Auswertung und Analyse
Rockerkriminalität
SG 14.5 Auswertung und Analyse Clankriminalität
Dezernat 15
Korruption, Umweltkriminalität
SG 15.1 - Grundsatzangelegenheiten, Korruption
SG 15.2 - Vernetzungsstelle Umweltkriminalität
Ermittlungskommissionen
Abteilung 1
Organisierte Kriminalität
Abteilung 2
Terrorismusbekämpfung und Staatsschutz
Dezernat 21
Ermittlungen
TD 21.1 - Ermittlungskommissionen VSTGB
Ermittlungskommissionen PMK (alle
Phänomenbereiche), VsnL
Dezernat 12
Wirtschaftskriminalität
SG 12.1 - Grundsatzfragen/Koordination/
Auswertung
Ermittlungskommissionen
Dezernat 22
Auswertung/Analyse, ZMI, Open Source
Intelligence (OSINT), Wissenschaftlicher Dienst
PMK, KPMD-PMK
SG 22.1 - Auswertung/Analyse, ZMI
TD 22.2 - Open Source Intelligence (OSINT)
TD 22.3 - Wissenschaftlicher
Dienst PMK
SG 22.4 - PMK Meldedienste, Kriminalpolizeilicher
Meldedienst (KPMD)
Dezernat 13
Finanzermittlungen
SG 13.1 - GFG 1
SG 13.2 - GFG 2
SG 13.3 - Verfahrensintegrierte
Finanzermittlungen/ Vermögensabschöpfung
SG 13.4 - Zentrale Informations- und
Koordinierungsstelle Finanzermittlung und
Gewinnabschöpfung,
Recherchestelle ZIVED
Ermittlungskommissionen
Dezernat 23
PMK Rechts und PMK SZ
SG 23.1 - KoSt Gefährder, Gemeinsames
Extremismus- und Terrorismusabwehrzentrum
(GETZ-) Rechts, Landesvertreter GETZ-Rechts
Bund
SG 23.2 - Prüffallbearbeitung,
Gefahrensachverhalte, Hasskriminalität, PMK
rechts
TD 23.3 PMK SZ, Spionage,
Gefahrensachverhalte PMK SZ, Landesvertreter
GETZ-SP
Schwerbehindertenvertretung
Zentralabteilung
Dezernat ZA 5
Polizeiärztlicher Dienst
Polizeiärztin
Direktor LKA NRW
Extremismusbeauftragter
Extremismusbeauftragter Vertreter
Datenschutzbeauftragter
Gleichstellungsbeauftragte
Inklusionsbeauftragter
Informationssicherheitsbeauftragter
Geheimschutzbeauftragte
Innenrevision
Fachkräfte für Arbeitssicherheit
Leitungsstab
SG LStab 1 - Grundsatzangelegenheiten, Gremien, internationale polizeiliche Zusammen-
arbeit, Informations- und Vorgangssteuerung, Einsatz/BAO
SG LStab 2 - Strategische Steuerung, Qualitätsmanagement, Controlling,
Wissenmanagement
SG LStab 3 - Presse-und Öffentlichkeitsarbeit
Personalrat
Abteilung 3
Strategische Kriminalitätsbekämpfung
Abteilung 6
Fachaufsicht und
Ermittlungsunterstützung
Abteilung 5
Kriminalwissenschaftliches und
-technisches Institut
Abteilung 4
Cybercrime (CCCC)
Dezernat 35
Verhaltensanalyse und Risikomanagement
SG 35.1 - Zentralstelle KURS NRW
SG 35.2 - Operative Fallanalyse (OFA/ViCLAS)
SG 35.3 - Zentralstelle PeRiskoP
Dezernat 56
Daktyloskopie, Gesichts- und Sprechererkennung,
Tonträgerauswertung
SG 56.1 - Daktyloskopisches Labor
SG 56.2 - AFIS I/Daktyloskopische Gutachten
SG 56.3 - AFIS II/Daktyloskopische Gutachten
TD 56.4 - Gesichts- und Sprechererkennung,
Tonträgerauswertung
Dezernat 43
Zentrale Auswertungs- und Sammelstelle (ZASt)
für die Bekämpfung von Missbrauchsabbildungen
von Kindern und Jugendlichen
SG 43.1 - ZASt Grundsatz,
Identifizierungsverfahren,
Bildvergleichssammlung, Schulfahndung,
Berichtswesen, Meldedienste/Verbundverfahren,
Gremien
SG 43.2 - ZASt NCMEC/ Landeszentrale
Bewertung 1
SG 43.3 - ZASt NCMEC/ Landeszentrale
Bewertung 2
SG 43.4 - ZASt NCMEC/ Landeszentrale
Bewertung 3
Dezernat 11
Ermittlungen OK, OK Rauschgift
SG 11.1 - Grundsatzfragen/ Koordination/
Auswertung
Ermittlungskommissionen
Dezernat ZA 2
Personalangelegenheiten, Gleichstellungs-
beauftragte, Fortbildung
SG ZA 2.1 - Personalentwicklung, Arbeitszeit,
BGM-POL; Stellenplan
SG ZA 2.2 - Personalverwaltung Beamte/
dienstrechtliche Angelegenheiten
TD ZA 2.3 - Personalverwaltung
Regierungsbeschäftigte/ Personalgewinnung, tarif-
und arbeitsrechtliche Angelegenheiten
SG ZA 2.4 - Fortbildung
Dezernat ZA 4
Vereins- und Wafffenrecht, Innenrevision,
Sponsoring, Rechtsangelegenheiten, Datenschutz,
Geheimschutz
Dezernat ZA 1
Haushalts-, Wirtschafts-,
Liegenschaftsmanagement, Zentrale
Vergabestelle
SG ZA 1.1 - Haushalts- und
Wirtschaftsangelegenheiten
SG ZA 1.2 - Liegenschaftsmanagement
SG ZA 1.3 - Zentrale Vergabestelle
Dezernat ZA 3
Informationstechnik und Anwenderunterstützung,
Informationssicherheit, Kfz-, Waffen- und
Geräteangelegenheiten;
SG ZA 3.1 - IT-Grundsatz, Planung und
Koordinierung, IT-Service Desk,
Lizenzmanagement
SG ZA 3.2 - Netzwerk/ TK-Anlage
SG ZA 3.3 - Server/Client, Logistik
SG ZA 3.4 - Kfz-, Waffen- und
Geräteangelegenheiten
Dezernat 44
Telekommunikatonsüberwachung (TKÜ)
SG 44.1 - Grundsatzaufgaben, operative TKÜ,
AIT
SG 44.2 - TKÜ Betrieb und Service
TD 44.3 - Digitale Forensik,
IUK-Ermittlungsunterstützung