diff --git a/backend/src/config/secureDatabase.ts b/backend/src/config/secureDatabase.ts index d7eb48f..9d69202 100644 --- a/backend/src/config/secureDatabase.ts +++ b/backend/src/config/secureDatabase.ts @@ -83,13 +83,13 @@ export const encryptedDb = { return db.prepare(` INSERT INTO employees ( - id, first_name, last_name, employee_number, photo, position, + id, first_name, last_name, employee_number, photo, position, official_title, department, email, email_hash, phone, phone_hash, mobile, office, availability, clearance_level, clearance_valid_until, clearance_issued_date, primary_unit_id, created_at, updated_at, created_by ) VALUES ( - @id, @first_name, @last_name, @employee_number, @photo, @position, + @id, @first_name, @last_name, @employee_number, @photo, @position, @official_title, @department, @email, @email_hash, @phone, @phone_hash, @mobile, @office, @availability, @clearance_level, @clearance_valid_until, @clearance_issued_date, @primary_unit_id, @@ -109,6 +109,7 @@ export const encryptedDb = { // Decrypt sensitive fields return { ...employee, + official_title: employee.official_title, email: FieldEncryption.decrypt(employee.email), phone: FieldEncryption.decrypt(employee.phone), mobile: FieldEncryption.decrypt(employee.mobile), @@ -136,6 +137,7 @@ export const encryptedDb = { return { ...emp, + official_title: emp.official_title, email: safeDecrypt(emp.email), phone: safeDecrypt(emp.phone), mobile: safeDecrypt(emp.mobile), @@ -164,6 +166,7 @@ export function initializeSecureDatabase() { employee_number TEXT UNIQUE NOT NULL, photo TEXT, position TEXT NOT NULL, + official_title TEXT, department TEXT NOT NULL, email TEXT NOT NULL, email_hash TEXT, @@ -190,6 +193,10 @@ export function initializeSecureDatabase() { if (!hasPrimaryUnitId) { db.exec(`ALTER TABLE employees ADD COLUMN primary_unit_id TEXT`) } + const hasOfficialTitle = cols.some(c => c.name === 'official_title') + if (!hasOfficialTitle) { + db.exec(`ALTER TABLE employees ADD COLUMN official_title TEXT`) + } } catch (e) { // ignore } diff --git a/backend/src/repositories/employeeRepository.ts b/backend/src/repositories/employeeRepository.ts index fc074d4..563c74b 100644 --- a/backend/src/repositories/employeeRepository.ts +++ b/backend/src/repositories/employeeRepository.ts @@ -8,6 +8,7 @@ export interface EmployeeInput { employeeNumber?: string | null photo?: string | null position?: string + officialTitle?: string | null department: string email: string phone?: string | null @@ -76,6 +77,7 @@ export function getAllWithDetails(): Employee[] { employeeNumber: (emp.employee_number && !String(emp.employee_number).startsWith('EMP')) ? emp.employee_number : undefined, photo: emp.photo, position: emp.position, + officialTitle: emp.official_title || undefined, department: emp.department, email: emp.email, phone: emp.phone, @@ -136,6 +138,7 @@ function buildEmployeeWithDetails(emp: any): Employee { mobile: emp.mobile, office: emp.office, availability: emp.availability, + officialTitle: emp.official_title || undefined, skills: skills.map((s: any) => ({ id: s.id, name: s.name, @@ -166,6 +169,7 @@ export function createEmployee(input: EmployeeInput, actorUserId: string): { id: const now = new Date().toISOString() const employeeId = uuidv4() const position = input.position || 'Teammitglied' + const officialTitle = input.officialTitle || null const phone = input.phone || 'Nicht angegeben' const availability = input.availability || 'available' const employeeNumber = input.employeeNumber || `EMP${Date.now()}` @@ -186,6 +190,7 @@ export function createEmployee(input: EmployeeInput, actorUserId: string): { id: employee_number: employeeNumber, photo: input.photo || null, position, + official_title: officialTitle, department: input.department, email: input.email, phone, @@ -249,14 +254,14 @@ export function updateEmployee(id: string, input: EmployeeInput, actorUserId: st db.prepare(` UPDATE employees SET - first_name = ?, last_name = ?, position = ?, department = ?, + first_name = ?, last_name = ?, position = ?, official_title = ?, department = ?, email = ?, email_hash = ?, phone = ?, phone_hash = ?, mobile = ?, office = ?, availability = ?, clearance_level = ?, clearance_valid_until = ?, clearance_issued_date = ?, updated_at = ?, updated_by = ? WHERE id = ? `).run( - input.firstName, input.lastName, input.position || 'Teammitglied', input.department, + input.firstName, input.lastName, input.position || 'Teammitglied', input.officialTitle || null, input.department, // encryption handled in secure db layer caller; here store encrypted values already in input? Route prepares with FieldEncryption input.email, // already encrypted by route layer null, // email_hash set by route if needed diff --git a/backend/src/routes/employees.ts b/backend/src/routes/employees.ts index 2b875ab..38ac6e1 100644 --- a/backend/src/routes/employees.ts +++ b/backend/src/routes/employees.ts @@ -34,7 +34,7 @@ function mapProficiencyToLevel(proficiency: string): 'basic' | 'fluent' | 'nativ router.get('/', authenticate, requirePermission('employees:read'), async (req: AuthRequest, res, next) => { try { const employees = db.prepare(` - SELECT id, first_name, last_name, employee_number, photo, position, + SELECT id, first_name, last_name, employee_number, photo, position, official_title, department, email, phone, mobile, office, availability, clearance_level, clearance_valid_until, clearance_issued_date, created_at, updated_at, created_by, updated_by @@ -70,6 +70,7 @@ router.get('/', authenticate, requirePermission('employees:read'), async (req: A employeeNumber: emp.employee_number, photo: emp.photo, position: emp.position, + officialTitle: emp.official_title || undefined, department: emp.department, email: emp.email, phone: emp.phone, @@ -116,7 +117,7 @@ router.get('/:id', authenticate, requirePermission('employees:read'), async (req const { id } = req.params const emp = db.prepare(` - SELECT id, first_name, last_name, employee_number, photo, position, + SELECT id, first_name, last_name, employee_number, photo, position, official_title, department, email, phone, mobile, office, availability, clearance_level, clearance_valid_until, clearance_issued_date, created_at, updated_at, created_by, updated_by @@ -158,6 +159,7 @@ router.get('/:id', authenticate, requirePermission('employees:read'), async (req employeeNumber: emp.employee_number, photo: emp.photo, position: emp.position, + officialTitle: emp.official_title || undefined, department: emp.department, email: emp.email, phone: emp.phone, @@ -202,6 +204,7 @@ router.post('/', [ body('firstName').notEmpty().trim(), body('lastName').notEmpty().trim(), + body('officialTitle').optional().trim(), body('email').isEmail(), body('department').notEmpty().trim(), body('organizationUnitId').optional({ checkFalsy: true }).isUUID(), @@ -221,7 +224,7 @@ router.post('/', const now = new Date().toISOString() const { - firstName, lastName, employeeNumber, photo, position, + firstName, lastName, employeeNumber, photo, position, officialTitle, department, email, phone, mobile, office, availability, clearance, skills, languages, specializations, userRole, createUser, organizationUnitId, organizationRole @@ -253,11 +256,11 @@ router.post('/', // Insert employee with default values for missing fields db.prepare(` INSERT INTO employees ( - id, first_name, last_name, employee_number, photo, position, + id, first_name, last_name, employee_number, photo, position, official_title, department, email, phone, mobile, office, availability, primary_unit_id, clearance_level, clearance_valid_until, clearance_issued_date, created_at, updated_at, created_by - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( employeeId, firstName, @@ -265,6 +268,7 @@ router.post('/', employeeNumber || null, photo || null, position || 'Teammitglied', // Default position + officialTitle || null, resolvedDepartment, email, phone || 'Nicht angegeben', // Default phone @@ -430,6 +434,7 @@ router.put('/:id', body('firstName').notEmpty().trim(), body('lastName').notEmpty().trim(), body('position').notEmpty().trim(), + body('officialTitle').optional().trim(), body('department').notEmpty().trim(), body('email').isEmail(), body('phone').notEmpty().trim(), @@ -449,7 +454,7 @@ router.put('/:id', const now = new Date().toISOString() const { - firstName, lastName, position, department, email, phone, + firstName, lastName, position, officialTitle, department, email, phone, mobile, office, availability, clearance, skills, languages, specializations } = req.body @@ -465,13 +470,13 @@ router.put('/:id', // Update employee db.prepare(` UPDATE employees SET - first_name = ?, last_name = ?, position = ?, department = ?, + first_name = ?, last_name = ?, position = ?, official_title = ?, department = ?, email = ?, phone = ?, mobile = ?, office = ?, availability = ?, clearance_level = ?, clearance_valid_until = ?, clearance_issued_date = ?, updated_at = ?, updated_by = ? WHERE id = ? `).run( - firstName, lastName, position, department, + firstName, lastName, position, officialTitle || null, department, email, phone, mobile || null, office || null, availability, clearance?.level || null, clearance?.validUntil || null, clearance?.issuedDate || null, now, req.user!.id, id @@ -508,6 +513,7 @@ router.put('/:id', firstName, lastName, position, + officialTitle: officialTitle || null, department, email, phone, diff --git a/backend/src/routes/employeesSecure.ts b/backend/src/routes/employeesSecure.ts index 96613a6..4dbcdb2 100644 --- a/backend/src/routes/employeesSecure.ts +++ b/backend/src/routes/employeesSecure.ts @@ -121,6 +121,7 @@ router.get('/', authenticate, requirePermission('employees:read'), async (req: A employeeNumber: (emp.employee_number && !String(emp.employee_number).startsWith('EMP')) ? emp.employee_number : undefined, photo: emp.photo, position: emp.position, + officialTitle: emp.official_title || undefined, department: emp.department, email: emp.email, phone: emp.phone, @@ -203,6 +204,7 @@ router.get('/public', authenticate, async (req: AuthRequest, res, next) => { employeeNumber: (emp.employee_number && !String(emp.employee_number).startsWith('EMP')) ? emp.employee_number : undefined, photo: emp.photo, position: emp.position, + officialTitle: emp.official_title || undefined, department: emp.department, email: emp.email, phone: emp.phone, @@ -285,6 +287,7 @@ router.get('/:id', authenticate, requirePermission('employees:read'), async (req employeeNumber: (emp.employee_number && !String(emp.employee_number).startsWith('EMP')) ? emp.employee_number : undefined, photo: emp.photo, position: emp.position, + officialTitle: emp.official_title || undefined, department: emp.department, email: emp.email, phone: emp.phone, @@ -354,7 +357,7 @@ router.post('/', const now = new Date().toISOString() const { - firstName, lastName, employeeNumber, photo, position = 'Teammitglied', + firstName, lastName, employeeNumber, photo, position = 'Teammitglied', officialTitle, department, email, phone = 'Nicht angegeben', mobile, office, availability = 'available', clearance, skills = [], languages = [], specializations = [], userRole, createUser, primaryUnitId, assignmentRole @@ -380,6 +383,7 @@ router.post('/', employee_number: finalEmployeeNumber, photo: photo || null, position, + official_title: officialTitle || null, department, email, phone, @@ -513,6 +517,7 @@ router.post('/', employeeNumber: finalEmployeeNumber, photo: photo || null, position, + officialTitle: officialTitle || null, department, email, phone, @@ -591,6 +596,7 @@ router.put('/:id', body('firstName').notEmpty().trim().escape(), body('lastName').notEmpty().trim().escape(), body('position').optional().trim().escape(), + body('officialTitle').optional().trim().escape(), body('department').notEmpty().trim().escape(), body('email').isEmail().normalizeEmail(), body('phone').optional().trim(), @@ -612,7 +618,7 @@ router.put('/:id', const now = new Date().toISOString() const { - firstName, lastName, position = 'Teammitglied', department, email, phone = 'Nicht angegeben', + firstName, lastName, position = 'Teammitglied', officialTitle, department, email, phone = 'Nicht angegeben', mobile, office, availability = 'available', clearance, skills, languages, specializations, employeeNumber } = req.body @@ -629,14 +635,14 @@ router.put('/:id', // Update employee with encrypted fields db.prepare(` UPDATE employees SET - first_name = ?, last_name = ?, position = ?, department = ?, + first_name = ?, last_name = ?, position = ?, official_title = ?, department = ?, email = ?, email_hash = ?, phone = ?, phone_hash = ?, mobile = ?, office = ?, availability = ?, clearance_level = ?, clearance_valid_until = ?, clearance_issued_date = ?, updated_at = ?, updated_by = ? WHERE id = ? `).run( - firstName, lastName, position, department, + firstName, lastName, position, officialTitle || null, department, FieldEncryption.encrypt(email), FieldEncryption.hash(email), FieldEncryption.encrypt(phone || ''), @@ -702,6 +708,7 @@ router.put('/:id', firstName, lastName, position, + officialTitle: officialTitle || null, department, email, phone, diff --git a/backend/src/usecases/employees.ts b/backend/src/usecases/employees.ts index 2a99902..bfb98ca 100644 --- a/backend/src/usecases/employees.ts +++ b/backend/src/usecases/employees.ts @@ -18,6 +18,7 @@ export async function createEmployeeUC(req: Request, body: any, actorUserId: str employeeNumber: body.employeeNumber, photo: body.photo, position: body.position, + officialTitle: body.officialTitle, department: body.department, email: body.email, phone: body.phone, @@ -40,6 +41,7 @@ export async function createEmployeeUC(req: Request, body: any, actorUserId: str employeeNumber: body.employeeNumber || null, photo: body.photo || null, position: body.position || 'Teammitglied', + officialTitle: body.officialTitle || null, department: body.department, email: body.email, phone: body.phone || 'Nicht angegeben', @@ -65,6 +67,7 @@ export async function updateEmployeeUC(req: Request, id: string, body: any, acto firstName: body.firstName, lastName: body.lastName, position: body.position, + officialTitle: body.officialTitle, department: body.department, email: body.email, phone: body.phone, @@ -84,6 +87,7 @@ export async function updateEmployeeUC(req: Request, id: string, body: any, acto firstName: body.firstName, lastName: body.lastName, position: body.position, + officialTitle: body.officialTitle, department: body.department, email: body.email, phone: body.phone, diff --git a/frontend/src/components/EmployeeCard.tsx b/frontend/src/components/EmployeeCard.tsx index eff457e..b329806 100644 --- a/frontend/src/components/EmployeeCard.tsx +++ b/frontend/src/components/EmployeeCard.tsx @@ -63,6 +63,11 @@ export default function EmployeeCard({ employee, onClick }: EmployeeCardProps) {
Position: {employee.position}
+ {employee.officialTitle && ( ++ Amtsbezeichnung: {employee.officialTitle} +
+ )}Dienststelle: {employee.department}
diff --git a/frontend/src/views/EmployeeDetail.tsx b/frontend/src/views/EmployeeDetail.tsx index 352b53b..5a284bf 100644 --- a/frontend/src/views/EmployeeDetail.tsx +++ b/frontend/src/views/EmployeeDetail.tsx @@ -168,6 +168,12 @@ export default function EmployeeDetail() { Position:{employee.position}
+ {employee.officialTitle && ( +{employee.officialTitle}
+{employee.department}
diff --git a/frontend/src/views/EmployeeForm.tsx b/frontend/src/views/EmployeeForm.tsx index d94e5b9..1f91ea4 100644 --- a/frontend/src/views/EmployeeForm.tsx +++ b/frontend/src/views/EmployeeForm.tsx @@ -17,6 +17,7 @@ export default function EmployeeForm() { lastName: '', employeeNumber: '', position: '', + officialTitle: '', department: '', email: '', phone: '', @@ -219,6 +220,7 @@ export default function EmployeeForm() { validUntil: new Date(new Date().setFullYear(new Date().getFullYear() + 5)), issuedDate: new Date() } : undefined, + officialTitle: formData.officialTitle || undefined, createdAt: new Date(), updatedAt: new Date(), createdBy: 'admin' @@ -394,6 +396,20 @@ export default function EmployeeForm() { )}Beispiele: Sachbearbeiter, Führungskraft g. D., Führungskraft h. D.
+ setForm((p: any) => ({ ...p, position: e.target.value }))} + placeholder="z. B. Sachbearbeitung, Teamleitung" + /> +Beispiele: Sachbearbeitung, Teamleitung, Stabsstelle.
+Freifeld für Amts- bzw. Dienstbezeichnungen (z. B. KOK, RBe, EKHK).
Angabe zum Standort, z. B. Gebäude, Etage und Raum.
Dieser Status wird in der Mitarbeitendenübersicht und Teamplanung angezeigt.
+