Dieser Commit ist enthalten in:
Claude Project Manager
2025-09-28 20:00:33 +02:00
Ursprung f8098ab136
Commit b462f69281
6 geänderte Dateien mit 889 neuen und 35 gelöschten Zeilen

Datei anzeigen

@ -5,9 +5,9 @@
## Project Overview
- **Path**: `A:\GiTea\SkillMate`
- **Files**: 191 files
- **Size**: 3.3 MB
- **Last Modified**: 2025-09-26 00:23
- **Files**: 189 files
- **Size**: 5.3 MB
- **Last Modified**: 2025-09-28 01:28
## Technology Stack
@ -15,7 +15,6 @@
- JavaScript
- Python
- React TypeScript
- Shell
- TypeScript
### Frameworks & Libraries
@ -27,12 +26,12 @@
ANWENDUNGSBESCHREIBUNG.txt
CHANGES_ORGANIGRAMM.md
CLAUDE_PROJECT_README.md
debug-console.cmd
EXE-ERSTELLEN.md
gitea_push_debug.txt
install-dependencies.cmd
INSTALLATION.md
LICENSE.txt
main.py
Organigramm_ohne_Namen.pdf
admin-panel/
│ ├── index.html
│ ├── package-lock.json
@ -52,11 +51,11 @@ admin-panel/
│ │ ├── index.ts
│ │ ├── Layout.tsx
│ │ ├── MoonIcon.tsx
│ │ ├── OrganizationSelector.tsx
│ │ ├── SearchIcon.tsx
│ │ ├── SettingsIcon.tsx
│ │ ├── SunIcon.tsx
│ │ ── SyncStatus.tsx
│ │ └── UsersIcon.tsx
│ │ ── SyncStatus.tsx
│ ├── services/
│ │ ├── api.ts
│ │ └── networkApi.ts
@ -94,9 +93,11 @@ backend/
│ │ ├── purge-users.js
│ │ ├── reset-admin.js
│ │ ├── run-migrations.js
│ │ ├── seed-demo-data.ts
│ │ ├── seed-lka-structure.js
│ │ ├── seed-organization.js
│ │ ├── seed-skills-from-frontend.js
│ │ ├── seed-skills-from-shared.js
│ │ └── migrations/
│ │ └── 0001_users_email_encrypt.js
│ ├── src/
@ -127,6 +128,7 @@ backend/
│ │ │ ├── emailService.ts
│ │ │ ├── encryption.ts
│ │ │ ├── reminderService.ts
│ │ │ ├── skillSeeder.ts
│ │ │ ├── syncScheduler.ts
│ │ │ └── syncService.ts
│ │ ├── usecases/
@ -245,3 +247,4 @@ This project is managed with Claude Project Manager. To work with this project:
- README updated on 2025-09-23 19:19:20
- README updated on 2025-09-25 22:01:37
- README updated on 2025-09-27 12:01:06
- README updated on 2025-09-28 18:39:07

Datei anzeigen

@ -0,0 +1,279 @@
import { useEffect, useState } from 'react'
import type { OrganizationalUnit } from '@skillmate/shared'
import { ChevronRight, Building2, Search, X } from 'lucide-react'
import { api } from '../services/api'
interface OrganizationSelectorProps {
value?: string | null
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('')
const [searchTerm, setSearchTerm] = useState('')
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set())
const [loading, setLoading] = useState(false)
useEffect(() => {
if (showModal) {
loadOrganization()
}
}, [showModal])
useEffect(() => {
if (value && hierarchy.length > 0) {
const unit = findUnitById(hierarchy, value)
if (unit) {
const path = getUnitPath(hierarchy, unit)
setSelectedUnit(unit)
setCurrentUnitName(path)
}
}
if (!value) {
setSelectedUnit(null)
setCurrentUnitName('')
}
}, [value, hierarchy])
const loadOrganization = async () => {
setLoading(true)
try {
const response = await api.get('/organization/hierarchy')
if (response.data.success) {
const units = response.data.data as OrganizationalUnit[]
setHierarchy(units)
const defaults = new Set<string>()
const expandToLevel = (nodes: OrganizationalUnit[], level = 0) => {
if (level > 1) return
nodes.forEach(node => {
defaults.add(node.id)
if (node.children) expandToLevel(node.children, level + 1)
})
}
expandToLevel(units)
setExpandedNodes(defaults)
}
} 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 result = findUnitById(unit.children, id)
if (result) return result
}
}
return null
}
const getUnitPath = (units: OrganizationalUnit[], target: OrganizationalUnit): string => {
const path: string[] = []
const traverse = (nodes: OrganizationalUnit[], trail: string[] = []): boolean => {
for (const node of nodes) {
const currentTrail = [...trail, node.name]
if (node.id === target.id) {
path.push(...currentTrail)
return true
}
if (node.children && traverse(node.children, currentTrail)) {
return true
}
}
return false
}
traverse(units)
return path.join(' → ')
}
const toggleExpand = (unitId: string) => {
setExpandedNodes(prev => {
const next = new Set(prev)
if (next.has(unitId)) {
next.delete(unitId)
} else {
next.add(unitId)
}
return next
})
}
const handleSelect = (unit: OrganizationalUnit | null) => {
if (!unit) {
setSelectedUnit(null)
setCurrentUnitName('')
onChange(null, '')
return
}
setSelectedUnit(unit)
const path = getUnitPath(hierarchy, unit)
setCurrentUnitName(path)
onChange(unit.id, path)
setShowModal(false)
}
const typeBadgeStyle = (type: string) => {
const colors: Record<string, string> = {
direktion: 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-200',
abteilung: 'bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-200',
dezernat: 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-200',
sachgebiet: 'bg-orange-100 text-orange-800 dark:bg-orange-900/40 dark:text-orange-200',
teildezernat: 'bg-pink-100 text-pink-800 dark:bg-pink-900/40 dark:text-pink-200',
stabsstelle: 'bg-gray-100 text-gray-800 dark:bg-gray-900/40 dark:text-gray-200',
sonderenheit: 'bg-teal-100 text-teal-800 dark:bg-teal-900/40 dark:text-teal-200'
}
return colors[type] || 'bg-gray-100 text-gray-800 dark:bg-gray-800/40 dark:text-gray-200'
}
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 hasMatchingDescendant = (unit: OrganizationalUnit): boolean => {
if (!unit.children) return false
for (const child of unit.children) {
if (matchesSearch(child) || hasMatchingDescendant(child)) return true
}
return false
}
const renderUnit = (unit: OrganizationalUnit, level = 0) => {
const isVisible = matchesSearch(unit) || hasMatchingDescendant(unit)
if (!isVisible) return null
const hasChildren = unit.children && unit.children.length > 0
const expanded = expandedNodes.has(unit.id)
return (
<div key={unit.id} className={level ? 'ml-4' : ''}>
<div
className={`flex items-center gap-2 p-2 rounded transition-colors ${
selectedUnit?.id === unit.id
? 'bg-blue-100 dark:bg-blue-800/40'
: 'hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
>
{hasChildren && (
<button
type="button"
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600"
onClick={event => {
event.stopPropagation()
toggleExpand(unit.id)
}}
>
<ChevronRight
className={`w-4 h-4 transition-transform ${expanded ? 'rotate-90' : ''}`}
/>
</button>
)}
<button
type="button"
className="flex-1 text-left"
onClick={() => handleSelect(unit)}
>
<div className="flex items-center gap-3">
<Building2 className="w-5 h-5 text-tertiary" />
<div>
<div className="font-medium text-secondary">{unit.name}</div>
<div className="text-xs text-tertiary flex items-center gap-2 mt-0.5">
{unit.code && <span>{unit.code}</span>}
<span className={`px-2 py-0.5 rounded-full ${typeBadgeStyle(unit.type)}`}>
{unit.type}
</span>
</div>
</div>
</div>
</button>
</div>
{hasChildren && expanded && (
<div className="mt-1">
{unit.children!.map(child => renderUnit(child, level + 1))}
</div>
)}
</div>
)
}
return (
<div>
<div className="flex items-center gap-3">
<div className="flex-1">
<div className={`input-field flex items-center justify-between ${disabled ? 'opacity-60 cursor-not-allowed' : 'cursor-pointer'}`} onClick={() => !disabled && setShowModal(true)}>
<span className={currentUnitName ? 'text-secondary' : 'text-tertiary'}>
{currentUnitName || 'Organisationseinheit auswählen'}
</span>
<ChevronRight className="w-4 h-4 text-tertiary" />
</div>
</div>
{currentUnitName && !disabled && (
<button
type="button"
className="text-sm text-tertiary hover:text-error flex items-center gap-1"
onClick={() => handleSelect(null)}
>
<X className="w-4 h-4" />
Zurücksetzen
</button>
)}
</div>
{showModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-xl w-full max-w-3xl max-h-[90vh] flex flex-col">
<div className="flex items-center justify-between border-b border-border-default px-6 py-4">
<h2 className="text-lg font-semibold text-primary">Organisationseinheit auswählen</h2>
<button
type="button"
onClick={() => setShowModal(false)}
className="text-tertiary hover:text-secondary"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="px-6 py-4 border-b border-border-default">
<div className="relative">
<Search className="w-4 h-4 text-tertiary absolute left-3 top-3" />
<input
type="text"
value={searchTerm}
onChange={event => setSearchTerm(event.target.value)}
className="input-field pl-9"
placeholder="Suche nach Name oder Code"
/>
</div>
</div>
<div className="flex-1 overflow-y-auto px-6 py-4">
{loading ? (
<div className="text-center text-tertiary">Lade Organisation...</div>
) : (
hierarchy.map(unit => renderUnit(unit))
)}
</div>
<div className="px-6 py-4 border-t border-border-default flex justify-end">
<button
type="button"
className="btn-secondary"
onClick={() => setShowModal(false)}
>
Abbrechen
</button>
</div>
</div>
</div>
)}
</div>
)
}

Datei anzeigen

@ -3,4 +3,5 @@ export { default as UsersIcon } from './UsersIcon'
export { default as SearchIcon } from './SearchIcon'
export { default as SettingsIcon } from './SettingsIcon'
export { default as SunIcon } from './SunIcon'
export { default as MoonIcon } from './MoonIcon'
export { default as MoonIcon } from './MoonIcon'
export { default as OrganizationSelector } from './OrganizationSelector'

Datei anzeigen

@ -1,8 +1,9 @@
import { useNavigate } from 'react-router-dom'
import { useForm } from 'react-hook-form'
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { api } from '../services/api'
import type { UserRole } from '@skillmate/shared'
import type { EmployeeUnitRole, UserRole } from '@skillmate/shared'
import { OrganizationSelector } from '../components'
interface CreateEmployeeData {
firstName: string
@ -18,10 +19,12 @@ export default function CreateEmployee() {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState('')
const [createdUser, setCreatedUser] = useState<{password?: string}>({})
const [creationDone, setCreationDone] = useState(false)
const [createdUser, setCreatedUser] = useState<{ password?: string }>({})
const [selectedUnitId, setSelectedUnitId] = useState<string | null>(null)
const [selectedUnitName, setSelectedUnitName] = useState('')
const [selectedUnitRole, setSelectedUnitRole] = useState<EmployeeUnitRole>('mitarbeiter')
const { register, handleSubmit, formState: { errors }, watch } = useForm<CreateEmployeeData>({
const { register, handleSubmit, formState: { errors }, watch, setValue } = useForm<CreateEmployeeData>({
defaultValues: {
userRole: 'user',
createUser: true
@ -30,6 +33,25 @@ export default function CreateEmployee() {
const watchCreateUser = watch('createUser')
useEffect(() => {
if (selectedUnitName) {
setValue('department', selectedUnitName)
}
}, [selectedUnitName, setValue])
const getUnitRoleLabel = (role: EmployeeUnitRole): string => {
switch (role) {
case 'leiter':
return 'Leitung'
case 'stellvertreter':
return 'Stellvertretung'
case 'beauftragter':
return 'Beauftragte:r'
default:
return 'Mitarbeiter:in'
}
}
const onSubmit = async (data: CreateEmployeeData) => {
try {
setLoading(true)
@ -42,7 +64,9 @@ export default function CreateEmployee() {
email: data.email,
department: data.department,
userRole: data.createUser ? data.userRole : undefined,
createUser: data.createUser
createUser: data.createUser,
organizationUnitId: selectedUnitId || undefined,
organizationRole: selectedUnitId ? selectedUnitRole : undefined
}
const response = await api.post('/employees', payload)
@ -53,8 +77,6 @@ export default function CreateEmployee() {
} else {
setSuccess('Mitarbeiter erfolgreich erstellt!')
}
// Manuelles Weiterklicken statt Auto-Navigation, damit Passwort kopiert werden kann
setCreationDone(true)
} catch (err: any) {
console.error('Error:', err.response?.data)
const errorMessage = err.response?.data?.error?.message || 'Speichern fehlgeschlagen'
@ -107,6 +129,11 @@ export default function CreateEmployee() {
{success && (
<div className="bg-green-50 text-green-800 px-4 py-3 rounded-input text-sm mb-6">
{success}
{selectedUnitName && (
<div className="mt-2 text-green-700">
Zugeordnete Einheit: <strong>{selectedUnitName}</strong> · {getUnitRoleLabel(selectedUnitRole)}
</div>
)}
{createdUser.password && (
<div className="mt-4 p-4 bg-white rounded border border-green-300">
<h4 className="font-semibold mb-2">🔑 Temporäres Passwort:</h4>
@ -192,8 +219,9 @@ export default function CreateEmployee() {
</label>
<input
{...register('department', { required: 'Abteilung ist erforderlich' })}
className="input-field w-full"
className={`input-field w-full ${selectedUnitId ? 'bg-gray-100 cursor-not-allowed' : ''}`}
placeholder="IT, Personal, Marketing, etc."
readOnly={Boolean(selectedUnitId)}
/>
{errors.department && (
<p className="text-error text-sm mt-1">{errors.department.message}</p>
@ -202,6 +230,54 @@ export default function CreateEmployee() {
</div>
</div>
<div className="card mb-6">
<h2 className="text-title-card font-poppins font-semibold text-primary mb-6">
Organisation & Rolle
</h2>
<div className="space-y-5">
<div>
<label className="block text-body font-medium text-secondary mb-2">
Organisationseinheit
</label>
<OrganizationSelector
value={selectedUnitId}
onChange={(unitId, unitPath) => {
setSelectedUnitId(unitId)
setSelectedUnitName(unitPath)
if (!unitId) {
setSelectedUnitRole('mitarbeiter')
}
}}
/>
<p className="text-sm text-secondary-light mt-2">
Wählen Sie die primäre Organisationseinheit für den neuen Mitarbeiter. Die Abteilung wird automatisch anhand der Auswahl gesetzt.
</p>
</div>
{selectedUnitId && (
<div>
<label className="block text-body font-medium text-secondary mb-2">
Rolle innerhalb der Einheit
</label>
<select
value={selectedUnitRole}
onChange={event => setSelectedUnitRole(event.target.value as EmployeeUnitRole)}
className="input-field w-full"
>
<option value="leiter">Leiter:in</option>
<option value="stellvertreter">Stellvertretung</option>
<option value="mitarbeiter">Mitarbeiter:in</option>
<option value="beauftragter">Beauftragte:r</option>
</select>
<p className="text-sm text-secondary-light mt-2">
Diese Rolle wird für Vertretungs- und Organigramm-Funktionen verwendet.
</p>
</div>
)}
</div>
</div>
<div className="card mb-6">
<h2 className="text-title-card font-poppins font-semibold text-primary mb-6">
Benutzerkonto
@ -253,6 +329,7 @@ export default function CreateEmployee() {
<ul className="space-y-2 text-body text-secondary">
<li> Der Mitarbeiter wird mit Grunddaten angelegt (Position: "Mitarbeiter", Telefon: "Nicht angegeben")</li>
<li> {watchCreateUser ? 'Ein Benutzerkonto wird erstellt und ein temporäres Passwort generiert' : 'Kein Benutzerkonto wird erstellt'}</li>
<li> {selectedUnitId ? `Die Organisationseinheit ${selectedUnitName} (${getUnitRoleLabel(selectedUnitRole)}) wird als primäre Zuordnung hinterlegt` : 'Organisationseinheit kann später im Organigramm zugewiesen werden'}</li>
<li> Der Mitarbeiter kann später im Frontend seine Profildaten vervollständigen</li>
<li> Alle Daten werden verschlüsselt in der Datenbank gespeichert</li>
</ul>

Datei anzeigen

@ -0,0 +1,445 @@
import dotenv from 'dotenv'
import { v4 as uuidv4 } from 'uuid'
import bcrypt from 'bcryptjs'
import { initializeSecureDatabase, encryptedDb, db } from '../src/config/secureDatabase'
import { initializeDatabase } from '../src/config/database'
import { FieldEncryption } from '../src/services/encryption'
dotenv.config()
initializeSecureDatabase()
initializeDatabase()
type SampleSkill = {
id: string
level: string
verified?: boolean
}
type SampleEmployee = {
firstName: string
lastName: string
username: string
email: string
role: 'admin' | 'superuser' | 'user'
department: string
position: string
employeeNumber: string
phone: string
mobile?: string
office?: string
availability?: string
skills: SampleSkill[]
languages?: { language: string; proficiency: string; certified?: number }[]
clearance?: { level: string; validUntil: string; issuedDate: string }
}
const SAMPLE_EMPLOYEES: SampleEmployee[] = [
{
firstName: 'Anna',
lastName: 'Meyer',
username: 'a.meyer',
email: 'anna.meyer@example.local',
role: 'admin',
department: 'Leitungsstab',
position: 'Leitung PMO',
employeeNumber: 'SM-001',
phone: '+49 201 123-1001',
mobile: '+49 171 2001001',
office: 'LS-2.14',
availability: 'available',
skills: [
{ id: 'communication.languages.de', level: '9', verified: true },
{ id: 'analytical.data_analysis.statistics', level: '7' },
{ id: 'technical.programming.python', level: '6' }
],
languages: [
{ language: 'de', proficiency: 'C2', certified: 1 },
{ language: 'en', proficiency: 'C1' }
],
clearance: {
level: 'Ü3',
validUntil: '2027-06-30',
issuedDate: '2022-07-01'
}
},
{
firstName: 'Daniel',
lastName: 'Schulz',
username: 'd.schulz',
email: 'daniel.schulz@example.local',
role: 'superuser',
department: 'Cybercrime',
position: 'Teamleiter Digitale Forensik',
employeeNumber: 'SM-002',
phone: '+49 201 123-1002',
mobile: '+49 171 2001002',
office: 'CC-3.04',
availability: 'available',
skills: [
{ id: 'technical.security.forensics', level: '8', verified: true },
{ id: 'analytical.intelligence.threat', level: '6' },
{ id: 'technical.programming.python', level: '5' }
],
languages: [
{ language: 'de', proficiency: 'C1' },
{ language: 'en', proficiency: 'B2' }
]
},
{
firstName: 'Miriam',
lastName: 'Koch',
username: 'm.koch',
email: 'miriam.koch@example.local',
role: 'user',
department: 'Kriminalprävention',
position: 'Referentin Prävention',
employeeNumber: 'SM-003',
phone: '+49 201 123-1003',
availability: 'busy',
skills: [
{ id: 'communication.interpersonal.presentation', level: '7' },
{ id: 'operational.investigation.interrogation', level: '4' }
],
languages: [
{ language: 'de', proficiency: 'C2' },
{ language: 'fr', proficiency: 'B2' }
]
},
{
firstName: 'Jonas',
lastName: 'Becker',
username: 'j.becker',
email: 'jonas.becker@example.local',
role: 'user',
department: 'Cybercrime',
position: 'Analyst OSINT',
employeeNumber: 'SM-004',
phone: '+49 201 123-1004',
skills: [
{ id: 'analytical.intelligence.osint', level: '8', verified: true },
{ id: 'analytical.intelligence.pattern', level: '6' }
],
languages: [
{ language: 'de', proficiency: 'C1' },
{ language: 'en', proficiency: 'C1' }
]
},
{
firstName: 'Leonie',
lastName: 'Graf',
username: 'l.graf',
email: 'leonie.graf@example.local',
role: 'superuser',
department: 'Organisierte Kriminalität',
position: 'Ermittlerin',
employeeNumber: 'SM-005',
phone: '+49 201 123-1005',
availability: 'available',
skills: [
{ id: 'operational.investigation.surveillance', level: '7' },
{ id: 'operational.tactical.planning', level: '6' }
],
clearance: {
level: 'Ü2',
validUntil: '2026-05-15',
issuedDate: '2021-05-16'
}
},
{
firstName: 'Yusuf',
lastName: 'Öztürk',
username: 'y.oeztuerk',
email: 'yusuf.oeztuerk@example.local',
role: 'user',
department: 'Cybercrime',
position: 'Incident Responder',
employeeNumber: 'SM-006',
phone: '+49 201 123-1006',
skills: [
{ id: 'technical.security.siem', level: '7' },
{ id: 'technical.security.malware', level: '5' }
],
languages: [
{ language: 'de', proficiency: 'C1' },
{ language: 'tr', proficiency: 'C1' }
]
},
{
firstName: 'Klara',
lastName: 'Heinrich',
username: 'k.heinrich',
email: 'klara.heinrich@example.local',
role: 'user',
department: 'Kriminalprävention',
position: 'Datenanalystin',
employeeNumber: 'SM-007',
phone: '+49 201 123-1007',
skills: [
{ id: 'analytical.data_analysis.statistics', level: '8' },
{ id: 'analytical.data_analysis.financial', level: '6' }
]
},
{
firstName: 'Sebastian',
lastName: 'Ulrich',
username: 's.ulrich',
email: 'sebastian.ulrich@example.local',
role: 'user',
department: 'Cybercrime',
position: 'Digitaler Spurensicherer',
employeeNumber: 'SM-008',
phone: '+49 201 123-1008',
availability: 'offline',
skills: [
{ id: 'operational.investigation.evidence', level: '7', verified: true },
{ id: 'technical.security.forensics', level: '6' }
]
},
{
firstName: 'Jana',
lastName: 'Reuter',
username: 'j.reuter',
email: 'jana.reuter@example.local',
role: 'superuser',
department: 'Nachrichtendienstliche Analyse',
position: 'Analystin Gefährdungsbewertung',
employeeNumber: 'SM-009',
phone: '+49 201 123-1009',
skills: [
{ id: 'analytical.intelligence.threat', level: '8', verified: true },
{ id: 'analytical.intelligence.risk', level: '7' }
],
languages: [
{ language: 'de', proficiency: 'C2' },
{ language: 'en', proficiency: 'C1' },
{ language: 'ru', proficiency: 'B2' }
]
},
{
firstName: 'Harald',
lastName: 'Pohl',
username: 'h.pohl',
email: 'harald.pohl@example.local',
role: 'user',
department: 'Mobiles Einsatzkommando',
position: 'Einsatzführer',
employeeNumber: 'SM-010',
phone: '+49 201 123-1010',
availability: 'available',
skills: [
{ id: 'operational.tactical.protection', level: '8' },
{ id: 'certifications.weapons.sniper', level: '7', verified: true }
],
clearance: {
level: 'Ü2',
validUntil: '2025-12-31',
issuedDate: '2020-01-01'
}
},
{
firstName: 'Melanie',
lastName: 'Franke',
username: 'm.franke',
email: 'melanie.franke@example.local',
role: 'user',
department: 'Kriminalprävention',
position: 'Projektmanagerin Prävention',
employeeNumber: 'SM-011',
phone: '+49 201 123-1011',
skills: [
{ id: 'communication.interpersonal.teamwork', level: '8' },
{ id: 'communication.interpersonal.conflict', level: '6' }
]
},
{
firstName: 'Farid',
lastName: 'Rahmani',
username: 'f.rahmani',
email: 'farid.rahmani@example.local',
role: 'user',
department: 'Cybercrime',
position: 'Entwickler Automatisierung',
employeeNumber: 'SM-012',
phone: '+49 201 123-1012',
skills: [
{ id: 'technical.programming.javascript', level: '7' },
{ id: 'technical.programming.python', level: '8' }
],
languages: [
{ language: 'de', proficiency: 'B2' },
{ language: 'fa', proficiency: 'C1' }
]
},
{
firstName: 'Lena',
lastName: 'Zimmer',
username: 'l.zimmer',
email: 'lena.zimmer@example.local',
role: 'user',
department: 'Kriminalprävention',
position: 'Datenvisualisierung',
employeeNumber: 'SM-013',
phone: '+49 201 123-1013',
availability: 'busy',
skills: [
{ id: 'analytical.data_analysis.network_analysis', level: '6' },
{ id: 'communication.presentation.presentation', level: '5' }
]
},
{
firstName: 'Erik',
lastName: 'Brandt',
username: 'e.brandt',
email: 'erik.brandt@example.local',
role: 'superuser',
department: 'Nachrichtendienstliche Analyse',
position: 'Projektleiter Prognosemodelle',
employeeNumber: 'SM-014',
phone: '+49 201 123-1014',
skills: [
{ id: 'analytical.intelligence.forecasting', level: '8' },
{ id: 'analytical.data_analysis.statistics', level: '7' }
],
languages: [
{ language: 'de', proficiency: 'C1' },
{ language: 'en', proficiency: 'C1' }
]
},
{
firstName: 'Sofia',
lastName: 'Lindner',
username: 's.lindner',
email: 'sofia.lindner@example.local',
role: 'user',
department: 'Cybercrime',
position: 'Malware Analystin',
employeeNumber: 'SM-015',
phone: '+49 201 123-1015',
skills: [
{ id: 'technical.security.malware', level: '8' },
{ id: 'technical.security.crypto', level: '6' }
],
languages: [
{ language: 'de', proficiency: 'C1' },
{ language: 'en', proficiency: 'B2' }
]
}
]
const PASSWORD = 'test123'
const deleteExistingByUsername = db.prepare('DELETE FROM users WHERE username = ?')
const deleteEmployeeById = db.prepare('DELETE FROM employees WHERE id = ?')
const selectUserByUsername = db.prepare('SELECT id, employee_id FROM users WHERE username = ?')
const deleteEmployeeSkills = db.prepare('DELETE FROM employee_skills WHERE employee_id = ?')
const deleteLanguageSkills = db.prepare('DELETE FROM language_skills WHERE employee_id = ?')
const insertSkill = db.prepare(`
INSERT INTO employee_skills (
employee_id, skill_id, level, verified, verified_by, verified_date
) VALUES (?, ?, ?, ?, ?, ?)
`)
const insertLanguage = db.prepare(`
INSERT INTO language_skills (
id, employee_id, language, proficiency, certified, certificate_type, is_native, can_interpret
) VALUES (?, ?, ?, ?, ?, ?, 0, 0)
`)
const insertUser = db.prepare(`
INSERT INTO users (
id, username, email, email_hash, password, role, employee_id, is_active, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
`)
const nowIso = () => new Date().toISOString()
const hashPassword = (plain: string) => bcrypt.hashSync(plain, 12)
function upsertEmployee(sample: SampleEmployee) {
const existingUser = selectUserByUsername.get(sample.username) as { id: string; employee_id: string } | undefined
if (existingUser) {
deleteEmployeeSkills.run(existingUser.employee_id)
deleteLanguageSkills.run(existingUser.employee_id)
deleteEmployeeById.run(existingUser.employee_id)
deleteExistingByUsername.run(sample.username)
}
const employeeId = uuidv4()
const createdAt = nowIso()
const availability = sample.availability || 'available'
encryptedDb.insertEmployee({
id: employeeId,
first_name: sample.firstName,
last_name: sample.lastName,
employee_number: sample.employeeNumber,
photo: null,
position: sample.position,
department: sample.department,
email: sample.email,
phone: sample.phone,
mobile: sample.mobile || null,
office: sample.office || null,
availability,
clearance_level: sample.clearance?.level || null,
clearance_valid_until: sample.clearance?.validUntil || null,
clearance_issued_date: sample.clearance?.issuedDate || null,
primary_unit_id: null,
created_at: createdAt,
updated_at: createdAt,
created_by: 'system'
})
const verifiedBy = sample.role === 'admin' ? 'system-admin' : 'system'
for (const skill of sample.skills) {
insertSkill.run(
employeeId,
skill.id,
skill.level,
skill.verified ? 1 : 0,
skill.verified ? verifiedBy : null,
skill.verified ? createdAt : null
)
}
for (const language of sample.languages || []) {
insertLanguage.run(
uuidv4(),
employeeId,
language.language,
language.proficiency,
language.certified || 0,
language.certified ? 'Zertifikat' : null
)
}
const userId = uuidv4()
const encryptedEmail = FieldEncryption.encrypt(sample.email) || ''
const emailHash = FieldEncryption.hash(sample.email)
const hashedPassword = hashPassword(PASSWORD)
const timestamps = nowIso()
insertUser.run(
userId,
sample.username,
encryptedEmail,
emailHash,
hashedPassword,
sample.role,
employeeId,
timestamps,
timestamps
)
}
db.transaction(() => {
for (const employee of SAMPLE_EMPLOYEES) {
upsertEmployee(employee)
}
})()
console.log(`${SAMPLE_EMPLOYEES.length} Demo-Mitarbeiter mit Passwort "${PASSWORD}" angelegt.`)

Datei anzeigen

@ -5,7 +5,7 @@ import bcrypt from 'bcrypt'
import { db } from '../config/database'
import { authenticate, authorize, AuthRequest } from '../middleware/auth'
import { requirePermission, requireEditPermission } from '../middleware/roleAuth'
import { Employee, LanguageSkill, Skill, UserRole } from '@skillmate/shared'
import { Employee, LanguageSkill, Skill, UserRole, EmployeeUnitRole } from '@skillmate/shared'
import { syncService } from '../services/syncService'
import { FieldEncryption } from '../services/encryption'
@ -203,7 +203,9 @@ router.post('/',
body('firstName').notEmpty().trim(),
body('lastName').notEmpty().trim(),
body('email').isEmail(),
body('department').notEmpty().trim()
body('department').notEmpty().trim(),
body('organizationUnitId').optional({ checkFalsy: true }).isUUID(),
body('organizationRole').optional({ checkFalsy: true }).isIn(['leiter', 'stellvertreter', 'mitarbeiter', 'beauftragter'])
],
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
@ -221,30 +223,55 @@ router.post('/',
const {
firstName, lastName, employeeNumber, photo, position,
department, email, phone, mobile, office, availability,
clearance, skills, languages, specializations, userRole, createUser
clearance, skills, languages, specializations,
userRole, createUser, organizationUnitId, organizationRole
} = req.body
if (organizationRole && !organizationUnitId) {
return res.status(400).json({
success: false,
error: { message: 'Organization role requires unit selection' }
})
}
let resolvedDepartment = department
let resolvedUnitId: string | null = null
let resolvedUnitRole: EmployeeUnitRole = 'mitarbeiter'
if (organizationUnitId) {
const unitRow = db.prepare('SELECT id, name FROM organizational_units WHERE id = ? AND is_active = 1').get(organizationUnitId) as { id: string; name: string } | undefined
if (!unitRow) {
return res.status(404).json({ success: false, error: { message: 'Organization unit not found' } })
}
resolvedUnitId = unitRow.id
resolvedDepartment = unitRow.name
if (organizationRole) {
resolvedUnitRole = organizationRole as EmployeeUnitRole
}
}
// Insert employee with default values for missing fields
db.prepare(`
INSERT INTO employees (
id, first_name, last_name, employee_number, photo, position,
department, email, phone, mobile, office, availability,
department, email, phone, mobile, office, availability, primary_unit_id,
clearance_level, clearance_valid_until, clearance_issued_date,
created_at, updated_at, created_by
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
employeeId,
firstName,
lastName,
employeeNumber || null,
photo || null,
employeeId,
firstName,
lastName,
employeeNumber || null,
photo || null,
position || 'Mitarbeiter', // Default position
department,
email,
resolvedDepartment,
email,
phone || 'Nicht angegeben', // Default phone
mobile || null,
office || null,
mobile || null,
office || null,
availability || 'available', // Default availability
resolvedUnitId,
clearance?.level || null,
clearance?.validUntil || null,
clearance?.issuedDate || null,
@ -313,7 +340,7 @@ router.post('/',
employeeNumber: employeeNumber || null,
photo: photo || null,
position: position || 'Mitarbeiter',
department,
department: resolvedDepartment,
email,
phone: phone || 'Nicht angegeben',
mobile: mobile || null,
@ -323,11 +350,13 @@ router.post('/',
skills: skills || [],
languages: languages || [],
specializations: specializations || [],
primaryUnitId: resolvedUnitId,
unitRole: resolvedUnitId ? resolvedUnitRole : undefined,
createdAt: now,
updatedAt: now,
createdBy: req.user!.id
}
// Create user account if requested
let userId = null
let temporaryPassword = null
@ -356,6 +385,24 @@ router.post('/',
}
}
if (resolvedUnitId) {
const assignmentId = uuidv4()
db.prepare(`
INSERT INTO employee_unit_assignments (
id, employee_id, unit_id, role, start_date, end_date,
is_primary, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, NULL, 1, ?, ?)
`).run(
assignmentId,
employeeId,
resolvedUnitId,
resolvedUnitRole,
now,
now,
now
)
}
await syncService.queueSync('employees', 'create', newEmployee)
res.status(201).json({
@ -363,7 +410,9 @@ router.post('/',
data: {
id: employeeId,
userId: userId,
temporaryPassword: temporaryPassword
temporaryPassword: temporaryPassword,
primaryUnitId: resolvedUnitId,
unitRole: resolvedUnitId ? resolvedUnitRole : undefined
},
message: `Employee created successfully${createUser ? ' with user account' : ''}`
})
@ -558,4 +607,4 @@ router.post('/search', authenticate, requirePermission('employees:read'), async
}
})
export default router
export default router