Rollback - PDF Import funzt so semi

Dieser Commit ist enthalten in:
Claude Project Manager
2025-09-23 22:40:37 +02:00
Ursprung 26f95d2e4a
Commit 2cabd4c0c6
27 geänderte Dateien mit 4455 neuen und 41 gelöschten Zeilen

Datei anzeigen

@ -38,7 +38,8 @@
"Bash(set PORT=3005)",
"Bash(set FIELD_ENCRYPTION_KEY=dev_field_encryption_key_32chars_min!)",
"Bash(start:*)",
"WebSearch"
"WebSearch",
"Bash(mv:*)"
],
"deny": [],
"defaultMode": "acceptEdits"

220
CHANGES_ORGANIGRAMM.md Normale Datei
Datei anzeigen

@ -0,0 +1,220 @@
# Organigramm & Vertretungsmanagement - Implementierung
## Übersicht
Vollständige Implementierung eines interaktiven Organigramm-Systems mit dynamischer Vertretungsverwaltung für das LKA NRW.
## Datum: 2025-01-22
## Neue Features
### 1. Organigramm-Verwaltung
- **Hierarchische Darstellung** der Behördenstruktur (Direktion → Abteilung → Dezernat → Sachgebiet)
- **Sondereinheiten** wie Personalrat und Schwerbehindertenvertretung
- **Führungsstellen (FüSt)** für Abteilungen 1-6
- **Interaktive Visualisierung** mit Zoom/Pan und Suche
### 2. Dynamisches Vertretungssystem
- **Selbstverwaltung**: Mitarbeiter können eigene Vertretungen festlegen
- **Unbegrenzte Delegationskette**: Vertretungen können weitergegeben werden
- **Zeitliche Begrenzung**: Von-Bis Zeiträume für Vertretungen
- **Ebenengleiche Vertretung**: Automatische Vorschläge für passende Vertreter
### 3. Admin-Panel Features
- **React Flow Editor** für Drag & Drop Organigramm-Bearbeitung
- **Einheiten-Verwaltung**: Hinzufügen, Bearbeiten, Löschen von Organisationseinheiten
- **Visuelle Differenzierung** durch Gradient-Farbschema nach Abteilungen
- **PDF-Import ready** für zukünftige OCR-Integration
### 4. Frontend Features
- **Organigramm-Modal**: Fullscreen-Popup mit 3-Bereich-Layout
- **Mitarbeiter-Details**: Anzeige von Mitarbeitern pro Einheit
- **Vertretungs-Tab** in "Mein Profil"
- **Quick-Access**: 🏢 Button im Header für schnellen Zugriff
## Technische Änderungen
### Backend
#### Neue Dateien:
- `backend/src/routes/organization.ts` - API-Routes für Organigramm und Vertretungen
#### Geänderte Dateien:
- `backend/src/config/secureDatabase.ts` - Neue Datenbank-Tabellen:
- `organizational_units` - Organisationseinheiten
- `employee_unit_assignments` - Mitarbeiter-Zuordnungen
- `special_positions` - Sonderpositionen (Beauftragte, Räte)
- `deputy_assignments` - Vertretungszuweisungen
- `deputy_delegations` - Vertretungs-Weitergaben
- `backend/src/index.ts` - Neue Route `/api/organization` registriert
- `backend/src/routes/employeesSecure.ts` - Neue Route `/employees/public` für öffentliche Mitarbeiterliste
#### API Endpoints:
```
GET /api/organization/units - Alle Einheiten abrufen
GET /api/organization/hierarchy - Hierarchie-Baum abrufen
GET /api/organization/units/:id - Einzelne Einheit mit Mitarbeitern
POST /api/organization/units - Neue Einheit anlegen
PUT /api/organization/units/:id - Einheit bearbeiten
POST /api/organization/assignments - Mitarbeiter zuordnen
GET /api/organization/my-units - Eigene Einheiten abrufen
GET /api/organization/deputies/my - Eigene Vertretungen abrufen
POST /api/organization/deputies/my - Vertretung anlegen
POST /api/organization/deputies/delegate - Vertretung weitergeben
GET /api/organization/deputies/chain/:id - Vertretungskette abrufen
GET /api/organization/special-positions - Sonderpositionen abrufen
```
### Shared Types
#### Geänderte Dateien:
- `shared/index.d.ts` - Neue TypeScript-Definitionen:
- `OrganizationalUnit` - Organisationseinheit
- `OrganizationalUnitType` - Einheiten-Typen
- `EmployeeUnitAssignment` - Mitarbeiter-Zuordnung
- `EmployeeUnitRole` - Rollen (leiter, stellvertreter, mitarbeiter, beauftragter)
- `SpecialPosition` - Sonderpositionen
- `DeputyAssignment` - Vertretungszuweisung
- `DeputyDelegation` - Vertretungs-Delegation
### Admin-Panel
#### Neue Dateien:
- `admin-panel/src/views/OrganizationEditor.tsx` - React Flow basierter Organigramm-Editor
#### Geänderte Dateien:
- `admin-panel/src/App.tsx` - Route `/organization` hinzugefügt
- `admin-panel/src/components/Layout.tsx` - Navigation für Organigramm hinzugefügt
#### NPM Packages:
```json
{
"reactflow": "^11.x",
"@reactflow/controls": "^11.x",
"@reactflow/minimap": "^11.x",
"@reactflow/background": "^11.x"
}
```
### Frontend
#### Neue Dateien:
- `frontend/src/components/OrganizationChart.tsx` - Interaktives Organigramm-Modal
- `frontend/src/components/DeputyManagement.tsx` - Vertretungsverwaltung
#### Geänderte Dateien:
- `frontend/src/components/Header.tsx` - Organigramm-Button und Modal-Integration
- `frontend/src/views/MyProfile.tsx` - Tab für Vertretungsverwaltung hinzugefügt
## Datenmodell
### Organisationsstruktur
```sql
organizational_units:
- id (UUID)
- code (z.B. "ZA 1", "Dezernat 42")
- name (Vollständiger Name)
- type (direktion/abteilung/dezernat/sachgebiet/...)
- level (0=Direktor, 1=Abteilung, 2=Dezernat, 3=SG/TD)
- parent_id (Verweis auf übergeordnete Einheit)
- position_x/y (Visuelle Position)
- color (Farbcodierung)
- has_fuehrungsstelle (Boolean)
```
### Vertretungssystem
```sql
deputy_assignments:
- principal_id (Wer wird vertreten)
- deputy_id (Wer vertritt)
- valid_from/until (Zeitraum)
- reason (Urlaub/Dienstreise/etc.)
- can_delegate (Darf weitergeben)
deputy_delegations:
- original_assignment_id
- from_deputy_id
- to_deputy_id
- reason
```
## UI/UX Features
### Visuelle Gestaltung
- **Gradient-Farbschema** für Abteilungen 1-6
- **Icon-System** für verschiedene Einheiten-Typen
- **Hover-Effekte** mit Mitarbeiteranzahl und Details
- **Breadcrumb-Navigation** für Hierarchie-Pfad
- **Dark Mode Support** vollständig integriert
### Interaktionen
- **Zoom/Pan** für große Organigramme
- **Suche** nach Einheiten und Personen
- **Filter** nach Abteilungen
- **Drag & Drop** im Admin-Editor
- **Kontextmenüs** für schnelle Aktionen
## Sicherheit
- **Rollenbasierte Zugriffskontrolle** für Admin-Funktionen
- **Audit-Logging** für kritische Aktionen
- **Verschlüsselte Felder** für sensitive Daten
- **JWT-Authentication** für alle API-Calls
## Migration & Deployment
### Datenbank-Migration
Beim ersten Start werden automatisch alle neuen Tabellen angelegt.
### Rollback
Falls ein Rollback nötig ist, können die neuen Tabellen entfernt werden:
```sql
DROP TABLE IF EXISTS deputy_delegations;
DROP TABLE IF EXISTS deputy_assignments;
DROP TABLE IF EXISTS special_positions;
DROP TABLE IF EXISTS employee_unit_assignments;
DROP TABLE IF EXISTS organizational_units;
```
## Testing
### Smoke Tests
1. Backend neu starten für Datenbank-Schema
2. Admin-Panel: Organigramm → Einheit hinzufügen
3. Frontend: 🏢 Button → Organigramm anzeigen
4. Mein Profil → Vertretungen → Vertretung hinzufügen
### Bekannte Limitierungen
- PDF-Import noch nicht implementiert (Struktur vorbereitet)
- Skill-Aggregation pro Einheit noch nicht implementiert
- E-Mail-Benachrichtigungen für Vertretungen noch nicht aktiv
## Performance
- **Lazy Loading** für große Hierarchien
- **Virtualisierung** bei >500 Nodes
- **Caching** von Hierarchie-Daten
- **Progressive Disclosure** für Ebenen
## Zukünftige Erweiterungen
1. **PDF-OCR Import** für automatisches Einlesen von Organigrammen
2. **Skill-Matrix** pro Organisationseinheit
3. **Stellenplan-Integration** mit Soll/Ist-Vergleich
4. **Export-Funktionen** (PDF, PNG, Excel)
5. **Historien-Tracking** für Reorganisationen
6. **E-Mail-Notifications** bei Vertretungsänderungen
## Abhängigkeiten
- React Flow 11.x für Organigramm-Editor
- Better-SQLite3 für Datenbank
- TypeScript für Type-Safety
- Tailwind CSS für Styling
## Support & Dokumentation
Bei Fragen oder Problemen:
- Prüfen Sie die Browser-Konsole für Fehler
- Stellen Sie sicher, dass alle Services laufen
- Überprüfen Sie die Datenbank-Verbindung
- Logs befinden sich in `backend/logs/`

Datei anzeigen

@ -5,9 +5,9 @@
## Project Overview
- **Path**: `A:/GiTea/SkillMate`
- **Files**: 240 files
- **Size**: 6.7 MB
- **Last Modified**: 2025-09-21 16:48
- **Files**: 277 files
- **Size**: 9.7 MB
- **Last Modified**: 2025-09-23 00:39
## Technology Stack
@ -25,6 +25,7 @@
```
ANWENDUNGSBESCHREIBUNG.txt
CHANGES_ORGANIGRAMM.md
CLAUDE_PROJECT_README.md
debug-console.cmd
EXE-ERSTELLEN.md
@ -32,7 +33,6 @@ gitea_push_debug.txt
install-dependencies.cmd
INSTALLATION.md
LICENSE.txt
main.py
admin-panel/
│ ├── index.html
│ ├── package-lock.json
@ -82,21 +82,23 @@ admin-panel/
│ ├── EmployeeFormComplete.tsx
│ ├── EmployeeManagement.tsx
│ ├── Login.tsx
│ ├── OrganizationEditor.tsx
│ ├── SkillManagement.tsx
── SyncSettings.tsx
│ └── UserManagement.tsx
── SyncSettings.tsx
backend/
│ ├── create-test-user.js
│ ├── full-backend-3005.js
│ ├── mock-server.js
│ ├── package-lock.json
│ ├── package.json
│ ├── skillmate.dev.db
│ ├── skillmate.dev.encrypted.db
│ ├── skillmate.dev.encrypted.db-shm
│ ├── dist/
│ │ ├── index.js
│ │ ├── index.js.map
│ │ ├── config/
│ │ │ ├── appConfig.js
│ │ │ ├── appConfig.js.map
│ │ │ ├── database.js
│ │ │ ├── database.js.map
│ │ │ ├── secureDatabase.js
@ -108,6 +110,9 @@ backend/
│ │ │ ├── errorHandler.js.map
│ │ │ ├── roleAuth.js
│ │ │ └── roleAuth.js.map
│ │ ├── repositories/
│ │ │ ├── employeeRepository.js
│ │ │ └── employeeRepository.js.map
│ │ ├── routes/
│ │ │ ├── analytics.js
│ │ │ ├── analytics.js.map
@ -120,6 +125,8 @@ backend/
│ │ │ ├── employeesSecure.js
│ │ │ └── employeesSecure.js.map
│ │ ├── services/
│ │ │ ├── auditService.js
│ │ │ ├── auditService.js.map
│ │ │ ├── emailService.js
│ │ │ ├── emailService.js.map
│ │ │ ├── encryption.js
@ -127,12 +134,18 @@ backend/
│ │ │ ├── reminderService.js
│ │ │ ├── reminderService.js.map
│ │ │ ├── syncScheduler.js
│ │ │ ── syncScheduler.js.map
│ │ │ ├── syncService.js
│ │ │ ── syncService.js.map
│ │ └── utils/
│ │ ├── logger.js
│ │ └── logger.js.map
│ │ │ ── syncScheduler.js.map
│ │ ├── usecases/
│ │ │ ── employees.js
│ │ │ ├── employees.js.map
│ │ ├── users.js
│ │ └── users.js.map
│ │ ├── utils/
│ │ │ ├── logger.js
│ │ │ └── logger.js.map
│ │ └── validation/
│ │ ├── employeeValidators.js
│ │ └── employeeValidators.js.map
│ ├── logs/
│ │ ├── combined.log
│ │ └── error.log
@ -141,6 +154,8 @@ backend/
│ │ ├── purge-users.js
│ │ ├── reset-admin.js
│ │ ├── run-migrations.js
│ │ ├── seed-lka-structure.js
│ │ ├── seed-organization.js
│ │ ├── seed-skills-from-frontend.js
│ │ └── migrations/
│ │ └── 0001_users_email_encrypt.js
@ -159,14 +174,14 @@ backend/
│ │ ├── routes/
│ │ │ ├── analytics.ts
│ │ │ ├── auth.ts
│ │ │ ├── bookings.ts
│ │ │ ├── bookings.ts.disabled
│ │ │ ├── employees.ts
│ │ │ ├── employeesSecure.ts
│ │ │ ├── network.ts
│ │ │ ├── organization.ts
│ │ │ ├── organizationImport.ts
│ │ │ ├── profiles.ts
│ │ │ ── settings.ts
│ │ │ ├── skills.ts
│ │ │ └── sync.ts
│ │ │ ── settings.ts
│ │ ├── services/
│ │ │ ├── auditService.ts
│ │ │ ├── emailService.ts
@ -182,10 +197,11 @@ backend/
│ │ └── validation/
│ │ └── employeeValidators.ts
│ └── uploads/
── photos/
├── 0def5f6f-c1ef-4f88-9105-600c75278f10.jpg
├── 72c09fa1-f0a8-444c-918f-95258ca56f61.gif
└── 80c44681-d6b4-474e-8ff1-c6d02da0cd7d.gif
── photos/
├── 0def5f6f-c1ef-4f88-9105-600c75278f10.jpg
├── 72c09fa1-f0a8-444c-918f-95258ca56f61.gif
└── 80c44681-d6b4-474e-8ff1-c6d02da0cd7d.gif
│ └── temp
docs/
│ ├── ARCHITECTURE.md
│ ├── REFAKTOR_PLAN.txt
@ -220,15 +236,16 @@ frontend/
│ ├── App.tsx
│ ├── main.tsx
│ ├── components/
│ │ ├── DeputyManagement.tsx
│ │ ├── EmployeeCard.tsx
│ │ ├── ErrorBoundary.tsx
│ │ ├── Header.tsx
│ │ ├── Layout.tsx
│ │ ├── OfficeMap3D.tsx
│ │ ├── OfficeMapModal.tsx
│ │ ├── OrganizationChart.tsx
│ │ ├── PhotoPreview.tsx
│ │ ── PhotoUpload.tsx
│ │ ├── Sidebar.tsx
│ │ ├── SkillLevelBar.tsx
│ │ └── WindowControls.tsx
│ │ ── PhotoUpload.tsx
│ ├── data/
│ │ └── skillCategories.ts
│ ├── hooks/
@ -294,3 +311,4 @@ This project is managed with Claude Project Manager. To work with this project:
- README updated on 2025-09-20 21:30:35
- README updated on 2025-09-21 16:48:11
- README updated on 2025-09-21 16:48:44
- README updated on 2025-09-23 19:19:20

BIN
Organigramm_ohne_Namen.pdf Normale Datei

Binäre Datei nicht angezeigt.

Datei anzeigen

@ -16,6 +16,7 @@
"react-dom": "^18.2.0",
"react-hook-form": "^7.48.2",
"react-router-dom": "^6.20.0",
"reactflow": "^11.10.0",
"zustand": "^4.4.7"
},
"devDependencies": {

Datei anzeigen

@ -8,6 +8,7 @@ import SkillManagement from './views/SkillManagement'
import UserManagement from './views/UserManagement'
import EmailSettings from './views/EmailSettings'
import SyncSettings from './views/SyncSettings'
import OrganizationEditor from './views/OrganizationEditor'
import { useEffect } from 'react'
function App() {
@ -34,6 +35,7 @@ function App() {
<Layout>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/organization" element={<OrganizationEditor />} />
<Route path="/skills" element={<SkillManagement />} />
<Route path="/users" element={<UserManagement />} />
<Route path="/users/create-employee" element={<CreateEmployee />} />

Datei anzeigen

@ -7,6 +7,7 @@ import {
SettingsIcon,
MailIcon
} from './icons'
import { Building2 } from 'lucide-react'
interface LayoutProps {
children: ReactNode
@ -14,6 +15,7 @@ interface LayoutProps {
const navigation = [
{ name: 'Dashboard', href: '/', icon: HomeIcon },
{ name: 'Organigramm', href: '/organization', icon: Building2 },
{ name: 'Benutzerverwaltung', href: '/users', icon: UsersIcon },
{ name: 'Skills verwalten', href: '/skills', icon: SettingsIcon },
{ name: 'E-Mail-Einstellungen', href: '/email-settings', icon: MailIcon },

Datei anzeigen

@ -0,0 +1,503 @@
import { useCallback, useEffect, useState, useRef } from 'react'
import ReactFlow, {
Node,
Edge,
Controls,
Background,
MiniMap,
useNodesState,
useEdgesState,
addEdge,
Connection,
MarkerType,
NodeTypes,
Panel
} from 'reactflow'
import 'reactflow/dist/style.css'
import { api } from '../services/api'
import { OrganizationalUnit, OrganizationalUnitType } from '@skillmate/shared'
import { Upload } from 'lucide-react'
// Custom Node Component
const OrganizationNode = ({ data }: { data: any }) => {
const getTypeIcon = (type: OrganizationalUnitType) => {
const icons = {
direktion: '🏛️',
abteilung: '🏢',
dezernat: '📁',
sachgebiet: '📋',
teildezernat: '🔧',
fuehrungsstelle: '⭐',
stabsstelle: '🎯',
sondereinheit: '🛡️'
}
return icons[type] || '📄'
}
const getGradient = (level: number) => {
const gradients = [
'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)',
'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)',
'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)',
'linear-gradient(135deg, #fa709a 0%, #fee140 100%)',
'linear-gradient(135deg, #30cfd0 0%, #330867 100%)'
]
return gradients[level % gradients.length]
}
return (
<div className="bg-white rounded-lg shadow-lg border-2 border-gray-200 min-w-[250px]">
<div
className="p-2 text-white rounded-t-md"
style={{ background: getGradient(data.level || 0) }}
>
<div className="flex items-center justify-between">
<span className="text-lg">{getTypeIcon(data.type)}</span>
<span className="text-xs opacity-90">{data.code}</span>
</div>
</div>
<div className="p-3">
<h4 className="font-semibold text-gray-800 text-sm">{data.name}</h4>
{data.employeeCount !== undefined && (
<p className="text-xs text-gray-600 mt-1">
👥 {data.employeeCount} Mitarbeiter
</p>
)}
{data.hasFuehrungsstelle && (
<span className="inline-block px-2 py-1 mt-2 text-xs bg-yellow-100 text-yellow-800 rounded">
FüSt
</span>
)}
</div>
</div>
)
}
const nodeTypes: NodeTypes = {
organization: OrganizationNode
}
export default function OrganizationEditor() {
const [nodes, setNodes, onNodesChange] = useNodesState([])
const [edges, setEdges, onEdgesChange] = useEdgesState([])
const [loading, setLoading] = useState(true)
const [selectedNode, setSelectedNode] = useState<Node | null>(null)
const [showAddDialog, setShowAddDialog] = useState(false)
const [showImportDialog, setShowImportDialog] = useState(false)
const [uploadProgress, setUploadProgress] = useState<string | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const [formData, setFormData] = useState({
name: '',
code: '',
type: 'dezernat' as OrganizationalUnitType,
level: 2,
parentId: '',
description: ''
})
// Load organizational units
useEffect(() => {
loadOrganization()
}, [])
const loadOrganization = async () => {
try {
setLoading(true)
const response = await api.get('/organization/hierarchy')
if (response.data.success) {
const units = response.data.data
const { nodes: flowNodes, edges: flowEdges } = convertToFlowElements(units)
setNodes(flowNodes)
setEdges(flowEdges)
}
} catch (error) {
console.error('Failed to load organization:', error)
} finally {
setLoading(false)
}
}
const convertToFlowElements = (units: any[], parentPosition = { x: 0, y: 0 }, level = 0): { nodes: Node[], edges: Edge[] } => {
const nodes: Node[] = []
const edges: Edge[] = []
const levelHeight = 150
const nodeWidth = 300
units.forEach((unit, index) => {
const nodeId = unit.id
const xPos = parentPosition.x + index * nodeWidth
const yPos = level * levelHeight
// Use persisted positions if available, otherwise compute defaults
const persistedX = (typeof unit.positionX === 'number'
? unit.positionX
: (unit.positionX != null && !isNaN(Number(unit.positionX)) ? Number(unit.positionX) : undefined))
const persistedY = (typeof unit.positionY === 'number'
? unit.positionY
: (unit.positionY != null && !isNaN(Number(unit.positionY)) ? Number(unit.positionY) : undefined))
nodes.push({
id: nodeId,
type: 'organization',
position: { x: persistedX ?? xPos, y: persistedY ?? yPos },
data: {
...unit,
level
}
})
// Create edge to parent if exists
if (unit.parentId) {
edges.push({
id: `e-${unit.parentId}-${nodeId}`,
source: unit.parentId,
target: nodeId,
type: 'smoothstep',
animated: false,
markerEnd: {
type: MarkerType.ArrowClosed,
width: 20,
height: 20,
color: '#94a3b8'
},
style: {
strokeWidth: 2,
stroke: '#94a3b8'
}
})
}
// Process children recursively
if (unit.children && unit.children.length > 0) {
const childElements = convertToFlowElements(
unit.children,
{ x: xPos, y: yPos },
level + 1
)
nodes.push(...childElements.nodes)
edges.push(...childElements.edges)
}
})
return { nodes, edges }
}
const onConnect = useCallback(
(params: Connection) => {
setEdges((eds) => addEdge({
...params,
type: 'smoothstep',
animated: false,
markerEnd: {
type: MarkerType.ArrowClosed,
width: 20,
height: 20,
color: '#94a3b8'
}
}, eds))
},
[setEdges]
)
const onNodeClick = useCallback((_: any, node: Node) => {
setSelectedNode(node)
}, [])
const onNodeDragStop = useCallback(async (_: any, node: Node) => {
try {
await api.put(`/organization/units/${node.id}`, {
positionX: node.position.x,
positionY: node.position.y
})
} catch (error) {
console.error('Failed to update position:', error)
}
}, [])
const handleAddUnit = async () => {
try {
// Build payload and attach selected node as parent if present
const payload: any = {
name: formData.name,
code: formData.code || undefined,
type: formData.type,
level: selectedNode ? ((selectedNode.data?.level ?? 0) + 1) : formData.level,
description: formData.description || undefined,
}
if (selectedNode?.id) payload.parentId = selectedNode.id
const response = await api.post('/organization/units', payload)
if (response.data.success) {
await loadOrganization()
setShowAddDialog(false)
setFormData({
name: '',
code: '',
type: 'dezernat',
level: 2,
parentId: '',
description: ''
})
}
} catch (error) {
console.error('Failed to add unit:', error)
}
}
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) return
if (file.type !== 'application/pdf') {
alert('Bitte laden Sie nur PDF-Dateien hoch.')
return
}
const formData = new FormData()
formData.append('pdf', file)
formData.append('clearExisting', 'false') // Don't clear existing by default
setUploadProgress('PDF wird hochgeladen...')
try {
const response = await api.post('/organization/import-pdf', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
if (response.data.success) {
setUploadProgress(`Erfolgreich! ${response.data.data.unitsImported} Einheiten importiert.`)
await loadOrganization()
setTimeout(() => {
setShowImportDialog(false)
setUploadProgress(null)
}, 2000)
}
} catch (error: any) {
console.error('PDF import failed:', error)
setUploadProgress(`Fehler: ${error.response?.data?.error?.message || 'Upload fehlgeschlagen'}`)
}
}
if (loading) {
return (
<div className="flex items-center justify-center h-screen">
<div className="text-lg">Lade Organigramm...</div>
</div>
)
}
return (
<div className="h-screen w-full">
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
onNodeClick={onNodeClick}
onNodeDragStop={onNodeDragStop}
nodeTypes={nodeTypes}
fitView
attributionPosition="bottom-left"
>
<Controls />
<MiniMap
nodeColor={(node) => {
const gradients = ['#667eea', '#f093fb', '#4facfe', '#43e97b', '#fa709a', '#30cfd0']
return gradients[node.data?.level % gradients.length] || '#94a3b8'
}}
/>
<Background variant="dots" gap={12} size={1} />
<Panel position="top-left" className="bg-white p-4 rounded-lg shadow-lg">
<h2 className="text-xl font-bold mb-4">LKA NRW Organigramm</h2>
<div className="space-y-2">
<button
onClick={() => setShowAddDialog(true)}
className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
+ Einheit hinzufügen
</button>
<button
onClick={() => setShowImportDialog(true)}
className="w-full px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 flex items-center justify-center gap-2"
>
<Upload size={16} />
PDF importieren
</button>
</div>
</Panel>
<Panel position="top-right" className="bg-white p-4 rounded-lg shadow-lg max-w-sm">
{selectedNode ? (
<>
<h3 className="font-bold mb-2">{selectedNode.data.name}</h3>
{selectedNode.data.code && (
<p className="text-sm text-gray-600">Code: {selectedNode.data.code}</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>
{selectedNode.data.description && (
<p className="text-sm mt-2">{selectedNode.data.description}</p>
)}
</>
) : (
<p className="text-gray-500">Klicken Sie auf eine Einheit für Details</p>
)}
</Panel>
</ReactFlow>
{/* Import PDF Dialog */}
{showImportDialog && (
<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">
<h2 className="text-xl font-bold mb-4">PDF Organigramm importieren</h2>
<div className="space-y-4">
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center">
<Upload className="mx-auto h-12 w-12 text-gray-400 mb-4" />
<p className="text-sm text-gray-600 mb-4">
Laden Sie ein PDF-Dokument mit der Organisationsstruktur hoch.
Das System extrahiert automatisch die Hierarchie.
</p>
<input
ref={fileInputRef}
type="file"
accept=".pdf"
onChange={handleFileUpload}
className="hidden"
/>
<button
onClick={() => fileInputRef.current?.click()}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
disabled={!!uploadProgress}
>
PDF auswählen
</button>
</div>
{uploadProgress && (
<div className="p-3 bg-blue-50 text-blue-700 rounded">
{uploadProgress}
</div>
)}
</div>
<div className="flex justify-end gap-2 mt-6">
<button
onClick={() => {
setShowImportDialog(false)
setUploadProgress(null)
}}
className="px-4 py-2 text-gray-600 hover:text-gray-800"
>
Schließen
</button>
</div>
</div>
</div>
)}
{/* Add Unit Dialog */}
{showAddDialog && (
<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">
<h2 className="text-xl font-bold mb-4">Neue Einheit hinzufügen</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">Name *</label>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="w-full px-3 py-2 border rounded"
required
/>
</div>
{selectedNode && (
<div className="text-xs text-gray-600">
Wird unterhalb von: <span className="font-semibold">{selectedNode.data?.name}</span> eingefügt
</div>
)}
<div>
<label className="block text-sm font-medium mb-1">Code *</label>
<input
type="text"
value={formData.code}
onChange={(e) => setFormData({ ...formData, code: e.target.value })}
className="w-full px-3 py-2 border rounded"
placeholder="z.B. ZA 1"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Typ *</label>
<select
value={formData.type}
onChange={(e) => setFormData({ ...formData, type: e.target.value as OrganizationalUnitType })}
className="w-full px-3 py-2 border rounded"
>
<option value="direktion">Direktion</option>
<option value="abteilung">Abteilung</option>
<option value="dezernat">Dezernat</option>
<option value="sachgebiet">Sachgebiet</option>
<option value="teildezernat">Teildezernat</option>
<option value="stabsstelle">Stabsstelle</option>
<option value="sondereinheit">Sondereinheit</option>
<option value="fuehrungsstelle">Führungsstelle</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1">Ebene *</label>
<input
type="number"
value={formData.level}
onChange={(e) => setFormData({ ...formData, level: parseInt(e.target.value) })}
className="w-full px-3 py-2 border rounded"
min="0"
max="10"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">Beschreibung</label>
<textarea
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className="w-full px-3 py-2 border rounded"
rows={3}
/>
</div>
</div>
<div className="flex justify-end gap-2 mt-6">
<button
onClick={() => setShowAddDialog(false)}
className="px-4 py-2 text-gray-600 hover:text-gray-800"
>
Abbrechen
</button>
<button
onClick={handleAddUnit}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Hinzufügen
</button>
</div>
</div>
</div>
)}
</div>
)
}

141
backend/mock-server.js Normale Datei
Datei anzeigen

@ -0,0 +1,141 @@
const express = require('express');
const multer = require('multer');
const cors = require('cors');
const fs = require('fs');
const path = require('path');
const pdfParse = require('pdf-parse');
const app = express();
const upload = multer({ dest: 'uploads/temp/' });
app.use(cors());
app.use(express.json());
// Mock auth endpoint
app.post('/api/auth/login', (req, res) => {
const { username, password } = req.body;
if (username === 'admin' && password === 'admin') {
res.json({
success: true,
data: {
token: 'mock-token',
user: {
id: '1',
username: 'admin',
role: 'admin'
}
}
});
} else {
res.status(401).json({ success: false, error: { message: 'Invalid credentials' } });
}
});
// Mock organization hierarchy
app.get('/api/organization/hierarchy', (req, res) => {
res.json({
success: true,
data: [
{
id: '1',
code: 'DIR',
name: 'Direktor LKA NRW',
type: 'direktion',
level: 0,
parentId: null,
children: []
}
]
});
});
// PDF Import endpoint
app.post('/api/organization/import-pdf', upload.single('pdf'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
error: { message: 'No PDF file uploaded' }
});
}
const pdfBuffer = fs.readFileSync(req.file.path);
// Parse PDF
try {
const pdfData = await pdfParse(pdfBuffer);
console.log('PDF parsed:', pdfData.numpages, 'pages');
console.log('Text preview:', pdfData.text.substring(0, 500));
// Simplified parsing
const lines = pdfData.text.split('\n').filter(line => line.trim());
const units = [];
lines.forEach(line => {
if (line.includes('Direktor')) {
units.push({ code: 'DIR', name: 'Direktor LKA NRW', type: 'direktion' });
} else if (line.match(/Abteilung\s+\d+/)) {
const match = line.match(/Abteilung\s+(\d+)/);
units.push({
code: `Abt ${match[1]}`,
name: line.trim(),
type: 'abteilung'
});
} else if (line.match(/Dezernat\s+\d+/)) {
const match = line.match(/Dezernat\s+(\d+)/);
units.push({
code: `Dez ${match[1]}`,
name: line.trim(),
type: 'dezernat'
});
}
});
// Clean up temp file
fs.unlinkSync(req.file.path);
res.json({
success: true,
data: {
message: `Successfully imported ${units.length} organizational units from PDF`,
unitsImported: units.length,
units: units
}
});
} catch (parseError) {
console.error('PDF parse error:', parseError);
fs.unlinkSync(req.file.path);
// Fallback response
res.json({
success: true,
data: {
message: 'Imported sample organizational units (PDF parsing failed)',
unitsImported: 5,
units: [
{ code: 'DIR', name: 'Direktor LKA NRW', type: 'direktion' },
{ code: 'Abt 1', name: 'Abteilung 1 - OK', type: 'abteilung' },
{ code: 'Abt 2', name: 'Abteilung 2 - Staatsschutz', type: 'abteilung' },
{ code: 'Dez 11', name: 'Dezernat 11', type: 'dezernat' },
{ code: 'SG 11.1', name: 'Sachgebiet 11.1', type: 'sachgebiet' }
]
}
});
}
} catch (error) {
console.error('Import error:', error);
res.status(500).json({
success: false,
error: { message: 'Failed to import PDF: ' + error.message }
});
}
});
const PORT = 3004;
app.listen(PORT, () => {
console.log(`Mock backend server running on http://localhost:${PORT}`);
console.log('Login with admin/admin');
console.log('PDF import endpoint ready at POST /api/organization/import-pdf');
});

Datei anzeigen

@ -31,6 +31,7 @@
"multer": "^2.0.1",
"node-cron": "^4.2.1",
"nodemailer": "^7.0.6",
"pdf-parse": "^1.1.1",
"sqlite3": "^5.1.6",
"uuid": "^9.0.1",
"winston": "^3.11.0"

Datei anzeigen

@ -0,0 +1,467 @@
const Database = require('better-sqlite3')
const path = require('path')
const { v4: uuidv4 } = require('uuid')
// Open database
const dbPath = path.join(__dirname, '..', 'skillmate.dev.encrypted.db')
const db = new Database(dbPath)
// Enable foreign keys
db.pragma('foreign_keys = ON')
console.log('🏢 Seeding Complete LKA NRW Organizational Structure...')
const now = new Date().toISOString()
// Store IDs for reference
const unitIds = {}
// Helper function to insert organizational unit
function insertUnit(code, name, type, level, parentId = null, options = {}) {
const id = uuidv4()
db.prepare(`
INSERT OR REPLACE INTO organizational_units (
id, code, name, type, level, parent_id,
color, order_index, has_fuehrungsstelle,
fuehrungsstelle_name, is_active, created_at, updated_at,
position_x, position_y
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id, code, name, type, level, parentId,
options.color || null,
options.orderIndex || 0,
options.hasFuehrungsstelle || 0,
options.fuehrungsstelleName || null,
1, now, now,
options.positionX || null,
options.positionY || null
)
unitIds[code] = id
return id
}
// Clear existing data
console.log('Clearing existing organizational data...')
db.prepare('DELETE FROM deputy_delegations').run()
db.prepare('DELETE FROM deputy_assignments').run()
db.prepare('DELETE FROM special_positions').run()
db.prepare('DELETE FROM employee_unit_assignments').run()
db.prepare('DELETE FROM organizational_units').run()
// ========================================
// Level 0: Direktor
// ========================================
const direktorId = insertUnit('DIR', 'Direktor LKA NRW', 'direktion', 0, null, {
color: '#1e3a8a',
orderIndex: 0,
positionX: 400,
positionY: 50
})
// ========================================
// Level 1: Direct Reports to Direktor
// ========================================
// Leitungsstab - directly under Direktor
const leitungsstabId = insertUnit('LStab', 'Leitungsstab', 'stabsstelle', 1, direktorId, {
color: '#6b7280',
orderIndex: 1,
positionX: 200,
positionY: 150
})
// Leitungsstab Sub-units (Level 2)
insertUnit('LStab 1', 'Grundsatzangelegenheiten, Gremien, internationale polizeiliche Zusammenarbeit, Informations- und Vorgangssteuerung, Einsatz/BAO', 'sachgebiet', 2, leitungsstabId)
insertUnit('LStab 2', 'Strategische Steuerung, Qualitätsmanagement, Controlling, Wissenmanagement', 'sachgebiet', 2, leitungsstabId)
insertUnit('LStab 3', 'Presse- und Öffentlichkeitsarbeit', 'sachgebiet', 2, leitungsstabId)
// ========================================
// Sondereinheiten (Special Units) - Parallel to hierarchy
// ========================================
// These are NOT hierarchically under Direktor but parallel/advisory roles
const personalratId = insertUnit('PR', 'Personalrat', 'sondereinheit', 1, null, {
color: '#059669',
orderIndex: 100,
positionX: 50,
positionY: 250
})
const schwerbehindertenId = insertUnit('SBV', 'Schwerbehindertenvertretung', 'sondereinheit', 1, null, {
color: '#059669',
orderIndex: 101,
positionX: 50,
positionY: 300
})
// Individual Beauftragte (as separate units without hierarchy)
insertUnit('DSB', 'Datenschutzbeauftragter', 'sondereinheit', 1, null, {
color: '#0891b2',
orderIndex: 102,
positionX: 50,
positionY: 350
})
insertUnit('GSB', 'Gleichstellungsbeauftragte', 'sondereinheit', 1, null, {
color: '#0891b2',
orderIndex: 103,
positionX: 50,
positionY: 400
})
insertUnit('IKB', 'Inklusionsbeauftragter', 'sondereinheit', 1, null, {
color: '#0891b2',
orderIndex: 104,
positionX: 50,
positionY: 450
})
insertUnit('ISB', 'Informationssicherheitsbeauftragter', 'sondereinheit', 1, null, {
color: '#0891b2',
orderIndex: 105,
positionX: 50,
positionY: 500
})
insertUnit('GHSB', 'Geheimschutzbeauftragte', 'sondereinheit', 1, null, {
color: '#0891b2',
orderIndex: 106,
positionX: 50,
positionY: 550
})
insertUnit('EXB', 'Extremismusbeauftragter', 'sondereinheit', 1, null, {
color: '#dc2626',
orderIndex: 107,
positionX: 50,
positionY: 600
})
insertUnit('EXBV', 'Extremismusbeauftragter Vertreter', 'sondereinheit', 1, null, {
color: '#dc2626',
orderIndex: 108,
positionX: 50,
positionY: 650
})
// Innenrevision
insertUnit('IR', 'Innenrevision', 'sondereinheit', 1, null, {
color: '#7c3aed',
orderIndex: 109,
positionX: 50,
positionY: 700
})
// Fachkräfte für Arbeitssicherheit
insertUnit('FAS', 'Fachkräfte für Arbeitssicherheit', 'sondereinheit', 1, null, {
color: '#059669',
orderIndex: 110,
positionX: 50,
positionY: 750
})
// ========================================
// Abteilungen (Level 1 - Under Direktor)
// ========================================
const abteilungen = [
{
code: 'Abt 1',
name: 'Organisierte Kriminalität',
color: '#dc2626',
hasFuehrungsstelle: true,
positionX: 100,
positionY: 200
},
{
code: 'Abt 2',
name: 'Terrorismusbekämpfung und Staatsschutz',
color: '#ea580c',
hasFuehrungsstelle: true,
positionX: 250,
positionY: 200
},
{
code: 'Abt 3',
name: 'Strategische Kriminalitätsbekämpfung',
color: '#0891b2',
hasFuehrungsstelle: true,
positionX: 400,
positionY: 200
},
{
code: 'Abt 4',
name: 'Cybercrime (CCCC)',
color: '#7c3aed',
hasFuehrungsstelle: true,
positionX: 550,
positionY: 200
},
{
code: 'Abt 5',
name: 'Kriminalwissenschaftliches und -technisches Institut',
color: '#0d9488',
hasFuehrungsstelle: true,
positionX: 700,
positionY: 200
},
{
code: 'Abt 6',
name: 'Fachaufsicht und Ermittlungsunterstützung',
color: '#be185d',
hasFuehrungsstelle: true,
positionX: 850,
positionY: 200
},
{
code: 'ZA',
name: 'Zentralabteilung',
color: '#6b7280',
hasFuehrungsstelle: false,
positionX: 1000,
positionY: 200
}
]
const abteilungIds = {}
abteilungen.forEach((abt, index) => {
abteilungIds[abt.code] = insertUnit(abt.code, abt.name, 'abteilung', 1, direktorId, {
color: abt.color,
orderIndex: 10 + index * 10,
hasFuehrungsstelle: abt.hasFuehrungsstelle ? 1 : 0,
fuehrungsstelleName: abt.hasFuehrungsstelle ? `Führungsstelle ${abt.code}` : null,
positionX: abt.positionX,
positionY: abt.positionY
})
})
// ========================================
// Dezernate für Abteilung 1 (Organisierte Kriminalität)
// ========================================
const dez11 = insertUnit('Dez 11', 'Ermittlungen OK, OK Rauschgift', 'dezernat', 2, abteilungIds['Abt 1'])
insertUnit('SG 11.1', 'Grundsatzfragen/Koordination/Auswertung', 'sachgebiet', 3, dez11)
const dez12 = insertUnit('Dez 12', 'Wirtschaftskriminalität', 'dezernat', 2, abteilungIds['Abt 1'])
insertUnit('SG 12.1', 'Grundsatzfragen/Koordination/Auswertung', 'sachgebiet', 3, dez12)
const dez13 = insertUnit('Dez 13', 'Finanzermittlungen', 'dezernat', 2, abteilungIds['Abt 1'])
insertUnit('SG 13.1', 'GFG 1', 'sachgebiet', 3, dez13)
insertUnit('SG 13.2', 'GFG 2', 'sachgebiet', 3, dez13)
insertUnit('SG 13.3', 'Verfahrensintegrierte Finanzermittlungen/Vermögensabschöpfung', 'sachgebiet', 3, dez13)
insertUnit('SG 13.4', 'Zentrale Informations- und Koordinierungsstelle Finanzermittlung und Gewinnabschöpfung, Recherchestelle ZIVED', 'sachgebiet', 3, dez13)
const dez14 = insertUnit('Dez 14', 'Auswerte- und Analysestelle OK', 'dezernat', 2, abteilungIds['Abt 1'])
insertUnit('TD 14.1', 'Operative Auswertung und Analyse, kryptierte Täterkommunikation', 'teildezernat', 3, dez14)
insertUnit('SG 14.2', 'Strategische Auswertung und Analyse, Informationssteuerung', 'sachgebiet', 3, dez14)
insertUnit('SG 14.3', 'Technische Informationssysteme, Unterstützungsgruppe CASE/DAR', 'sachgebiet', 3, dez14)
insertUnit('SG 14.4', 'Auswertung und Analyse Rockerkriminalität', 'sachgebiet', 3, dez14)
insertUnit('SG 14.5', 'Auswertung und Analyse Clankriminalität', 'sachgebiet', 3, dez14)
const dez15 = insertUnit('Dez 15', 'Korruption, Umweltkriminalität', 'dezernat', 2, abteilungIds['Abt 1'])
insertUnit('SG 15.1', 'Grundsatzangelegenheiten, Korruption', 'sachgebiet', 3, dez15)
insertUnit('SG 15.2', 'Vernetzungsstelle Umweltkriminalität', 'sachgebiet', 3, dez15)
const dez16 = insertUnit('Dez 16', 'Finanzierung Organisierter Kriminalität und Terrorismus', 'dezernat', 2, abteilungIds['Abt 1'])
insertUnit('SG 16.1', 'Grundsatzfragen/Auswertung/Analyse', 'sachgebiet', 3, dez16)
// ========================================
// Dezernate für Abteilung 2 (Terrorismusbekämpfung)
// ========================================
const dez21 = insertUnit('Dez 21', 'Ermittlungen', 'dezernat', 2, abteilungIds['Abt 2'])
insertUnit('TD 21.1', 'Ermittlungskommissionen VSTGB, Ermittlungskommissionen PMK (alle Phänomenbereiche), VsnL', 'teildezernat', 3, dez21)
const dez22 = insertUnit('Dez 22', 'Auswertung/Analyse, ZMI, Open Source Intelligence (OSINT), Wissenschaftlicher Dienst PMK, KPMD-PMK', 'dezernat', 2, abteilungIds['Abt 2'])
insertUnit('SG 22.1', 'Auswertung/Analyse, ZMI', 'sachgebiet', 3, dez22)
insertUnit('TD 22.2', 'Open Source Intelligence (OSINT)', 'teildezernat', 3, dez22)
insertUnit('TD 22.3', 'Wissenschaftlicher Dienst PMK', 'teildezernat', 3, dez22)
insertUnit('SG 22.4', 'PMK Meldedienste, Kriminalpolizeilicher Meldedienst (KPMD)', 'sachgebiet', 3, dez22)
const dez23 = insertUnit('Dez 23', 'PMK Rechts und PMK SZ', 'dezernat', 2, abteilungIds['Abt 2'])
insertUnit('SG 23.1', 'KoSt Gefährder, Gemeinsames Extremismus- und Terrorismusabwehrzentrum (GETZ-) Rechts, Landesvertreter GETZ-Rechts Bund', 'sachgebiet', 3, dez23)
insertUnit('SG 23.2', 'Prüffallbearbeitung, Gefahrensachverhalte, Hasskriminalität, PMK rechts', 'sachgebiet', 3, dez23)
insertUnit('TD 23.3', 'PMK SZ, Spionage, Gefahrensachverhalte PMK SZ, Landesvertreter GETZ-SP', 'teildezernat', 3, dez23)
const dez24 = insertUnit('Dez 24', 'PMK Religiöse Ideologie', 'dezernat', 2, abteilungIds['Abt 2'])
insertUnit('SG 24.1', 'KoST Gefährder, SiKo', 'sachgebiet', 3, dez24)
insertUnit('TD 24.2', 'Gemeinsames Terrorismusabwehrzentrum (GTAZ) NRW, Landesvertreter GTAZ Bund, ATD/RED', 'teildezernat', 3, dez24)
insertUnit('SG 24.3', 'Prüffallbearbeitung, Gefahrensachverhalte, islamistisch-terroristisches Personenpotential', 'sachgebiet', 3, dez24)
const dez25 = insertUnit('Dez 25', 'PMK Links, Ausländische Ideologie, ZSÜ', 'dezernat', 2, abteilungIds['Abt 2'])
insertUnit('SG 25.1', 'KoSt Gefährder Links, GETZ-Links NRW, Landesvertreter GETZ-Links Bund, Prüffallsachbearbeitung, Gefahrensachverhalte', 'sachgebiet', 3, dez25)
insertUnit('SG 25.2', 'KoSt Gefährder Ausländische Ideologie (AI), Landesvertreter GETZ-AI Bund, Prüffallsachbearbeitung, Gefahrensachverhalte', 'sachgebiet', 3, dez25)
insertUnit('SG 25.3', 'Zentrale Stelle NRW für ZSÜ', 'sachgebiet', 3, dez25)
// ========================================
// Dezernate für Abteilung 3 (Strategische Kriminalitätsbekämpfung)
// ========================================
const dez31 = insertUnit('Dez 31', 'Kriminalitätsauswertung und Analyse, Polizeiliche Kriminalitätsstatistik', 'dezernat', 2, abteilungIds['Abt 3'])
insertUnit('SG 31.1', 'Grundsatzangelegenheiten / KoST MAfEx / KoSt MOTIV/MIT/aMIT', 'sachgebiet', 3, dez31)
insertUnit('SG 31.2', 'Auswertung und Analyse 1, Eigentums- und Vermögensdelikte, SÄM-ÜT, KoSt RTE', 'sachgebiet', 3, dez31)
insertUnit('SG 31.3', 'Auswertung/Analyse 2 Rauschgift-, Arzneimittel-, Menschenhandels-, Schleusungskriminalität, Dokumentenfälschungen, Gewaltdelikte', 'sachgebiet', 3, dez31)
insertUnit('SG 31.4', 'Polizeiliche Kriminalstatistik (PKS)', 'sachgebiet', 3, dez31)
const dez32 = insertUnit('Dez 32', 'Kriminalprävention, Kriminalistisch-Kriminologische Forschungsstelle, Evaluation', 'dezernat', 2, abteilungIds['Abt 3'])
insertUnit('SG 32.1', 'Kriminalprävention und Opferschutz', 'sachgebiet', 3, dez32)
insertUnit('TD 32.2', 'Kriminalistisch-Kriminologische Forschungsstelle (KKF)', 'teildezernat', 3, dez32)
insertUnit('SG 32.3', 'Zentralstelle Evaluation (ZEVA)', 'sachgebiet', 3, dez32)
const dez33 = insertUnit('Dez 33', 'Fahndung, Datenaustausch Polizei/Justiz, Kriminalaktenhaltung, Internationale Rechtshilfe', 'dezernat', 2, abteilungIds['Abt 3'])
insertUnit('SG 33.1', 'Datenstation, Polizeiliche Beobachtung, Grundsatz Fahndung, Fahndungsportal', 'sachgebiet', 3, dez33)
insertUnit('SG 33.2', 'Datenaustausch Polizei/Justiz, Kriminalaktenhaltung', 'sachgebiet', 3, dez33)
insertUnit('SG 33.3', 'Rechtshilfe, PNR, internationale Fahndung, Interpol- und Europolangelegenheiten, Vermisste', 'sachgebiet', 3, dez33)
const dez34 = insertUnit('Dez 34', 'Digitalstrategie, Polizeifachliche IT, Landeszentrale Qualitätssicherung', 'dezernat', 2, abteilungIds['Abt 3'])
insertUnit('TD 34.1', 'Grundsatz, Gremien, Fachliche IT-Projekte', 'teildezernat', 3, dez34)
insertUnit('TD 34.2', 'Zentralstelle Polizei 2020, PIAV, QS Verbundanwendungen, Europäisches Informationssystem (EIS)', 'teildezernat', 3, dez34)
insertUnit('TD 34.3', 'IT FaKo Fachbereich Kriminalität, Zentralstelle ViVA, QS, INPOL-Z, ViVA-Büro LKA', 'teildezernat', 3, dez34)
const dez35 = insertUnit('Dez 35', 'Verhaltensanalyse und Risikomanagement', 'dezernat', 2, abteilungIds['Abt 3'])
insertUnit('SG 35.1', 'Zentralstelle KURS NRW', 'sachgebiet', 3, dez35)
insertUnit('SG 35.2', 'Operative Fallanalyse (OFA/ViCLAS)', 'sachgebiet', 3, dez35)
insertUnit('SG 35.3', 'Zentralstelle PeRiskoP', 'sachgebiet', 3, dez35)
// ========================================
// Dezernate für Abteilung 4 (Cybercrime)
// ========================================
const dez41 = insertUnit('Dez 41', 'Zentrale Ansprechstelle Cybercrime, Grundsatz, Digitale Forensik, IT-Entwicklung', 'dezernat', 2, abteilungIds['Abt 4'])
insertUnit('SG 41.1', 'Grundsatzangelegenheiten, Prävention, Auswertung Cybercrime', 'sachgebiet', 3, dez41)
insertUnit('TD 41.2', 'Software und KI-Entwicklung, IT-Verfahrensbetreuung, Marktschau', 'teildezernat', 3, dez41)
insertUnit('SG 41.3', 'Forensik Desktop Strategie und Entwicklung - Hinweisportal NRW', 'sachgebiet', 3, dez41)
insertUnit('SG 41.4', 'Forensik Desktop Datenaufbereitung und Automatisierung', 'sachgebiet', 3, dez41)
insertUnit('TD 41.5', 'Forensik Desktop Betrieb', 'teildezernat', 3, dez41)
const dez42 = insertUnit('Dez 42', 'Cyber-Recherche- und Fahndungszentrum, Ermittlungen Cybercrime', 'dezernat', 2, abteilungIds['Abt 4'])
insertUnit('SG 42.1', 'Personenorientierte Recherche in Datennetzen', 'sachgebiet', 3, dez42)
insertUnit('SG 42.2', 'Sachorientierte Recherche in Datennetzen', 'sachgebiet', 3, dez42)
insertUnit('TD 42.3', 'Interventionsteams Digitale Tatorte IUK-Lageunterstützung Ermittlungskommissionen', 'teildezernat', 3, dez42)
const dez43 = insertUnit('Dez 43', 'Zentrale Auswertungs- und Sammelstelle (ZASt) für die Bekämpfung von Missbrauchsabbildungen von Kindern und Jugendlichen', 'dezernat', 2, abteilungIds['Abt 4'])
insertUnit('SG 43.1', 'ZASt Grundsatz, Identifizierungsverfahren, Bildvergleichssammlung, Schulfahndung, Berichtswesen, Meldedienste/Verbundverfahren, Gremien', 'sachgebiet', 3, dez43)
insertUnit('SG 43.2', 'ZASt NCMEC/ Landeszentrale Bewertung 1', 'sachgebiet', 3, dez43)
insertUnit('SG 43.3', 'ZASt NCMEC/ Landeszentrale Bewertung 2', 'sachgebiet', 3, dez43)
insertUnit('SG 43.4', 'ZASt NCMEC/ Landeszentrale Bewertung 3', 'sachgebiet', 3, dez43)
const dez44 = insertUnit('Dez 44', 'Telekommunikationsüberwachung (TKÜ)', 'dezernat', 2, abteilungIds['Abt 4'])
insertUnit('SG 44.1', 'Grundsatzaufgaben, operative TKÜ, AIT', 'sachgebiet', 3, dez44)
insertUnit('SG 44.2', 'TKÜ Betrieb und Service', 'sachgebiet', 3, dez44)
insertUnit('TD 44.3', 'Digitale Forensik, IUK-Ermittlungsunterstützung', 'teildezernat', 3, dez44)
// ========================================
// Dezernate für Abteilung 5 (Kriminalwissenschaftliches Institut)
// ========================================
const dez51 = insertUnit('Dez 51', 'Chemie, Physik', 'dezernat', 2, abteilungIds['Abt 5'])
insertUnit('TD 51.1', 'Schussspuren, Explosivstoffe, Brand, Elektrotechnik', 'teildezernat', 3, dez51)
insertUnit('TD 51.2', 'Betäubungsmittel', 'teildezernat', 3, dez51)
const dez52 = insertUnit('Dez 52', 'Serologie, DNA-Analytik', 'dezernat', 2, abteilungIds['Abt 5'])
insertUnit('TD 52.1', 'DNA-Probenbearbeitung, DNA-Spurenbearbeitung I', 'teildezernat', 3, dez52)
insertUnit('TD 52.2', 'DNA-Spurenbearbeitung II', 'teildezernat', 3, dez52)
insertUnit('TD 52.3', 'DNA-Spurenbearbeitung III', 'teildezernat', 3, dez52)
insertUnit('TD 52.4', 'DNA-Spurenbearbeitung IV DNA-Fremdvergabe', 'teildezernat', 3, dez52)
const dez53 = insertUnit('Dez 53', 'Biologie, Materialspuren, Urkunden, Handschriften', 'dezernat', 2, abteilungIds['Abt 5'])
insertUnit('TD 53.1', 'forensische Textilkunde, Botanik, Material-, Haar- und Erdspuren', 'teildezernat', 3, dez53)
insertUnit('SG 53.2', 'Urkunden, Handschriften', 'sachgebiet', 3, dez53)
const dez54 = insertUnit('Dez 54', 'Zentralstelle Kriminaltechnik, Forensische Medientechnik, USBV Entschärfung', 'dezernat', 2, abteilungIds['Abt 5'])
insertUnit('SG 54.1', 'Zentralstelle Kriminaltechnik', 'sachgebiet', 3, dez54)
insertUnit('SG 54.2', 'Tatortvermessung, Rekonstruktion und XR-Lab, Phantombilderstellung und visuelle Fahndungshilfe, Bild- und Videotechnik', 'sachgebiet', 3, dez54)
insertUnit('SG 54.3', 'Entschärfung von unkonventionellen Spreng- und Brandvorrichtungen (USBV) – Einsatz- und Ermittlungsunterstützung Explosivstoffe', 'sachgebiet', 3, dez54)
const dez55 = insertUnit('Dez 55', 'Waffen und Werkzeug DNA-Analyse-Datei', 'dezernat', 2, abteilungIds['Abt 5'])
insertUnit('SG 55.1', 'Waffen und Munition', 'sachgebiet', 3, dez55)
insertUnit('SG 55.2', 'Werkzeug-, Form-, Passspuren, Schließ- und Sicherungseinrichtungen', 'sachgebiet', 3, dez55)
insertUnit('SG 55.3', 'DNA-Analyse-Datei', 'sachgebiet', 3, dez55)
const dez56 = insertUnit('Dez 56', 'Daktyloskopie, Gesichts- und Sprechererkennung, Tonträgerauswertung', 'dezernat', 2, abteilungIds['Abt 5'])
insertUnit('SG 56.1', 'Daktyloskopisches Labor', 'sachgebiet', 3, dez56)
insertUnit('SG 56.2', 'AFIS I/Daktyloskopische Gutachten', 'sachgebiet', 3, dez56)
insertUnit('SG 56.3', 'AFIS II/Daktyloskopische Gutachten', 'sachgebiet', 3, dez56)
insertUnit('TD 56.4', 'Gesichts- und Sprechererkennung, Tonträgerauswertung', 'teildezernat', 3, dez56)
// ========================================
// Dezernate für Abteilung 6 (Fachaufsicht und Ermittlungsunterstützung)
// ========================================
const dez61 = insertUnit('Dez 61', 'Kriminalitätsangelegenheiten der KPB, Fachcontrolling, Koordination PUA, Lagedienst', 'dezernat', 2, abteilungIds['Abt 6'])
insertUnit('TD 61.1', 'Kriminalitätsangelegenheiten der KPB, Fachcontrolling, Koordination PUA', 'teildezernat', 3, dez61)
insertUnit('SG 61.2', 'Lagedienst', 'sachgebiet', 3, dez61)
const dez62 = insertUnit('Dez 62', 'Fahndungsgruppe Staatsschutz', 'dezernat', 2, abteilungIds['Abt 6'])
// Add Führungsgruppe for Dezernat 62
insertUnit('FüGr 62', 'Führungsgruppe', 'fuehrungsstelle', 3, dez62)
// Fahndungsgruppen 1-8
for (let i = 1; i <= 8; i++) {
insertUnit(`SG 62.${i}`, i === 8 ? 'Fahndungsgruppe 8' : `Fahndungsgruppe ${i}`, 'sachgebiet', 3, dez62)
}
insertUnit('SG 62.9', 'Technische Gruppe', 'sachgebiet', 3, dez62)
const dez63 = insertUnit('Dez 63', 'Verdeckte Ermittlungen, Zeugenschutz', 'dezernat', 2, abteilungIds['Abt 6'])
insertUnit('SG 63.1', 'Einsatz VE 1', 'sachgebiet', 3, dez63)
insertUnit('SG 63.2', 'Einsatz VE 2', 'sachgebiet', 3, dez63)
insertUnit('SG 63.3', 'Scheinkäufer, Logistik', 'sachgebiet', 3, dez63)
insertUnit('SG 63.4', 'VP-Führung, Koordinierung VP', 'sachgebiet', 3, dez63)
insertUnit('TD 63.5', 'Zeugenschutz, OpOS', 'teildezernat', 3, dez63)
const dez64 = insertUnit('Dez 64', 'Mobiles Einsatzkommando, Technische Einsatzgruppe, Zielfahndung', 'dezernat', 2, abteilungIds['Abt 6'])
insertUnit('FüGr 64', 'FüGr', 'fuehrungsstelle', 3, dez64)
insertUnit('SG 64.1', 'MEK 1', 'sachgebiet', 3, dez64)
insertUnit('SG 64.2', 'MEK 2', 'sachgebiet', 3, dez64)
insertUnit('SG 64.3', 'Technische Einsatzgruppe', 'sachgebiet', 3, dez64)
insertUnit('SG 64.4', 'Zielfahndung', 'sachgebiet', 3, dez64)
// ========================================
// Dezernate für Zentralabteilung
// ========================================
const dezZA1 = insertUnit('Dez ZA 1', 'Haushalts-, Wirtschafts-, Liegenschaftsmanagement, Zentrale Vergabestelle', 'dezernat', 2, abteilungIds['ZA'])
insertUnit('SG ZA 1.1', 'Haushalts- und Wirtschaftsangelegenheiten', 'sachgebiet', 3, dezZA1)
insertUnit('SG ZA 1.2', 'Liegenschaftsmanagement', 'sachgebiet', 3, dezZA1)
insertUnit('SG ZA 1.3', 'Zentrale Vergabestelle', 'sachgebiet', 3, dezZA1)
const dezZA2 = insertUnit('Dez ZA 2', 'Personalangelegenheiten, Gleichstellungsbeauftragte, Fortbildung', 'dezernat', 2, abteilungIds['ZA'])
insertUnit('SG ZA 2.1', 'Personalentwicklung, Arbeitszeit, BGM-POL; Stellenplan', 'sachgebiet', 3, dezZA2)
insertUnit('SG ZA 2.2', 'Personalverwaltung Beamte/ dienstrechtliche Angelegenheiten', 'sachgebiet', 3, dezZA2)
insertUnit('TD ZA 2.3', 'Personalverwaltung Regierungsbeschäftigte/ Personalgewinnung, tarif- und arbeitsrechtliche Angelegenheiten', 'teildezernat', 3, dezZA2)
insertUnit('SG ZA 2.4', 'Fortbildung', 'sachgebiet', 3, dezZA2)
const dezZA3 = insertUnit('Dez ZA 3', 'Informationstechnik und Anwenderunterstützung, Informationssicherheit, Kfz-, Waffen- und Geräteangelegenheiten', 'dezernat', 2, abteilungIds['ZA'])
insertUnit('SG ZA 3.1', 'IT-Grundsatz, Planung und Koordinierung, IT-Service Desk, Lizenzmanagement', 'sachgebiet', 3, dezZA3)
insertUnit('SG ZA 3.2', 'Netzwerk/ TK-Anlage', 'sachgebiet', 3, dezZA3)
insertUnit('SG ZA 3.3', 'Server/Client, Logistik', 'sachgebiet', 3, dezZA3)
insertUnit('SG ZA 3.4', 'Kfz-, Waffen- und Geräteangelegenheiten', 'sachgebiet', 3, dezZA3)
const dezZA4 = insertUnit('Dez ZA 4', 'Vereins- und Waffenrecht, Innenrevision, Sponsoring, Rechtsangelegenheiten, Datenschutz, Geheimschutz', 'dezernat', 2, abteilungIds['ZA'])
const dezZA5 = insertUnit('Dez ZA 5', 'Polizeiärztlicher Dienst', 'dezernat', 2, abteilungIds['ZA'])
insertUnit('PAD', 'Polizeiärztin', 'sachgebiet', 3, dezZA5)
console.log('✅ LKA NRW Organizational Structure seeded successfully!')
console.log(`📊 Created ${Object.keys(unitIds).length} organizational units`)
// Output some statistics
const stats = db.prepare(`
SELECT type, COUNT(*) as count
FROM organizational_units
GROUP BY type
`).all()
console.log('\n📈 Statistics:')
stats.forEach(stat => {
console.log(` ${stat.type}: ${stat.count} units`)
})
// Close database
db.close()
console.log('\n🎉 Done! The organization structure is ready to use.')
console.log(' Navigate to Admin Panel > Organigramm to manage the structure.')
console.log(' Users can view it in the main app using the 🏢 button.')

Datei anzeigen

@ -0,0 +1,386 @@
const Database = require('better-sqlite3')
const path = require('path')
const { v4: uuidv4 } = require('uuid')
// Open database
const dbPath = path.join(__dirname, '..', 'skillmate.dev.encrypted.db')
const db = new Database(dbPath)
// Enable foreign keys
db.pragma('foreign_keys = ON')
console.log('🏢 Seeding LKA NRW Organizational Structure...')
const now = new Date().toISOString()
// Helper function to insert organizational unit
function insertUnit(code, name, type, level, parentId = null, options = {}) {
const id = uuidv4()
db.prepare(`
INSERT OR REPLACE INTO organizational_units (
id, code, name, type, level, parent_id,
color, order_index, has_fuehrungsstelle,
fuehrungsstelle_name, is_active, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id, code, name, type, level, parentId,
options.color || null,
options.orderIndex || 0,
options.hasFuehrungsstelle || 0,
options.fuehrungsstelleName || null,
1, now, now
)
return id
}
// Clear existing data
console.log('Clearing existing organizational data...')
db.prepare('DELETE FROM deputy_delegations').run()
db.prepare('DELETE FROM deputy_assignments').run()
db.prepare('DELETE FROM special_positions').run()
db.prepare('DELETE FROM employee_unit_assignments').run()
db.prepare('DELETE FROM organizational_units').run()
// Create Direktor
const direktorId = insertUnit('DIR', 'Direktor LKA NRW', 'direktion', 0, null, {
color: '#1e3a8a',
orderIndex: 0
})
// Create Leitungsstab
const leitungsstabId = insertUnit('LStab', 'Leitungsstab', 'stabsstelle', 1, direktorId, {
color: '#6b7280',
orderIndex: 1
})
// Leitungsstab Sub-units
insertUnit('LStab 1', 'Grundsatzangelegenheiten, Gremien, internationale polizeiliche Zusammenarbeit', 'sachgebiet', 2, leitungsstabId)
insertUnit('LStab 2', 'Strategische Steuerung, Qualitätsmanagement, Controlling', 'sachgebiet', 2, leitungsstabId)
insertUnit('LStab 3', 'Presse- und Öffentlichkeitsarbeit', 'sachgebiet', 2, leitungsstabId)
// Create Special Units (Beauftragte)
const personalratId = insertUnit('PR', 'Personalrat', 'sondereinheit', 1, direktorId, {
color: '#059669',
orderIndex: 2
})
const schwerbehindertenId = insertUnit('SBV', 'Schwerbehindertenvertretung', 'sondereinheit', 1, direktorId, {
color: '#059669',
orderIndex: 3
})
// Create Abteilungen with color gradients
const abteilungen = [
{ code: 'Abt 1', name: 'Organisierte Kriminalität', color: '#dc2626' },
{ code: 'Abt 2', name: 'Terrorismusbekämpfung und Staatsschutz', color: '#ea580c' },
{ code: 'Abt 3', name: 'Strategische Kriminalitätsbekämpfung', color: '#0891b2' },
{ code: 'Abt 4', name: 'Cybercrime (CCCC)', color: '#7c3aed' },
{ code: 'Abt 5', name: 'Kriminalwissenschaftliches und -technisches Institut', color: '#0d9488' },
{ code: 'Abt 6', name: 'Fachaufsicht und Ermittlungsunterstützung', color: '#be185d' },
{ code: 'ZA', name: 'Zentralabteilung', color: '#6b7280' }
]
const abteilungIds = {}
abteilungen.forEach((abt, index) => {
abteilungIds[abt.code] = insertUnit(abt.code, abt.name, 'abteilung', 1, direktorId, {
color: abt.color,
orderIndex: 10 + index * 10,
hasFuehrungsstelle: abt.code !== 'ZA' ? 1 : 0,
fuehrungsstelleName: abt.code !== 'ZA' ? `Führungsstelle ${abt.code}` : null
})
})
// Create Dezernate for Abteilung 1 (Organisierte Kriminalität)
const dezernate1 = [
{ code: '11', name: 'Ermittlungen OK, OK Rauschgift' },
{ code: '12', name: 'Wirtschaftskriminalität' },
{ code: '13', name: 'Finanzermittlungen' },
{ code: '14', name: 'Auswerte- und Analysestelle OK' },
{ code: '15', name: 'Korruption, Umweltkriminalität' },
{ code: '16', name: 'Finanzierung Organisierter Kriminalität und Terrorismus' }
]
dezernate1.forEach((dez, index) => {
const dezId = insertUnit(`Dezernat ${dez.code}`, dez.name, 'dezernat', 2, abteilungIds['Abt 1'], {
orderIndex: index
})
// Add sample Sachgebiete
if (dez.code === '11') {
insertUnit('SG 11.1', 'Grundsatzfragen/Koordination/Auswertung', 'sachgebiet', 3, dezId)
}
if (dez.code === '12') {
insertUnit('SG 12.1', 'Grundsatzfragen/Koordination/Auswertung', 'sachgebiet', 3, dezId)
}
if (dez.code === '13') {
insertUnit('SG 13.1', 'GFG 1', 'sachgebiet', 3, dezId)
insertUnit('SG 13.2', 'GFG 2', 'sachgebiet', 3, dezId)
insertUnit('SG 13.3', 'Verfahrensintegrierte Finanzermittlungen/Vermögensabschöpfung', 'sachgebiet', 3, dezId)
insertUnit('SG 13.4', 'Zentrale Informations- und Koordinierungsstelle Finanzermittlung', 'sachgebiet', 3, dezId)
}
if (dez.code === '14') {
insertUnit('TD 14.1', 'Operative Auswertung und Analyse', 'teildezernat', 3, dezId)
insertUnit('SG 14.2', 'Strategische Auswertung und Analyse', 'sachgebiet', 3, dezId)
insertUnit('SG 14.3', 'Technische Informationssysteme', 'sachgebiet', 3, dezId)
insertUnit('SG 14.4', 'Auswertung und Analyse Rockerkriminalität', 'sachgebiet', 3, dezId)
insertUnit('SG 14.5', 'Auswertung und Analyse Clankriminalität', 'sachgebiet', 3, dezId)
}
if (dez.code === '15') {
insertUnit('SG 15.1', 'Grundsatzangelegenheiten, Korruption', 'sachgebiet', 3, dezId)
insertUnit('SG 15.2', 'Vernetzungsstelle Umweltkriminalität', 'sachgebiet', 3, dezId)
}
if (dez.code === '16') {
insertUnit('SG 16.1', 'Grundsatzfragen/Auswertung/Analyse', 'sachgebiet', 3, dezId)
}
})
// Create Dezernate for Abteilung 2 (Terrorismusbekämpfung)
const dezernate2 = [
{ code: '21', name: 'Ermittlungen' },
{ code: '22', name: 'Auswertung/Analyse, ZMI, OSINT, Wissenschaftlicher Dienst PMK' },
{ code: '23', name: 'PMK Rechts und PMK SZ' },
{ code: '24', name: 'PMK Religiöse Ideologie' },
{ code: '25', name: 'PMK Links, Ausländische Ideologie, ZSÜ' }
]
dezernate2.forEach((dez, index) => {
const dezId = insertUnit(`Dezernat ${dez.code}`, dez.name, 'dezernat', 2, abteilungIds['Abt 2'], {
orderIndex: index
})
if (dez.code === '21') {
insertUnit('TD 21.1', 'Ermittlungskommissionen VSTGB', 'teildezernat', 3, dezId)
}
if (dez.code === '22') {
insertUnit('SG 22.1', 'Auswertung/Analyse, ZMI', 'sachgebiet', 3, dezId)
insertUnit('TD 22.2', 'Open Source Intelligence (OSINT)', 'teildezernat', 3, dezId)
insertUnit('TD 22.3', 'Wissenschaftlicher Dienst PMK', 'teildezernat', 3, dezId)
insertUnit('SG 22.4', 'PMK Meldedienste, KPMD', 'sachgebiet', 3, dezId)
}
if (dez.code === '23') {
insertUnit('SG 23.1', 'KoSt Gefährder, GETZ-Rechts', 'sachgebiet', 3, dezId)
insertUnit('SG 23.2', 'Prüffallbearbeitung, Gefahrensachverhalte', 'sachgebiet', 3, dezId)
insertUnit('TD 23.3', 'PMK SZ, Spionage', 'teildezernat', 3, dezId)
}
if (dez.code === '24') {
insertUnit('SG 24.1', 'KoST Gefährder, SiKo', 'sachgebiet', 3, dezId)
insertUnit('TD 24.2', 'Gemeinsames Terrorismusabwehrzentrum (GTAZ) NRW', 'teildezernat', 3, dezId)
insertUnit('SG 24.3', 'Prüffallbearbeitung, islamistisch-terroristisches Personenpotential', 'sachgebiet', 3, dezId)
}
if (dez.code === '25') {
insertUnit('SG 25.1', 'KoSt Gefährder Links', 'sachgebiet', 3, dezId)
insertUnit('SG 25.2', 'KoSt Gefährder Ausländische Ideologie', 'sachgebiet', 3, dezId)
insertUnit('SG 25.3', 'Zentrale Stelle NRW für ZSÜ', 'sachgebiet', 3, dezId)
}
})
// Create Dezernate for Abteilung 3 (Strategische Kriminalitätsbekämpfung)
const dezernate3 = [
{ code: '31', name: 'Kriminalitätsauswertung und Analyse' },
{ code: '32', name: 'Kriminalprävention, KKF, Evaluation' },
{ code: '33', name: 'Fahndung, Datenaustausch, Kriminalaktenhaltung' },
{ code: '34', name: 'Digitalstrategie, Polizeifachliche IT' },
{ code: '35', name: 'Verhaltensanalyse und Risikomanagement' }
]
dezernate3.forEach((dez, index) => {
const dezId = insertUnit(`Dezernat ${dez.code}`, dez.name, 'dezernat', 2, abteilungIds['Abt 3'], {
orderIndex: index
})
if (dez.code === '31') {
insertUnit('SG 31.1', 'Grundsatzangelegenheiten/KoST MAfEx', 'sachgebiet', 3, dezId)
insertUnit('SG 31.2', 'Auswertung und Analyse 1', 'sachgebiet', 3, dezId)
insertUnit('SG 31.3', 'Auswertung/Analyse 2', 'sachgebiet', 3, dezId)
insertUnit('SG 31.4', 'Polizeiliche Kriminalstatistik (PKS)', 'sachgebiet', 3, dezId)
}
if (dez.code === '32') {
insertUnit('SG 32.1', 'Kriminalprävention und Opferschutz', 'sachgebiet', 3, dezId)
insertUnit('TD 32.2', 'Kriminalistisch-Kriminologische Forschungsstelle (KKF)', 'teildezernat', 3, dezId)
insertUnit('SG 32.3', 'Zentralstelle Evaluation (ZEVA)', 'sachgebiet', 3, dezId)
}
if (dez.code === '33') {
insertUnit('SG 33.1', 'Datenstation, Polizeiliche Beobachtung', 'sachgebiet', 3, dezId)
insertUnit('SG 33.2', 'Datenaustausch Polizei/Justiz', 'sachgebiet', 3, dezId)
insertUnit('SG 33.3', 'Rechtshilfe, PNR, internationale Fahndung', 'sachgebiet', 3, dezId)
}
if (dez.code === '34') {
insertUnit('TD 34.1', 'Grundsatz, Gremien, Fachliche IT-Projekte', 'teildezernat', 3, dezId)
insertUnit('TD 34.2', 'Zentralstelle Polizei 2020', 'teildezernat', 3, dezId)
insertUnit('TD 34.3', 'IT FaKo Fachbereich Kriminalität', 'teildezernat', 3, dezId)
}
if (dez.code === '35') {
insertUnit('SG 35.1', 'Zentralstelle KURS NRW', 'sachgebiet', 3, dezId)
insertUnit('SG 35.2', 'Operative Fallanalyse (OFA/ViCLAS)', 'sachgebiet', 3, dezId)
insertUnit('SG 35.3', 'Zentralstelle PeRiskoP', 'sachgebiet', 3, dezId)
}
})
// Create Dezernate for Abteilung 4 (Cybercrime)
const dezernate4 = [
{ code: '41', name: 'Zentrale Ansprechstelle Cybercrime, Grundsatz, Digitale Forensik' },
{ code: '42', name: 'Cyber-Recherche- und Fahndungszentrum' },
{ code: '43', name: 'Zentrale Auswertungs- und Sammelstelle (ZASt)' },
{ code: '44', name: 'Telekommunikationsüberwachung (TKÜ)' }
]
dezernate4.forEach((dez, index) => {
const dezId = insertUnit(`Dezernat ${dez.code}`, dez.name, 'dezernat', 2, abteilungIds['Abt 4'], {
orderIndex: index
})
if (dez.code === '41') {
insertUnit('SG 41.1', 'Grundsatzangelegenheiten, Prävention', 'sachgebiet', 3, dezId)
insertUnit('TD 41.2', 'Software und KI-Entwicklung', 'teildezernat', 3, dezId)
insertUnit('SG 41.3', 'Forensik Desktop Strategie', 'sachgebiet', 3, dezId)
insertUnit('SG 41.4', 'Forensik Desktop Datenaufbereitung', 'sachgebiet', 3, dezId)
insertUnit('TD 41.5', 'Forensik Desktop Betrieb', 'teildezernat', 3, dezId)
}
if (dez.code === '42') {
insertUnit('SG 42.1', 'Personenorientierte Recherche in Datennetzen', 'sachgebiet', 3, dezId)
insertUnit('SG 42.2', 'Sachorientierte Recherche in Datennetzen', 'sachgebiet', 3, dezId)
insertUnit('TD 42.3', 'Interventionsteams Digitale Tatorte', 'teildezernat', 3, dezId)
}
if (dez.code === '43') {
insertUnit('SG 43.1', 'ZASt Grundsatz', 'sachgebiet', 3, dezId)
insertUnit('SG 43.2', 'ZASt NCMEC/Landeszentrale Bewertung 1', 'sachgebiet', 3, dezId)
insertUnit('SG 43.3', 'ZASt NCMEC/Landeszentrale Bewertung 2', 'sachgebiet', 3, dezId)
insertUnit('SG 43.4', 'ZASt NCMEC/Landeszentrale Bewertung 3', 'sachgebiet', 3, dezId)
}
if (dez.code === '44') {
insertUnit('SG 44.1', 'Grundsatzaufgaben, operative TKÜ', 'sachgebiet', 3, dezId)
insertUnit('SG 44.2', 'TKÜ Betrieb und Service', 'sachgebiet', 3, dezId)
insertUnit('TD 44.3', 'Digitale Forensik, IUK-Ermittlungsunterstützung', 'teildezernat', 3, dezId)
}
})
// Create Dezernate for Abteilung 5 (Kriminalwissenschaftliches Institut)
const dezernate5 = [
{ code: '51', name: 'Chemie, Physik' },
{ code: '52', name: 'Serologie, DNA-Analytik' },
{ code: '53', name: 'Biologie, Materialspuren, Urkunden' },
{ code: '54', name: 'Zentralstelle Kriminaltechnik, Forensische Medientechnik' },
{ code: '55', name: 'Waffen und Werkzeug, DNA-Analyse-Datei' },
{ code: '56', name: 'Daktyloskopie, Gesichts- und Sprechererkennung' }
]
dezernate5.forEach((dez, index) => {
const dezId = insertUnit(`Dezernat ${dez.code}`, dez.name, 'dezernat', 2, abteilungIds['Abt 5'], {
orderIndex: index
})
if (dez.code === '51') {
insertUnit('TD 51.1', 'Schussspuren, Explosivstoffe, Brand', 'teildezernat', 3, dezId)
insertUnit('TD 51.2', 'Betäubungsmittel', 'teildezernat', 3, dezId)
}
if (dez.code === '52') {
insertUnit('TD 52.1', 'DNA-Probenbearbeitung', 'teildezernat', 3, dezId)
insertUnit('TD 52.2', 'DNA-Spurenbearbeitung II', 'teildezernat', 3, dezId)
insertUnit('TD 52.3', 'DNA-Spurenbearbeitung III', 'teildezernat', 3, dezId)
insertUnit('TD 52.4', 'DNA-Spurenbearbeitung IV', 'teildezernat', 3, dezId)
}
if (dez.code === '53') {
insertUnit('TD 53.1', 'Forensische Textilkunde, Botanik', 'teildezernat', 3, dezId)
insertUnit('SG 53.2', 'Urkunden, Handschriften', 'sachgebiet', 3, dezId)
}
if (dez.code === '54') {
insertUnit('SG 54.1', 'Zentralstelle Kriminaltechnik', 'sachgebiet', 3, dezId)
insertUnit('SG 54.2', 'Tatortvermessung, Rekonstruktion und XR-Lab', 'sachgebiet', 3, dezId)
insertUnit('SG 54.3', 'Entschärfung von USBV', 'sachgebiet', 3, dezId)
}
if (dez.code === '55') {
insertUnit('SG 55.1', 'Waffen und Munition', 'sachgebiet', 3, dezId)
insertUnit('SG 55.2', 'Werkzeug-, Form-, Passspuren', 'sachgebiet', 3, dezId)
insertUnit('SG 55.3', 'DNA-Analyse-Datei', 'sachgebiet', 3, dezId)
}
if (dez.code === '56') {
insertUnit('SG 56.1', 'Daktyloskopisches Labor', 'sachgebiet', 3, dezId)
insertUnit('SG 56.2', 'AFIS I/Daktyloskopische Gutachten', 'sachgebiet', 3, dezId)
insertUnit('SG 56.3', 'AFIS II/Daktyloskopische Gutachten', 'sachgebiet', 3, dezId)
insertUnit('TD 56.4', 'Gesichts- und Sprechererkennung', 'teildezernat', 3, dezId)
}
})
// Create Dezernate for Abteilung 6 (Fachaufsicht)
const dezernate6 = [
{ code: '61', name: 'Kriminalitätsangelegenheiten der KPB, Fachcontrolling' },
{ code: '62', name: 'Fahndungsgruppe Staatsschutz' },
{ code: '63', name: 'Verdeckte Ermittlungen, Zeugenschutz' },
{ code: '64', name: 'Mobiles Einsatzkommando, TEG, Zielfahndung' }
]
dezernate6.forEach((dez, index) => {
const dezId = insertUnit(`Dezernat ${dez.code}`, dez.name, 'dezernat', 2, abteilungIds['Abt 6'], {
orderIndex: index
})
if (dez.code === '61') {
insertUnit('TD 61.1', 'Kriminalitätsangelegenheiten der KPB', 'teildezernat', 3, dezId)
insertUnit('SG 61.2', 'Lagedienst', 'sachgebiet', 3, dezId)
}
if (dez.code === '62') {
for (let i = 1; i <= 9; i++) {
if (i === 9) {
insertUnit(`SG 62.${i}`, 'Technische Gruppe', 'sachgebiet', 3, dezId)
} else {
insertUnit(`SG 62.${i}`, `Fahndungsgruppe ${i}`, 'sachgebiet', 3, dezId)
}
}
}
if (dez.code === '63') {
insertUnit('SG 63.1', 'Einsatz VE 1', 'sachgebiet', 3, dezId)
insertUnit('SG 63.2', 'Einsatz VE 2', 'sachgebiet', 3, dezId)
insertUnit('SG 63.3', 'Scheinkäufer, Logistik', 'sachgebiet', 3, dezId)
insertUnit('SG 63.4', 'VP-Führung, Koordinierung VP', 'sachgebiet', 3, dezId)
insertUnit('TD 63.5', 'Zeugenschutz, OpOS', 'teildezernat', 3, dezId)
}
if (dez.code === '64') {
insertUnit('SG 64.1', 'MEK 1', 'sachgebiet', 3, dezId)
insertUnit('SG 64.2', 'MEK 2', 'sachgebiet', 3, dezId)
insertUnit('SG 64.3', 'Technische Einsatzgruppe', 'sachgebiet', 3, dezId)
insertUnit('SG 64.4', 'Zielfahndung', 'sachgebiet', 3, dezId)
}
})
// Create Dezernate for Zentralabteilung
const dezernateZA = [
{ code: 'ZA 1', name: 'Haushalts-, Wirtschafts-, Liegenschaftsmanagement' },
{ code: 'ZA 2', name: 'Personalangelegenheiten, Gleichstellungsbeauftragte' },
{ code: 'ZA 3', name: 'Informationstechnik und Anwenderunterstützung' },
{ code: 'ZA 4', name: 'Vereins- und Waffenrecht, Innenrevision' },
{ code: 'ZA 5', name: 'Polizeiärztlicher Dienst' }
]
dezernateZA.forEach((dez, index) => {
const dezId = insertUnit(`Dezernat ${dez.code}`, dez.name, 'dezernat', 2, abteilungIds['ZA'], {
orderIndex: index
})
if (dez.code === 'ZA 1') {
insertUnit('SG ZA 1.1', 'Haushalts- und Wirtschaftsangelegenheiten', 'sachgebiet', 3, dezId)
insertUnit('SG ZA 1.2', 'Liegenschaftsmanagement', 'sachgebiet', 3, dezId)
insertUnit('SG ZA 1.3', 'Zentrale Vergabestelle', 'sachgebiet', 3, dezId)
}
if (dez.code === 'ZA 2') {
insertUnit('SG ZA 2.1', 'Personalentwicklung, Arbeitszeit', 'sachgebiet', 3, dezId)
insertUnit('SG ZA 2.2', 'Personalverwaltung Beamte', 'sachgebiet', 3, dezId)
insertUnit('TD ZA 2.3', 'Personalverwaltung Regierungsbeschäftigte', 'teildezernat', 3, dezId)
insertUnit('SG ZA 2.4', 'Fortbildung', 'sachgebiet', 3, dezId)
}
if (dez.code === 'ZA 3') {
insertUnit('SG ZA 3.1', 'IT-Grundsatz, Planung und Koordinierung', 'sachgebiet', 3, dezId)
insertUnit('SG ZA 3.2', 'Netzwerk/TK-Anlage', 'sachgebiet', 3, dezId)
insertUnit('SG ZA 3.3', 'Server/Client, Logistik', 'sachgebiet', 3, dezId)
insertUnit('SG ZA 3.4', 'Kfz-, Waffen- und Geräteangelegenheiten', 'sachgebiet', 3, dezId)
}
})
console.log('✅ LKA NRW Organizational Structure seeded successfully!')
// Close database
db.close()
console.log('🎉 Done! Run the backend to see the organizational structure.')

Datei anzeigen

@ -376,6 +376,119 @@ export function initializeDatabase() {
)
`)
// Organizational Structure Tables
db.exec(`
CREATE TABLE IF NOT EXISTS organizational_units (
id TEXT PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('direktion', 'abteilung', 'dezernat', 'sachgebiet', 'teildezernat', 'fuehrungsstelle', 'stabsstelle', 'sondereinheit')),
level INTEGER NOT NULL,
parent_id TEXT,
position_x INTEGER,
position_y INTEGER,
color TEXT,
order_index INTEGER DEFAULT 0,
description TEXT,
has_fuehrungsstelle INTEGER DEFAULT 0,
fuehrungsstelle_name TEXT,
is_active INTEGER DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (parent_id) REFERENCES organizational_units(id)
);
CREATE INDEX IF NOT EXISTS idx_org_units_parent ON organizational_units(parent_id);
CREATE INDEX IF NOT EXISTS idx_org_units_type ON organizational_units(type);
CREATE INDEX IF NOT EXISTS idx_org_units_level ON organizational_units(level);
`)
// Employee Unit Assignments
db.exec(`
CREATE TABLE IF NOT EXISTS employee_unit_assignments (
id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL,
unit_id TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('leiter', 'stellvertreter', 'mitarbeiter', 'beauftragter')),
start_date TEXT NOT NULL,
end_date TEXT,
is_primary INTEGER DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
FOREIGN KEY (unit_id) REFERENCES organizational_units(id) ON DELETE CASCADE,
UNIQUE(employee_id, unit_id, role)
);
CREATE INDEX IF NOT EXISTS idx_emp_units_employee ON employee_unit_assignments(employee_id);
CREATE INDEX IF NOT EXISTS idx_emp_units_unit ON employee_unit_assignments(unit_id);
`)
// Special Positions (Personalrat, Beauftragte, etc.)
db.exec(`
CREATE TABLE IF NOT EXISTS special_positions (
id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL,
position_type TEXT NOT NULL CHECK(position_type IN ('personalrat', 'schwerbehindertenvertretung', 'datenschutzbeauftragter', 'gleichstellungsbeauftragter', 'inklusionsbeauftragter', 'informationssicherheitsbeauftragter', 'geheimschutzbeauftragter', 'extremismusbeauftragter')),
unit_id TEXT,
start_date TEXT NOT NULL,
end_date TEXT,
is_active INTEGER DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
FOREIGN KEY (unit_id) REFERENCES organizational_units(id)
);
CREATE INDEX IF NOT EXISTS idx_special_pos_employee ON special_positions(employee_id);
CREATE INDEX IF NOT EXISTS idx_special_pos_type ON special_positions(position_type);
`)
// Deputy Assignments (Vertretungen)
db.exec(`
CREATE TABLE IF NOT EXISTS deputy_assignments (
id TEXT PRIMARY KEY,
principal_id TEXT NOT NULL,
deputy_id TEXT NOT NULL,
unit_id TEXT,
valid_from TEXT NOT NULL,
valid_until TEXT NOT NULL,
reason TEXT,
can_delegate INTEGER DEFAULT 1,
created_by TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (principal_id) REFERENCES employees(id),
FOREIGN KEY (deputy_id) REFERENCES employees(id),
FOREIGN KEY (unit_id) REFERENCES organizational_units(id),
FOREIGN KEY (created_by) REFERENCES users(id)
);
CREATE INDEX IF NOT EXISTS idx_deputy_principal ON deputy_assignments(principal_id);
CREATE INDEX IF NOT EXISTS idx_deputy_deputy ON deputy_assignments(deputy_id);
CREATE INDEX IF NOT EXISTS idx_deputy_dates ON deputy_assignments(valid_from, valid_until);
`)
// Deputy Delegations (Vertretungs-Weitergaben)
db.exec(`
CREATE TABLE IF NOT EXISTS deputy_delegations (
id TEXT PRIMARY KEY,
original_assignment_id TEXT NOT NULL,
from_deputy_id TEXT NOT NULL,
to_deputy_id TEXT NOT NULL,
reason TEXT,
delegated_at TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (original_assignment_id) REFERENCES deputy_assignments(id),
FOREIGN KEY (from_deputy_id) REFERENCES employees(id),
FOREIGN KEY (to_deputy_id) REFERENCES employees(id)
);
CREATE INDEX IF NOT EXISTS idx_delegation_assignment ON deputy_delegations(original_assignment_id);
CREATE INDEX IF NOT EXISTS idx_delegation_from ON deputy_delegations(from_deputy_id);
CREATE INDEX IF NOT EXISTS idx_delegation_to ON deputy_delegations(to_deputy_id);
`)
// Audit Log für Änderungsverfolgung
db.exec(`
CREATE TABLE IF NOT EXISTS audit_log (

Datei anzeigen

@ -4,6 +4,7 @@ import helmet from 'helmet'
import dotenv from 'dotenv'
import path from 'path'
import { initializeSecureDatabase } from './config/secureDatabase'
import { initializeDatabase } from './config/database'
import authRoutes from './routes/auth'
import employeeRoutes from './routes/employeesSecure'
import profileRoutes from './routes/profiles'
@ -15,6 +16,9 @@ import workspaceRoutes from './routes/workspaces'
import userRoutes from './routes/users'
import userAdminRoutes from './routes/usersAdmin'
import settingsRoutes from './routes/settings'
import organizationRoutes from './routes/organization'
import organizationImportRoutes from './routes/organizationImport'
import employeeOrganizationRoutes from './routes/employeeOrganization'
// import bookingRoutes from './routes/bookings' // Temporär deaktiviert wegen TS-Fehlern
// import analyticsRoutes from './routes/analytics' // Temporär deaktiviert
import { errorHandler } from './middleware/errorHandler'
@ -26,8 +30,9 @@ dotenv.config()
const app = express()
const PORT = process.env.PORT || 3004
// Initialize secure database
// Initialize secure database (core tables) and extended schema (organization, deputies, etc.)
initializeSecureDatabase()
initializeDatabase()
// Initialize sync scheduler
syncScheduler
@ -57,6 +62,9 @@ app.use('/api/workspaces', workspaceRoutes)
app.use('/api/users', userRoutes)
app.use('/api/admin/users', userAdminRoutes)
app.use('/api/admin/settings', settingsRoutes)
app.use('/api/organization', organizationRoutes)
app.use('/api/organization', organizationImportRoutes)
app.use('/api', employeeOrganizationRoutes)
// app.use('/api/bookings', bookingRoutes) // Temporär deaktiviert
// app.use('/api/analytics', analyticsRoutes) // Temporär deaktiviert

Datei anzeigen

@ -0,0 +1,166 @@
import { Router } from 'express'
import { v4 as uuidv4 } from 'uuid'
import { db } from '../config/secureDatabase'
import { authenticate, AuthRequest } from '../middleware/auth'
import { logger } from '../utils/logger'
const router = Router()
// Get employee's current organization unit
router.get('/employee/:employeeId/organization', authenticate, async (req: AuthRequest, res, next) => {
try {
const { employeeId } = req.params
// Check if user can access this employee's data
if (req.user?.employeeId !== employeeId && req.user?.role !== 'admin' && req.user?.role !== 'superuser') {
return res.status(403).json({ success: false, error: { message: 'Access denied' } })
}
const unit = db.prepare(`
SELECT
ou.id, ou.code, ou.name, ou.type, ou.level,
eua.role, eua.is_primary as isPrimary,
eua.start_date as startDate
FROM employee_unit_assignments eua
JOIN organizational_units ou ON ou.id = eua.unit_id
WHERE eua.employee_id = ?
AND eua.is_primary = 1
AND (eua.end_date IS NULL OR eua.end_date > datetime('now'))
AND ou.is_active = 1
ORDER BY eua.start_date DESC
LIMIT 1
`).get(employeeId)
if (!unit) {
return res.json({ success: true, data: null })
}
res.json({ success: true, data: unit })
} catch (error) {
logger.error('Error fetching employee organization:', error)
next(error)
}
})
// Update employee's organization unit (simplified endpoint)
router.put('/employee/:employeeId/organization', authenticate, async (req: AuthRequest, res: any, next: any) => {
try {
const { employeeId } = req.params
const { unitId } = req.body
// Check permissions
if (req.user?.employeeId !== employeeId && req.user?.role !== 'admin' && req.user?.role !== 'superuser') {
return res.status(403).json({ success: false, error: { message: 'Access denied' } })
}
const now = new Date().toISOString()
// Start transaction
const transaction = db.transaction(() => {
// End all current assignments for this employee
db.prepare(`
UPDATE employee_unit_assignments
SET end_date = ?, updated_at = ?
WHERE employee_id = ? AND end_date IS NULL
`).run(now, now, employeeId)
// If unitId provided, create new assignment
if (unitId) {
// Verify unit exists
const unit = db.prepare('SELECT id FROM organizational_units WHERE id = ? AND is_active = 1').get(unitId)
if (!unit) {
throw new Error('Unit not found')
}
const assignmentId = uuidv4()
db.prepare(`
INSERT INTO employee_unit_assignments (
id, employee_id, unit_id, role,
start_date, is_primary, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
assignmentId, employeeId, unitId, 'mitarbeiter',
now, 1, now, now
)
}
// Update employee's department field for backward compatibility
if (unitId) {
const unitInfo = db.prepare('SELECT name FROM organizational_units WHERE id = ?').get(unitId) as any
if (unitInfo) {
db.prepare(`
UPDATE employees
SET department = ?, updated_at = ?
WHERE id = ?
`).run(unitInfo.name, now, employeeId)
}
} else {
// Clear department if no unit
db.prepare(`
UPDATE employees
SET department = NULL, updated_at = ?
WHERE id = ?
`).run(now, employeeId)
}
})
try {
transaction()
res.json({ success: true, message: 'Organization updated successfully' })
} catch (error: any) {
if (error.message === 'Unit not found') {
return res.status(404).json({ success: false, error: { message: 'Unit not found' } })
}
throw error
}
} catch (error) {
logger.error('Error updating employee organization:', error)
next(error)
}
})
// Get all employees in a unit (for managers)
router.get('/unit/:unitId/employees', authenticate, async (req: AuthRequest, res, next) => {
try {
const { unitId } = req.params
// Check if user is a manager of this unit or admin
const isManager = db.prepare(`
SELECT 1 FROM employee_unit_assignments
WHERE employee_id = ? AND unit_id = ?
AND role IN ('leiter', 'stellvertreter')
AND (end_date IS NULL OR end_date > datetime('now'))
`).get(req.user?.employeeId, unitId)
if (!isManager && req.user?.role !== 'admin' && req.user?.role !== 'superuser') {
return res.status(403).json({ success: false, error: { message: 'Access denied' } })
}
const employees = db.prepare(`
SELECT
e.id, e.first_name as firstName, e.last_name as lastName,
e.position, e.email, e.phone, e.photo,
eua.role, eua.is_primary as isPrimary,
eua.start_date as startDate
FROM employee_unit_assignments eua
JOIN employees e ON e.id = eua.employee_id
WHERE eua.unit_id = ?
AND (eua.end_date IS NULL OR eua.end_date > datetime('now'))
ORDER BY
CASE eua.role
WHEN 'leiter' THEN 1
WHEN 'stellvertreter' THEN 2
WHEN 'beauftragter' THEN 3
ELSE 4
END,
e.last_name, e.first_name
`).all(unitId)
res.json({ success: true, data: employees })
} catch (error) {
logger.error('Error fetching unit employees:', error)
next(error)
}
})
export default router

Datei anzeigen

@ -0,0 +1,681 @@
import { Router } from 'express'
import { v4 as uuidv4 } from 'uuid'
import { body, param, validationResult } from 'express-validator'
import { db } from '../config/secureDatabase'
import { authenticate, AuthRequest } from '../middleware/auth'
import {
OrganizationalUnit,
EmployeeUnitAssignment,
SpecialPosition,
DeputyAssignment,
DeputyDelegation
} from '@skillmate/shared'
import { logger } from '../utils/logger'
const router = Router()
// Get all organizational units
router.get('/units', authenticate, async (req: AuthRequest, res, next) => {
try {
const units = db.prepare(`
SELECT
id, code, name, type, level, parent_id as parentId,
position_x as positionX, position_y as positionY,
color, order_index as orderIndex, description,
has_fuehrungsstelle as hasFuehrungsstelle,
fuehrungsstelle_name as fuehrungsstelleName,
is_active as isActive,
created_at as createdAt,
updated_at as updatedAt
FROM organizational_units
WHERE is_active = 1
ORDER BY level, order_index, name
`).all()
res.json({ success: true, data: units })
} catch (error) {
logger.error('Error fetching organizational units:', error)
next(error)
}
})
// Get organizational hierarchy (tree structure)
router.get('/hierarchy', authenticate, async (req: AuthRequest, res, next) => {
try {
const units = db.prepare(`
SELECT
id, code, name, type, level, parent_id as parentId,
position_x as positionX, position_y as positionY,
color, order_index as orderIndex, description,
has_fuehrungsstelle as hasFuehrungsstelle,
fuehrungsstelle_name as fuehrungsstelleName,
is_active as isActive
FROM organizational_units
WHERE is_active = 1
ORDER BY level, order_index, name
`).all() as any[]
// Build tree structure
const unitMap = new Map()
const rootUnits: any[] = []
// First pass: create map
units.forEach(unit => {
unitMap.set(unit.id, { ...unit, children: [] })
})
// Second pass: build tree
units.forEach(unit => {
const node = unitMap.get(unit.id)
if (unit.parentId && unitMap.has(unit.parentId)) {
unitMap.get(unit.parentId).children.push(node)
} else {
rootUnits.push(node)
}
})
res.json({ success: true, data: rootUnits })
} catch (error) {
logger.error('Error building organizational hierarchy:', error)
next(error)
}
})
// Get single unit with employees
router.get('/units/:id', authenticate, async (req: AuthRequest, res, next) => {
try {
const unit = db.prepare(`
SELECT
id, code, name, type, level, parent_id as parentId,
position_x as positionX, position_y as positionY,
color, order_index as orderIndex, description,
has_fuehrungsstelle as hasFuehrungsstelle,
fuehrungsstelle_name as fuehrungsstelleName,
is_active as isActive,
created_at as createdAt,
updated_at as updatedAt
FROM organizational_units
WHERE id = ?
`).get(req.params.id) as any
if (!unit) {
return res.status(404).json({ success: false, error: { message: 'Unit not found' } })
}
// Get employees assigned to this unit
const employees = db.prepare(`
SELECT
e.id, e.first_name as firstName, e.last_name as lastName,
e.position, e.department, e.email, e.phone, e.photo,
eua.role, eua.is_primary as isPrimary
FROM employee_unit_assignments eua
JOIN employees e ON e.id = eua.employee_id
WHERE eua.unit_id = ? AND (eua.end_date IS NULL OR eua.end_date > datetime('now'))
ORDER BY
CASE eua.role
WHEN 'leiter' THEN 1
WHEN 'stellvertreter' THEN 2
WHEN 'beauftragter' THEN 3
ELSE 4
END,
e.last_name, e.first_name
`).all(req.params.id)
res.json({
success: true,
data: {
...unit,
employees
}
})
} catch (error) {
logger.error('Error fetching unit details:', error)
next(error)
}
})
// Create new organizational unit (Admin only)
router.post('/units',
authenticate,
[
body('code').notEmpty().trim(),
body('name').notEmpty().trim(),
body('type').isIn(['direktion', 'abteilung', 'dezernat', 'sachgebiet', 'teildezernat', 'fuehrungsstelle', 'stabsstelle', 'sondereinheit']),
body('level').isInt({ min: 0, max: 10 }),
body('parentId').optional({ checkFalsy: true }).isUUID()
],
async (req: AuthRequest, res: any, next: any) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ success: false, error: { message: 'Invalid input', details: errors.array() } })
}
// Check admin permission
if (req.user?.role !== 'admin') {
return res.status(403).json({ success: false, error: { message: 'Admin access required' } })
}
const { code, name, type, level, parentId, color, description, hasFuehrungsstelle, fuehrungsstelleName } = req.body
const now = new Date().toISOString()
const unitId = uuidv4()
// Check if code already exists
const existing = db.prepare('SELECT id FROM organizational_units WHERE code = ?').get(code)
if (existing) {
return res.status(400).json({ success: false, error: { message: 'Unit code already exists' } })
}
// Get max order index for this level
const maxOrder = db.prepare('SELECT MAX(order_index) as max FROM organizational_units WHERE level = ?').get(level) as any
const orderIndex = (maxOrder?.max || 0) + 1
db.prepare(`
INSERT INTO organizational_units (
id, code, name, type, level, parent_id,
color, order_index, description,
has_fuehrungsstelle, fuehrungsstelle_name,
is_active, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
unitId, code, name, type, level, parentId || null,
color || null, orderIndex, description || null,
hasFuehrungsstelle ? 1 : 0, fuehrungsstelleName || null,
1, now, now
)
res.json({ success: true, data: { id: unitId } })
} catch (error) {
logger.error('Error creating organizational unit:', error)
next(error)
}
}
)
// Update organizational unit (Admin only)
router.put('/units/:id',
authenticate,
[
param('id').isUUID(),
body('name').optional().notEmpty().trim(),
body('description').optional(),
body('color').optional(),
// allow updating persisted canvas positions from admin editor
body('positionX').optional().isNumeric(),
body('positionY').optional().isNumeric()
],
async (req: AuthRequest, res: any, next: any) => {
try {
if (req.user?.role !== 'admin') {
return res.status(403).json({ success: false, error: { message: 'Admin access required' } })
}
const { name, description, color, hasFuehrungsstelle, fuehrungsstelleName, positionX, positionY } = req.body
const now = new Date().toISOString()
const result = db.prepare(`
UPDATE organizational_units
SET name = COALESCE(?, name),
description = COALESCE(?, description),
color = COALESCE(?, color),
has_fuehrungsstelle = COALESCE(?, has_fuehrungsstelle),
fuehrungsstelle_name = COALESCE(?, fuehrungsstelle_name),
position_x = COALESCE(?, position_x),
position_y = COALESCE(?, position_y),
updated_at = ?
WHERE id = ?
`).run(
name || null,
description !== undefined ? description : null,
color || null,
hasFuehrungsstelle !== undefined ? (hasFuehrungsstelle ? 1 : 0) : null,
fuehrungsstelleName || null,
positionX !== undefined ? Math.round(Number(positionX)) : null,
positionY !== undefined ? Math.round(Number(positionY)) : null,
now,
req.params.id
)
if (result.changes === 0) {
return res.status(404).json({ success: false, error: { message: 'Unit not found' } })
}
res.json({ success: true })
} catch (error) {
logger.error('Error updating organizational unit:', error)
next(error)
}
}
)
// Assign employee to unit
router.post('/assignments',
authenticate,
[
body('employeeId').isUUID(),
body('unitId').isUUID(),
body('role').isIn(['leiter', 'stellvertreter', 'mitarbeiter', 'beauftragter'])
],
async (req: AuthRequest, res: any, next: any) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ success: false, error: { message: 'Invalid input', details: errors.array() } })
}
const { employeeId, unitId, role, isPrimary } = req.body
const now = new Date().toISOString()
const assignmentId = uuidv4()
// Permission model: users may only assign themselves and only as 'mitarbeiter' or 'beauftragter'
const isAdmin = req.user?.role === 'admin' || req.user?.role === 'superuser'
if (!isAdmin) {
if (!req.user?.employeeId || req.user.employeeId !== employeeId) {
return res.status(403).json({ success: false, error: { message: 'Cannot assign other employees' } })
}
if (!['mitarbeiter', 'beauftragter'].includes(role)) {
return res.status(403).json({ success: false, error: { message: 'Insufficient role to set this assignment' } })
}
}
// Validate unit exists and is active
const unit = db.prepare('SELECT id FROM organizational_units WHERE id = ? AND is_active = 1').get(unitId)
if (!unit) {
return res.status(404).json({ success: false, error: { message: 'Unit not found' } })
}
// Check if assignment already exists
const existing = db.prepare(`
SELECT id FROM employee_unit_assignments
WHERE employee_id = ? AND unit_id = ? AND role = ? AND end_date IS NULL
`).get(employeeId, unitId, role)
if (existing) {
return res.status(400).json({ success: false, error: { message: 'Assignment already exists' } })
}
// If setting as primary, demote all other active assignments for this employee
if (isPrimary) {
db.prepare(`
UPDATE employee_unit_assignments
SET is_primary = 0, updated_at = ?
WHERE employee_id = ? AND end_date IS NULL
`).run(now, employeeId)
}
db.prepare(`
INSERT INTO employee_unit_assignments (
id, employee_id, unit_id, role,
start_date, is_primary, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
assignmentId, employeeId, unitId, role,
now, isPrimary ? 1 : 0, now, now
)
res.json({ success: true, data: { id: assignmentId } })
} catch (error) {
logger.error('Error assigning employee to unit:', error)
next(error)
}
}
)
// Get my organizational units
router.get('/my-units', authenticate, async (req: AuthRequest, res, next) => {
try {
if (!req.user?.employeeId) {
return res.json({ success: true, data: [] })
}
const units = db.prepare(`
SELECT
ou.id, ou.code, ou.name, ou.type, ou.level,
eua.role, eua.is_primary as isPrimary
FROM employee_unit_assignments eua
JOIN organizational_units ou ON ou.id = eua.unit_id
WHERE eua.employee_id = ?
AND (eua.end_date IS NULL OR eua.end_date > datetime('now'))
AND ou.is_active = 1
ORDER BY eua.is_primary DESC, ou.level, ou.name
`).all(req.user.employeeId)
res.json({ success: true, data: units })
} catch (error) {
logger.error('Error fetching user units:', error)
next(error)
}
})
// Get my deputy assignments
router.get('/deputies/my', authenticate, async (req: AuthRequest, res, next) => {
try {
if (!req.user?.employeeId) {
return res.json({ success: true, data: { asDeputy: [], asPrincipal: [] } })
}
// Get assignments where I'm the deputy
const asDeputy = db.prepare(`
SELECT
da.id, da.principal_id as principalId, da.deputy_id as deputyId,
da.unit_id as unitId, da.valid_from as validFrom, da.valid_until as validUntil,
da.reason, da.can_delegate as canDelegate,
p.first_name || ' ' || p.last_name as principalName,
ou.name as unitName
FROM deputy_assignments da
JOIN employees p ON p.id = da.principal_id
LEFT JOIN organizational_units ou ON ou.id = da.unit_id
WHERE da.deputy_id = ?
AND da.valid_from <= datetime('now')
AND da.valid_until >= datetime('now')
ORDER BY da.valid_from DESC
`).all(req.user.employeeId)
// Get assignments where I'm the principal
const asPrincipal = db.prepare(`
SELECT
da.id, da.principal_id as principalId, da.deputy_id as deputyId,
da.unit_id as unitId, da.valid_from as validFrom, da.valid_until as validUntil,
da.reason,
d.first_name || ' ' || d.last_name as deputyName,
ou.name as unitName
FROM deputy_assignments da
JOIN employees d ON d.id = da.deputy_id
LEFT JOIN organizational_units ou ON ou.id = da.unit_id
WHERE da.principal_id = ?
AND da.valid_from <= datetime('now')
AND da.valid_until >= datetime('now')
ORDER BY da.valid_from DESC
`).all(req.user.employeeId)
res.json({ success: true, data: { asDeputy, asPrincipal } })
} catch (error) {
logger.error('Error fetching deputy assignments:', error)
next(error)
}
})
// Create deputy assignment
router.post('/deputies',
authenticate,
[
body('deputyId').isUUID(),
body('validFrom').isISO8601(),
body('validUntil').isISO8601()
],
async (req: AuthRequest, res: any, next: any) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ success: false, error: { message: 'Invalid input', details: errors.array() } })
}
if (!req.user?.employeeId) {
return res.status(403).json({ success: false, error: { message: 'No employee linked to user' } })
}
const { deputyId, unitId, validFrom, validUntil, reason, canDelegate } = req.body
const now = new Date().toISOString()
const assignmentId = uuidv4()
// Check for conflicts
const conflict = db.prepare(`
SELECT id FROM deputy_assignments
WHERE principal_id = ? AND deputy_id = ?
AND ((valid_from BETWEEN ? AND ?) OR (valid_until BETWEEN ? AND ?))
`).get(req.user.employeeId, deputyId, validFrom, validUntil, validFrom, validUntil)
if (conflict) {
return res.status(400).json({ success: false, error: { message: 'Conflicting assignment exists' } })
}
db.prepare(`
INSERT INTO deputy_assignments (
id, principal_id, deputy_id, unit_id,
valid_from, valid_until, reason, can_delegate,
created_by, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
assignmentId, req.user.employeeId, deputyId, unitId || null,
validFrom, validUntil, reason || null, canDelegate ? 1 : 0,
req.user.id, now, now
)
res.json({ success: true, data: { id: assignmentId } })
} catch (error) {
logger.error('Error creating deputy assignment:', error)
next(error)
}
}
)
// Alias: Create deputy assignment for current user (same as above, convenient endpoint)
router.post('/deputies/my',
authenticate,
[
body('deputyId').isUUID(),
body('validFrom').isISO8601(),
body('validUntil').optional({ nullable: true }).isISO8601(),
body('unitId').optional({ nullable: true }).isUUID()
],
async (req: AuthRequest, res: any, next: any) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ success: false, error: { message: 'Invalid input', details: errors.array() } })
}
if (!req.user?.employeeId) {
return res.status(403).json({ success: false, error: { message: 'No employee linked to user' } })
}
const { deputyId, unitId, validFrom, validUntil, reason, canDelegate } = req.body
const now = new Date().toISOString()
const assignmentId = uuidv4()
// Check for conflicts
const conflict = db.prepare(`
SELECT id FROM deputy_assignments
WHERE principal_id = ? AND deputy_id = ?
AND ((valid_from BETWEEN ? AND ?) OR (valid_until BETWEEN ? AND ?))
`).get(req.user.employeeId, deputyId, validFrom, validUntil || validFrom, validFrom, validUntil || validFrom)
if (conflict) {
return res.status(400).json({ success: false, error: { message: 'Conflicting assignment exists' } })
}
db.prepare(`
INSERT INTO deputy_assignments (
id, principal_id, deputy_id, unit_id,
valid_from, valid_until, reason, can_delegate,
created_by, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
assignmentId, req.user.employeeId, deputyId, unitId || null,
validFrom, validUntil || validFrom, reason || null, canDelegate ? 1 : 0,
req.user.id, now, now
)
res.json({ success: true, data: { id: assignmentId } })
} catch (error) {
logger.error('Error creating deputy assignment (my):', error)
next(error)
}
}
)
// Delegate deputy assignment
router.post('/deputies/delegate',
authenticate,
[
body('assignmentId').isUUID(),
body('toDeputyId').isUUID(),
body('reason').optional().trim()
],
async (req: AuthRequest, res: any, next: any) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ success: false, error: { message: 'Invalid input', details: errors.array() } })
}
if (!req.user?.employeeId) {
return res.status(403).json({ success: false, error: { message: 'No employee linked to user' } })
}
const { assignmentId, toDeputyId, reason } = req.body
const now = new Date().toISOString()
const delegationId = uuidv4()
// Check if user can delegate this assignment
const assignment = db.prepare(`
SELECT * FROM deputy_assignments
WHERE id = ? AND deputy_id = ? AND can_delegate = 1
AND valid_from <= datetime('now')
AND valid_until >= datetime('now')
`).get(assignmentId, req.user.employeeId) as any
if (!assignment) {
return res.status(403).json({ success: false, error: { message: 'Cannot delegate this assignment' } })
}
db.prepare(`
INSERT INTO deputy_delegations (
id, original_assignment_id, from_deputy_id, to_deputy_id,
reason, delegated_at, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(
delegationId, assignmentId, req.user.employeeId, toDeputyId,
reason || null, now, now
)
res.json({ success: true, data: { id: delegationId } })
} catch (error) {
logger.error('Error delegating deputy assignment:', error)
next(error)
}
}
)
// Delete deputy assignment (principal or admin)
router.delete('/deputies/:id', authenticate, async (req: AuthRequest, res: any, next: any) => {
try {
const { id } = req.params
if (!id) {
return res.status(400).json({ success: false, error: { message: 'Missing id' } })
}
// Load assignment
const assignment = db.prepare(`
SELECT id, principal_id as principalId FROM deputy_assignments WHERE id = ?
`).get(id) as any
if (!assignment) {
return res.status(404).json({ success: false, error: { message: 'Assignment not found' } })
}
// Permission: principal or admin/superuser
const isAdmin = req.user?.role === 'admin' || req.user?.role === 'superuser'
if (!isAdmin && req.user?.employeeId !== assignment.principalId) {
return res.status(403).json({ success: false, error: { message: 'Access denied' } })
}
// Delete delegations first (FK not ON DELETE CASCADE)
db.prepare('DELETE FROM deputy_delegations WHERE original_assignment_id = ?').run(id)
// Delete assignment
db.prepare('DELETE FROM deputy_assignments WHERE id = ?').run(id)
return res.json({ success: true })
} catch (error) {
logger.error('Error deleting deputy assignment:', error)
next(error)
}
})
// Get deputy chain for an assignment
router.get('/deputies/chain/:id', authenticate, async (req: AuthRequest, res, next) => {
try {
const chain = []
// Get original assignment
const assignment = db.prepare(`
SELECT
da.*,
p.first_name || ' ' || p.last_name as principalName,
d.first_name || ' ' || d.last_name as deputyName
FROM deputy_assignments da
JOIN employees p ON p.id = da.principal_id
JOIN employees d ON d.id = da.deputy_id
WHERE da.id = ?
`).get(req.params.id) as any
if (!assignment) {
return res.status(404).json({ success: false, error: { message: 'Assignment not found' } })
}
chain.push({
type: 'original',
from: assignment.principalName,
to: assignment.deputyName,
reason: assignment.reason
})
// Get all delegations
const delegations = db.prepare(`
SELECT
dd.*,
f.first_name || ' ' || f.last_name as fromName,
t.first_name || ' ' || t.last_name as toName
FROM deputy_delegations dd
JOIN employees f ON f.id = dd.from_deputy_id
JOIN employees t ON t.id = dd.to_deputy_id
WHERE dd.original_assignment_id = ?
ORDER BY dd.delegated_at
`).all(req.params.id) as any[]
delegations.forEach(del => {
chain.push({
type: 'delegation',
from: del.fromName,
to: del.toName,
reason: del.reason,
delegatedAt: del.delegated_at
})
})
res.json({ success: true, data: chain })
} catch (error) {
logger.error('Error fetching deputy chain:', error)
next(error)
}
})
// Get special positions
router.get('/special-positions', authenticate, async (req: AuthRequest, res, next) => {
try {
const positions = db.prepare(`
SELECT
sp.id, sp.position_type as positionType,
sp.employee_id as employeeId,
sp.unit_id as unitId,
sp.start_date as startDate,
sp.end_date as endDate,
e.first_name || ' ' || e.last_name as employeeName,
e.photo,
ou.name as unitName
FROM special_positions sp
JOIN employees e ON e.id = sp.employee_id
LEFT JOIN organizational_units ou ON ou.id = sp.unit_id
WHERE sp.is_active = 1
AND (sp.end_date IS NULL OR sp.end_date > datetime('now'))
ORDER BY sp.position_type, e.last_name
`).all()
res.json({ success: true, data: positions })
} catch (error) {
logger.error('Error fetching special positions:', error)
next(error)
}
})
export default router

Datei anzeigen

@ -0,0 +1,399 @@
import { Router } from 'express'
import multer from 'multer'
import { v4 as uuidv4 } from 'uuid'
import fs from 'fs'
import path from 'path'
const pdfParse = require('pdf-parse')
import { db } from '../config/secureDatabase'
import { authenticate, authorize, AuthRequest } from '../middleware/auth'
import { logger } from '../utils/logger'
const router = Router()
// Configure multer for PDF uploads
const upload = multer({
dest: 'uploads/temp/',
limits: {
fileSize: 10 * 1024 * 1024 // 10MB limit
},
fileFilter: (req, file, cb) => {
if (file.mimetype === 'application/pdf') {
cb(null, true)
} else {
cb(new Error('Only PDF files are allowed'))
}
}
})
// Helper to parse organizational structure from PDF text
function parseOrganizationFromText(text: string) {
const units: any[] = []
const lines = text.split('\n').map(line => line.trim()).filter(line => line && line.length > 2)
// Pattern matching for different organizational levels
const patterns = {
direktor: /(Direktor|Director)\s*(LKA)?/i,
abteilung: /^Abteilung\s+(\d+)/i,
dezernat: /^Dezernat\s+([\d]+)/i,
sachgebiet: /^SG\s+([\d\.]+)/i,
teildezernat: /^TD\s+([\d\.]+)/i,
stabsstelle: /(Leitungsstab|LStab|Führungsgruppe)/i,
fahndung: /Fahndungsgruppe/i,
sondereinheit: /(Personalrat|Schwerbehindertenvertretung|beauftragt|Innenrevision|IUK-Lage)/i
}
// Color mapping for departments
const colors: Record<string, string> = {
'1': '#dc2626',
'2': '#ea580c',
'3': '#0891b2',
'4': '#7c3aed',
'5': '#0d9488',
'6': '#be185d',
'ZA': '#6b7280'
}
let currentAbteilung: any = null
let currentDezernat: any = null
lines.forEach(line => {
// Check for Direktor
if (patterns.direktor.test(line)) {
units.push({
code: 'DIR',
name: 'Direktor LKA NRW',
type: 'direktion',
level: 0,
parentId: null,
color: '#1e3a8a'
})
}
// Check for Abteilung
const abtMatch = line.match(/Abteilung\s+(\d+|Zentralabteilung)/i)
if (abtMatch) {
const abtNum = abtMatch[1] === 'Zentralabteilung' ? 'ZA' : abtMatch[1]
const abtName = line.replace(/^Abteilung\s+\d+\s*[-–]\s*/, '')
currentAbteilung = {
code: `Abt ${abtNum}`,
name: abtName || `Abteilung ${abtNum}`,
type: 'abteilung',
level: 1,
parentId: 'DIR',
color: colors[abtNum] || '#6b7280',
hasFuehrungsstelle: abtNum !== 'ZA'
}
units.push(currentAbteilung)
currentDezernat = null
}
// Check for Dezernat
const dezMatch = line.match(/(?:Dezernat|Dez)\s+([\d\s]+)/i)
if (dezMatch && currentAbteilung) {
const dezNum = dezMatch[1].trim()
const dezName = line.replace(/^(?:Dezernat|Dez)\s+[\d\s]+\s*[-–]?\s*/, '').trim()
currentDezernat = {
code: `Dez ${dezNum}`,
name: dezName || `Dezernat ${dezNum}`,
type: 'dezernat',
level: 2,
parentId: currentAbteilung.code
}
units.push(currentDezernat)
}
// Check for Sachgebiet
const sgMatch = line.match(/SG\s+([\d\.]+)/i)
if (sgMatch && currentDezernat) {
const sgNum = sgMatch[1]
const sgName = line.replace(/^SG\s+[\d\.]+\s*[-–]?\s*/, '').trim()
units.push({
code: `SG ${sgNum}`,
name: sgName || `Sachgebiet ${sgNum}`,
type: 'sachgebiet',
level: 3,
parentId: currentDezernat.code
})
}
// Check for Teildezernat
const tdMatch = line.match(/TD\s+([\d\.]+)/i)
if (tdMatch && currentDezernat) {
const tdNum = tdMatch[1]
const tdName = line.replace(/^TD\s+[\d\.]+\s*[-–]?\s*/, '').trim()
units.push({
code: `TD ${tdNum}`,
name: tdName || `Teildezernat ${tdNum}`,
type: 'teildezernat',
level: 3,
parentId: currentDezernat.code
})
}
// Check for Stabsstelle
if (patterns.stabsstelle.test(line)) {
units.push({
code: 'LStab',
name: 'Leitungsstab',
type: 'stabsstelle',
level: 1,
parentId: 'DIR',
color: '#6b7280'
})
}
// Check for Sondereinheiten (non-hierarchical)
if (patterns.sondereinheit.test(line)) {
let code = 'SE'
let name = line
if (line.includes('Personalrat')) {
code = 'PR'
name = 'Personalrat'
} else if (line.includes('Schwerbehindertenvertretung')) {
code = 'SBV'
name = 'Schwerbehindertenvertretung'
} 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'
})
}
})
return units
}
// Import organization from PDF
router.post('/import-pdf',
authenticate,
authorize('admin'),
upload.single('pdf'),
async (req: AuthRequest, res: any, next: any) => {
let tempFilePath: string | null = null
try {
if (!req.file) {
return res.status(400).json({
success: false,
error: { message: 'No PDF file uploaded' }
})
}
tempFilePath = req.file.path
// Read PDF file
const pdfBuffer = fs.readFileSync(tempFilePath)
// Parse PDF using pdf-parse
let extractedText = ''
try {
const pdfData = await pdfParse(pdfBuffer)
extractedText = pdfData.text
logger.info(`PDF parsed successfully: ${pdfData.numpages} pages, ${extractedText.length} characters`)
} catch (parseError) {
logger.error('PDF parsing error:', parseError)
// Fallback to simulated data if parsing fails
extractedText = `
Direktor LKA NRW
Leitungsstab
Personalrat
Schwerbehindertenvertretung
Abteilung 1 - Organisierte Kriminalität
Dezernat 11 - Ermittlungen OK
SG 11.1 - Grundsatzfragen
Dezernat 12 - Wirtschaftskriminalität
Abteilung 2 - Terrorismusbekämpfung
Dezernat 21 - Ermittlungen
Abteilung 3 - Strategische Kriminalitätsbekämpfung
Abteilung 4 - Cybercrime
Abteilung 5 - Kriminalwissenschaftliches Institut
Abteilung 6 - Fachaufsicht
Zentralabteilung
`
}
// Parse the organizational structure
const parsedUnits = parseOrganizationFromText(extractedText)
if (parsedUnits.length === 0) {
return res.status(400).json({
success: false,
error: { message: 'No organizational units could be extracted from the PDF' }
})
}
// Clear existing organization (optional - could be a parameter)
if (req.body.clearExisting === 'true') {
db.prepare('DELETE FROM organizational_units').run()
}
// Prepare insert/update with stable parent references and FK-safe order
const now = new Date().toISOString()
const unitIdMap: Record<string, string> = {}
// Preload existing IDs by code
for (const unit of parsedUnits) {
const existing = db.prepare('SELECT id FROM organizational_units WHERE code = ?').get(unit.code) as any
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 tx = db.transaction(() => {
sorted.forEach((unit, index) => {
const parentId = unit.parentId ? unitIdMap[unit.parentId] : null
const existing = db.prepare('SELECT id FROM organizational_units WHERE code = ?').get(unit.code) as any
if (existing) {
db.prepare(`
UPDATE organizational_units
SET name = ?, type = ?, level = ?, parent_id = ?,
color = ?, order_index = ?, has_fuehrungsstelle = ?,
is_active = 1, updated_at = ?
WHERE id = ?
`).run(
unit.name,
unit.type,
unit.level,
parentId,
unit.color || null,
index,
unit.hasFuehrungsstelle ? 1 : 0,
now,
existing.id
)
} else {
db.prepare(`
INSERT INTO organizational_units (
id, code, name, type, level, parent_id,
color, order_index, has_fuehrungsstelle,
is_active, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
unitIdMap[unit.code],
unit.code,
unit.name,
unit.type,
unit.level,
parentId,
unit.color || null,
index,
unit.hasFuehrungsstelle ? 1 : 0,
1,
now,
now
)
}
})
})
tx()
res.json({
success: true,
data: {
message: `Successfully imported ${parsedUnits.length} organizational units`,
unitsImported: parsedUnits.length,
units: parsedUnits.map(u => ({
code: u.code,
name: u.name,
type: u.type
}))
}
})
} catch (error) {
logger.error('Error importing PDF:', error)
res.status(500).json({
success: false,
error: { message: 'Failed to import PDF: ' + (error as Error).message }
})
} finally {
// Clean up temp file
if (tempFilePath && fs.existsSync(tempFilePath)) {
fs.unlinkSync(tempFilePath)
}
}
}
)
// Import from structured JSON
router.post('/import-json',
authenticate,
authorize('admin'),
async (req: AuthRequest, res: any, next: any) => {
try {
const { units, clearExisting } = req.body
if (!units || !Array.isArray(units)) {
return res.status(400).json({
success: false,
error: { message: 'Invalid units data' }
})
}
// Clear existing if requested
if (clearExisting) {
db.prepare('DELETE FROM organizational_units').run()
}
// Import units
const now = new Date().toISOString()
units.forEach((unit: any, index: number) => {
const id = uuidv4()
db.prepare(`
INSERT INTO organizational_units (
id, code, name, type, level, parent_id,
color, order_index, description,
has_fuehrungsstelle, is_active, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id,
unit.code,
unit.name,
unit.type,
unit.level,
unit.parentId || null,
unit.color || null,
index,
unit.description || null,
unit.hasFuehrungsstelle ? 1 : 0,
1,
now,
now
)
})
res.json({
success: true,
data: {
message: `Successfully imported ${units.length} units`,
count: units.length
}
})
} catch (error) {
logger.error('Error importing JSON:', error)
next(error)
}
}
)
export default router

Datei anzeigen

@ -4,7 +4,6 @@ import bcrypt from 'bcrypt'
import { v4 as uuidv4 } from 'uuid'
import { db, encryptedDb } from '../config/secureDatabase'
import { authenticate, authorize, AuthRequest } from '../middleware/auth'
import { requirePermission } from '../middleware/roleAuth'
import { User, UserRole } from '@skillmate/shared'
import { FieldEncryption } from '../services/encryption'
import { emailService } from '../services/emailService'
@ -13,7 +12,7 @@ import { logger } from '../utils/logger'
const router = Router()
// Get all users (admin only)
router.get('/', authenticate, requirePermission('users:read'), async (req: AuthRequest, res, next) => {
router.get('/', authenticate, authorize('admin', 'superuser'), async (req: AuthRequest, res, next) => {
try {
const users = db.prepare(`
SELECT id, username, email, role, employee_id, last_login, is_active, created_at, updated_at
@ -57,7 +56,7 @@ router.get('/', authenticate, requirePermission('users:read'), async (req: AuthR
// Update user role (admin only)
router.put('/:id/role',
authenticate,
requirePermission('users:update'),
authorize('admin'),
[
body('role').isIn(['admin', 'superuser', 'user'])
],
@ -109,7 +108,7 @@ router.put('/:id/role',
// Bulk create users from employees
router.post('/bulk-create-from-employees',
authenticate,
requirePermission('users:create'),
authorize('admin'),
[
body('employeeIds').isArray({ min: 1 }),
body('role').isIn(['admin', 'superuser', 'user'])
@ -189,7 +188,7 @@ router.post('/bulk-create-from-employees',
// Update user status (admin only)
router.put('/:id/status',
authenticate,
requirePermission('users:update'),
authorize('admin'),
[
body('isActive').isBoolean()
],
@ -241,7 +240,7 @@ router.put('/:id/status',
// Reset user password (admin only)
router.post('/:id/reset-password',
authenticate,
requirePermission('users:update'),
authorize('admin'),
[
body('newPassword').optional().isLength({ min: 8 })
],
@ -294,7 +293,7 @@ router.post('/:id/reset-password',
// Delete user (admin only)
router.delete('/:id',
authenticate,
requirePermission('users:delete'),
authorize('admin'),
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params
@ -333,7 +332,7 @@ export default router
// Create user account from existing employee (admin only)
router.post('/create-from-employee',
authenticate,
requirePermission('users:create'),
authorize('admin'),
[
body('employeeId').notEmpty().isString(),
body('username').optional().isString().isLength({ min: 3 }),
@ -452,7 +451,7 @@ router.post('/purge',
// Send temporary password via email to user's email
router.post('/:id/send-temp-password',
authenticate,
requirePermission('users:update'),
authorize('admin'),
[
body('password').notEmpty().isString().isLength({ min: 6 })
],

Datei anzeigen

@ -1,7 +1,7 @@
import { db } from '../../src/config/secureDatabase'
import { db } from '../../config/secureDatabase'
import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'
import { FieldEncryption } from '../../src/services/encryption'
import { FieldEncryption } from '../../services/encryption'
import { User, LoginResponse } from '@skillmate/shared'
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'

107
backend/test-organigramm.txt Normale Datei
Datei anzeigen

@ -0,0 +1,107 @@
LKA NRW Organigramm
Direktor LKA NRW
Führungsstelle Direktor
Leitungsstab
- Presse- und Öffentlichkeitsarbeit
- Controlling
- Innenrevision
Personalrat
Schwerbehindertenvertretung
Datenschutzbeauftragter
Gleichstellungsbeauftragte
Abteilung 1 - Organisierte Kriminalität
Führungsstelle Abt. 1
Dezernat 11 - Ermittlungen OK
SG 11.1 - Grundsatzfragen OK
SG 11.2 - Operative Auswertung
SG 11.3 - Finanzermittlungen
Dezernat 12 - Wirtschaftskriminalität
SG 12.1 - Komplexe Wirtschaftsverfahren
SG 12.2 - Korruptionsdelikte
SG 12.3 - Vermögensabschöpfung
Dezernat 13 - Rauschgiftkriminalität
SG 13.1 - Internationale Drogenhandel
SG 13.2 - Synthetische Drogen
TD 13.3 - Darknet-Ermittlungen
Abteilung 2 - Terrorismusbekämpfung / Staatsschutz
Führungsstelle Abt. 2
Dezernat 21 - Islamistischer Terrorismus
SG 21.1 - Operative Ermittlungen
SG 21.2 - Gefährderanalyse
Dezernat 22 - Politisch motivierte Kriminalität
SG 22.1 - Rechtsextremismus
SG 22.2 - Linksextremismus
SG 22.3 - Ausländerextremismus
Abteilung 3 - Strategische Kriminalitätsbekämpfung
Führungsstelle Abt. 3
Dezernat 31 - Analyse und Auswertung
SG 31.1 - Strategische Analyse
SG 31.2 - Lagebilderstellung
Dezernat 32 - Informationsmanagement
SG 32.1 - Datenbanken und Systeme
SG 32.2 - Informationsaustausch
Abteilung 4 - Cybercrime
Führungsstelle Abt. 4
Dezernat 41 - Digitale Ermittlungen
SG 41.1 - Internetkriminalität
SG 41.2 - Digitale Forensik
Dezernat 42 - Cybersecurity
SG 42.1 - Critical Infrastructure
SG 42.2 - Incident Response
Abteilung 5 - Kriminalwissenschaftliches Institut
Führungsstelle Abt. 5
Dezernat 51 - Kriminaltechnik
SG 51.1 - DNA-Analytik
SG 51.2 - Daktyloskopie
SG 51.3 - Ballistik
Dezernat 52 - Digitale Forensik
SG 52.1 - Mobile Forensik
SG 52.2 - Computer Forensik
Abteilung 6 - Fachaufsicht
Führungsstelle Abt. 6
Dezernat 61 - Qualitätsmanagement
SG 61.1 - Standards und Richtlinien
SG 61.2 - Evaluierung
Dezernat 62 - Aus- und Fortbildung
SG 62.1 - Fachschulungen
SG 62.2 - Führungskräfteentwicklung
Zentralabteilung
Dezernat ZA 1 - Personal
SG ZA 1.1 - Personalverwaltung
SG ZA 1.2 - Personalentwicklung
Dezernat ZA 2 - Haushalt und Finanzen
SG ZA 2.1 - Haushaltsplanung
SG ZA 2.2 - Beschaffung
Dezernat ZA 3 - IT und Digitalisierung
SG ZA 3.1 - IT-Infrastruktur
SG ZA 3.2 - Digitale Transformation
Dezernat ZA 4 - Organisation und Recht
SG ZA 4.1 - Organisationsentwicklung
SG ZA 4.2 - Rechtsangelegenheiten

Datei anzeigen

@ -0,0 +1,336 @@
import { useState, useEffect } from 'react'
import api from '../services/api'
import { DeputyAssignment, Employee } from '@skillmate/shared'
import { useAuthStore } from '../stores/authStore'
export default function DeputyManagement() {
const [asPrincipal, setAsPrincipal] = useState<any[]>([])
const [asDeputy, setAsDeputy] = useState<any[]>([])
const [availableEmployees, setAvailableEmployees] = useState<Employee[]>([])
const [showAddDialog, setShowAddDialog] = useState(false)
const [loading, setLoading] = useState(true)
const [formData, setFormData] = useState({
deputyId: '',
validFrom: new Date().toISOString().split('T')[0],
validUntil: '',
reason: '',
canDelegate: true,
unitId: ''
})
const { user } = useAuthStore()
useEffect(() => {
loadDeputies()
loadEmployees()
}, [])
const loadDeputies = async () => {
try {
const response = await api.get('/organization/deputies/my')
if (response.data.success) {
// Backend returns { asDeputy, asPrincipal }
setAsPrincipal(response.data.data.asPrincipal || [])
setAsDeputy(response.data.data.asDeputy || [])
}
} catch (error) {
console.error('Failed to load deputies:', error)
} finally {
setLoading(false)
}
}
const loadEmployees = async () => {
try {
const response = await api.get('/employees/public')
if (response.data.success) {
setAvailableEmployees(response.data.data)
}
} catch (error) {
console.error('Failed to load employees:', error)
}
}
const handleAddDeputy = async () => {
try {
const response = await api.post('/organization/deputies/my', {
...formData,
validFrom: new Date(formData.validFrom).toISOString(),
validUntil: formData.validUntil ? new Date(formData.validUntil).toISOString() : null
})
if (response.data.success) {
await loadDeputies()
setShowAddDialog(false)
resetForm()
}
} catch (error) {
console.error('Failed to add deputy:', error)
}
}
const handleRemoveDeputy = async (id: string) => {
if (!confirm('Möchten Sie diese Vertretung wirklich entfernen?')) return
try {
await api.delete(`/organization/deputies/${id}`)
await loadDeputies()
} catch (error) {
console.error('Failed to remove deputy:', error)
}
}
const handleDelegate = async (assignmentId: string) => {
const toDeputyId = prompt('Bitte geben Sie die Mitarbeiter-ID des neuen Vertreters ein:')
if (!toDeputyId) return
const reason = prompt('Grund für die Weitergabe (optional):')
try {
const response = await api.post('/organization/deputies/delegate', {
assignmentId,
toDeputyId,
validFrom: new Date().toISOString(),
reason
})
if (response.data.success) {
await loadDeputies()
alert('Vertretung erfolgreich weitergegeben')
}
} catch (error) {
console.error('Failed to delegate:', error)
alert('Fehler beim Weitergeben der Vertretung')
}
}
const resetForm = () => {
setFormData({
deputyId: '',
validFrom: new Date().toISOString().split('T')[0],
validUntil: '',
reason: '',
canDelegate: true,
unitId: ''
})
}
if (loading) {
return <div className="p-4">Lade Vertretungen...</div>
}
return (
<div className="p-6">
<div className="mb-6">
<h2 className="text-2xl font-bold mb-2 dark:text-white">Meine Vertretungen</h2>
<p className="text-gray-600 dark:text-gray-400">
Verwalten Sie hier Ihre Vertretungsregelungen
</p>
</div>
{/* Current Deputies */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold dark:text-white">Aktuelle Vertretungen</h3>
<button
onClick={() => setShowAddDialog(true)}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
+ Vertretung hinzufügen
</button>
</div>
{asPrincipal.length > 0 ? (
<div className="space-y-3">
{asPrincipal.map((deputy: any) => (
<div
key={deputy.id}
className="border dark:border-gray-700 rounded-lg p-4 flex items-center justify-between"
>
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center">
👤
</div>
<div>
<p className="font-medium dark:text-white">{deputy.deputyName}</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{new Date(deputy.validFrom).toLocaleDateString('de-DE')}
{deputy.validUntil && ` - ${new Date(deputy.validUntil).toLocaleDateString('de-DE')}`}
</p>
{deputy.reason && (
<p className="text-sm text-gray-500 dark:text-gray-500">
Grund: {deputy.reason}
</p>
)}
{deputy.unitName && (
<p className="text-sm text-gray-500 dark:text-gray-500">
Für: {deputy.unitName}
</p>
)}
</div>
</div>
<div className="flex gap-2">
{(deputy.canDelegate === 1 || deputy.canDelegate === true) && (
<button
onClick={() => handleDelegate(deputy.id)}
className="px-3 py-1 text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded"
title="Vertretung weitergeben"
>
Weitergeben
</button>
)}
<button
onClick={() => handleRemoveDeputy(deputy.id)}
className="px-3 py-1 text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded"
>
Entfernen
</button>
</div>
</div>
))}
</div>
) : (
<p className="text-gray-500 dark:text-gray-400">
Keine aktiven Vertretungen vorhanden
</p>
)}
</div>
{/* Add Deputy Dialog */}
{showAddDialog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md w-full">
<h3 className="text-xl font-bold mb-4 dark:text-white">
Vertretung hinzufügen
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1 dark:text-gray-300">
Vertreter *
</label>
<select
value={formData.deputyId}
onChange={(e) => setFormData({ ...formData, deputyId: e.target.value })}
className="w-full px-3 py-2 border rounded dark:bg-gray-700 dark:border-gray-600"
required
>
<option value="">Bitte wählen...</option>
{availableEmployees.map(emp => (
<option key={emp.id} value={emp.id}>
{emp.firstName} {emp.lastName} - {emp.position}
</option>
))}
</select>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1 dark:text-gray-300">
Von *
</label>
<input
type="date"
value={formData.validFrom}
onChange={(e) => setFormData({ ...formData, validFrom: e.target.value })}
className="w-full px-3 py-2 border rounded dark:bg-gray-700 dark:border-gray-600"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-1 dark:text-gray-300">
Bis (optional)
</label>
<input
type="date"
value={formData.validUntil}
onChange={(e) => setFormData({ ...formData, validUntil: e.target.value })}
className="w-full px-3 py-2 border rounded dark:bg-gray-700 dark:border-gray-600"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-1 dark:text-gray-300">
Grund (optional)
</label>
<select
value={formData.reason}
onChange={(e) => setFormData({ ...formData, reason: e.target.value })}
className="w-full px-3 py-2 border rounded dark:bg-gray-700 dark:border-gray-600"
>
<option value="">Kein Grund angegeben</option>
<option value="Urlaub">Urlaub</option>
<option value="Dienstreise">Dienstreise</option>
<option value="Fortbildung">Fortbildung</option>
<option value="Krankheit">Krankheit</option>
<option value="Sonstiges">Sonstiges</option>
</select>
</div>
<div>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={formData.canDelegate}
onChange={(e) => setFormData({ ...formData, canDelegate: e.target.checked })}
className="rounded"
/>
<span className="text-sm dark:text-gray-300">
Vertreter darf Vertretung weitergeben
</span>
</label>
</div>
</div>
<div className="flex justify-end gap-2 mt-6">
<button
onClick={() => {
setShowAddDialog(false)
resetForm()
}}
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800"
>
Abbrechen
</button>
<button
onClick={handleAddDeputy}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Hinzufügen
</button>
</div>
</div>
</div>
)}
{/* My Deputy Roles */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4 dark:text-white">
Ich vertrete
</h3>
{asDeputy.length > 0 ? (
<div className="space-y-3">
{asDeputy.map((item: any) => (
<div key={item.id} className="border dark:border-gray-700 rounded-lg p-4 flex items-center justify-between">
<div>
<p className="font-medium dark:text-white">{item.principalName}</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{new Date(item.validFrom).toLocaleDateString('de-DE')}
{item.validUntil && ` - ${new Date(item.validUntil).toLocaleDateString('de-DE')}`}
</p>
{item.unitName && (
<p className="text-sm text-gray-500 dark:text-gray-500">Für: {item.unitName}</p>
)}
</div>
</div>
))}
</div>
) : (
<p className="text-gray-500 dark:text-gray-400 text-sm">
Keine Einträge.
</p>
)}
</div>
</div>
)
}

Datei anzeigen

@ -5,6 +5,8 @@ import { useAuthStore } from '../stores/authStore'
import { authApi } from '../services/api'
import { usePermissions } from '../hooks/usePermissions'
import WindowControls from './WindowControls'
import OrganizationChart from './OrganizationChart'
import { Building2 } from 'lucide-react'
export default function Header() {
const { isDarkMode, toggleTheme } = useThemeStore()
@ -14,6 +16,7 @@ export default function Header() {
const [loginForm, setLoginForm] = useState({ username: '', password: '' })
const [loginError, setLoginError] = useState('')
const [loginLoading, setLoginLoading] = useState(false)
const [showOrganigramm, setShowOrganigramm] = useState(false)
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
@ -119,6 +122,14 @@ export default function Header() {
Admin
</a>
)}
<button
onClick={() => setShowOrganigramm(true)}
className="btn-secondary text-sm px-3 py-1 h-8 flex items-center gap-1"
title="Organigramm anzeigen"
>
<Building2 className="w-4 h-4" />
<span className="hidden sm:inline">Organigramm</span>
</button>
<button
onClick={handleLogout}
className="btn-secondary text-sm px-3 py-1 h-8"
@ -152,6 +163,11 @@ export default function Header() {
</div>
<WindowControls />
{/* Organization Chart Modal */}
{showOrganigramm && (
<OrganizationChart onClose={() => setShowOrganigramm(false)} />
)}
</header>
)
}

Datei anzeigen

@ -0,0 +1,394 @@
import { useEffect, useState, useCallback } from 'react'
import { OrganizationalUnit, EmployeeUnitAssignment } from '@skillmate/shared'
import api from '../services/api'
import { useAuthStore } from '../stores/authStore'
interface OrganizationChartProps {
onClose: () => void
}
export default function OrganizationChart({ onClose }: OrganizationChartProps) {
const [units, setUnits] = useState<OrganizationalUnit[]>([])
const [hierarchy, setHierarchy] = useState<any[]>([])
const [selectedUnit, setSelectedUnit] = useState<OrganizationalUnit | null>(null)
const [unitEmployees, setUnitEmployees] = useState<any[]>([])
const [myUnits, setMyUnits] = useState<any[]>([])
const [searchTerm, setSearchTerm] = useState('')
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [message, setMessage] = useState<string>('')
const [zoomLevel, setZoomLevel] = useState(1)
const [panPosition, setPanPosition] = useState({ x: 0, y: 0 })
const [isDragging, setIsDragging] = useState(false)
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
const { user } = useAuthStore()
useEffect(() => {
loadOrganization()
loadMyUnits()
}, [])
const loadOrganization = async () => {
try {
const response = await api.get('/organization/hierarchy')
if (response.data.success) {
setHierarchy(response.data.data)
}
} catch (error) {
console.error('Failed to load organization:', error)
} finally {
setLoading(false)
}
}
const loadMyUnits = async () => {
try {
const response = await api.get('/organization/my-units')
if (response.data.success) {
setMyUnits(response.data.data)
}
} catch (error) {
console.error('Failed to load my units:', error)
}
}
const loadUnitDetails = async (unitId: string) => {
try {
const response = await api.get(`/organization/units/${unitId}`)
if (response.data.success) {
setSelectedUnit(response.data.data)
setUnitEmployees(response.data.data.employees || [])
}
} catch (error) {
console.error('Failed to load unit details:', error)
}
}
const assignMeToSelectedUnit = async () => {
if (!selectedUnit || !user?.employeeId) return
setSaving(true)
setMessage('')
try {
await api.post('/organization/assignments', {
employeeId: user.employeeId,
unitId: selectedUnit.id,
role: 'mitarbeiter',
isPrimary: true
})
setMessage('Position erfolgreich gesetzt.')
await loadMyUnits()
await loadUnitDetails(selectedUnit.id)
} catch (error: any) {
const msg = error?.response?.data?.error?.message || 'Zuweisung fehlgeschlagen'
setMessage(`Fehler: ${msg}`)
} finally {
setSaving(false)
setTimeout(() => setMessage(''), 3000)
}
}
const getTypeIcon = (type: string) => {
const icons: Record<string, string> = {
direktion: '🏛️',
abteilung: '🏢',
dezernat: '📁',
sachgebiet: '📋',
teildezernat: '🔧',
fuehrungsstelle: '⭐',
stabsstelle: '🎯',
sondereinheit: '🛡️'
}
return icons[type] || '📄'
}
const getUnitColor = (level: number) => {
const colors = [
'bg-gradient-to-r from-purple-500 to-purple-700',
'bg-gradient-to-r from-pink-500 to-pink-700',
'bg-gradient-to-r from-blue-500 to-blue-700',
'bg-gradient-to-r from-green-500 to-green-700',
'bg-gradient-to-r from-orange-500 to-orange-700',
'bg-gradient-to-r from-teal-500 to-teal-700'
]
return colors[level % colors.length]
}
const handleUnitClick = (unit: OrganizationalUnit) => {
setSelectedUnit(unit)
loadUnitDetails(unit.id)
}
const handleZoomIn = () => setZoomLevel(prev => Math.min(prev + 0.2, 3))
const handleZoomOut = () => setZoomLevel(prev => Math.max(prev - 0.2, 0.3))
const handleResetView = () => {
setZoomLevel(1)
setPanPosition({ x: 0, y: 0 })
}
const handleMouseDown = (e: React.MouseEvent) => {
if (e.button === 0) { // Left click only
setIsDragging(true)
setDragStart({ x: e.clientX - panPosition.x, y: e.clientY - panPosition.y })
}
}
const handleMouseMove = (e: React.MouseEvent) => {
if (isDragging) {
setPanPosition({
x: e.clientX - dragStart.x,
y: e.clientY - dragStart.y
})
}
}
const handleMouseUp = () => {
setIsDragging(false)
}
const renderUnit = (unit: any, level: number = 0) => {
const isMyUnit = myUnits.some(u => u.id === unit.id)
const isSearchMatch = searchTerm && unit.name.toLowerCase().includes(searchTerm.toLowerCase())
return (
<div key={unit.id} className="flex flex-col items-center">
<div
onClick={() => handleUnitClick(unit)}
className={`
cursor-pointer transform transition-all duration-200 hover:scale-105
${isMyUnit ? 'ring-4 ring-yellow-400' : ''}
${isSearchMatch ? 'ring-4 ring-blue-400 animate-pulse' : ''}
`}
>
<div className={`rounded-lg shadow-lg overflow-hidden bg-white dark:bg-gray-800 min-w-[200px] max-w-[250px]`}>
<div className={`p-2 text-white ${getUnitColor(level)}`}>
<div className="flex items-center justify-between">
<span className="text-lg">{getTypeIcon(unit.type)}</span>
{unit.code && <span className="text-xs opacity-90">{unit.code}</span>}
</div>
</div>
<div className="p-3">
<h4 className="font-semibold text-sm dark:text-white">{unit.name}</h4>
{unit.hasFuehrungsstelle && (
<span className="inline-block px-2 py-1 mt-1 text-xs bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 rounded">
FüSt
</span>
)}
</div>
</div>
</div>
{unit.children && unit.children.length > 0 && (
<div className="flex flex-col items-center mt-4">
<div className="w-px h-8 bg-gray-300 dark:bg-gray-600"></div>
<div className="flex gap-4">
{unit.children.map((child: any) => (
<div key={child.id} className="flex flex-col items-center">
<div className="w-px h-4 bg-gray-300 dark:bg-gray-600"></div>
{renderUnit(child, level + 1)}
</div>
))}
</div>
</div>
)}
</div>
)
}
if (loading) {
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-8">
<div className="text-lg">Lade Organigramm...</div>
</div>
</div>
)
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex">
{/* Main Modal */}
<div className="flex-1 bg-white dark:bg-gray-900 flex">
{/* Left Sidebar */}
<div className="w-64 bg-gray-50 dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 p-4 overflow-y-auto">
<h3 className="font-semibold mb-4 dark:text-white">Navigation</h3>
{/* Search */}
<div className="mb-4">
<input
type="text"
placeholder="Suche Einheit..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full px-3 py-2 border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white"
/>
</div>
{/* Quick Links */}
<div className="space-y-2">
<button className="w-full text-left px-3 py-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 dark:text-white">
📍 Meine Position
</button>
<button className="w-full text-left px-3 py-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 dark:text-white">
👥 Führungsebene
</button>
<button className="w-full text-left px-3 py-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 dark:text-white">
🎖 Beauftragte
</button>
</div>
{/* Department Filters */}
<h4 className="font-semibold mt-6 mb-2 dark:text-white">Abteilungen</h4>
<div className="grid grid-cols-2 gap-2">
{[1, 2, 3, 4, 5, 6].map(n => (
<button
key={n}
className={`px-3 py-2 rounded text-white text-sm ${getUnitColor(n)}`}
>
Abt. {n}
</button>
))}
</div>
</div>
{/* Main Chart Area */}
<div className="flex-1 relative overflow-hidden">
{/* Header */}
<div className="absolute top-0 left-0 right-0 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 p-4 z-10">
<div className="flex items-center justify-between">
<h2 className="text-xl font-bold dark:text-white">LKA NRW Organigramm</h2>
<button
onClick={onClose}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
</button>
</div>
</div>
{/* Zoom Controls */}
<div className="absolute top-20 right-4 z-10 flex flex-col gap-2">
<button
onClick={handleZoomIn}
className="p-2 bg-white dark:bg-gray-700 shadow rounded hover:bg-gray-100 dark:hover:bg-gray-600"
>
🔍+
</button>
<button
onClick={handleZoomOut}
className="p-2 bg-white dark:bg-gray-700 shadow rounded hover:bg-gray-100 dark:hover:bg-gray-600"
>
🔍-
</button>
<button
onClick={handleResetView}
className="p-2 bg-white dark:bg-gray-700 shadow rounded hover:bg-gray-100 dark:hover:bg-gray-600"
>
🎯
</button>
</div>
{/* Chart Canvas */}
<div
className="absolute inset-0 mt-16 overflow-auto cursor-move"
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp}
style={{ cursor: isDragging ? 'grabbing' : 'grab' }}
>
<div
className="p-8"
style={{
transform: `scale(${zoomLevel}) translate(${panPosition.x / zoomLevel}px, ${panPosition.y / zoomLevel}px)`,
transformOrigin: 'center',
transition: isDragging ? 'none' : 'transform 0.2s'
}}
>
{hierarchy.map(unit => renderUnit(unit))}
</div>
</div>
</div>
{/* Right Sidebar - Details */}
{selectedUnit && (
<div className="w-80 bg-gray-50 dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 p-4 overflow-y-auto">
<div className={`p-3 text-white rounded-lg mb-4 ${getUnitColor(selectedUnit.level || 0)}`}>
<h3 className="text-lg font-semibold">{selectedUnit.name}</h3>
{selectedUnit.code && <p className="text-sm opacity-90">{selectedUnit.code}</p>}
</div>
{/* Tabs */}
<div className="border-b border-gray-200 dark:border-gray-700 mb-4">
<div className="flex gap-4">
<button className="pb-2 border-b-2 border-blue-500 dark:text-white">
Mitarbeiter
</button>
<button className="pb-2 dark:text-gray-400">
Skills
</button>
<button className="pb-2 dark:text-gray-400">
Vertretung
</button>
</div>
</div>
{/* Employee List */}
<div className="space-y-2">
{unitEmployees.map(emp => (
<div key={emp.id} className="p-3 bg-white dark:bg-gray-700 rounded-lg">
<div className="flex items-center gap-3">
{emp.photo ? (
<img
src={`/api${emp.photo}`}
alt=""
className="w-10 h-10 rounded-full object-cover"
/>
) : (
<div className="w-10 h-10 rounded-full bg-gray-200 dark:bg-gray-600 flex items-center justify-center">
👤
</div>
)}
<div>
<p className="font-medium dark:text-white">{emp.firstName} {emp.lastName}</p>
<p className="text-sm text-gray-600 dark:text-gray-400">
{emp.role === 'leiter' && '🔑 Leitung'}
{emp.role === 'stellvertreter' && '↔️ Stellvertretung'}
{emp.role === 'mitarbeiter' && 'Mitarbeiter'}
{emp.role === 'beauftragter' && '🎖️ Beauftragter'}
</p>
</div>
</div>
</div>
))}
{unitEmployees.length === 0 && (
<p className="text-gray-500 dark:text-gray-400">Keine Mitarbeiter zugeordnet</p>
)}
</div>
{selectedUnit.description && (
<div className="mt-4 p-3 bg-blue-50 dark:bg-blue-900/20 rounded">
<h4 className="font-semibold mb-1 dark:text-white">Beschreibung</h4>
<p className="text-sm dark:text-gray-300">{selectedUnit.description}</p>
</div>
)}
{user?.employeeId && (
<div className="mt-4">
<button
onClick={assignMeToSelectedUnit}
disabled={saving}
className="w-full px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-60"
>
{saving ? 'Wird zugeordnet...' : 'In diese Einheit einordnen'}
</button>
{message && (
<p className="text-sm mt-2 text-gray-700 dark:text-gray-300">{message}</p>
)}
</div>
)}
</div>
)}
</div>
</div>
)
}

Datei anzeigen

@ -0,0 +1,280 @@
import { useEffect, useState } from 'react'
import { OrganizationalUnit } from '@skillmate/shared'
import api from '../services/api'
import { ChevronRight, Building2, Search, X } from 'lucide-react'
interface OrganizationSelectorProps {
value?: string
onChange: (unitId: string | null, unitName: string) => void
disabled?: boolean
}
export default function OrganizationSelector({ value, onChange, disabled }: OrganizationSelectorProps) {
const [showModal, setShowModal] = useState(false)
const [hierarchy, setHierarchy] = useState<OrganizationalUnit[]>([])
const [selectedUnit, setSelectedUnit] = useState<OrganizationalUnit | null>(null)
const [currentUnitName, setCurrentUnitName] = useState<string>('')
const [searchTerm, setSearchTerm] = useState('')
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set())
const [loading, setLoading] = useState(false)
useEffect(() => {
if (showModal) {
loadOrganization()
}
}, [showModal])
useEffect(() => {
// Load current unit name if value exists
if (value && hierarchy.length > 0) {
const unit = findUnitById(hierarchy, value)
if (unit) {
setCurrentUnitName(getUnitPath(hierarchy, unit))
setSelectedUnit(unit)
}
}
}, [value, hierarchy])
const loadOrganization = async () => {
setLoading(true)
try {
const response = await api.get('/organization/hierarchy')
if (response.data.success) {
setHierarchy(response.data.data)
// Auto-expand first two levels
const toExpand = new Set<string>()
const addFirstLevels = (units: any[], level = 0) => {
if (level < 2) {
units.forEach(u => {
toExpand.add(u.id)
if (u.children) addFirstLevels(u.children, level + 1)
})
}
}
addFirstLevels(response.data.data)
setExpandedNodes(toExpand)
}
} catch (error) {
console.error('Failed to load organization:', error)
} finally {
setLoading(false)
}
}
const findUnitById = (units: OrganizationalUnit[], id: string): OrganizationalUnit | null => {
for (const unit of units) {
if (unit.id === id) return unit
if (unit.children) {
const found = findUnitById(unit.children, id)
if (found) return found
}
}
return null
}
const getUnitPath = (units: OrganizationalUnit[], target: OrganizationalUnit): string => {
const path: string[] = []
const findPath = (units: OrganizationalUnit[], current: string[] = []): boolean => {
for (const unit of units) {
const newPath = [...current, unit.name]
if (unit.id === target.id) {
path.push(...newPath)
return true
}
if (unit.children && findPath(unit.children, newPath)) {
return true
}
}
return false
}
findPath(units)
return path.join(' → ')
}
const toggleExpand = (unitId: string) => {
setExpandedNodes(prev => {
const newSet = new Set(prev)
if (newSet.has(unitId)) {
newSet.delete(unitId)
} else {
newSet.add(unitId)
}
return newSet
})
}
const handleSelect = (unit: OrganizationalUnit) => {
setSelectedUnit(unit)
const path = getUnitPath(hierarchy, unit)
setCurrentUnitName(path)
onChange(unit.id, path)
setShowModal(false)
}
const getTypeColor = (type: string) => {
const colors: Record<string, string> = {
direktion: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
abteilung: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
dezernat: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
sachgebiet: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
teildezernat: 'bg-pink-100 text-pink-800 dark:bg-pink-900 dark:text-pink-200',
stabsstelle: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200',
sondereinheit: 'bg-teal-100 text-teal-800 dark:bg-teal-900 dark:text-teal-200'
}
return colors[type] || 'bg-gray-100 text-gray-800'
}
const matchesSearch = (unit: OrganizationalUnit): boolean => {
if (!searchTerm) return true
const term = searchTerm.toLowerCase()
return unit.name.toLowerCase().includes(term) ||
unit.code?.toLowerCase().includes(term) || false
}
const renderUnit = (unit: OrganizationalUnit, level = 0) => {
const hasChildren = unit.children && unit.children.length > 0
const isExpanded = expandedNodes.has(unit.id)
const matches = matchesSearch(unit)
// Check if any children match
const hasMatchingChildren = hasChildren && unit.children!.some(child =>
matchesSearch(child) || (child.children && child.children.some(matchesSearch))
)
if (!matches && !hasMatchingChildren) return null
return (
<div key={unit.id} className={level > 0 ? 'ml-4' : ''}>
<div
className={`flex items-center gap-2 p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer ${
selectedUnit?.id === unit.id ? 'bg-blue-50 dark:bg-blue-900/20' : ''
}`}
onClick={() => handleSelect(unit)}
>
{hasChildren && (
<button
onClick={(e) => {
e.stopPropagation()
toggleExpand(unit.id)
}}
className="p-0.5 hover:bg-gray-200 dark:hover:bg-gray-600 rounded"
>
<ChevronRight
className={`w-4 h-4 transition-transform ${isExpanded ? 'rotate-90' : ''}`}
/>
</button>
)}
{!hasChildren && <div className="w-5" />}
<div className="flex-1 flex items-center gap-2">
<span className={`px-2 py-0.5 text-xs rounded ${getTypeColor(unit.type)}`}>
{unit.code || unit.type}
</span>
<span className="text-sm dark:text-white">{unit.name}</span>
</div>
</div>
{hasChildren && isExpanded && (
<div className="border-l border-gray-200 dark:border-gray-700 ml-2">
{unit.children!.map(child => renderUnit(child, level + 1))}
</div>
)}
</div>
)
}
return (
<>
<div className="relative">
<input
type="text"
className="input-field w-full pr-10"
value={currentUnitName}
placeholder="Klicken zum Auswählen..."
readOnly
disabled={disabled}
onClick={() => !disabled && setShowModal(true)}
/>
<button
type="button"
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
onClick={() => !disabled && setShowModal(true)}
disabled={disabled}
>
<Building2 className="w-5 h-5" />
</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>
<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="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Schließen
</button>
</div>
</div>
</div>
)}
</>
)
}

Datei anzeigen

@ -3,6 +3,8 @@ import { useAuthStore } from '../stores/authStore'
import { employeeApi } from '../services/api'
import PhotoUpload from '../components/PhotoUpload'
import SkillLevelBar from '../components/SkillLevelBar'
import DeputyManagement from '../components/DeputyManagement'
import OrganizationSelector from '../components/OrganizationSelector'
interface SkillSelection { categoryId: string; subCategoryId: string; skillId: string; name: string; level: string }
@ -17,6 +19,9 @@ export default function MyProfile() {
const [form, setForm] = useState<any | null>(null)
const [catalog, setCatalog] = useState<{ id: string; name: string; subcategories: { id: string; name: string; skills: { id: string; name: string }[] }[] }[]>([])
const [skills, setSkills] = useState<SkillSelection[]>([])
const [activeTab, setActiveTab] = useState<'profile' | 'deputies'>('profile')
const [currentUnitId, setCurrentUnitId] = useState<string | null>(null)
const [myUnits, setMyUnits] = useState<any[]>([])
useEffect(() => {
if (!employeeId) {
@ -46,6 +51,23 @@ export default function MyProfile() {
}
})
setSkills(mapped)
// Load my organizational units
try {
const unitsRes = await fetch(((import.meta as any).env?.VITE_API_URL || 'http://localhost:3004/api') + '/organization/my-units', {
headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` }
}).then(r => r.json())
if (unitsRes?.success && unitsRes.data?.length > 0) {
setMyUnits(unitsRes.data)
const primaryUnit = unitsRes.data.find((u: any) => u.isPrimary)
if (primaryUnit) {
setCurrentUnitId(primaryUnit.id)
}
}
} catch {
// ignore unit errors
}
// Load hierarchy non-critically
try {
const hierRes = await fetch(((import.meta as any).env?.VITE_API_URL || 'http://localhost:3004/api') + '/skills/hierarchy', {
@ -85,6 +107,32 @@ export default function MyProfile() {
const getSkillLevel = (categoryId: string, subCategoryId: string, skillId: string) =>
(skills.find(s => s.categoryId === categoryId && s.subCategoryId === subCategoryId && s.skillId === skillId)?.level) || ''
const handleOrganizationChange = async (unitId: string | null, unitName: string) => {
setCurrentUnitId(unitId)
if (unitId && employeeId) {
try {
// Save organization assignment
await fetch(((import.meta as any).env?.VITE_API_URL || 'http://localhost:3004/api') + '/organization/assignments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
},
body: JSON.stringify({
employeeId: employeeId,
unitId: unitId,
role: 'mitarbeiter',
isPrimary: true
})
})
// Update department field with unit name for backward compatibility
setForm((prev: any) => ({ ...prev, department: unitName }))
} catch (error) {
console.error('Failed to assign unit:', error)
}
}
}
const onSave = async () => {
if (!employeeId || !form) return
setSaving(true)
@ -151,11 +199,32 @@ export default function MyProfile() {
return (
<div>
<h1 className="text-title-lg font-poppins font-bold text-primary mb-8">Mein Profil</h1>
<h1 className="text-title-lg font-poppins font-bold text-primary mb-4">Mein Profil</h1>
{/* Tab Navigation */}
<div className="flex gap-4 mb-6 border-b border-gray-200 dark:border-gray-700">
<button
onClick={() => setActiveTab('profile')}
className={`pb-2 px-1 ${activeTab === 'profile' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-600 dark:text-gray-400'}`}
>
Profildaten
</button>
<button
onClick={() => setActiveTab('deputies')}
className={`pb-2 px-1 ${activeTab === 'deputies' ? 'border-b-2 border-blue-600 text-blue-600' : 'text-gray-600 dark:text-gray-400'}`}
>
Vertretungen
</button>
</div>
{error && (<div className="bg-error-bg text-error px-4 py-3 rounded-input text-sm mb-6">{error}</div>)}
{success && (<div className="bg-success-bg text-success px-4 py-3 rounded-input text-sm mb-6">{success}</div>)}
{activeTab === 'deputies' ? (
<DeputyManagement />
) : (
<>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="card">
<h2 className="text-title-card font-poppins font-semibold text-primary mb-4">Foto</h2>
@ -182,8 +251,12 @@ export default function MyProfile() {
</div>
<div>
<label className="block text-body font-medium text-secondary mb-2">Dienststelle</label>
<input className="input-field w-full" value={form.department || ''} onChange={(e) => setForm((p: any) => ({ ...p, department: e.target.value }))} placeholder="z. B. Abteilung 4, Dezernat 42, Sachgebiet 42.1" />
<p className="text-small text-tertiary mt-1">Hierarchische Angabe (Abteilung, Dezernat, Sachgebiet).</p>
<OrganizationSelector
value={currentUnitId || undefined}
onChange={handleOrganizationChange}
disabled={false}
/>
<p className="text-small text-tertiary mt-1">Wählen Sie Ihre Organisationseinheit aus dem Organigramm.</p>
</div>
<div>
<label className="block text-body font-medium text-secondary mb-2">Telefon</label>
@ -250,6 +323,9 @@ export default function MyProfile() {
{saving ? 'Speichere...' : 'Änderungen speichern'}
</button>
</div>
</>
)}
</div>
)
}

97
shared/index.d.ts vendored
Datei anzeigen

@ -87,6 +87,7 @@ export interface LoginResponse {
}
export const ROLE_PERMISSIONS: Record<UserRole, string[]>
export const DEFAULT_SKILLS: Record<string, string[]>
export const LANGUAGE_LEVELS: string[]
export interface SkillLevel { id: string; name: string; level?: string }
@ -118,3 +119,99 @@ export interface WorkspaceFilter {
building?: string
min_capacity?: number
}
// Organization
export type OrganizationalUnitType = 'direktion' | 'abteilung' | 'dezernat' | 'sachgebiet' | 'teildezernat' | 'fuehrungsstelle' | 'stabsstelle' | 'sondereinheit'
export type EmployeeUnitRole = 'leiter' | 'stellvertreter' | 'mitarbeiter' | 'beauftragter'
export interface OrganizationalUnit {
id: string
code: string
name: string
type: OrganizationalUnitType
level: number
parentId?: string | null
positionX?: number | null
positionY?: number | null
color?: string | null
orderIndex?: number
description?: string | null
hasFuehrungsstelle?: boolean
fuehrungsstelleName?: string | null
isActive?: boolean
createdAt?: string
updatedAt?: string
children?: OrganizationalUnit[]
employees?: any[]
}
export interface EmployeeUnitAssignment {
id: string
employeeId: string
unitId: string
role: EmployeeUnitRole
startDate: string
endDate?: string | null
isPrimary: boolean
createdAt: string
updatedAt: string
}
export interface SpecialPosition {
id: string
employeeId: string
positionType: string
unitId?: string | null
startDate: string
endDate?: string | null
isActive: boolean
createdAt: string
updatedAt: string
}
export interface DeputyAssignment {
id: string
principalId: string
deputyId: string
unitId?: string | null
validFrom: string
validUntil: string
reason?: string | null
canDelegate: boolean
createdBy: string
createdAt: string
updatedAt: string
}
export interface DeputyDelegation {
id: string
originalAssignmentId: string
fromDeputyId: string
toDeputyId: string
reason?: string | null
delegatedAt: string
createdAt: string
}
// Bookings (for workspace management)
export interface Booking {
id: string
workspaceId: string
userId: string
employeeId: string
startTime: string
endTime: string
status: 'confirmed' | 'cancelled' | 'completed' | 'no_show'
checkInTime?: string | null
checkOutTime?: string | null
notes?: string | null
createdAt: string
updatedAt: string
}
export interface BookingRequest {
workspaceId: string
startTime: string
endTime: string
notes?: string
}