Rollback - PDF Import funzt so semi

Dieser Commit ist enthalten in:
Claude Project Manager
2025-09-23 22:40:37 +02:00
Ursprung 26f95d2e4a
Commit 2cabd4c0c6
27 geänderte Dateien mit 4455 neuen und 41 gelöschten Zeilen

Datei anzeigen

@ -376,6 +376,119 @@ export function initializeDatabase() {
)
`)
// Organizational Structure Tables
db.exec(`
CREATE TABLE IF NOT EXISTS organizational_units (
id TEXT PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('direktion', 'abteilung', 'dezernat', 'sachgebiet', 'teildezernat', 'fuehrungsstelle', 'stabsstelle', 'sondereinheit')),
level INTEGER NOT NULL,
parent_id TEXT,
position_x INTEGER,
position_y INTEGER,
color TEXT,
order_index INTEGER DEFAULT 0,
description TEXT,
has_fuehrungsstelle INTEGER DEFAULT 0,
fuehrungsstelle_name TEXT,
is_active INTEGER DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (parent_id) REFERENCES organizational_units(id)
);
CREATE INDEX IF NOT EXISTS idx_org_units_parent ON organizational_units(parent_id);
CREATE INDEX IF NOT EXISTS idx_org_units_type ON organizational_units(type);
CREATE INDEX IF NOT EXISTS idx_org_units_level ON organizational_units(level);
`)
// Employee Unit Assignments
db.exec(`
CREATE TABLE IF NOT EXISTS employee_unit_assignments (
id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL,
unit_id TEXT NOT NULL,
role TEXT NOT NULL CHECK(role IN ('leiter', 'stellvertreter', 'mitarbeiter', 'beauftragter')),
start_date TEXT NOT NULL,
end_date TEXT,
is_primary INTEGER DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
FOREIGN KEY (unit_id) REFERENCES organizational_units(id) ON DELETE CASCADE,
UNIQUE(employee_id, unit_id, role)
);
CREATE INDEX IF NOT EXISTS idx_emp_units_employee ON employee_unit_assignments(employee_id);
CREATE INDEX IF NOT EXISTS idx_emp_units_unit ON employee_unit_assignments(unit_id);
`)
// Special Positions (Personalrat, Beauftragte, etc.)
db.exec(`
CREATE TABLE IF NOT EXISTS special_positions (
id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL,
position_type TEXT NOT NULL CHECK(position_type IN ('personalrat', 'schwerbehindertenvertretung', 'datenschutzbeauftragter', 'gleichstellungsbeauftragter', 'inklusionsbeauftragter', 'informationssicherheitsbeauftragter', 'geheimschutzbeauftragter', 'extremismusbeauftragter')),
unit_id TEXT,
start_date TEXT NOT NULL,
end_date TEXT,
is_active INTEGER DEFAULT 1,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
FOREIGN KEY (unit_id) REFERENCES organizational_units(id)
);
CREATE INDEX IF NOT EXISTS idx_special_pos_employee ON special_positions(employee_id);
CREATE INDEX IF NOT EXISTS idx_special_pos_type ON special_positions(position_type);
`)
// Deputy Assignments (Vertretungen)
db.exec(`
CREATE TABLE IF NOT EXISTS deputy_assignments (
id TEXT PRIMARY KEY,
principal_id TEXT NOT NULL,
deputy_id TEXT NOT NULL,
unit_id TEXT,
valid_from TEXT NOT NULL,
valid_until TEXT NOT NULL,
reason TEXT,
can_delegate INTEGER DEFAULT 1,
created_by TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (principal_id) REFERENCES employees(id),
FOREIGN KEY (deputy_id) REFERENCES employees(id),
FOREIGN KEY (unit_id) REFERENCES organizational_units(id),
FOREIGN KEY (created_by) REFERENCES users(id)
);
CREATE INDEX IF NOT EXISTS idx_deputy_principal ON deputy_assignments(principal_id);
CREATE INDEX IF NOT EXISTS idx_deputy_deputy ON deputy_assignments(deputy_id);
CREATE INDEX IF NOT EXISTS idx_deputy_dates ON deputy_assignments(valid_from, valid_until);
`)
// Deputy Delegations (Vertretungs-Weitergaben)
db.exec(`
CREATE TABLE IF NOT EXISTS deputy_delegations (
id TEXT PRIMARY KEY,
original_assignment_id TEXT NOT NULL,
from_deputy_id TEXT NOT NULL,
to_deputy_id TEXT NOT NULL,
reason TEXT,
delegated_at TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (original_assignment_id) REFERENCES deputy_assignments(id),
FOREIGN KEY (from_deputy_id) REFERENCES employees(id),
FOREIGN KEY (to_deputy_id) REFERENCES employees(id)
);
CREATE INDEX IF NOT EXISTS idx_delegation_assignment ON deputy_delegations(original_assignment_id);
CREATE INDEX IF NOT EXISTS idx_delegation_from ON deputy_delegations(from_deputy_id);
CREATE INDEX IF NOT EXISTS idx_delegation_to ON deputy_delegations(to_deputy_id);
`)
// Audit Log für Änderungsverfolgung
db.exec(`
CREATE TABLE IF NOT EXISTS audit_log (

Datei anzeigen

@ -4,6 +4,7 @@ import helmet from 'helmet'
import dotenv from 'dotenv'
import path from 'path'
import { initializeSecureDatabase } from './config/secureDatabase'
import { initializeDatabase } from './config/database'
import authRoutes from './routes/auth'
import employeeRoutes from './routes/employeesSecure'
import profileRoutes from './routes/profiles'
@ -15,6 +16,9 @@ import workspaceRoutes from './routes/workspaces'
import userRoutes from './routes/users'
import userAdminRoutes from './routes/usersAdmin'
import settingsRoutes from './routes/settings'
import organizationRoutes from './routes/organization'
import organizationImportRoutes from './routes/organizationImport'
import employeeOrganizationRoutes from './routes/employeeOrganization'
// import bookingRoutes from './routes/bookings' // Temporär deaktiviert wegen TS-Fehlern
// import analyticsRoutes from './routes/analytics' // Temporär deaktiviert
import { errorHandler } from './middleware/errorHandler'
@ -26,8 +30,9 @@ dotenv.config()
const app = express()
const PORT = process.env.PORT || 3004
// Initialize secure database
// Initialize secure database (core tables) and extended schema (organization, deputies, etc.)
initializeSecureDatabase()
initializeDatabase()
// Initialize sync scheduler
syncScheduler
@ -57,6 +62,9 @@ app.use('/api/workspaces', workspaceRoutes)
app.use('/api/users', userRoutes)
app.use('/api/admin/users', userAdminRoutes)
app.use('/api/admin/settings', settingsRoutes)
app.use('/api/organization', organizationRoutes)
app.use('/api/organization', organizationImportRoutes)
app.use('/api', employeeOrganizationRoutes)
// app.use('/api/bookings', bookingRoutes) // Temporär deaktiviert
// app.use('/api/analytics', analyticsRoutes) // Temporär deaktiviert

Datei anzeigen

@ -0,0 +1,166 @@
import { Router } from 'express'
import { v4 as uuidv4 } from 'uuid'
import { db } from '../config/secureDatabase'
import { authenticate, AuthRequest } from '../middleware/auth'
import { logger } from '../utils/logger'
const router = Router()
// Get employee's current organization unit
router.get('/employee/:employeeId/organization', authenticate, async (req: AuthRequest, res, next) => {
try {
const { employeeId } = req.params
// Check if user can access this employee's data
if (req.user?.employeeId !== employeeId && req.user?.role !== 'admin' && req.user?.role !== 'superuser') {
return res.status(403).json({ success: false, error: { message: 'Access denied' } })
}
const unit = db.prepare(`
SELECT
ou.id, ou.code, ou.name, ou.type, ou.level,
eua.role, eua.is_primary as isPrimary,
eua.start_date as startDate
FROM employee_unit_assignments eua
JOIN organizational_units ou ON ou.id = eua.unit_id
WHERE eua.employee_id = ?
AND eua.is_primary = 1
AND (eua.end_date IS NULL OR eua.end_date > datetime('now'))
AND ou.is_active = 1
ORDER BY eua.start_date DESC
LIMIT 1
`).get(employeeId)
if (!unit) {
return res.json({ success: true, data: null })
}
res.json({ success: true, data: unit })
} catch (error) {
logger.error('Error fetching employee organization:', error)
next(error)
}
})
// Update employee's organization unit (simplified endpoint)
router.put('/employee/:employeeId/organization', authenticate, async (req: AuthRequest, res: any, next: any) => {
try {
const { employeeId } = req.params
const { unitId } = req.body
// Check permissions
if (req.user?.employeeId !== employeeId && req.user?.role !== 'admin' && req.user?.role !== 'superuser') {
return res.status(403).json({ success: false, error: { message: 'Access denied' } })
}
const now = new Date().toISOString()
// Start transaction
const transaction = db.transaction(() => {
// End all current assignments for this employee
db.prepare(`
UPDATE employee_unit_assignments
SET end_date = ?, updated_at = ?
WHERE employee_id = ? AND end_date IS NULL
`).run(now, now, employeeId)
// If unitId provided, create new assignment
if (unitId) {
// Verify unit exists
const unit = db.prepare('SELECT id FROM organizational_units WHERE id = ? AND is_active = 1').get(unitId)
if (!unit) {
throw new Error('Unit not found')
}
const assignmentId = uuidv4()
db.prepare(`
INSERT INTO employee_unit_assignments (
id, employee_id, unit_id, role,
start_date, is_primary, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
assignmentId, employeeId, unitId, 'mitarbeiter',
now, 1, now, now
)
}
// Update employee's department field for backward compatibility
if (unitId) {
const unitInfo = db.prepare('SELECT name FROM organizational_units WHERE id = ?').get(unitId) as any
if (unitInfo) {
db.prepare(`
UPDATE employees
SET department = ?, updated_at = ?
WHERE id = ?
`).run(unitInfo.name, now, employeeId)
}
} else {
// Clear department if no unit
db.prepare(`
UPDATE employees
SET department = NULL, updated_at = ?
WHERE id = ?
`).run(now, employeeId)
}
})
try {
transaction()
res.json({ success: true, message: 'Organization updated successfully' })
} catch (error: any) {
if (error.message === 'Unit not found') {
return res.status(404).json({ success: false, error: { message: 'Unit not found' } })
}
throw error
}
} catch (error) {
logger.error('Error updating employee organization:', error)
next(error)
}
})
// Get all employees in a unit (for managers)
router.get('/unit/:unitId/employees', authenticate, async (req: AuthRequest, res, next) => {
try {
const { unitId } = req.params
// Check if user is a manager of this unit or admin
const isManager = db.prepare(`
SELECT 1 FROM employee_unit_assignments
WHERE employee_id = ? AND unit_id = ?
AND role IN ('leiter', 'stellvertreter')
AND (end_date IS NULL OR end_date > datetime('now'))
`).get(req.user?.employeeId, unitId)
if (!isManager && req.user?.role !== 'admin' && req.user?.role !== 'superuser') {
return res.status(403).json({ success: false, error: { message: 'Access denied' } })
}
const employees = db.prepare(`
SELECT
e.id, e.first_name as firstName, e.last_name as lastName,
e.position, e.email, e.phone, e.photo,
eua.role, eua.is_primary as isPrimary,
eua.start_date as startDate
FROM employee_unit_assignments eua
JOIN employees e ON e.id = eua.employee_id
WHERE eua.unit_id = ?
AND (eua.end_date IS NULL OR eua.end_date > datetime('now'))
ORDER BY
CASE eua.role
WHEN 'leiter' THEN 1
WHEN 'stellvertreter' THEN 2
WHEN 'beauftragter' THEN 3
ELSE 4
END,
e.last_name, e.first_name
`).all(unitId)
res.json({ success: true, data: employees })
} catch (error) {
logger.error('Error fetching unit employees:', error)
next(error)
}
})
export default router

Datei anzeigen

@ -0,0 +1,681 @@
import { Router } from 'express'
import { v4 as uuidv4 } from 'uuid'
import { body, param, validationResult } from 'express-validator'
import { db } from '../config/secureDatabase'
import { authenticate, AuthRequest } from '../middleware/auth'
import {
OrganizationalUnit,
EmployeeUnitAssignment,
SpecialPosition,
DeputyAssignment,
DeputyDelegation
} from '@skillmate/shared'
import { logger } from '../utils/logger'
const router = Router()
// Get all organizational units
router.get('/units', authenticate, async (req: AuthRequest, res, next) => {
try {
const units = db.prepare(`
SELECT
id, code, name, type, level, parent_id as parentId,
position_x as positionX, position_y as positionY,
color, order_index as orderIndex, description,
has_fuehrungsstelle as hasFuehrungsstelle,
fuehrungsstelle_name as fuehrungsstelleName,
is_active as isActive,
created_at as createdAt,
updated_at as updatedAt
FROM organizational_units
WHERE is_active = 1
ORDER BY level, order_index, name
`).all()
res.json({ success: true, data: units })
} catch (error) {
logger.error('Error fetching organizational units:', error)
next(error)
}
})
// Get organizational hierarchy (tree structure)
router.get('/hierarchy', authenticate, async (req: AuthRequest, res, next) => {
try {
const units = db.prepare(`
SELECT
id, code, name, type, level, parent_id as parentId,
position_x as positionX, position_y as positionY,
color, order_index as orderIndex, description,
has_fuehrungsstelle as hasFuehrungsstelle,
fuehrungsstelle_name as fuehrungsstelleName,
is_active as isActive
FROM organizational_units
WHERE is_active = 1
ORDER BY level, order_index, name
`).all() as any[]
// Build tree structure
const unitMap = new Map()
const rootUnits: any[] = []
// First pass: create map
units.forEach(unit => {
unitMap.set(unit.id, { ...unit, children: [] })
})
// Second pass: build tree
units.forEach(unit => {
const node = unitMap.get(unit.id)
if (unit.parentId && unitMap.has(unit.parentId)) {
unitMap.get(unit.parentId).children.push(node)
} else {
rootUnits.push(node)
}
})
res.json({ success: true, data: rootUnits })
} catch (error) {
logger.error('Error building organizational hierarchy:', error)
next(error)
}
})
// Get single unit with employees
router.get('/units/:id', authenticate, async (req: AuthRequest, res, next) => {
try {
const unit = db.prepare(`
SELECT
id, code, name, type, level, parent_id as parentId,
position_x as positionX, position_y as positionY,
color, order_index as orderIndex, description,
has_fuehrungsstelle as hasFuehrungsstelle,
fuehrungsstelle_name as fuehrungsstelleName,
is_active as isActive,
created_at as createdAt,
updated_at as updatedAt
FROM organizational_units
WHERE id = ?
`).get(req.params.id) as any
if (!unit) {
return res.status(404).json({ success: false, error: { message: 'Unit not found' } })
}
// Get employees assigned to this unit
const employees = db.prepare(`
SELECT
e.id, e.first_name as firstName, e.last_name as lastName,
e.position, e.department, e.email, e.phone, e.photo,
eua.role, eua.is_primary as isPrimary
FROM employee_unit_assignments eua
JOIN employees e ON e.id = eua.employee_id
WHERE eua.unit_id = ? AND (eua.end_date IS NULL OR eua.end_date > datetime('now'))
ORDER BY
CASE eua.role
WHEN 'leiter' THEN 1
WHEN 'stellvertreter' THEN 2
WHEN 'beauftragter' THEN 3
ELSE 4
END,
e.last_name, e.first_name
`).all(req.params.id)
res.json({
success: true,
data: {
...unit,
employees
}
})
} catch (error) {
logger.error('Error fetching unit details:', error)
next(error)
}
})
// Create new organizational unit (Admin only)
router.post('/units',
authenticate,
[
body('code').notEmpty().trim(),
body('name').notEmpty().trim(),
body('type').isIn(['direktion', 'abteilung', 'dezernat', 'sachgebiet', 'teildezernat', 'fuehrungsstelle', 'stabsstelle', 'sondereinheit']),
body('level').isInt({ min: 0, max: 10 }),
body('parentId').optional({ checkFalsy: true }).isUUID()
],
async (req: AuthRequest, res: any, next: any) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ success: false, error: { message: 'Invalid input', details: errors.array() } })
}
// Check admin permission
if (req.user?.role !== 'admin') {
return res.status(403).json({ success: false, error: { message: 'Admin access required' } })
}
const { code, name, type, level, parentId, color, description, hasFuehrungsstelle, fuehrungsstelleName } = req.body
const now = new Date().toISOString()
const unitId = uuidv4()
// Check if code already exists
const existing = db.prepare('SELECT id FROM organizational_units WHERE code = ?').get(code)
if (existing) {
return res.status(400).json({ success: false, error: { message: 'Unit code already exists' } })
}
// Get max order index for this level
const maxOrder = db.prepare('SELECT MAX(order_index) as max FROM organizational_units WHERE level = ?').get(level) as any
const orderIndex = (maxOrder?.max || 0) + 1
db.prepare(`
INSERT INTO organizational_units (
id, code, name, type, level, parent_id,
color, order_index, description,
has_fuehrungsstelle, fuehrungsstelle_name,
is_active, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
unitId, code, name, type, level, parentId || null,
color || null, orderIndex, description || null,
hasFuehrungsstelle ? 1 : 0, fuehrungsstelleName || null,
1, now, now
)
res.json({ success: true, data: { id: unitId } })
} catch (error) {
logger.error('Error creating organizational unit:', error)
next(error)
}
}
)
// Update organizational unit (Admin only)
router.put('/units/:id',
authenticate,
[
param('id').isUUID(),
body('name').optional().notEmpty().trim(),
body('description').optional(),
body('color').optional(),
// allow updating persisted canvas positions from admin editor
body('positionX').optional().isNumeric(),
body('positionY').optional().isNumeric()
],
async (req: AuthRequest, res: any, next: any) => {
try {
if (req.user?.role !== 'admin') {
return res.status(403).json({ success: false, error: { message: 'Admin access required' } })
}
const { name, description, color, hasFuehrungsstelle, fuehrungsstelleName, positionX, positionY } = req.body
const now = new Date().toISOString()
const result = db.prepare(`
UPDATE organizational_units
SET name = COALESCE(?, name),
description = COALESCE(?, description),
color = COALESCE(?, color),
has_fuehrungsstelle = COALESCE(?, has_fuehrungsstelle),
fuehrungsstelle_name = COALESCE(?, fuehrungsstelle_name),
position_x = COALESCE(?, position_x),
position_y = COALESCE(?, position_y),
updated_at = ?
WHERE id = ?
`).run(
name || null,
description !== undefined ? description : null,
color || null,
hasFuehrungsstelle !== undefined ? (hasFuehrungsstelle ? 1 : 0) : null,
fuehrungsstelleName || null,
positionX !== undefined ? Math.round(Number(positionX)) : null,
positionY !== undefined ? Math.round(Number(positionY)) : null,
now,
req.params.id
)
if (result.changes === 0) {
return res.status(404).json({ success: false, error: { message: 'Unit not found' } })
}
res.json({ success: true })
} catch (error) {
logger.error('Error updating organizational unit:', error)
next(error)
}
}
)
// Assign employee to unit
router.post('/assignments',
authenticate,
[
body('employeeId').isUUID(),
body('unitId').isUUID(),
body('role').isIn(['leiter', 'stellvertreter', 'mitarbeiter', 'beauftragter'])
],
async (req: AuthRequest, res: any, next: any) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ success: false, error: { message: 'Invalid input', details: errors.array() } })
}
const { employeeId, unitId, role, isPrimary } = req.body
const now = new Date().toISOString()
const assignmentId = uuidv4()
// Permission model: users may only assign themselves and only as 'mitarbeiter' or 'beauftragter'
const isAdmin = req.user?.role === 'admin' || req.user?.role === 'superuser'
if (!isAdmin) {
if (!req.user?.employeeId || req.user.employeeId !== employeeId) {
return res.status(403).json({ success: false, error: { message: 'Cannot assign other employees' } })
}
if (!['mitarbeiter', 'beauftragter'].includes(role)) {
return res.status(403).json({ success: false, error: { message: 'Insufficient role to set this assignment' } })
}
}
// Validate unit exists and is active
const unit = db.prepare('SELECT id FROM organizational_units WHERE id = ? AND is_active = 1').get(unitId)
if (!unit) {
return res.status(404).json({ success: false, error: { message: 'Unit not found' } })
}
// Check if assignment already exists
const existing = db.prepare(`
SELECT id FROM employee_unit_assignments
WHERE employee_id = ? AND unit_id = ? AND role = ? AND end_date IS NULL
`).get(employeeId, unitId, role)
if (existing) {
return res.status(400).json({ success: false, error: { message: 'Assignment already exists' } })
}
// If setting as primary, demote all other active assignments for this employee
if (isPrimary) {
db.prepare(`
UPDATE employee_unit_assignments
SET is_primary = 0, updated_at = ?
WHERE employee_id = ? AND end_date IS NULL
`).run(now, employeeId)
}
db.prepare(`
INSERT INTO employee_unit_assignments (
id, employee_id, unit_id, role,
start_date, is_primary, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
assignmentId, employeeId, unitId, role,
now, isPrimary ? 1 : 0, now, now
)
res.json({ success: true, data: { id: assignmentId } })
} catch (error) {
logger.error('Error assigning employee to unit:', error)
next(error)
}
}
)
// Get my organizational units
router.get('/my-units', authenticate, async (req: AuthRequest, res, next) => {
try {
if (!req.user?.employeeId) {
return res.json({ success: true, data: [] })
}
const units = db.prepare(`
SELECT
ou.id, ou.code, ou.name, ou.type, ou.level,
eua.role, eua.is_primary as isPrimary
FROM employee_unit_assignments eua
JOIN organizational_units ou ON ou.id = eua.unit_id
WHERE eua.employee_id = ?
AND (eua.end_date IS NULL OR eua.end_date > datetime('now'))
AND ou.is_active = 1
ORDER BY eua.is_primary DESC, ou.level, ou.name
`).all(req.user.employeeId)
res.json({ success: true, data: units })
} catch (error) {
logger.error('Error fetching user units:', error)
next(error)
}
})
// Get my deputy assignments
router.get('/deputies/my', authenticate, async (req: AuthRequest, res, next) => {
try {
if (!req.user?.employeeId) {
return res.json({ success: true, data: { asDeputy: [], asPrincipal: [] } })
}
// Get assignments where I'm the deputy
const asDeputy = db.prepare(`
SELECT
da.id, da.principal_id as principalId, da.deputy_id as deputyId,
da.unit_id as unitId, da.valid_from as validFrom, da.valid_until as validUntil,
da.reason, da.can_delegate as canDelegate,
p.first_name || ' ' || p.last_name as principalName,
ou.name as unitName
FROM deputy_assignments da
JOIN employees p ON p.id = da.principal_id
LEFT JOIN organizational_units ou ON ou.id = da.unit_id
WHERE da.deputy_id = ?
AND da.valid_from <= datetime('now')
AND da.valid_until >= datetime('now')
ORDER BY da.valid_from DESC
`).all(req.user.employeeId)
// Get assignments where I'm the principal
const asPrincipal = db.prepare(`
SELECT
da.id, da.principal_id as principalId, da.deputy_id as deputyId,
da.unit_id as unitId, da.valid_from as validFrom, da.valid_until as validUntil,
da.reason,
d.first_name || ' ' || d.last_name as deputyName,
ou.name as unitName
FROM deputy_assignments da
JOIN employees d ON d.id = da.deputy_id
LEFT JOIN organizational_units ou ON ou.id = da.unit_id
WHERE da.principal_id = ?
AND da.valid_from <= datetime('now')
AND da.valid_until >= datetime('now')
ORDER BY da.valid_from DESC
`).all(req.user.employeeId)
res.json({ success: true, data: { asDeputy, asPrincipal } })
} catch (error) {
logger.error('Error fetching deputy assignments:', error)
next(error)
}
})
// Create deputy assignment
router.post('/deputies',
authenticate,
[
body('deputyId').isUUID(),
body('validFrom').isISO8601(),
body('validUntil').isISO8601()
],
async (req: AuthRequest, res: any, next: any) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ success: false, error: { message: 'Invalid input', details: errors.array() } })
}
if (!req.user?.employeeId) {
return res.status(403).json({ success: false, error: { message: 'No employee linked to user' } })
}
const { deputyId, unitId, validFrom, validUntil, reason, canDelegate } = req.body
const now = new Date().toISOString()
const assignmentId = uuidv4()
// Check for conflicts
const conflict = db.prepare(`
SELECT id FROM deputy_assignments
WHERE principal_id = ? AND deputy_id = ?
AND ((valid_from BETWEEN ? AND ?) OR (valid_until BETWEEN ? AND ?))
`).get(req.user.employeeId, deputyId, validFrom, validUntil, validFrom, validUntil)
if (conflict) {
return res.status(400).json({ success: false, error: { message: 'Conflicting assignment exists' } })
}
db.prepare(`
INSERT INTO deputy_assignments (
id, principal_id, deputy_id, unit_id,
valid_from, valid_until, reason, can_delegate,
created_by, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
assignmentId, req.user.employeeId, deputyId, unitId || null,
validFrom, validUntil, reason || null, canDelegate ? 1 : 0,
req.user.id, now, now
)
res.json({ success: true, data: { id: assignmentId } })
} catch (error) {
logger.error('Error creating deputy assignment:', error)
next(error)
}
}
)
// Alias: Create deputy assignment for current user (same as above, convenient endpoint)
router.post('/deputies/my',
authenticate,
[
body('deputyId').isUUID(),
body('validFrom').isISO8601(),
body('validUntil').optional({ nullable: true }).isISO8601(),
body('unitId').optional({ nullable: true }).isUUID()
],
async (req: AuthRequest, res: any, next: any) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ success: false, error: { message: 'Invalid input', details: errors.array() } })
}
if (!req.user?.employeeId) {
return res.status(403).json({ success: false, error: { message: 'No employee linked to user' } })
}
const { deputyId, unitId, validFrom, validUntil, reason, canDelegate } = req.body
const now = new Date().toISOString()
const assignmentId = uuidv4()
// Check for conflicts
const conflict = db.prepare(`
SELECT id FROM deputy_assignments
WHERE principal_id = ? AND deputy_id = ?
AND ((valid_from BETWEEN ? AND ?) OR (valid_until BETWEEN ? AND ?))
`).get(req.user.employeeId, deputyId, validFrom, validUntil || validFrom, validFrom, validUntil || validFrom)
if (conflict) {
return res.status(400).json({ success: false, error: { message: 'Conflicting assignment exists' } })
}
db.prepare(`
INSERT INTO deputy_assignments (
id, principal_id, deputy_id, unit_id,
valid_from, valid_until, reason, can_delegate,
created_by, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
assignmentId, req.user.employeeId, deputyId, unitId || null,
validFrom, validUntil || validFrom, reason || null, canDelegate ? 1 : 0,
req.user.id, now, now
)
res.json({ success: true, data: { id: assignmentId } })
} catch (error) {
logger.error('Error creating deputy assignment (my):', error)
next(error)
}
}
)
// Delegate deputy assignment
router.post('/deputies/delegate',
authenticate,
[
body('assignmentId').isUUID(),
body('toDeputyId').isUUID(),
body('reason').optional().trim()
],
async (req: AuthRequest, res: any, next: any) => {
try {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ success: false, error: { message: 'Invalid input', details: errors.array() } })
}
if (!req.user?.employeeId) {
return res.status(403).json({ success: false, error: { message: 'No employee linked to user' } })
}
const { assignmentId, toDeputyId, reason } = req.body
const now = new Date().toISOString()
const delegationId = uuidv4()
// Check if user can delegate this assignment
const assignment = db.prepare(`
SELECT * FROM deputy_assignments
WHERE id = ? AND deputy_id = ? AND can_delegate = 1
AND valid_from <= datetime('now')
AND valid_until >= datetime('now')
`).get(assignmentId, req.user.employeeId) as any
if (!assignment) {
return res.status(403).json({ success: false, error: { message: 'Cannot delegate this assignment' } })
}
db.prepare(`
INSERT INTO deputy_delegations (
id, original_assignment_id, from_deputy_id, to_deputy_id,
reason, delegated_at, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?)
`).run(
delegationId, assignmentId, req.user.employeeId, toDeputyId,
reason || null, now, now
)
res.json({ success: true, data: { id: delegationId } })
} catch (error) {
logger.error('Error delegating deputy assignment:', error)
next(error)
}
}
)
// Delete deputy assignment (principal or admin)
router.delete('/deputies/:id', authenticate, async (req: AuthRequest, res: any, next: any) => {
try {
const { id } = req.params
if (!id) {
return res.status(400).json({ success: false, error: { message: 'Missing id' } })
}
// Load assignment
const assignment = db.prepare(`
SELECT id, principal_id as principalId FROM deputy_assignments WHERE id = ?
`).get(id) as any
if (!assignment) {
return res.status(404).json({ success: false, error: { message: 'Assignment not found' } })
}
// Permission: principal or admin/superuser
const isAdmin = req.user?.role === 'admin' || req.user?.role === 'superuser'
if (!isAdmin && req.user?.employeeId !== assignment.principalId) {
return res.status(403).json({ success: false, error: { message: 'Access denied' } })
}
// Delete delegations first (FK not ON DELETE CASCADE)
db.prepare('DELETE FROM deputy_delegations WHERE original_assignment_id = ?').run(id)
// Delete assignment
db.prepare('DELETE FROM deputy_assignments WHERE id = ?').run(id)
return res.json({ success: true })
} catch (error) {
logger.error('Error deleting deputy assignment:', error)
next(error)
}
})
// Get deputy chain for an assignment
router.get('/deputies/chain/:id', authenticate, async (req: AuthRequest, res, next) => {
try {
const chain = []
// Get original assignment
const assignment = db.prepare(`
SELECT
da.*,
p.first_name || ' ' || p.last_name as principalName,
d.first_name || ' ' || d.last_name as deputyName
FROM deputy_assignments da
JOIN employees p ON p.id = da.principal_id
JOIN employees d ON d.id = da.deputy_id
WHERE da.id = ?
`).get(req.params.id) as any
if (!assignment) {
return res.status(404).json({ success: false, error: { message: 'Assignment not found' } })
}
chain.push({
type: 'original',
from: assignment.principalName,
to: assignment.deputyName,
reason: assignment.reason
})
// Get all delegations
const delegations = db.prepare(`
SELECT
dd.*,
f.first_name || ' ' || f.last_name as fromName,
t.first_name || ' ' || t.last_name as toName
FROM deputy_delegations dd
JOIN employees f ON f.id = dd.from_deputy_id
JOIN employees t ON t.id = dd.to_deputy_id
WHERE dd.original_assignment_id = ?
ORDER BY dd.delegated_at
`).all(req.params.id) as any[]
delegations.forEach(del => {
chain.push({
type: 'delegation',
from: del.fromName,
to: del.toName,
reason: del.reason,
delegatedAt: del.delegated_at
})
})
res.json({ success: true, data: chain })
} catch (error) {
logger.error('Error fetching deputy chain:', error)
next(error)
}
})
// Get special positions
router.get('/special-positions', authenticate, async (req: AuthRequest, res, next) => {
try {
const positions = db.prepare(`
SELECT
sp.id, sp.position_type as positionType,
sp.employee_id as employeeId,
sp.unit_id as unitId,
sp.start_date as startDate,
sp.end_date as endDate,
e.first_name || ' ' || e.last_name as employeeName,
e.photo,
ou.name as unitName
FROM special_positions sp
JOIN employees e ON e.id = sp.employee_id
LEFT JOIN organizational_units ou ON ou.id = sp.unit_id
WHERE sp.is_active = 1
AND (sp.end_date IS NULL OR sp.end_date > datetime('now'))
ORDER BY sp.position_type, e.last_name
`).all()
res.json({ success: true, data: positions })
} catch (error) {
logger.error('Error fetching special positions:', error)
next(error)
}
})
export default router

Datei anzeigen

@ -0,0 +1,399 @@
import { Router } from 'express'
import multer from 'multer'
import { v4 as uuidv4 } from 'uuid'
import fs from 'fs'
import path from 'path'
const pdfParse = require('pdf-parse')
import { db } from '../config/secureDatabase'
import { authenticate, authorize, AuthRequest } from '../middleware/auth'
import { logger } from '../utils/logger'
const router = Router()
// Configure multer for PDF uploads
const upload = multer({
dest: 'uploads/temp/',
limits: {
fileSize: 10 * 1024 * 1024 // 10MB limit
},
fileFilter: (req, file, cb) => {
if (file.mimetype === 'application/pdf') {
cb(null, true)
} else {
cb(new Error('Only PDF files are allowed'))
}
}
})
// Helper to parse organizational structure from PDF text
function parseOrganizationFromText(text: string) {
const units: any[] = []
const lines = text.split('\n').map(line => line.trim()).filter(line => line && line.length > 2)
// Pattern matching for different organizational levels
const patterns = {
direktor: /(Direktor|Director)\s*(LKA)?/i,
abteilung: /^Abteilung\s+(\d+)/i,
dezernat: /^Dezernat\s+([\d]+)/i,
sachgebiet: /^SG\s+([\d\.]+)/i,
teildezernat: /^TD\s+([\d\.]+)/i,
stabsstelle: /(Leitungsstab|LStab|Führungsgruppe)/i,
fahndung: /Fahndungsgruppe/i,
sondereinheit: /(Personalrat|Schwerbehindertenvertretung|beauftragt|Innenrevision|IUK-Lage)/i
}
// Color mapping for departments
const colors: Record<string, string> = {
'1': '#dc2626',
'2': '#ea580c',
'3': '#0891b2',
'4': '#7c3aed',
'5': '#0d9488',
'6': '#be185d',
'ZA': '#6b7280'
}
let currentAbteilung: any = null
let currentDezernat: any = null
lines.forEach(line => {
// Check for Direktor
if (patterns.direktor.test(line)) {
units.push({
code: 'DIR',
name: 'Direktor LKA NRW',
type: 'direktion',
level: 0,
parentId: null,
color: '#1e3a8a'
})
}
// Check for Abteilung
const abtMatch = line.match(/Abteilung\s+(\d+|Zentralabteilung)/i)
if (abtMatch) {
const abtNum = abtMatch[1] === 'Zentralabteilung' ? 'ZA' : abtMatch[1]
const abtName = line.replace(/^Abteilung\s+\d+\s*[-–]\s*/, '')
currentAbteilung = {
code: `Abt ${abtNum}`,
name: abtName || `Abteilung ${abtNum}`,
type: 'abteilung',
level: 1,
parentId: 'DIR',
color: colors[abtNum] || '#6b7280',
hasFuehrungsstelle: abtNum !== 'ZA'
}
units.push(currentAbteilung)
currentDezernat = null
}
// Check for Dezernat
const dezMatch = line.match(/(?:Dezernat|Dez)\s+([\d\s]+)/i)
if (dezMatch && currentAbteilung) {
const dezNum = dezMatch[1].trim()
const dezName = line.replace(/^(?:Dezernat|Dez)\s+[\d\s]+\s*[-–]?\s*/, '').trim()
currentDezernat = {
code: `Dez ${dezNum}`,
name: dezName || `Dezernat ${dezNum}`,
type: 'dezernat',
level: 2,
parentId: currentAbteilung.code
}
units.push(currentDezernat)
}
// Check for Sachgebiet
const sgMatch = line.match(/SG\s+([\d\.]+)/i)
if (sgMatch && currentDezernat) {
const sgNum = sgMatch[1]
const sgName = line.replace(/^SG\s+[\d\.]+\s*[-–]?\s*/, '').trim()
units.push({
code: `SG ${sgNum}`,
name: sgName || `Sachgebiet ${sgNum}`,
type: 'sachgebiet',
level: 3,
parentId: currentDezernat.code
})
}
// Check for Teildezernat
const tdMatch = line.match(/TD\s+([\d\.]+)/i)
if (tdMatch && currentDezernat) {
const tdNum = tdMatch[1]
const tdName = line.replace(/^TD\s+[\d\.]+\s*[-–]?\s*/, '').trim()
units.push({
code: `TD ${tdNum}`,
name: tdName || `Teildezernat ${tdNum}`,
type: 'teildezernat',
level: 3,
parentId: currentDezernat.code
})
}
// Check for Stabsstelle
if (patterns.stabsstelle.test(line)) {
units.push({
code: 'LStab',
name: 'Leitungsstab',
type: 'stabsstelle',
level: 1,
parentId: 'DIR',
color: '#6b7280'
})
}
// Check for Sondereinheiten (non-hierarchical)
if (patterns.sondereinheit.test(line)) {
let code = 'SE'
let name = line
if (line.includes('Personalrat')) {
code = 'PR'
name = 'Personalrat'
} else if (line.includes('Schwerbehindertenvertretung')) {
code = 'SBV'
name = 'Schwerbehindertenvertretung'
} else if (line.includes('Datenschutzbeauftragter')) {
code = 'DSB'
name = 'Datenschutzbeauftragter'
} else if (line.includes('Gleichstellungsbeauftragte')) {
code = 'GSB'
name = 'Gleichstellungsbeauftragte'
} else if (line.includes('Innenrevision')) {
code = 'IR'
name = 'Innenrevision'
}
units.push({
code,
name,
type: 'sondereinheit',
level: 1,
parentId: null, // Non-hierarchical
color: '#059669'
})
}
})
return units
}
// Import organization from PDF
router.post('/import-pdf',
authenticate,
authorize('admin'),
upload.single('pdf'),
async (req: AuthRequest, res: any, next: any) => {
let tempFilePath: string | null = null
try {
if (!req.file) {
return res.status(400).json({
success: false,
error: { message: 'No PDF file uploaded' }
})
}
tempFilePath = req.file.path
// Read PDF file
const pdfBuffer = fs.readFileSync(tempFilePath)
// Parse PDF using pdf-parse
let extractedText = ''
try {
const pdfData = await pdfParse(pdfBuffer)
extractedText = pdfData.text
logger.info(`PDF parsed successfully: ${pdfData.numpages} pages, ${extractedText.length} characters`)
} catch (parseError) {
logger.error('PDF parsing error:', parseError)
// Fallback to simulated data if parsing fails
extractedText = `
Direktor LKA NRW
Leitungsstab
Personalrat
Schwerbehindertenvertretung
Abteilung 1 - Organisierte Kriminalität
Dezernat 11 - Ermittlungen OK
SG 11.1 - Grundsatzfragen
Dezernat 12 - Wirtschaftskriminalität
Abteilung 2 - Terrorismusbekämpfung
Dezernat 21 - Ermittlungen
Abteilung 3 - Strategische Kriminalitätsbekämpfung
Abteilung 4 - Cybercrime
Abteilung 5 - Kriminalwissenschaftliches Institut
Abteilung 6 - Fachaufsicht
Zentralabteilung
`
}
// Parse the organizational structure
const parsedUnits = parseOrganizationFromText(extractedText)
if (parsedUnits.length === 0) {
return res.status(400).json({
success: false,
error: { message: 'No organizational units could be extracted from the PDF' }
})
}
// Clear existing organization (optional - could be a parameter)
if (req.body.clearExisting === 'true') {
db.prepare('DELETE FROM organizational_units').run()
}
// Prepare insert/update with stable parent references and FK-safe order
const now = new Date().toISOString()
const unitIdMap: Record<string, string> = {}
// Preload existing IDs by code
for (const unit of parsedUnits) {
const existing = db.prepare('SELECT id FROM organizational_units WHERE code = ?').get(unit.code) as any
unitIdMap[unit.code] = existing?.id || uuidv4()
}
// Sort by level ascending so parents are processed first
const sorted = [...parsedUnits].sort((a, b) => (a.level ?? 0) - (b.level ?? 0))
const tx = db.transaction(() => {
sorted.forEach((unit, index) => {
const parentId = unit.parentId ? unitIdMap[unit.parentId] : null
const existing = db.prepare('SELECT id FROM organizational_units WHERE code = ?').get(unit.code) as any
if (existing) {
db.prepare(`
UPDATE organizational_units
SET name = ?, type = ?, level = ?, parent_id = ?,
color = ?, order_index = ?, has_fuehrungsstelle = ?,
is_active = 1, updated_at = ?
WHERE id = ?
`).run(
unit.name,
unit.type,
unit.level,
parentId,
unit.color || null,
index,
unit.hasFuehrungsstelle ? 1 : 0,
now,
existing.id
)
} else {
db.prepare(`
INSERT INTO organizational_units (
id, code, name, type, level, parent_id,
color, order_index, has_fuehrungsstelle,
is_active, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
unitIdMap[unit.code],
unit.code,
unit.name,
unit.type,
unit.level,
parentId,
unit.color || null,
index,
unit.hasFuehrungsstelle ? 1 : 0,
1,
now,
now
)
}
})
})
tx()
res.json({
success: true,
data: {
message: `Successfully imported ${parsedUnits.length} organizational units`,
unitsImported: parsedUnits.length,
units: parsedUnits.map(u => ({
code: u.code,
name: u.name,
type: u.type
}))
}
})
} catch (error) {
logger.error('Error importing PDF:', error)
res.status(500).json({
success: false,
error: { message: 'Failed to import PDF: ' + (error as Error).message }
})
} finally {
// Clean up temp file
if (tempFilePath && fs.existsSync(tempFilePath)) {
fs.unlinkSync(tempFilePath)
}
}
}
)
// Import from structured JSON
router.post('/import-json',
authenticate,
authorize('admin'),
async (req: AuthRequest, res: any, next: any) => {
try {
const { units, clearExisting } = req.body
if (!units || !Array.isArray(units)) {
return res.status(400).json({
success: false,
error: { message: 'Invalid units data' }
})
}
// Clear existing if requested
if (clearExisting) {
db.prepare('DELETE FROM organizational_units').run()
}
// Import units
const now = new Date().toISOString()
units.forEach((unit: any, index: number) => {
const id = uuidv4()
db.prepare(`
INSERT INTO organizational_units (
id, code, name, type, level, parent_id,
color, order_index, description,
has_fuehrungsstelle, is_active, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
id,
unit.code,
unit.name,
unit.type,
unit.level,
unit.parentId || null,
unit.color || null,
index,
unit.description || null,
unit.hasFuehrungsstelle ? 1 : 0,
1,
now,
now
)
})
res.json({
success: true,
data: {
message: `Successfully imported ${units.length} units`,
count: units.length
}
})
} catch (error) {
logger.error('Error importing JSON:', error)
next(error)
}
}
)
export default router

Datei anzeigen

@ -4,7 +4,6 @@ import bcrypt from 'bcrypt'
import { v4 as uuidv4 } from 'uuid'
import { db, encryptedDb } from '../config/secureDatabase'
import { authenticate, authorize, AuthRequest } from '../middleware/auth'
import { requirePermission } from '../middleware/roleAuth'
import { User, UserRole } from '@skillmate/shared'
import { FieldEncryption } from '../services/encryption'
import { emailService } from '../services/emailService'
@ -13,7 +12,7 @@ import { logger } from '../utils/logger'
const router = Router()
// Get all users (admin only)
router.get('/', authenticate, requirePermission('users:read'), async (req: AuthRequest, res, next) => {
router.get('/', authenticate, authorize('admin', 'superuser'), async (req: AuthRequest, res, next) => {
try {
const users = db.prepare(`
SELECT id, username, email, role, employee_id, last_login, is_active, created_at, updated_at
@ -57,7 +56,7 @@ router.get('/', authenticate, requirePermission('users:read'), async (req: AuthR
// Update user role (admin only)
router.put('/:id/role',
authenticate,
requirePermission('users:update'),
authorize('admin'),
[
body('role').isIn(['admin', 'superuser', 'user'])
],
@ -109,7 +108,7 @@ router.put('/:id/role',
// Bulk create users from employees
router.post('/bulk-create-from-employees',
authenticate,
requirePermission('users:create'),
authorize('admin'),
[
body('employeeIds').isArray({ min: 1 }),
body('role').isIn(['admin', 'superuser', 'user'])
@ -189,7 +188,7 @@ router.post('/bulk-create-from-employees',
// Update user status (admin only)
router.put('/:id/status',
authenticate,
requirePermission('users:update'),
authorize('admin'),
[
body('isActive').isBoolean()
],
@ -241,7 +240,7 @@ router.put('/:id/status',
// Reset user password (admin only)
router.post('/:id/reset-password',
authenticate,
requirePermission('users:update'),
authorize('admin'),
[
body('newPassword').optional().isLength({ min: 8 })
],
@ -294,7 +293,7 @@ router.post('/:id/reset-password',
// Delete user (admin only)
router.delete('/:id',
authenticate,
requirePermission('users:delete'),
authorize('admin'),
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const { id } = req.params
@ -333,7 +332,7 @@ export default router
// Create user account from existing employee (admin only)
router.post('/create-from-employee',
authenticate,
requirePermission('users:create'),
authorize('admin'),
[
body('employeeId').notEmpty().isString(),
body('username').optional().isString().isLength({ min: 3 }),
@ -452,7 +451,7 @@ router.post('/purge',
// Send temporary password via email to user's email
router.post('/:id/send-temp-password',
authenticate,
requirePermission('users:update'),
authorize('admin'),
[
body('password').notEmpty().isString().isLength({ min: 6 })
],

Datei anzeigen

@ -1,7 +1,7 @@
import { db } from '../../src/config/secureDatabase'
import { db } from '../../config/secureDatabase'
import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'
import { FieldEncryption } from '../../src/services/encryption'
import { FieldEncryption } from '../../services/encryption'
import { User, LoginResponse } from '@skillmate/shared'
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'