diff --git a/CLAUDE_PROJECT_README.md b/CLAUDE_PROJECT_README.md index 4f630b1..b517bec 100644 --- a/CLAUDE_PROJECT_README.md +++ b/CLAUDE_PROJECT_README.md @@ -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 diff --git a/admin-panel/src/components/OrganizationSelector.tsx b/admin-panel/src/components/OrganizationSelector.tsx new file mode 100644 index 0000000..c9678be --- /dev/null +++ b/admin-panel/src/components/OrganizationSelector.tsx @@ -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([]) + const [selectedUnit, setSelectedUnit] = useState(null) + const [currentUnitName, setCurrentUnitName] = useState('') + const [searchTerm, setSearchTerm] = useState('') + const [expandedNodes, setExpandedNodes] = useState>(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() + 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 = { + 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 ( +
+
+ {hasChildren && ( + + )} + +
+ {hasChildren && expanded && ( +
+ {unit.children!.map(child => renderUnit(child, level + 1))} +
+ )} +
+ ) + } + + return ( +
+
+
+
!disabled && setShowModal(true)}> + + {currentUnitName || 'Organisationseinheit auswählen'} + + +
+
+ {currentUnitName && !disabled && ( + + )} +
+ + {showModal && ( +
+
+
+

Organisationseinheit auswählen

+ +
+ +
+
+ + setSearchTerm(event.target.value)} + className="input-field pl-9" + placeholder="Suche nach Name oder Code" + /> +
+
+ +
+ {loading ? ( +
Lade Organisation...
+ ) : ( + hierarchy.map(unit => renderUnit(unit)) + )} +
+ +
+ +
+
+
+ )} +
+ ) +} diff --git a/admin-panel/src/components/index.ts b/admin-panel/src/components/index.ts index 01c695f..ffdc3c6 100644 --- a/admin-panel/src/components/index.ts +++ b/admin-panel/src/components/index.ts @@ -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' \ No newline at end of file +export { default as MoonIcon } from './MoonIcon' +export { default as OrganizationSelector } from './OrganizationSelector' diff --git a/admin-panel/src/views/CreateEmployee.tsx b/admin-panel/src/views/CreateEmployee.tsx index ddfa4e4..f3e19db 100644 --- a/admin-panel/src/views/CreateEmployee.tsx +++ b/admin-panel/src/views/CreateEmployee.tsx @@ -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(null) + const [selectedUnitName, setSelectedUnitName] = useState('') + const [selectedUnitRole, setSelectedUnitRole] = useState('mitarbeiter') - const { register, handleSubmit, formState: { errors }, watch } = useForm({ + const { register, handleSubmit, formState: { errors }, watch, setValue } = useForm({ 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 && (
{success} + {selectedUnitName && ( +
+ Zugeordnete Einheit: {selectedUnitName} · {getUnitRoleLabel(selectedUnitRole)} +
+ )} {createdUser.password && (

🔑 Temporäres Passwort:

@@ -192,8 +219,9 @@ export default function CreateEmployee() { {errors.department && (

{errors.department.message}

@@ -202,6 +230,54 @@ export default function CreateEmployee() {
+
+

+ Organisation & Rolle +

+ +
+
+ + { + setSelectedUnitId(unitId) + setSelectedUnitName(unitPath) + if (!unitId) { + setSelectedUnitRole('mitarbeiter') + } + }} + /> +

+ Wählen Sie die primäre Organisationseinheit für den neuen Mitarbeiter. Die Abteilung wird automatisch anhand der Auswahl gesetzt. +

+
+ + {selectedUnitId && ( +
+ + +

+ Diese Rolle wird für Vertretungs- und Organigramm-Funktionen verwendet. +

+
+ )} +
+
+

Benutzerkonto @@ -253,6 +329,7 @@ export default function CreateEmployee() {
  • • Der Mitarbeiter wird mit Grunddaten angelegt (Position: "Mitarbeiter", Telefon: "Nicht angegeben")
  • • {watchCreateUser ? 'Ein Benutzerkonto wird erstellt und ein temporäres Passwort generiert' : 'Kein Benutzerkonto wird erstellt'}
  • +
  • • {selectedUnitId ? `Die Organisationseinheit ${selectedUnitName} (${getUnitRoleLabel(selectedUnitRole)}) wird als primäre Zuordnung hinterlegt` : 'Organisationseinheit kann später im Organigramm zugewiesen werden'}
  • • Der Mitarbeiter kann später im Frontend seine Profildaten vervollständigen
  • • Alle Daten werden verschlüsselt in der Datenbank gespeichert
diff --git a/backend/scripts/seed-demo-data.ts b/backend/scripts/seed-demo-data.ts new file mode 100644 index 0000000..a51712f --- /dev/null +++ b/backend/scripts/seed-demo-data.ts @@ -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.`) diff --git a/backend/src/routes/employees.ts b/backend/src/routes/employees.ts index 905b6f2..79129d1 100644 --- a/backend/src/routes/employees.ts +++ b/backend/src/routes/employees.ts @@ -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 \ No newline at end of file +export default router