updated every file for database changes; starting scheduling debugging

This commit is contained in:
2025-10-21 00:51:23 +02:00
parent 3c4fbc0798
commit 3127692d29
27 changed files with 1861 additions and 866 deletions

View File

@@ -39,7 +39,6 @@ export interface RegisterRequest {
}
function generateEmail(firstname: string, lastname: string): string {
// Convert German umlauts to their expanded forms
const convertUmlauts = (str: string): string => {
return str
.toLowerCase()
@@ -49,7 +48,6 @@ function generateEmail(firstname: string, lastname: string): string {
.replace(/ß/g, 'ss');
};
// Remove any remaining special characters and convert to lowercase
const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, '');
const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, '');
return `${cleanFirstname}.${cleanLastname}@sp.de`;
@@ -70,8 +68,8 @@ export const login = async (req: Request, res: Response) => {
const user = await db.get<any>(
`SELECT
e.id, e.email, e.password, e.firstname, e.lastname,
e.employee_type as employeeType, e.contract_type as contractType,
e.can_work_alone as canWorkAlone, e.is_active as isActive,
e.employee_type, e.contract_type,
e.can_work_alone, e.is_active, e.is_trainee,
er.role
FROM employees e
LEFT JOIN employee_roles er ON e.id = er.employee_id
@@ -87,6 +85,15 @@ export const login = async (req: Request, res: Response) => {
return res.status(401).json({ error: 'Ungültige Anmeldedaten' });
}
// Update last_login with current timestamp
const currentTimestamp = new Date().toISOString();
await db.run(
'UPDATE employees SET last_login = ? WHERE id = ?',
[currentTimestamp, user.id]
);
console.log('✅ Timestamp set:', currentTimestamp);
// Verify password
const validPassword = await bcrypt.compare(password, user.password);
console.log('🔑 Password valid:', validPassword);
@@ -116,7 +123,12 @@ export const login = async (req: Request, res: Response) => {
const { password: _, ...userWithoutPassword } = user;
const userResponse = {
...userWithoutPassword,
roles: user.role ? [user.role] : ['user'] // Convert single role to array for frontend compatibility
employeeType: user.employee_type,
contractType: user.contract_type,
canWorkAlone: user.can_work_alone === 1,
isActive: user.is_active === 1,
isTrainee: user.is_trainee === 1,
roles: user.role ? [user.role] : ['user']
};
console.log('✅ Login successful for:', user.email);
@@ -147,8 +159,8 @@ export const getCurrentUser = async (req: Request, res: Response) => {
const user = await db.get<any>(
`SELECT
e.id, e.email, e.firstname, e.lastname,
e.employee_type as employeeType, e.contract_type as contractType,
e.can_work_alone as canWorkAlone, e.is_active as isActive,
e.employee_type, e.contract_type,
e.can_work_alone, e.is_active, e.is_trainee,
er.role
FROM employees e
LEFT JOIN employee_roles er ON e.id = er.employee_id
@@ -167,6 +179,11 @@ export const getCurrentUser = async (req: Request, res: Response) => {
// Format user response with roles array
const userResponse = {
...user,
employeeType: user.employee_type,
contractType: user.contract_type,
canWorkAlone: user.can_work_alone === 1,
isActive: user.is_active === 1,
isTrainee: user.is_trainee === 1,
roles: user.role ? [user.role] : ['user']
};
@@ -237,18 +254,18 @@ export const register = async (req: Request, res: Response) => {
await db.run('BEGIN TRANSACTION');
try {
// Insert user without role (role is now in employee_roles table)
// ✅ CORRECTED: Use valid 'personell' type with proper contract type
const result = await db.run(
`INSERT INTO employees (id, email, password, firstname, lastname, employee_type, contract_type, can_work_alone, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[employeeId, email, hashedPassword, firstname, lastname, 'experienced', 'small', false, 1]
`INSERT INTO employees (id, email, password, firstname, lastname, employee_type, contract_type, can_work_alone, is_active, is_trainee)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[employeeId, email, hashedPassword, firstname, lastname, 'personell', 'small', false, 1, false]
);
if (!result.lastID) {
throw new Error('Benutzer konnte nicht erstellt werden');
}
// UPDATED: Insert roles into employee_roles table
// Insert roles into employee_roles table
for (const role of roles) {
await db.run(
`INSERT INTO employee_roles (employee_id, role) VALUES (?, ?)`,
@@ -262,6 +279,7 @@ export const register = async (req: Request, res: Response) => {
const newUser = await db.get<any>(
`SELECT
e.id, e.email, e.firstname, e.lastname,
e.employee_type, e.contract_type, e.can_work_alone, e.is_active, e.is_trainee,
er.role
FROM employees e
LEFT JOIN employee_roles er ON e.id = er.employee_id
@@ -273,6 +291,11 @@ export const register = async (req: Request, res: Response) => {
// Format response with roles array
const userResponse = {
...newUser,
employeeType: newUser.employee_type,
contractType: newUser.contract_type,
canWorkAlone: newUser.can_work_alone === 1,
isActive: newUser.is_active === 1,
isTrainee: newUser.is_trainee === 1,
roles: newUser.role ? [newUser.role] : ['user']
};
@@ -293,8 +316,6 @@ export const register = async (req: Request, res: Response) => {
export const logout = async (req: Request, res: Response) => {
try {
// Note: Since we're using JWTs, we don't need to do anything server-side
// The client should remove the token from storage
res.json({ message: 'Erfolgreich abgemeldet' });
} catch (error) {
console.error('Logout error:', error);

View File

@@ -7,7 +7,6 @@ import { AuthRequest } from '../middleware/auth.js';
import { CreateEmployeeRequest } from '../models/Employee.js';
function generateEmail(firstname: string, lastname: string): string {
// Convert German umlauts to their expanded forms
const convertUmlauts = (str: string): string => {
return str
.toLowerCase()
@@ -17,7 +16,6 @@ function generateEmail(firstname: string, lastname: string): string {
.replace(/ß/g, 'ss');
};
// Remove any remaining special characters and convert to lowercase
const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, '');
const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, '');
@@ -34,12 +32,13 @@ export const getEmployees = async (req: AuthRequest, res: Response): Promise<voi
let query = `
SELECT
e.id, e.email, e.firstname, e.lastname,
e.is_active as isActive,
e.employee_type as employeeType,
e.contract_type as contractType,
e.can_work_alone as canWorkAlone,
e.created_at as createdAt,
e.last_login as lastLogin,
e.is_active,
e.employee_type,
e.contract_type,
e.can_work_alone,
e.is_trainee,
e.created_at,
e.last_login,
er.role
FROM employees e
LEFT JOIN employee_roles er ON e.id = er.employee_id
@@ -49,14 +48,23 @@ export const getEmployees = async (req: AuthRequest, res: Response): Promise<voi
query += ' WHERE e.is_active = 1';
}
// UPDATED: Order by firstname and lastname
query += ' ORDER BY e.firstname, e.lastname';
const employees = await db.all<any>(query);
// Format employees with roles array for frontend compatibility
// Format employees with proper field names and roles array
const employeesWithRoles = employees.map(emp => ({
...emp,
id: emp.id,
email: emp.email,
firstname: emp.firstname,
lastname: emp.lastname,
isActive: emp.is_active === 1,
employeeType: emp.employee_type,
contractType: emp.contract_type,
canWorkAlone: emp.can_work_alone === 1,
isTrainee: emp.is_trainee === 1,
createdAt: emp.created_at,
lastLogin: emp.last_login,
roles: emp.role ? [emp.role] : ['user']
}));
@@ -72,16 +80,16 @@ export const getEmployee = async (req: AuthRequest, res: Response): Promise<void
try {
const { id } = req.params;
// UPDATED: Query with role from employee_roles table
const employee = await db.get<any>(`
SELECT
e.id, e.email, e.firstname, e.lastname,
e.is_active as isActive,
e.employee_type as employeeType,
e.contract_type as contractType,
e.can_work_alone as canWorkAlone,
e.created_at as createdAt,
e.last_login as lastLogin,
e.is_active,
e.employee_type,
e.contract_type,
e.can_work_alone,
e.is_trainee,
e.created_at,
e.last_login,
er.role
FROM employees e
LEFT JOIN employee_roles er ON e.id = er.employee_id
@@ -94,9 +102,19 @@ export const getEmployee = async (req: AuthRequest, res: Response): Promise<void
return;
}
// Format employee with roles array
// Format employee with proper field names
const employeeWithRoles = {
...employee,
id: employee.id,
email: employee.email,
firstname: employee.firstname,
lastname: employee.lastname,
isActive: employee.is_active === 1,
employeeType: employee.employee_type,
contractType: employee.contract_type,
canWorkAlone: employee.can_work_alone === 1,
isTrainee: employee.is_trainee === 1,
createdAt: employee.created_at,
lastLogin: employee.last_login,
roles: employee.role ? [employee.role] : ['user']
};
@@ -118,17 +136,64 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
password,
firstname,
lastname,
roles = ['user'], // UPDATED: Now uses roles array
roles = ['user'],
employeeType,
contractType,
canWorkAlone
canWorkAlone = false,
isTrainee = false
} = req.body as CreateEmployeeRequest;
// Validation
if (!password || !firstname || !lastname || !employeeType || !contractType) {
if (!password || !firstname || !lastname || !employeeType) {
console.log('❌ Validation failed: Missing required fields');
res.status(400).json({
error: 'Password, firstname, lastname, employeeType und contractType sind erforderlich'
error: 'Password, firstname, lastname und employeeType sind erforderlich'
});
return;
}
// ✅ ENHANCED: Validate employee type exists and get category info
const employeeTypeInfo = await db.get<{type: string, category: string, has_contract_type: number}>(
'SELECT type, category, has_contract_type FROM employee_types WHERE type = ?',
[employeeType]
);
if (!employeeTypeInfo) {
res.status(400).json({
error: `Ungültiger employeeType: ${employeeType}. Gültige Typen: manager, personell, apprentice, guest`
});
return;
}
// ✅ ENHANCED: Contract type validation based on category
if (employeeTypeInfo.has_contract_type === 1) {
// Internal types require contract type
if (!contractType) {
res.status(400).json({
error: `contractType ist erforderlich für employeeType: ${employeeType}`
});
return;
}
if (!['small', 'large', 'flexible'].includes(contractType)) {
res.status(400).json({
error: `Ungültiger contractType: ${contractType}. Gültige Werte: small, large, flexible`
});
return;
}
} else {
// External types (guest) should not have contract type
if (contractType) {
res.status(400).json({
error: `contractType ist nicht erlaubt für employeeType: ${employeeType}`
});
return;
}
}
// ✅ ENHANCED: isTrainee validation - only applicable for personell type
if (isTrainee && employeeType !== 'personell') {
res.status(400).json({
error: `isTrainee ist nur für employeeType 'personell' erlaubt`
});
return;
}
@@ -156,12 +221,12 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
await db.run('BEGIN TRANSACTION');
try {
// UPDATED: Insert employee without role (role is now in employee_roles table)
// Insert employee with proper contract type handling
await db.run(
`INSERT INTO employees (
id, email, password, firstname, lastname, employee_type, contract_type, can_work_alone,
is_active
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
is_active, is_trainee
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
employeeId,
email,
@@ -169,13 +234,14 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
firstname,
lastname,
employeeType,
contractType,
contractType, // Will be NULL for external types
canWorkAlone ? 1 : 0,
1
1,
isTrainee ? 1 : 0
]
);
// UPDATED: Insert roles into employee_roles table
// Insert roles into employee_roles table
for (const role of roles) {
await db.run(
`INSERT INTO employee_roles (employee_id, role) VALUES (?, ?)`,
@@ -185,16 +251,17 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
await db.run('COMMIT');
// Return created employee with role from employee_roles
// Return created employee
const newEmployee = await db.get<any>(`
SELECT
e.id, e.email, e.firstname, e.lastname,
e.is_active as isActive,
e.employee_type as employeeType,
e.contract_type as contractType,
e.can_work_alone as canWorkAlone,
e.created_at as createdAt,
e.last_login as lastLogin,
e.is_active,
e.employee_type,
e.contract_type,
e.can_work_alone,
e.is_trainee,
e.created_at,
e.last_login,
er.role
FROM employees e
LEFT JOIN employee_roles er ON e.id = er.employee_id
@@ -202,9 +269,19 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
LIMIT 1
`, [employeeId]);
// Format response with roles array
// Format response with proper field names
const employeeWithRoles = {
...newEmployee,
id: newEmployee.id,
email: newEmployee.email,
firstname: newEmployee.firstname,
lastname: newEmployee.lastname,
isActive: newEmployee.is_active === 1,
employeeType: newEmployee.employee_type,
contractType: newEmployee.contract_type,
canWorkAlone: newEmployee.can_work_alone === 1,
isTrainee: newEmployee.is_trainee === 1,
createdAt: newEmployee.created_at,
lastLogin: newEmployee.last_login,
roles: newEmployee.role ? [newEmployee.role] : ['user']
};
@@ -223,9 +300,12 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
export const updateEmployee = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const { id } = req.params;
const { firstname, lastname, roles, isActive, employeeType, contractType, canWorkAlone } = req.body;
const { firstname, lastname, roles, isActive, employeeType, contractType, canWorkAlone, isTrainee } = req.body;
console.log('📝 Update Employee Request:', { id, firstname, lastname, roles, isActive, employeeType, contractType, canWorkAlone });
console.log('📝 Update Employee Request:', {
id, firstname, lastname, roles, isActive,
employeeType, contractType, canWorkAlone, isTrainee
});
// Check if employee exists and get current data
const existingEmployee = await db.get<any>('SELECT * FROM employees WHERE id = ?', [id]);
@@ -234,6 +314,21 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
return;
}
// Validate employee type if provided
if (employeeType) {
const validEmployeeType = await db.get(
'SELECT type FROM employee_types WHERE type = ?',
[employeeType]
);
if (!validEmployeeType) {
res.status(400).json({
error: `Ungültiger employeeType: ${employeeType}`
});
return;
}
}
// Generate new email if firstname or lastname changed
let email = existingEmployee.email;
if (firstname || lastname) {
@@ -259,7 +354,7 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
await db.run('BEGIN TRANSACTION');
try {
// UPDATED: Update employee without role (role is now in employee_roles table)
// Update employee with new schema
await db.run(
`UPDATE employees
SET firstname = COALESCE(?, firstname),
@@ -268,12 +363,13 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
is_active = COALESCE(?, is_active),
employee_type = COALESCE(?, employee_type),
contract_type = COALESCE(?, contract_type),
can_work_alone = COALESCE(?, can_work_alone)
can_work_alone = COALESCE(?, can_work_alone),
is_trainee = COALESCE(?, is_trainee)
WHERE id = ?`,
[firstname, lastname, email, isActive, employeeType, contractType, canWorkAlone, id]
[firstname, lastname, email, isActive, employeeType, contractType, canWorkAlone, isTrainee, id]
);
// UPDATED: Update roles if provided
// Update roles if provided
if (roles) {
// Delete existing roles
await db.run('DELETE FROM employee_roles WHERE employee_id = ?', [id]);
@@ -291,16 +387,17 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
console.log('✅ Employee updated successfully with email:', email);
// Return updated employee with role from employee_roles
// Return updated employee
const updatedEmployee = await db.get<any>(`
SELECT
e.id, e.email, e.firstname, e.lastname,
e.is_active as isActive,
e.employee_type as employeeType,
e.contract_type as contractType,
e.can_work_alone as canWorkAlone,
e.created_at as createdAt,
e.last_login as lastLogin,
e.is_active,
e.employee_type,
e.contract_type,
e.can_work_alone,
e.is_trainee,
e.created_at,
e.last_login,
er.role
FROM employees e
LEFT JOIN employee_roles er ON e.id = er.employee_id
@@ -308,9 +405,19 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
LIMIT 1
`, [id]);
// Format response with roles array
// Format response with proper field names
const employeeWithRoles = {
...updatedEmployee,
id: updatedEmployee.id,
email: updatedEmployee.email,
firstname: updatedEmployee.firstname,
lastname: updatedEmployee.lastname,
isActive: updatedEmployee.is_active === 1,
employeeType: updatedEmployee.employee_type,
contractType: updatedEmployee.contract_type,
canWorkAlone: updatedEmployee.can_work_alone === 1,
isTrainee: updatedEmployee.is_trainee === 1,
createdAt: updatedEmployee.created_at,
lastLogin: updatedEmployee.last_login,
roles: updatedEmployee.role ? [updatedEmployee.role] : ['user']
};
@@ -434,6 +541,7 @@ export const getAvailabilities = async (req: AuthRequest, res: Response): Promis
id: avail.id,
employeeId: avail.employee_id,
planId: avail.plan_id,
shiftId: avail.shift_id,
dayOfWeek: avail.day_of_week,
timeSlotId: avail.time_slot_id,
preferenceLevel: avail.preference_level,
@@ -561,4 +669,34 @@ export const changePassword = async (req: AuthRequest, res: Response): Promise<v
console.error('Error changing password:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
export const updateLastLogin = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const { id } = req.params;
// Check if employee exists
const employee = await db.get('SELECT id FROM employees WHERE id = ?', [id]);
if (!employee) {
res.status(404).json({ error: 'Employee not found' });
return;
}
// Update last_login with current timestamp
const currentTimestamp = new Date().toISOString();
await db.run(
'UPDATE employees SET last_login = ? WHERE id = ?',
[currentTimestamp, id]
);
console.log(`✅ Last login updated for employee ${id}: ${currentTimestamp}`);
res.json({
message: 'Last login updated successfully',
lastLogin: currentTimestamp
});
} catch (error) {
console.error('Error updating last login:', error);
res.status(500).json({ error: 'Internal server error' });
}
};

View File

@@ -5,9 +5,7 @@ import { v4 as uuidv4 } from 'uuid';
import { randomUUID } from 'crypto';
import { db } from '../services/databaseService.js';
// Add the same email generation function
function generateEmail(firstname: string, lastname: string): string {
// Convert German umlauts to their expanded forms
const convertUmlauts = (str: string): string => {
return str
.toLowerCase()
@@ -17,7 +15,6 @@ function generateEmail(firstname: string, lastname: string): string {
.replace(/ß/g, 'ss');
};
// Remove any remaining special characters and convert to lowercase
const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, '');
const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, '');
@@ -98,14 +95,14 @@ export const setupAdmin = async (req: Request, res: Response): Promise<void> =>
await db.run('BEGIN TRANSACTION');
try {
// Create admin user in employees table
// ✅ CORRECTED: Create admin with valid 'manager' type and 'flexible' contract
await db.run(
`INSERT INTO employees (id, email, password, firstname, lastname, employee_type, contract_type, can_work_alone, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[adminId, email, hashedPassword, firstname, lastname, 'manager', 'large', true, 1]
`INSERT INTO employees (id, email, password, firstname, lastname, employee_type, contract_type, can_work_alone, is_active, is_trainee)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[adminId, email, hashedPassword, firstname, lastname, 'manager', 'flexible', true, 1, false]
);
// UPDATED: Assign admin role in employee_roles table
// Assign admin role in employee_roles table
await db.run(
`INSERT INTO employee_roles (employee_id, role) VALUES (?, ?)`,
[adminId, 'admin']

View File

@@ -1,3 +1,28 @@
-- Employee Types
CREATE TABLE IF NOT EXISTS employee_types (
type TEXT PRIMARY KEY,
category TEXT CHECK(category IN ('internal', 'external')) NOT NULL,
has_contract_type BOOLEAN NOT NULL DEFAULT FALSE
);
-- Default Employee Types
-- 'manager' and 'apprentice' contract_type_default = flexible
-- 'personell' contract_type_default = small
-- employee_types category 'external' contract_type = NONE
-- moved experienced and trainee into personell -> is_trainee boolean added in employees
INSERT OR IGNORE INTO employee_types (type, category, has_contract_type) VALUES
('manager', 'internal', 1),
('personell', 'internal', 1),
('apprentice', 'internal', 1),
('guest', 'external', 0);
-- Roles lookup table
CREATE TABLE IF NOT EXISTS roles (
role TEXT PRIMARY KEY CHECK(role IN ('admin', 'user', 'maintenance')),
authority_level INTEGER NOT NULL UNIQUE CHECK(authority_level BETWEEN 1 AND 100),
description TEXT
);
-- Employees table
CREATE TABLE IF NOT EXISTS employees (
id TEXT PRIMARY KEY,
@@ -5,30 +30,27 @@ CREATE TABLE IF NOT EXISTS employees (
password TEXT NOT NULL,
firstname TEXT NOT NULL,
lastname TEXT NOT NULL,
employee_type TEXT CHECK(employee_type IN ('manager', 'trainee', 'experienced')) NOT NULL,
contract_type TEXT CHECK(contract_type IN ('small', 'large')) NOT NULL,
employee_type TEXT NOT NULL REFERENCES employee_types(type),
contract_type TEXT CHECK(contract_type IN ('small', 'large', 'flexible')),
can_work_alone BOOLEAN DEFAULT FALSE,
is_trainee BOOLEAN DEFAULT FALSE NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login TEXT DEFAULT NULL
);
-- Roles lookup table
CREATE TABLE IF NOT EXISTS roles (
role TEXT PRIMARY KEY CHECK(role IN ('admin', 'user', 'maintenance'))
);
-- Junction table: many-to-many relationship
-- Roles Employee Junction table (NACH employees und roles)
CREATE TABLE IF NOT EXISTS employee_roles (
employee_id TEXT NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
role TEXT NOT NULL REFERENCES roles(role),
PRIMARY KEY (employee_id, role)
);
-- Insert default roles if they don't exist
INSERT OR IGNORE INTO roles (role) VALUES ('admin');
INSERT OR IGNORE INTO roles (role) VALUES ('user');
INSERT OR IGNORE INTO roles (role) VALUES ('maintenance');
-- Insert default roles (NACH roles Tabelle)
INSERT OR IGNORE INTO roles (role, authority_level, description) VALUES
('admin', 100, 'Vollzugriff'),
('maintenance', 50, 'Wartungszugriff'),
('user', 10, 'Standardbenutzer');
-- Shift plans table
CREATE TABLE IF NOT EXISTS shift_plans (
@@ -109,29 +131,21 @@ CREATE TABLE IF NOT EXISTS employee_availability (
UNIQUE(employee_id, plan_id, shift_id)
);
-- Performance indexes (UPDATED - removed role index from employees)
-- Performance indexes
CREATE INDEX IF NOT EXISTS idx_employees_email_active ON employees(email, is_active);
CREATE INDEX IF NOT EXISTS idx_employees_type_active ON employees(employee_type, is_active);
-- Index for employee_roles table (NEW)
CREATE INDEX IF NOT EXISTS idx_employee_roles_employee ON employee_roles(employee_id);
CREATE INDEX IF NOT EXISTS idx_employee_roles_role ON employee_roles(role);
CREATE INDEX IF NOT EXISTS idx_shift_plans_status_date ON shift_plans(status, start_date, end_date);
CREATE INDEX IF NOT EXISTS idx_shift_plans_created_by ON shift_plans(created_by);
CREATE INDEX IF NOT EXISTS idx_shift_plans_template ON shift_plans(is_template, status);
CREATE INDEX IF NOT EXISTS idx_time_slots_plan ON time_slots(plan_id);
CREATE INDEX IF NOT EXISTS idx_shifts_plan_day ON shifts(plan_id, day_of_week);
CREATE INDEX IF NOT EXISTS idx_shifts_required_employees ON shifts(required_employees);
CREATE INDEX IF NOT EXISTS idx_shifts_plan_time ON shifts(plan_id, time_slot_id, day_of_week);
CREATE INDEX IF NOT EXISTS idx_scheduled_shifts_plan_date ON scheduled_shifts(plan_id, date);
CREATE INDEX IF NOT EXISTS idx_scheduled_shifts_date_time ON scheduled_shifts(date, time_slot_id);
CREATE INDEX IF NOT EXISTS idx_scheduled_shifts_required_employees ON scheduled_shifts(required_employees);
CREATE INDEX IF NOT EXISTS idx_shift_assignments_employee ON shift_assignments(employee_id);
CREATE INDEX IF NOT EXISTS idx_shift_assignments_shift ON shift_assignments(scheduled_shift_id);
CREATE INDEX IF NOT EXISTS idx_employee_availability_employee_plan ON employee_availability(employee_id, plan_id);

View File

@@ -44,10 +44,11 @@ export const authMiddleware = (req: AuthRequest, res: Response, next: NextFuncti
export const requireRole = (roles: string[]) => {
return (req: AuthRequest, res: Response, next: NextFunction): void => {
if (!req.user || !roles.includes(req.user.role)) {
console.log('❌ Insufficient permissions for user:', req.user?.email);
console.log(`❌ Insufficient permissions for user: ${req.user?.email}, role: ${req.user?.role}, required: ${roles.join(', ')}`);
res.status(403).json({ error: 'Access denied. Insufficient permissions.' });
return;
}
console.log(`✅ Role check passed for user: ${req.user.email}, role: ${req.user.role}`);
next();
};
};

View File

@@ -4,10 +4,11 @@ export interface Employee {
email: string;
firstname: string;
lastname: string;
employeeType: 'manager' | 'trainee' | 'experienced';
contractType: 'small' | 'large';
employeeType: 'manager' | 'personell' | 'apprentice' | 'guest';
contractType?: 'small' | 'large' | 'flexible';
canWorkAlone: boolean;
isActive: boolean;
isTrainee: boolean;
createdAt: string;
lastLogin?: string | null;
roles?: string[];
@@ -18,19 +19,21 @@ export interface CreateEmployeeRequest {
firstname: string;
lastname: string;
roles?: string[];
employeeType: 'manager' | 'trainee' | 'experienced';
contractType: 'small' | 'large';
employeeType: 'manager' | 'personell' | 'apprentice' | 'guest';
contractType?: 'small' | 'large' | 'flexible';
canWorkAlone: boolean;
isTrainee?: boolean;
}
export interface UpdateEmployeeRequest {
firstname?: string;
lastname?: string;
roles?: string[];
employeeType?: 'manager' | 'trainee' | 'experienced';
contractType?: 'small' | 'large';
employeeType?: 'manager' | 'personell' | 'apprentice' | 'guest';
contractType?: 'small' | 'large' | 'flexible';
canWorkAlone?: boolean;
isActive?: boolean;
isTrainee?: boolean;
}
export interface EmployeeWithPassword extends Employee {
@@ -41,7 +44,7 @@ export interface EmployeeAvailability {
id: string;
employeeId: string;
planId: string;
shiftId: string; // Now references shift_id instead of time_slot_id + day_of_week
shiftId: string;
preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable
notes?: string;
}
@@ -83,4 +86,11 @@ export interface Role {
export interface EmployeeRole {
employeeId: string;
role: 'admin' | 'user' | 'maintenance';
}
// Employee type configuration
export interface EmployeeType {
type: 'manager' | 'personell' | 'apprentice' | 'guest';
category: 'internal' | 'external';
has_contract_type: boolean;
}

View File

@@ -4,19 +4,41 @@ import { EmployeeAvailability, ManagerAvailability } from '../Employee.js';
// Default employee data for quick creation
export const EMPLOYEE_DEFAULTS = {
role: 'user' as const,
employeeType: 'experienced' as const,
employeeType: 'personell' as const,
contractType: 'small' as const,
canWorkAlone: false,
isActive: true
isActive: true,
isTrainee: false
};
// Manager-specific defaults
export const MANAGER_DEFAULTS = {
role: 'admin' as const,
employeeType: 'manager' as const,
contractType: 'large' as const,
contractType: 'flexible' as const,
canWorkAlone: true,
isActive: true
isActive: true,
isTrainee: false
};
// Apprentice defaults
export const APPRENTICE_DEFAULTS = {
role: 'user' as const,
employeeType: 'apprentice' as const,
contractType: 'flexible' as const,
canWorkAlone: false,
isActive: true,
isTrainee: false
};
// Guest defaults
export const GUEST_DEFAULTS = {
role: 'user' as const,
employeeType: 'guest' as const,
contractType: undefined,
canWorkAlone: false,
isActive: true,
isTrainee: false
};
export const EMPLOYEE_TYPE_CONFIG = {
@@ -24,22 +46,37 @@ export const EMPLOYEE_TYPE_CONFIG = {
value: 'manager' as const,
label: 'Chef/Administrator',
color: '#e74c3c',
category: 'internal' as const,
hasContractType: true,
independent: true,
description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung'
},
experienced: {
value: 'experienced' as const,
label: 'Erfahren',
personell: {
value: 'personell' as const,
label: 'Personal',
color: '#3498db',
category: 'internal' as const,
hasContractType: true,
independent: true,
description: 'Langjährige Erfahrung, kann komplexe Aufgaben übernehmen'
description: 'Reguläre Mitarbeiter mit Vertrag'
},
trainee: {
value: 'trainee' as const,
label: 'Neuling',
color: '#27ae60',
apprentice: {
value: 'apprentice' as const,
label: 'Auszubildender',
color: '#9b59b6',
category: 'internal' as const,
hasContractType: true,
independent: false,
description: 'Benötigt Einarbeitung und Unterstützung'
description: 'Auszubildende mit flexiblem Vertrag'
},
guest: {
value: 'guest' as const,
label: 'Gast',
color: '#95a5a6',
category: 'external' as const,
hasContractType: false,
independent: false,
description: 'Externe Mitarbeiter ohne Vertrag'
}
} as const;
@@ -53,9 +90,10 @@ export const ROLE_CONFIG = [
export const CONTRACT_TYPE_DESCRIPTIONS = {
small: '1 Schicht pro Woche',
large: '2 Schichten pro Woche',
manager: 'Kein Vertragslimit - Immer MO und DI verfügbar'
flexible: 'Flexible Arbeitszeiten'
} as const;
// Availability preference descriptions
export const AVAILABILITY_PREFERENCES = {
1: { label: 'Bevorzugt', color: '#10b981', description: 'Möchte diese Schicht arbeiten' },
@@ -104,4 +142,17 @@ export function createManagerDefaultSchedule(managerId: string, planId: string,
}
return assignments;
}
export function getDefaultsByEmployeeType(employeeType: string) {
switch (employeeType) {
case 'manager':
return MANAGER_DEFAULTS;
case 'apprentice':
return APPRENTICE_DEFAULTS;
case 'guest':
return GUEST_DEFAULTS;
default:
return EMPLOYEE_DEFAULTS;
}
}

View File

@@ -1,9 +1,8 @@
// backend/src/models/helpers/employeeHelpers.ts
import { Employee, CreateEmployeeRequest, EmployeeAvailability } from '../Employee.js';
// Email generation function (same as in controllers)
// Email generation function
function generateEmail(firstname: string, lastname: string): string {
// Convert German umlauts to their expanded forms
const convertUmlauts = (str: string): string => {
return str
.toLowerCase()
@@ -13,19 +12,16 @@ function generateEmail(firstname: string, lastname: string): string {
.replace(/ß/g, 'ss');
};
// Remove any remaining special characters and convert to lowercase
const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, '');
const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, '');
return `${cleanFirstname}.${cleanLastname}@sp.de`;
}
// UPDATED: Validation for new employee model
// UPDATED: Validation for new employee model with employee types
export function validateEmployeeData(employee: CreateEmployeeRequest): string[] {
const errors: string[] = [];
// Email is now auto-generated, so no email validation needed
if (employee.password?.length < 6) {
errors.push('Password must be at least 6 characters long');
}
@@ -38,24 +34,70 @@ export function validateEmployeeData(employee: CreateEmployeeRequest): string[]
errors.push('Last name is required and must be at least 2 characters long');
}
// Validate employee type
const validEmployeeTypes = ['manager', 'personell', 'apprentice', 'guest'];
if (!employee.employeeType || !validEmployeeTypes.includes(employee.employeeType)) {
errors.push(`Employee type must be one of: ${validEmployeeTypes.join(', ')}`);
}
// Validate contract type based on employee type
if (employee.employeeType !== 'guest') {
// Internal types require contract type
if (!employee.contractType) {
errors.push(`Contract type is required for employee type: ${employee.employeeType}`);
} else {
const validContractTypes = ['small', 'large', 'flexible'];
if (!validContractTypes.includes(employee.contractType)) {
errors.push(`Contract type must be one of: ${validContractTypes.join(', ')}`);
}
}
} else {
// External types (guest) should not have contract type
if (employee.contractType) {
errors.push('Contract type is not allowed for guest employees');
}
}
// Validate isTrainee - only applicable for personell type
if (employee.isTrainee && employee.employeeType !== 'personell') {
errors.push('isTrainee is only allowed for personell employee type');
}
return errors;
}
// Generate email for employee (new helper function)
// Generate email for employee
export function generateEmployeeEmail(firstname: string, lastname: string): string {
return generateEmail(firstname, lastname);
}
// Simplified business logic helpers
// UPDATED: Business logic helpers for new employee types
export const isManager = (employee: Employee): boolean =>
employee.employeeType === 'manager';
export const isPersonell = (employee: Employee): boolean =>
employee.employeeType === 'personell';
export const isApprentice = (employee: Employee): boolean =>
employee.employeeType === 'apprentice';
export const isGuest = (employee: Employee): boolean =>
employee.employeeType === 'guest';
export const isInternal = (employee: Employee): boolean =>
['manager', 'personell', 'apprentice'].includes(employee.employeeType);
export const isExternal = (employee: Employee): boolean =>
employee.employeeType === 'guest';
// UPDATED: Trainee logic - now based on isTrainee field for personell type
export const isTrainee = (employee: Employee): boolean =>
employee.employeeType === 'trainee';
employee.employeeType === 'personell' && employee.isTrainee;
export const isExperienced = (employee: Employee): boolean =>
employee.employeeType === 'experienced';
employee.employeeType === 'personell' && !employee.isTrainee;
// Role-based helpers
export const isAdmin = (employee: Employee): boolean =>
employee.roles?.includes('admin') || false;
@@ -65,13 +107,11 @@ export const isMaintenance = (employee: Employee): boolean =>
export const isUser = (employee: Employee): boolean =>
employee.roles?.includes('user') || false;
// UPDATED: Work alone permission - managers and experienced personell can work alone
export const canEmployeeWorkAlone = (employee: Employee): boolean =>
employee.canWorkAlone && isExperienced(employee);
employee.canWorkAlone && (isManager(employee) || isExperienced(employee));
export const getEmployeeWorkHours = (employee: Employee): number =>
isManager(employee) ? 999 : (employee.contractType === 'small' ? 1 : 2);
// New helper for full name display
// Helper for full name display
export const getFullName = (employee: { firstname: string; lastname: string }): string =>
`${employee.firstname} ${employee.lastname}`;
@@ -92,4 +132,14 @@ export function validateAvailabilityData(availability: Omit<EmployeeAvailability
}
return errors;
}
}
// UPDATED: Helper to get employee type category
export const getEmployeeCategory = (employee: Employee): 'internal' | 'external' => {
return isInternal(employee) ? 'internal' : 'external';
};
// Helper to check if employee requires contract type
export const requiresContractType = (employee: Employee): boolean => {
return isInternal(employee);
};

View File

@@ -7,10 +7,10 @@ export interface Availability {
id: string;
employeeId: string;
planId: string;
shiftId: string; // Now references shift_id instead of time_slot_id + day_of_week
preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable
shiftId: string;
preferenceLevel: 1 | 2 | 3;
notes?: string;
// Optional convenience fields (can be joined from shifts and time_slots tables)
// Optional convenience fields
dayOfWeek?: number;
timeSlotId?: string;
timeSlotName?: string;
@@ -30,7 +30,7 @@ export interface Constraint {
maxHoursPerWeek?: number;
[key: string]: any;
};
weight?: number; // For soft constraints
weight?: number;
}
export interface ScheduleRequest {
@@ -153,8 +153,9 @@ export interface AvailabilityWithDetails extends Availability {
id: string;
firstname: string;
lastname: string;
employeeType: 'manager' | 'trainee' | 'experienced';
employeeType: 'manager' | 'personell' | 'apprentice' | 'guest';
canWorkAlone: boolean;
isTrainee: boolean;
};
shift?: {
dayOfWeek: number;

View File

@@ -9,7 +9,8 @@ import {
deleteEmployee,
getAvailabilities,
updateAvailabilities,
changePassword
changePassword,
updateLastLogin
} from '../controllers/employeeController.js';
const router = express.Router();
@@ -18,12 +19,13 @@ const router = express.Router();
router.use(authMiddleware);
// Employee CRUD Routes
router.get('/', authMiddleware, getEmployees);
router.get('/', requireRole(['admin']), getEmployees);
router.get('/:id', requireRole(['admin', 'instandhalter']), getEmployee);
router.post('/', requireRole(['admin']), createEmployee);
router.put('/:id', requireRole(['admin']), updateEmployee);
router.delete('/:id', requireRole(['admin']), deleteEmployee);
router.put('/:id/password', authMiddleware, changePassword);
router.put('/:id/last-login', authMiddleware, updateLastLogin);
// Availability Routes
router.get('/:employeeId/availabilities', authMiddleware, getAvailabilities);

View File

@@ -6,6 +6,25 @@ import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Define interfaces for type safety
interface TableColumnInfo {
cid: number;
name: string;
type: string;
notnull: number;
dflt_value: string | null;
pk: number;
}
interface CountResult {
count: number;
}
interface EmployeeRecord {
id: string;
// Note: roles column doesn't exist in new schema
}
// Helper function to ensure migrations are tracked
async function ensureMigrationTable() {
await db.exec(`
@@ -19,7 +38,7 @@ async function ensureMigrationTable() {
// Helper function to check if a migration has been applied
async function isMigrationApplied(migrationName: string): Promise<boolean> {
const result = await db.get<{ count: number }>(
const result = await db.get<CountResult>(
'SELECT COUNT(*) as count FROM applied_migrations WHERE name = ?',
[migrationName]
);
@@ -34,6 +53,85 @@ async function markMigrationAsApplied(migrationName: string) {
);
}
// UPDATED: Function to handle schema changes for the new employee type system
async function applySchemaUpdates() {
console.log('🔄 Applying schema updates for new employee type system...');
await db.run('BEGIN TRANSACTION');
try {
// 1. Create employee_types table if it doesn't exist
await db.exec(`
CREATE TABLE IF NOT EXISTS employee_types (
type TEXT PRIMARY KEY,
category TEXT CHECK(category IN ('internal', 'external')) NOT NULL,
has_contract_type BOOLEAN NOT NULL DEFAULT FALSE
)
`);
// 2. Insert default employee types
await db.run(`INSERT OR IGNORE INTO employee_types (type, category, has_contract_type) VALUES ('manager', 'internal', 1)`);
await db.run(`INSERT OR IGNORE INTO employee_types (type, category, has_contract_type) VALUES ('personell', 'internal', 1)`);
await db.run(`INSERT OR IGNORE INTO employee_types (type, category, has_contract_type) VALUES ('apprentice', 'internal', 1)`);
await db.run(`INSERT OR IGNORE INTO employee_types (type, category, has_contract_type) VALUES ('guest', 'external', 0)`);
// 3. Check if employees table needs to be altered
const employeesTableInfo = await db.all<TableColumnInfo>(`
PRAGMA table_info(employees)
`);
// FIXED: Check for employee_type column (not roles column)
const hasEmployeeType = employeesTableInfo.some((col: TableColumnInfo) => col.name === 'employee_type');
const hasIsTrainee = employeesTableInfo.some((col: TableColumnInfo) => col.name === 'is_trainee');
if (!hasEmployeeType) {
console.log('🔄 Adding employee_type column to employees table...');
await db.run('ALTER TABLE employees ADD COLUMN employee_type TEXT NOT NULL DEFAULT "personell"');
}
if (!hasIsTrainee) {
console.log('🔄 Adding is_trainee column to employees table...');
await db.run('ALTER TABLE employees ADD COLUMN is_trainee BOOLEAN DEFAULT FALSE NOT NULL');
}
// 4. Create roles table if it doesn't exist
await db.exec(`
CREATE TABLE IF NOT EXISTS roles (
role TEXT PRIMARY KEY CHECK(role IN ('admin', 'user', 'maintenance')),
authority_level INTEGER NOT NULL UNIQUE CHECK(authority_level BETWEEN 1 AND 100),
description TEXT
)
`);
// 5. Insert default roles
await db.run(`INSERT OR IGNORE INTO roles (role, authority_level, description) VALUES ('admin', 100, 'Vollzugriff')`);
await db.run(`INSERT OR IGNORE INTO roles (role, authority_level, description) VALUES ('maintenance', 50, 'Wartungszugriff')`);
await db.run(`INSERT OR IGNORE INTO roles (role, authority_level, description) VALUES ('user', 10, 'Standardbenutzer')`);
// 6. Create employee_roles junction table if it doesn't exist
await db.exec(`
CREATE TABLE IF NOT EXISTS employee_roles (
employee_id TEXT NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
role TEXT NOT NULL REFERENCES roles(role),
PRIMARY KEY (employee_id, role)
)
`);
// 7. REMOVED: Role migration logic since roles column doesn't exist in new schema
// In a fresh installation, we don't need to migrate existing roles
console.log(' Skipping role migration - fresh installation with new schema');
await db.run('COMMIT');
console.log('✅ Schema updates applied successfully');
} catch (error) {
await db.run('ROLLBACK');
console.error('❌ Schema updates failed:', error);
throw error;
}
}
export async function applyMigration() {
try {
console.log('📦 Starting database migration...');
@@ -41,60 +139,68 @@ export async function applyMigration() {
// Ensure migration tracking table exists
await ensureMigrationTable();
// Apply schema updates for new employee type system
await applySchemaUpdates();
// Get all migration files
const migrationsDir = join(__dirname, '../database/migrations');
const files = await readdir(migrationsDir);
// Sort files to ensure consistent order
const migrationFiles = files
.filter(f => f.endsWith('.sql'))
.sort();
// Process each migration file
for (const migrationFile of migrationFiles) {
if (await isMigrationApplied(migrationFile)) {
console.log(` Migration ${migrationFile} already applied, skipping...`);
continue;
}
try {
const files = await readdir(migrationsDir);
console.log(`📄 Applying migration: ${migrationFile}`);
const migrationPath = join(migrationsDir, migrationFile);
const migrationSQL = await readFile(migrationPath, 'utf-8');
// Sort files to ensure consistent order
const migrationFiles = files
.filter(f => f.endsWith('.sql'))
.sort();
// Split into individual statements
const statements = migrationSQL
.split(';')
.map(s => s.trim())
.filter(s => s.length > 0);
// Start transaction for this migration
await db.run('BEGIN TRANSACTION');
try {
// Execute each statement
for (const statement of statements) {
try {
await db.exec(statement);
console.log('✅ Executed:', statement.slice(0, 50) + '...');
} catch (error) {
const err = error as { code: string; message: string };
if (err.code === 'SQLITE_ERROR' && err.message.includes('duplicate column name')) {
console.log(' Column already exists, skipping...');
continue;
}
throw error;
}
// Process each migration file
for (const migrationFile of migrationFiles) {
if (await isMigrationApplied(migrationFile)) {
console.log(` Migration ${migrationFile} already applied, skipping...`);
continue;
}
// Mark migration as applied
await markMigrationAsApplied(migrationFile);
await db.run('COMMIT');
console.log(`✅ Migration ${migrationFile} applied successfully`);
console.log(`📄 Applying migration: ${migrationFile}`);
const migrationPath = join(migrationsDir, migrationFile);
const migrationSQL = await readFile(migrationPath, 'utf-8');
} catch (error) {
await db.run('ROLLBACK');
throw error;
// Split into individual statements
const statements = migrationSQL
.split(';')
.map(s => s.trim())
.filter(s => s.length > 0);
// Start transaction for this migration
await db.run('BEGIN TRANSACTION');
try {
// Execute each statement
for (const statement of statements) {
try {
await db.exec(statement);
console.log('✅ Executed:', statement.slice(0, 50) + '...');
} catch (error) {
const err = error as { code: string; message: string };
if (err.code === 'SQLITE_ERROR' && err.message.includes('duplicate column name')) {
console.log(' Column already exists, skipping...');
continue;
}
throw error;
}
}
// Mark migration as applied
await markMigrationAsApplied(migrationFile);
await db.run('COMMIT');
console.log(`✅ Migration ${migrationFile} applied successfully`);
} catch (error) {
await db.run('ROLLBACK');
throw error;
}
}
} catch (error) {
console.log(' No migration directory found or no migration files, skipping file-based migrations...');
}
console.log('✅ All migrations completed successfully');

View File

@@ -42,7 +42,7 @@ export async function initializeDatabase(): Promise<void> {
console.log('Existing tables found:', existingTables.map(t => t.name).join(', ') || 'none');
// UPDATED: Drop tables in correct dependency order
// UPDATED: Drop tables in correct dependency order for new schema
const tablesToDrop = [
'employee_availability',
'shift_assignments',
@@ -50,9 +50,10 @@ export async function initializeDatabase(): Promise<void> {
'shifts',
'time_slots',
'employee_roles',
'shift_plans',
'roles',
'employees',
'employee_types',
'shift_plans',
'applied_migrations'
];
@@ -100,15 +101,22 @@ export async function initializeDatabase(): Promise<void> {
}
}
// UPDATED: Insert default roles after creating the tables
// UPDATED: Insert default data in correct order
try {
console.log('Inserting default employee types...');
await db.run(`INSERT OR IGNORE INTO employee_types (type, category, has_contract_type) VALUES ('manager', 'internal', 1)`);
await db.run(`INSERT OR IGNORE INTO employee_types (type, category, has_contract_type) VALUES ('personell', 'internal', 1)`);
await db.run(`INSERT OR IGNORE INTO employee_types (type, category, has_contract_type) VALUES ('apprentice', 'internal', 1)`);
await db.run(`INSERT OR IGNORE INTO employee_types (type, category, has_contract_type) VALUES ('guest', 'external', 0)`);
console.log('✅ Default employee types inserted');
console.log('Inserting default roles...');
await db.run(`INSERT OR IGNORE INTO roles (role) VALUES ('admin')`);
await db.run(`INSERT OR IGNORE INTO roles (role) VALUES ('user')`);
await db.run(`INSERT OR IGNORE INTO roles (role) VALUES ('maintenance')`);
await db.run(`INSERT OR IGNORE INTO roles (role, authority_level, description) VALUES ('admin', 100, 'Vollzugriff')`);
await db.run(`INSERT OR IGNORE INTO roles (role, authority_level, description) VALUES ('maintenance', 50, 'Wartungszugriff')`);
await db.run(`INSERT OR IGNORE INTO roles (role, authority_level, description) VALUES ('user', 10, 'Standardbenutzer')`);
console.log('✅ Default roles inserted');
} catch (error) {
console.error('Error inserting default roles:', error);
console.error('Error inserting default data:', error);
await db.run('ROLLBACK');
throw error;
}

View File

@@ -97,16 +97,17 @@ export class SchedulingService {
const currentDate = new Date(startDate);
currentDate.setDate(startDate.getDate() + dayOffset);
const dayOfWeek = currentDate.getDay() === 0 ? 7 : currentDate.getDay(); // Convert Sunday from 0 to 7
const dayOfWeek = currentDate.getDay() === 0 ? 7 : currentDate.getDay();
const dayShifts = shiftPlan.shifts.filter(shift => shift.dayOfWeek === dayOfWeek);
dayShifts.forEach(shift => {
// ✅ Use day-of-week pattern instead of date-based pattern
const shiftId = `${shift.id}`;
// ✅ CRITICAL FIX: Use consistent shift ID format that matches availability lookup
const shiftId = `shift_${dayOfWeek}_${shift.timeSlotId}`;
const dateStr = currentDate.toISOString().split('T')[0];
shifts.push({
id: shiftId, // This matches what frontend expects
date: currentDate.toISOString().split('T')[0],
id: shiftId, // This will match what availabilities are looking for
date: dateStr,
timeSlotId: shift.timeSlotId,
requiredEmployees: shift.requiredEmployees,
minWorkers: 1,
@@ -114,7 +115,7 @@ export class SchedulingService {
isPriority: false
});
console.log(`✅ Generated shift: ${shiftId} for day ${dayOfWeek}, timeSlot ${shift.timeSlotId}`);
console.log(`✅ Generated shift: ${shiftId} for date ${dateStr}, day ${dayOfWeek}, timeSlot ${shift.timeSlotId}`);
});
}

View File

@@ -17,17 +17,42 @@ interface WorkerData {
function buildSchedulingModel(model: CPModel, data: WorkerData): void {
const { employees, shifts, availabilities, constraints } = data;
// Filter employees to only include active ones
const nonManagerEmployees = employees.filter(emp => emp.isActive && emp.employeeType !== 'manager');
const activeEmployees = employees.filter(emp => emp.isActive);
const trainees = nonManagerEmployees.filter(emp => emp.employeeType === 'trainee');
const experienced = nonManagerEmployees.filter(emp => emp.employeeType === 'experienced');
const schedulableEmployees = employees.filter(emp =>
emp.isActive &&
emp.employeeType === 'personell'
);
console.log(`Building model with ${nonManagerEmployees.length} employees, ${shifts.length} shifts`);
console.log(`Available shifts per week: ${shifts.length}`);
// Debug: Count constraints that will be created
console.log('\n🔧 CONSTRAINT ANALYSIS:');
let hardConstraints = 0;
let availabilityConstraints = 0;
// Count availability constraints
schedulableEmployees.forEach((employee: any) => {
shifts.forEach((shift: any) => {
const availability = availabilities.find(
(a: any) => a.employeeId === employee.id && a.shiftId === shift.id
);
if (!availability || availability.preferenceLevel === 3) {
availabilityConstraints++;
}
});
});
console.log(`Availability constraints to create: ${availabilityConstraints}`);
console.log(`Total possible assignments: ${schedulableEmployees.length * shifts.length}`);
const trainees = schedulableEmployees.filter(emp => emp.isTrainee);
const experienced = schedulableEmployees.filter(emp => !emp.isTrainee);
console.log(`Building model with ${schedulableEmployees.length} schedulable employees, ${shifts.length} shifts`);
console.log(`- Trainees: ${trainees.length}`);
console.log(`- Experienced: ${experienced.length}`);
console.log(`- Excluded: ${employees.filter(emp => !emp.isActive || emp.employeeType !== 'personell').length} employees (managers, apprentices, guests, inactive)`);
// 1. Create assignment variables for all possible assignments
nonManagerEmployees.forEach((employee: any) => {
schedulableEmployees.forEach((employee: any) => {
shifts.forEach((shift: any) => {
const varName = `assign_${employee.id}_${shift.id}`;
model.addVariable(varName, 'bool');
@@ -35,18 +60,18 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
});
// 2. Availability constraints
nonManagerEmployees.forEach((employee: any) => {
schedulableEmployees.forEach((employee: any) => {
shifts.forEach((shift: any) => {
const availability = availabilities.find(
(a: any) => a.employeeId === employee.id && a.shiftId === shift.id
);
// Hard constraint: never assign when preference level is 3 (unavailable)
if (availability?.preferenceLevel === 3) {
const varName = `assign_${employee.id}_${shift.id}`;
const varName = `assign_${employee.id}_${shift.id}`;
if (!availability || availability.preferenceLevel === 3) {
model.addConstraint(
`${varName} == 0`,
`Hard availability constraint for ${employee.name} in shift ${shift.id}`
`Employee ${employee.firstname} ${employee.lastname} has not signed up for shift ${shift.id}`
);
}
});
@@ -54,7 +79,7 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
// 3. Max 1 shift per day per employee
const shiftsByDate = groupShiftsByDate(shifts);
nonManagerEmployees.forEach((employee: any) => {
schedulableEmployees.forEach((employee: any) => {
Object.entries(shiftsByDate).forEach(([date, dayShifts]) => {
const dayAssignmentVars = (dayShifts as any[]).map(
(shift: any) => `assign_${employee.id}_${shift.id}`
@@ -71,7 +96,7 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
// 4. Shift staffing constraints
shifts.forEach((shift: any) => {
const assignmentVars = nonManagerEmployees.map(
const assignmentVars = schedulableEmployees.map(
(emp: any) => `assign_${emp.id}_${shift.id}`
);
@@ -117,13 +142,13 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
});
// 6. Employees who cannot work alone constraint
const employeesWhoCantWorkAlone = nonManagerEmployees.filter(emp => !emp.canWorkAlone);
const employeesWhoCantWorkAlone = schedulableEmployees.filter(emp => !emp.canWorkAlone);
console.log(`Found ${employeesWhoCantWorkAlone.length} employees who cannot work alone`);
employeesWhoCantWorkAlone.forEach((employee: any) => {
shifts.forEach((shift: any) => {
const employeeVar = `assign_${employee.id}_${shift.id}`;
const otherEmployees = nonManagerEmployees.filter(emp =>
const otherEmployees = schedulableEmployees.filter(emp =>
emp.id !== employee.id && emp.isActive
);
@@ -151,7 +176,7 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
const totalShifts = shifts.length;
console.log(`Total available shifts: ${totalShifts}`);
nonManagerEmployees.forEach((employee: any) => {
schedulableEmployees.forEach((employee: any) => {
const contractType = employee.contractType || 'large';
// EXACT SHIFTS PER WEEK
@@ -181,8 +206,8 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
// 8. Objective: Maximize preferred assignments with soft constraints
let objectiveExpression = '';
let softConstraintPenalty = '';
nonManagerEmployees.forEach((employee: any) => {
schedulableEmployees.forEach((employee: any) => {
shifts.forEach((shift: any) => {
const varName = `assign_${employee.id}_${shift.id}`;
const availability = availabilities.find(
@@ -191,25 +216,28 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
let score = 0;
if (availability) {
score = availability.preferenceLevel === 1 ? 10 :
availability.preferenceLevel === 2 ? 5 :
-10000; // Very heavy penalty for unavailable
} else {
// No availability info - slight preference to assign
score = 1;
// Only give positive scores for shifts employees actually signed up for
if (availability.preferenceLevel === 1) {
score = 100; // High reward for preferred shifts
} else if (availability.preferenceLevel === 2) {
score = 50; // Medium reward for available shifts
}
}
if (objectiveExpression) {
objectiveExpression += ` + ${score} * ${varName}`;
} else {
objectiveExpression = `${score} * ${varName}`;
if (score > 0) {
if (objectiveExpression) {
objectiveExpression += ` + ${score} * ${varName}`;
} else {
objectiveExpression = `${score} * ${varName}`;
}
}
});
});
if (objectiveExpression) {
model.maximize(objectiveExpression);
console.log('Objective function set with preference optimization');
console.log('Objective function set with strict availability enforcement');
} else {
console.warn('No valid objective expression could be created');
}
}
@@ -282,12 +310,12 @@ function detectViolations(assignments: any, employees: any[], shifts: any[]): st
const assignedEmployees = assignments[shift.id] || [];
const hasTrainee = assignedEmployees.some((empId: string) => {
const emp = employeeMap.get(empId);
return emp?.employeeType === 'trainee';
return emp?.isTrainee;
});
const hasExperienced = assignedEmployees.some((empId: string) => {
const emp = employeeMap.get(empId);
return emp?.employeeType === 'experienced';
return emp && !emp.isTrainee;
});
if (hasTrainee && !hasExperienced) {
@@ -330,9 +358,13 @@ function detectViolations(assignments: any, employees: any[], shifts: any[]): st
}
function assignManagersToShifts(assignments: any, managers: any[], shifts: any[], availabilities: any[]): any {
const managersToAssign = managers.filter(emp => emp.isActive && emp.employeeType === 'manager');
const managersToAssign = managers.filter(emp =>
emp.isActive &&
emp.employeeType === 'manager'
);
console.log(`Assigning ${managersToAssign.length} managers to shifts based on availability=1`);
console.log(`- Excluded ${managers.filter(emp => !emp.isActive || emp.employeeType !== 'manager').length} non-manager employees from auto-assignment`);
managersToAssign.forEach((manager: any) => {
shifts.forEach((shift: any) => {
@@ -349,7 +381,7 @@ function assignManagersToShifts(assignments: any, managers: any[], shifts: any[]
// Check if manager is already assigned (avoid duplicates)
if (!assignments[shift.id].includes(manager.id)) {
assignments[shift.id].push(manager.id);
console.log(`✅ Assigned manager ${manager.name} to shift ${shift.id} (availability=1)`);
console.log(`✅ Assigned manager ${manager.firstname} ${manager.lastname} to shift ${shift.id} (availability=1)`);
}
}
});
@@ -375,8 +407,110 @@ async function runScheduling() {
console.log(`Optimizing ${data.shifts.length} shifts for ${data.employees.length} employees`);
// 🆕 COMPREHENSIVE AVAILABILITY DEBUGGING
console.log('\n🔍 ===== AVAILABILITY ANALYSIS =====');
console.log(`Total shifts: ${data.shifts.length}`);
console.log(`Total availability records: ${data.availabilities.length}`);
// Count by preference level
const pref1Count = data.availabilities.filter(a => a.preferenceLevel === 1).length;
const pref2Count = data.availabilities.filter(a => a.preferenceLevel === 2).length;
const pref3Count = data.availabilities.filter(a => a.preferenceLevel === 3).length;
console.log(`Preference Level 1 (Preferred): ${pref1Count}`);
console.log(`Preference Level 2 (Available): ${pref2Count}`);
console.log(`Preference Level 3 (Unavailable): ${pref3Count}`);
// Analyze each shift
console.log('\n📊 AVAILABILITIES PER SHIFT:');
data.shifts.forEach((shift, index) => {
const shiftAvailabilities = data.availabilities.filter(avail => avail.shiftId === shift.id);
const pref1 = shiftAvailabilities.filter(a => a.preferenceLevel === 1).length;
const pref2 = shiftAvailabilities.filter(a => a.preferenceLevel === 2).length;
const pref3 = shiftAvailabilities.filter(a => a.preferenceLevel === 3).length;
console.log(`Shift ${index + 1}: ${shift.id}`);
console.log(` 📅 Date: ${shift.dayOfWeek}, TimeSlot: ${shift.timeSlotId}`);
console.log(` 👥 Required: ${shift.requiredEmployees}`);
console.log(` ✅ Preferred (1): ${pref1}`);
console.log(` 🔶 Available (2): ${pref2}`);
console.log(` ❌ Unavailable (3): ${pref3}`);
console.log(` 📋 Total signed up: ${pref1 + pref2}`);
// Show employee names for each preference level
if (pref1 > 0) {
const pref1Employees = shiftAvailabilities
.filter(a => a.preferenceLevel === 1)
.map(a => {
const emp = data.employees.find(e => e.id === a.employeeId);
return emp ? `${emp.firstname} ${emp.lastname}` : 'Unknown';
});
console.log(` Preferred employees: ${pref1Employees.join(', ')}`);
}
if (pref2 > 0) {
const pref2Employees = shiftAvailabilities
.filter(a => a.preferenceLevel === 2)
.map(a => {
const emp = data.employees.find(e => e.id === a.employeeId);
return emp ? `${emp.firstname} ${emp.lastname}` : 'Unknown';
});
console.log(` Available employees: ${pref2Employees.join(', ')}`);
}
});
// Employee-level analysis
console.log('\n👤 EMPLOYEE SIGNUP SUMMARY:');
const schedulableEmployees = data.employees.filter(emp =>
emp.isActive && emp.employeeType === 'personell'
);
schedulableEmployees.forEach(employee => {
const employeeAvailabilities = data.availabilities.filter(avail => avail.employeeId === employee.id);
const pref1 = employeeAvailabilities.filter(a => a.preferenceLevel === 1).length;
const pref2 = employeeAvailabilities.filter(a => a.preferenceLevel === 2).length;
const pref3 = employeeAvailabilities.filter(a => a.preferenceLevel === 3).length;
console.log(`${employee.firstname} ${employee.lastname} (${employee.contractType}):`);
console.log(` ✅ Preferred: ${pref1} shifts, 🔶 Available: ${pref2} shifts, ❌ Unavailable: ${pref3} shifts`);
});
// Check shift ID matching
console.log('\n🔍 SHIFT ID MATCHING ANALYSIS:');
const shiftsWithNoSignups = data.shifts.filter(shift => {
const shiftAvailabilities = data.availabilities.filter(avail => avail.shiftId === shift.id);
const signedUp = shiftAvailabilities.filter(a => a.preferenceLevel === 1 || a.preferenceLevel === 2);
return signedUp.length === 0;
});
console.log(`Shifts with NO signups: ${shiftsWithNoSignups.length}/${data.shifts.length}`);
shiftsWithNoSignups.forEach(shift => {
console.log(` ❌ No signups: ${shift.id} (${shift.dayOfWeek}, ${shift.timeSlotId})`);
});
console.log('===== END AVAILABILITY ANALYSIS =====\n');
const nonManagerEmployees = data.employees.filter(emp => emp.isActive && emp.employeeType !== 'manager');
// Check if we have any employees who signed up for shifts
const employeesWithSignups = new Set(
data.availabilities
.filter(avail => avail.preferenceLevel === 1 || avail.preferenceLevel === 2)
.map(avail => avail.employeeId)
);
if (employeesWithSignups.size === 0) {
console.log('❌ CRITICAL: No employees have signed up for any shifts!');
parentPort?.postMessage({
assignments: {},
violations: ['NO_SIGNUPS: No employees have signed up for any shifts with preference level 1 or 2'],
success: false,
resolutionReport: ['❌ Scheduling failed: No employees have signed up for any shifts'],
processingTime: Date.now() - startTime
});
return;
}
const model = new CPModel();
buildSchedulingModel(model, data);
@@ -406,7 +540,6 @@ async function runScheduling() {
// Extract assignments from solution (non-managers only)
assignments = extractAssignmentsFromSolution(solution, nonManagerEmployees, data.shifts);
// 🆕 ADD THIS: Assign managers to shifts where they have availability=1
assignments = assignManagersToShifts(assignments, data.employees, data.shifts, data.availabilities);
// Only detect violations for non-manager assignments
@@ -416,7 +549,6 @@ async function runScheduling() {
violations.push('NO_ASSIGNMENTS: Solver reported success but produced no assignments');
}
// Update resolution report
if (violations.length === 0) {
resolutionReport.push('✅ No constraint violations detected for non-manager employees');
} else {