Test
Dieser Commit ist enthalten in:
@ -5,9 +5,9 @@
|
|||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
- **Path**: `A:\GiTea\SkillMate`
|
- **Path**: `A:\GiTea\SkillMate`
|
||||||
- **Files**: 191 files
|
- **Files**: 189 files
|
||||||
- **Size**: 3.3 MB
|
- **Size**: 5.3 MB
|
||||||
- **Last Modified**: 2025-09-26 00:23
|
- **Last Modified**: 2025-09-28 01:28
|
||||||
|
|
||||||
## Technology Stack
|
## Technology Stack
|
||||||
|
|
||||||
@ -15,7 +15,6 @@
|
|||||||
- JavaScript
|
- JavaScript
|
||||||
- Python
|
- Python
|
||||||
- React TypeScript
|
- React TypeScript
|
||||||
- Shell
|
|
||||||
- TypeScript
|
- TypeScript
|
||||||
|
|
||||||
### Frameworks & Libraries
|
### Frameworks & Libraries
|
||||||
@ -27,12 +26,12 @@
|
|||||||
ANWENDUNGSBESCHREIBUNG.txt
|
ANWENDUNGSBESCHREIBUNG.txt
|
||||||
CHANGES_ORGANIGRAMM.md
|
CHANGES_ORGANIGRAMM.md
|
||||||
CLAUDE_PROJECT_README.md
|
CLAUDE_PROJECT_README.md
|
||||||
debug-console.cmd
|
|
||||||
EXE-ERSTELLEN.md
|
EXE-ERSTELLEN.md
|
||||||
gitea_push_debug.txt
|
gitea_push_debug.txt
|
||||||
install-dependencies.cmd
|
|
||||||
INSTALLATION.md
|
INSTALLATION.md
|
||||||
LICENSE.txt
|
LICENSE.txt
|
||||||
|
main.py
|
||||||
|
Organigramm_ohne_Namen.pdf
|
||||||
admin-panel/
|
admin-panel/
|
||||||
│ ├── index.html
|
│ ├── index.html
|
||||||
│ ├── package-lock.json
|
│ ├── package-lock.json
|
||||||
@ -52,11 +51,11 @@ admin-panel/
|
|||||||
│ │ ├── index.ts
|
│ │ ├── index.ts
|
||||||
│ │ ├── Layout.tsx
|
│ │ ├── Layout.tsx
|
||||||
│ │ ├── MoonIcon.tsx
|
│ │ ├── MoonIcon.tsx
|
||||||
|
│ │ ├── OrganizationSelector.tsx
|
||||||
│ │ ├── SearchIcon.tsx
|
│ │ ├── SearchIcon.tsx
|
||||||
│ │ ├── SettingsIcon.tsx
|
│ │ ├── SettingsIcon.tsx
|
||||||
│ │ ├── SunIcon.tsx
|
│ │ ├── SunIcon.tsx
|
||||||
│ │ ├── SyncStatus.tsx
|
│ │ └── SyncStatus.tsx
|
||||||
│ │ └── UsersIcon.tsx
|
|
||||||
│ ├── services/
|
│ ├── services/
|
||||||
│ │ ├── api.ts
|
│ │ ├── api.ts
|
||||||
│ │ └── networkApi.ts
|
│ │ └── networkApi.ts
|
||||||
@ -94,9 +93,11 @@ backend/
|
|||||||
│ │ ├── purge-users.js
|
│ │ ├── purge-users.js
|
||||||
│ │ ├── reset-admin.js
|
│ │ ├── reset-admin.js
|
||||||
│ │ ├── run-migrations.js
|
│ │ ├── run-migrations.js
|
||||||
|
│ │ ├── seed-demo-data.ts
|
||||||
│ │ ├── seed-lka-structure.js
|
│ │ ├── seed-lka-structure.js
|
||||||
│ │ ├── seed-organization.js
|
│ │ ├── seed-organization.js
|
||||||
│ │ ├── seed-skills-from-frontend.js
|
│ │ ├── seed-skills-from-frontend.js
|
||||||
|
│ │ ├── seed-skills-from-shared.js
|
||||||
│ │ └── migrations/
|
│ │ └── migrations/
|
||||||
│ │ └── 0001_users_email_encrypt.js
|
│ │ └── 0001_users_email_encrypt.js
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
@ -127,6 +128,7 @@ backend/
|
|||||||
│ │ │ ├── emailService.ts
|
│ │ │ ├── emailService.ts
|
||||||
│ │ │ ├── encryption.ts
|
│ │ │ ├── encryption.ts
|
||||||
│ │ │ ├── reminderService.ts
|
│ │ │ ├── reminderService.ts
|
||||||
|
│ │ │ ├── skillSeeder.ts
|
||||||
│ │ │ ├── syncScheduler.ts
|
│ │ │ ├── syncScheduler.ts
|
||||||
│ │ │ └── syncService.ts
|
│ │ │ └── syncService.ts
|
||||||
│ │ ├── usecases/
|
│ │ ├── 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-23 19:19:20
|
||||||
- README updated on 2025-09-25 22:01:37
|
- README updated on 2025-09-25 22:01:37
|
||||||
- README updated on 2025-09-27 12:01:06
|
- README updated on 2025-09-27 12:01:06
|
||||||
|
- README updated on 2025-09-28 18:39:07
|
||||||
|
|||||||
279
admin-panel/src/components/OrganizationSelector.tsx
Normale Datei
279
admin-panel/src/components/OrganizationSelector.tsx
Normale Datei
@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -4,3 +4,4 @@ export { default as SearchIcon } from './SearchIcon'
|
|||||||
export { default as SettingsIcon } from './SettingsIcon'
|
export { default as SettingsIcon } from './SettingsIcon'
|
||||||
export { default as SunIcon } from './SunIcon'
|
export { default as SunIcon } from './SunIcon'
|
||||||
export { default as MoonIcon } from './MoonIcon'
|
export { default as MoonIcon } from './MoonIcon'
|
||||||
|
export { default as OrganizationSelector } from './OrganizationSelector'
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { api } from '../services/api'
|
import { api } from '../services/api'
|
||||||
import type { UserRole } from '@skillmate/shared'
|
import type { EmployeeUnitRole, UserRole } from '@skillmate/shared'
|
||||||
|
import { OrganizationSelector } from '../components'
|
||||||
|
|
||||||
interface CreateEmployeeData {
|
interface CreateEmployeeData {
|
||||||
firstName: string
|
firstName: string
|
||||||
@ -18,10 +19,12 @@ export default function CreateEmployee() {
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [success, setSuccess] = useState('')
|
const [success, setSuccess] = useState('')
|
||||||
const [createdUser, setCreatedUser] = useState<{password?: string}>({})
|
const [createdUser, setCreatedUser] = useState<{ password?: string }>({})
|
||||||
const [creationDone, setCreationDone] = useState(false)
|
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: {
|
defaultValues: {
|
||||||
userRole: 'user',
|
userRole: 'user',
|
||||||
createUser: true
|
createUser: true
|
||||||
@ -30,6 +33,25 @@ export default function CreateEmployee() {
|
|||||||
|
|
||||||
const watchCreateUser = watch('createUser')
|
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) => {
|
const onSubmit = async (data: CreateEmployeeData) => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@ -42,7 +64,9 @@ export default function CreateEmployee() {
|
|||||||
email: data.email,
|
email: data.email,
|
||||||
department: data.department,
|
department: data.department,
|
||||||
userRole: data.createUser ? data.userRole : undefined,
|
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)
|
const response = await api.post('/employees', payload)
|
||||||
@ -53,8 +77,6 @@ export default function CreateEmployee() {
|
|||||||
} else {
|
} else {
|
||||||
setSuccess('Mitarbeiter erfolgreich erstellt!')
|
setSuccess('Mitarbeiter erfolgreich erstellt!')
|
||||||
}
|
}
|
||||||
// Manuelles Weiterklicken statt Auto-Navigation, damit Passwort kopiert werden kann
|
|
||||||
setCreationDone(true)
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Error:', err.response?.data)
|
console.error('Error:', err.response?.data)
|
||||||
const errorMessage = err.response?.data?.error?.message || 'Speichern fehlgeschlagen'
|
const errorMessage = err.response?.data?.error?.message || 'Speichern fehlgeschlagen'
|
||||||
@ -107,6 +129,11 @@ export default function CreateEmployee() {
|
|||||||
{success && (
|
{success && (
|
||||||
<div className="bg-green-50 text-green-800 px-4 py-3 rounded-input text-sm mb-6">
|
<div className="bg-green-50 text-green-800 px-4 py-3 rounded-input text-sm mb-6">
|
||||||
{success}
|
{success}
|
||||||
|
{selectedUnitName && (
|
||||||
|
<div className="mt-2 text-green-700">
|
||||||
|
Zugeordnete Einheit: <strong>{selectedUnitName}</strong> · {getUnitRoleLabel(selectedUnitRole)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{createdUser.password && (
|
{createdUser.password && (
|
||||||
<div className="mt-4 p-4 bg-white rounded border border-green-300">
|
<div className="mt-4 p-4 bg-white rounded border border-green-300">
|
||||||
<h4 className="font-semibold mb-2">🔑 Temporäres Passwort:</h4>
|
<h4 className="font-semibold mb-2">🔑 Temporäres Passwort:</h4>
|
||||||
@ -192,8 +219,9 @@ export default function CreateEmployee() {
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
{...register('department', { required: 'Abteilung ist erforderlich' })}
|
{...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."
|
placeholder="IT, Personal, Marketing, etc."
|
||||||
|
readOnly={Boolean(selectedUnitId)}
|
||||||
/>
|
/>
|
||||||
{errors.department && (
|
{errors.department && (
|
||||||
<p className="text-error text-sm mt-1">{errors.department.message}</p>
|
<p className="text-error text-sm mt-1">{errors.department.message}</p>
|
||||||
@ -202,6 +230,54 @@ export default function CreateEmployee() {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="card mb-6">
|
||||||
<h2 className="text-title-card font-poppins font-semibold text-primary mb-6">
|
<h2 className="text-title-card font-poppins font-semibold text-primary mb-6">
|
||||||
Benutzerkonto
|
Benutzerkonto
|
||||||
@ -253,6 +329,7 @@ export default function CreateEmployee() {
|
|||||||
<ul className="space-y-2 text-body text-secondary">
|
<ul className="space-y-2 text-body text-secondary">
|
||||||
<li>• Der Mitarbeiter wird mit Grunddaten angelegt (Position: "Mitarbeiter", Telefon: "Nicht angegeben")</li>
|
<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>• {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>• Der Mitarbeiter kann später im Frontend seine Profildaten vervollständigen</li>
|
||||||
<li>• Alle Daten werden verschlüsselt in der Datenbank gespeichert</li>
|
<li>• Alle Daten werden verschlüsselt in der Datenbank gespeichert</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
445
backend/scripts/seed-demo-data.ts
Normale Datei
445
backend/scripts/seed-demo-data.ts
Normale Datei
@ -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.`)
|
||||||
@ -5,7 +5,7 @@ import bcrypt from 'bcrypt'
|
|||||||
import { db } from '../config/database'
|
import { db } from '../config/database'
|
||||||
import { authenticate, authorize, AuthRequest } from '../middleware/auth'
|
import { authenticate, authorize, AuthRequest } from '../middleware/auth'
|
||||||
import { requirePermission, requireEditPermission } from '../middleware/roleAuth'
|
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 { syncService } from '../services/syncService'
|
||||||
import { FieldEncryption } from '../services/encryption'
|
import { FieldEncryption } from '../services/encryption'
|
||||||
|
|
||||||
@ -203,7 +203,9 @@ router.post('/',
|
|||||||
body('firstName').notEmpty().trim(),
|
body('firstName').notEmpty().trim(),
|
||||||
body('lastName').notEmpty().trim(),
|
body('lastName').notEmpty().trim(),
|
||||||
body('email').isEmail(),
|
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) => {
|
async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
@ -221,14 +223,38 @@ router.post('/',
|
|||||||
const {
|
const {
|
||||||
firstName, lastName, employeeNumber, photo, position,
|
firstName, lastName, employeeNumber, photo, position,
|
||||||
department, email, phone, mobile, office, availability,
|
department, email, phone, mobile, office, availability,
|
||||||
clearance, skills, languages, specializations, userRole, createUser
|
clearance, skills, languages, specializations,
|
||||||
|
userRole, createUser, organizationUnitId, organizationRole
|
||||||
} = req.body
|
} = 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
|
// Insert employee with default values for missing fields
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
INSERT INTO employees (
|
INSERT INTO employees (
|
||||||
id, first_name, last_name, employee_number, photo, position,
|
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,
|
clearance_level, clearance_valid_until, clearance_issued_date,
|
||||||
created_at, updated_at, created_by
|
created_at, updated_at, created_by
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
@ -239,12 +265,13 @@ router.post('/',
|
|||||||
employeeNumber || null,
|
employeeNumber || null,
|
||||||
photo || null,
|
photo || null,
|
||||||
position || 'Mitarbeiter', // Default position
|
position || 'Mitarbeiter', // Default position
|
||||||
department,
|
resolvedDepartment,
|
||||||
email,
|
email,
|
||||||
phone || 'Nicht angegeben', // Default phone
|
phone || 'Nicht angegeben', // Default phone
|
||||||
mobile || null,
|
mobile || null,
|
||||||
office || null,
|
office || null,
|
||||||
availability || 'available', // Default availability
|
availability || 'available', // Default availability
|
||||||
|
resolvedUnitId,
|
||||||
clearance?.level || null,
|
clearance?.level || null,
|
||||||
clearance?.validUntil || null,
|
clearance?.validUntil || null,
|
||||||
clearance?.issuedDate || null,
|
clearance?.issuedDate || null,
|
||||||
@ -313,7 +340,7 @@ router.post('/',
|
|||||||
employeeNumber: employeeNumber || null,
|
employeeNumber: employeeNumber || null,
|
||||||
photo: photo || null,
|
photo: photo || null,
|
||||||
position: position || 'Mitarbeiter',
|
position: position || 'Mitarbeiter',
|
||||||
department,
|
department: resolvedDepartment,
|
||||||
email,
|
email,
|
||||||
phone: phone || 'Nicht angegeben',
|
phone: phone || 'Nicht angegeben',
|
||||||
mobile: mobile || null,
|
mobile: mobile || null,
|
||||||
@ -323,6 +350,8 @@ router.post('/',
|
|||||||
skills: skills || [],
|
skills: skills || [],
|
||||||
languages: languages || [],
|
languages: languages || [],
|
||||||
specializations: specializations || [],
|
specializations: specializations || [],
|
||||||
|
primaryUnitId: resolvedUnitId,
|
||||||
|
unitRole: resolvedUnitId ? resolvedUnitRole : undefined,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
createdBy: req.user!.id
|
createdBy: req.user!.id
|
||||||
@ -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)
|
await syncService.queueSync('employees', 'create', newEmployee)
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
@ -363,7 +410,9 @@ router.post('/',
|
|||||||
data: {
|
data: {
|
||||||
id: employeeId,
|
id: employeeId,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
temporaryPassword: temporaryPassword
|
temporaryPassword: temporaryPassword,
|
||||||
|
primaryUnitId: resolvedUnitId,
|
||||||
|
unitRole: resolvedUnitId ? resolvedUnitRole : undefined
|
||||||
},
|
},
|
||||||
message: `Employee created successfully${createUser ? ' with user account' : ''}`
|
message: `Employee created successfully${createUser ? ' with user account' : ''}`
|
||||||
})
|
})
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren