Rollback - PDF Import funzt so semi
Dieser Commit ist enthalten in:
@ -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 (
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
166
backend/src/routes/employeeOrganization.ts
Normale Datei
166
backend/src/routes/employeeOrganization.ts
Normale Datei
@ -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
|
||||
681
backend/src/routes/organization.ts
Normale Datei
681
backend/src/routes/organization.ts
Normale Datei
@ -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
|
||||
399
backend/src/routes/organizationImport.ts
Normale Datei
399
backend/src/routes/organizationImport.ts
Normale Datei
@ -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
|
||||
@ -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 })
|
||||
],
|
||||
|
||||
@ -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'
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren