318 Zeilen
12 KiB
TypeScript
318 Zeilen
12 KiB
TypeScript
import { db, encryptedDb } from '../config/secureDatabase'
|
|
import { Employee } from '@skillmate/shared'
|
|
import { v4 as uuidv4 } from 'uuid'
|
|
|
|
export interface EmployeeInput {
|
|
firstName: string
|
|
lastName: string
|
|
employeeNumber?: string | null
|
|
photo?: string | null
|
|
position?: string
|
|
department: string
|
|
email: string
|
|
phone?: string | null
|
|
mobile?: string | null
|
|
office?: string | null
|
|
availability?: string
|
|
clearance?: { level?: string; validUntil?: string | Date | null; issuedDate?: string | Date | null } | null
|
|
skills?: Array<{ id: string; level?: any; verified?: boolean; verifiedBy?: string | null; verifiedDate?: string | Date | null }>
|
|
languages?: Array<{ code: string; level: string }>
|
|
specializations?: string[]
|
|
}
|
|
|
|
export function getAllWithDetails(): Employee[] {
|
|
const emps = (encryptedDb.getAllEmployees() as any[]) || []
|
|
if (emps.length === 0) return []
|
|
const ids = emps.map((e: any) => e.id)
|
|
const placeholders = ids.map(() => '?').join(',')
|
|
|
|
// Batch fetch related data
|
|
const skillsRows = db.prepare(`
|
|
SELECT es.employee_id, s.id, s.name, s.category, es.level, es.verified, es.verified_by, es.verified_date
|
|
FROM employee_skills es
|
|
JOIN skills s ON es.skill_id = s.id
|
|
WHERE es.employee_id IN (${placeholders})
|
|
`).all(...ids) as any[]
|
|
|
|
const langRows = db.prepare(`
|
|
SELECT employee_id, language, proficiency, certified, certificate_type, is_native, can_interpret
|
|
FROM language_skills
|
|
WHERE employee_id IN (${placeholders})
|
|
`).all(...ids) as any[]
|
|
|
|
const specRows = db.prepare(`
|
|
SELECT employee_id, name FROM specializations WHERE employee_id IN (${placeholders})
|
|
`).all(...ids) as any[]
|
|
|
|
const skillsByEmp = new Map<string, any[]>()
|
|
for (const r of skillsRows) {
|
|
if (!skillsByEmp.has(r.employee_id)) skillsByEmp.set(r.employee_id, [])
|
|
skillsByEmp.get(r.employee_id)!.push(r)
|
|
}
|
|
const langsByEmp = new Map<string, any[]>()
|
|
for (const r of langRows) {
|
|
if (!langsByEmp.has(r.employee_id)) langsByEmp.set(r.employee_id, [])
|
|
langsByEmp.get(r.employee_id)!.push(r)
|
|
}
|
|
const specsByEmp = new Map<string, string[]>()
|
|
for (const r of specRows) {
|
|
if (!specsByEmp.has(r.employee_id)) specsByEmp.set(r.employee_id, [])
|
|
specsByEmp.get(r.employee_id)!.push(r.name)
|
|
}
|
|
|
|
const mapProf = (p: string): 'basic' | 'fluent' | 'native' | 'business' => {
|
|
const m: Record<string, any> = { A1: 'basic', A2: 'basic', B1: 'business', B2: 'business', C1: 'fluent', C2: 'fluent', Muttersprache: 'native', native: 'native', fluent: 'fluent', advanced: 'business', intermediate: 'business', basic: 'basic' }
|
|
return m[p] || 'basic'
|
|
}
|
|
|
|
return emps.map((emp: any) => {
|
|
const s = skillsByEmp.get(emp.id) || []
|
|
const l = langsByEmp.get(emp.id) || []
|
|
const sp = specsByEmp.get(emp.id) || []
|
|
return {
|
|
id: emp.id,
|
|
firstName: emp.first_name,
|
|
lastName: emp.last_name,
|
|
employeeNumber: (emp.employee_number && !String(emp.employee_number).startsWith('EMP')) ? emp.employee_number : undefined,
|
|
photo: emp.photo,
|
|
position: emp.position,
|
|
department: emp.department,
|
|
email: emp.email,
|
|
phone: emp.phone,
|
|
mobile: emp.mobile,
|
|
office: emp.office,
|
|
availability: emp.availability,
|
|
skills: s.map((r: any) => ({ id: r.id, name: r.name, category: r.category, level: r.level, verified: Boolean(r.verified), verifiedBy: r.verified_by, verifiedDate: r.verified_date ? new Date(r.verified_date) : undefined })),
|
|
languages: l.map((r: any) => ({ code: r.language, level: mapProf(r.proficiency) })),
|
|
clearance: emp.clearance_level && emp.clearance_valid_until && emp.clearance_issued_date ? { level: emp.clearance_level, validUntil: new Date(emp.clearance_valid_until), issuedDate: new Date(emp.clearance_issued_date) } : undefined,
|
|
specializations: sp,
|
|
createdAt: new Date(emp.created_at),
|
|
updatedAt: new Date(emp.updated_at),
|
|
createdBy: emp.created_by,
|
|
updatedBy: emp.updated_by
|
|
} as Employee
|
|
})
|
|
}
|
|
|
|
export function getByIdWithDetails(id: string): Employee | null {
|
|
const emp = encryptedDb.getEmployee(id) as any
|
|
if (!emp) return null
|
|
return buildEmployeeWithDetails(emp)
|
|
}
|
|
|
|
function buildEmployeeWithDetails(emp: any): Employee {
|
|
const skills = db.prepare(`
|
|
SELECT s.id, s.name, s.category, es.level, es.verified, es.verified_by, es.verified_date
|
|
FROM employee_skills es
|
|
JOIN skills s ON es.skill_id = s.id
|
|
WHERE es.employee_id = ?
|
|
`).all(emp.id)
|
|
|
|
const languages = db.prepare(`
|
|
SELECT language, proficiency, certified, certificate_type, is_native, can_interpret
|
|
FROM language_skills
|
|
WHERE employee_id = ?
|
|
`).all(emp.id)
|
|
|
|
const specializations = db.prepare(`
|
|
SELECT name FROM specializations WHERE employee_id = ?
|
|
`).all(emp.id).map((s: any) => s.name)
|
|
|
|
const mapProf = (p: string): 'basic' | 'fluent' | 'native' | 'business' => {
|
|
const m: Record<string, any> = { A1: 'basic', A2: 'basic', B1: 'business', B2: 'business', C1: 'fluent', C2: 'fluent', Muttersprache: 'native', native: 'native', fluent: 'fluent', advanced: 'business', intermediate: 'business', basic: 'basic' }
|
|
return m[p] || 'basic'
|
|
}
|
|
|
|
return {
|
|
id: emp.id,
|
|
firstName: emp.first_name,
|
|
lastName: emp.last_name,
|
|
employeeNumber: (emp.employee_number && !String(emp.employee_number).startsWith('EMP')) ? emp.employee_number : undefined,
|
|
photo: emp.photo,
|
|
position: emp.position,
|
|
department: emp.department,
|
|
email: emp.email,
|
|
phone: emp.phone,
|
|
mobile: emp.mobile,
|
|
office: emp.office,
|
|
availability: emp.availability,
|
|
skills: skills.map((s: any) => ({
|
|
id: s.id,
|
|
name: s.name,
|
|
category: s.category,
|
|
level: s.level,
|
|
verified: Boolean(s.verified),
|
|
verifiedBy: s.verified_by,
|
|
verifiedDate: s.verified_date ? new Date(s.verified_date) : undefined
|
|
})),
|
|
languages: languages.map((l: any) => ({
|
|
code: l.language,
|
|
level: mapProf(l.proficiency)
|
|
})),
|
|
clearance: emp.clearance_level && emp.clearance_valid_until && emp.clearance_issued_date ? {
|
|
level: emp.clearance_level,
|
|
validUntil: new Date(emp.clearance_valid_until),
|
|
issuedDate: new Date(emp.clearance_issued_date)
|
|
} : undefined,
|
|
specializations,
|
|
createdAt: new Date(emp.created_at),
|
|
updatedAt: new Date(emp.updated_at),
|
|
createdBy: emp.created_by,
|
|
updatedBy: emp.updated_by
|
|
}
|
|
}
|
|
|
|
export function createEmployee(input: EmployeeInput, actorUserId: string): { id: string } {
|
|
const now = new Date().toISOString()
|
|
const employeeId = uuidv4()
|
|
const position = input.position || 'Teammitglied'
|
|
const phone = input.phone || 'Nicht angegeben'
|
|
const availability = input.availability || 'available'
|
|
const employeeNumber = input.employeeNumber || `EMP${Date.now()}`
|
|
|
|
const tx = db.transaction(() => {
|
|
// Uniqueness check for employee number
|
|
const existingEmployee = db.prepare('SELECT id FROM employees WHERE employee_number = ?').get(employeeNumber)
|
|
if (existingEmployee) {
|
|
const err: any = new Error('Employee number already exists')
|
|
err.statusCode = 409
|
|
throw err
|
|
}
|
|
|
|
encryptedDb.insertEmployee({
|
|
id: employeeId,
|
|
first_name: input.firstName,
|
|
last_name: input.lastName,
|
|
employee_number: employeeNumber,
|
|
photo: input.photo || null,
|
|
position,
|
|
department: input.department,
|
|
email: input.email,
|
|
phone,
|
|
mobile: input.mobile || null,
|
|
office: input.office || null,
|
|
availability,
|
|
clearance_level: input.clearance?.level || null,
|
|
clearance_valid_until: input.clearance?.validUntil || null,
|
|
clearance_issued_date: input.clearance?.issuedDate || null,
|
|
created_at: now,
|
|
updated_at: now,
|
|
created_by: actorUserId,
|
|
updated_by: actorUserId,
|
|
})
|
|
|
|
if (input.skills && input.skills.length > 0) {
|
|
const stmt = db.prepare(`
|
|
INSERT INTO employee_skills (employee_id, skill_id, level, verified, verified_by, verified_date)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`)
|
|
const checkSkillExists = db.prepare('SELECT id FROM skills WHERE id = ?')
|
|
for (const s of input.skills) {
|
|
const exists = checkSkillExists.get(s.id)
|
|
if (!exists) continue
|
|
stmt.run(employeeId, s.id, typeof s.level === 'number' ? s.level : (parseInt(String(s.level)) || null), s.verified ? 1 : 0, s.verifiedBy || null, s.verifiedDate || null)
|
|
}
|
|
}
|
|
|
|
if (input.languages && input.languages.length > 0) {
|
|
const stmt = db.prepare(`
|
|
INSERT INTO language_skills (id, employee_id, language, proficiency, certified, certificate_type, is_native, can_interpret)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
`)
|
|
for (const l of input.languages) {
|
|
stmt.run(uuidv4(), employeeId, l.code, l.level, 0, null, 0, 0)
|
|
}
|
|
}
|
|
|
|
if (input.specializations && input.specializations.length > 0) {
|
|
const stmt = db.prepare('INSERT INTO specializations (id, employee_id, name) VALUES (?, ?, ?)')
|
|
for (const name of input.specializations) {
|
|
stmt.run(uuidv4(), employeeId, name)
|
|
}
|
|
}
|
|
})
|
|
|
|
tx()
|
|
return { id: employeeId }
|
|
}
|
|
|
|
export function updateEmployee(id: string, input: EmployeeInput, actorUserId: string) {
|
|
const now = new Date().toISOString()
|
|
|
|
const tx = db.transaction(() => {
|
|
const existing = db.prepare('SELECT id FROM employees WHERE id = ?').get(id)
|
|
if (!existing) {
|
|
const err: any = new Error('Employee not found')
|
|
err.statusCode = 404
|
|
throw err
|
|
}
|
|
|
|
db.prepare(`
|
|
UPDATE employees SET
|
|
first_name = ?, last_name = ?, position = ?, 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,
|
|
// 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
|
|
input.phone || '',
|
|
null, // phone_hash set by route if needed
|
|
input.mobile || null,
|
|
input.office || null,
|
|
input.availability || 'available',
|
|
input.clearance?.level || null,
|
|
input.clearance?.validUntil || null,
|
|
input.clearance?.issuedDate || null,
|
|
now,
|
|
actorUserId,
|
|
id
|
|
)
|
|
|
|
if (input.skills) {
|
|
db.prepare('DELETE FROM employee_skills WHERE employee_id = ?').run(id)
|
|
if (input.skills.length > 0) {
|
|
const insert = db.prepare(`
|
|
INSERT INTO employee_skills (employee_id, skill_id, level, verified, verified_by, verified_date)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
`)
|
|
const checkSkillExists = db.prepare('SELECT id FROM skills WHERE id = ?')
|
|
for (const s of input.skills) {
|
|
const exists = checkSkillExists.get(s.id)
|
|
if (!exists) continue
|
|
insert.run(
|
|
id,
|
|
s.id,
|
|
typeof s.level === 'number' ? s.level : (parseInt(String(s.level)) || null),
|
|
s.verified ? 1 : 0,
|
|
s.verifiedBy || null,
|
|
s.verifiedDate || null
|
|
)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
tx()
|
|
}
|
|
|
|
export function deleteEmployee(id: string) {
|
|
const tx = db.transaction(() => {
|
|
const existing = db.prepare('SELECT id FROM employees WHERE id = ?').get(id)
|
|
if (!existing) {
|
|
const err: any = new Error('Employee not found')
|
|
err.statusCode = 404
|
|
throw err
|
|
}
|
|
db.prepare('DELETE FROM employee_skills WHERE employee_id = ?').run(id)
|
|
db.prepare('DELETE FROM language_skills WHERE employee_id = ?').run(id)
|
|
db.prepare('DELETE FROM specializations WHERE employee_id = ?').run(id)
|
|
db.prepare('DELETE FROM employees WHERE id = ?').run(id)
|
|
})
|
|
tx()
|
|
}
|