diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/authController.ts index 6f9ed3d..95f3944 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/authController.ts @@ -34,9 +34,8 @@ export interface JWTPayload { export interface RegisterRequest { email: string; password: string; - name: string; - //employee_type?: string; - //is_sufficiently_independent?: string; + firstname: String; + lastname: String; role?: string; } @@ -53,7 +52,7 @@ export const login = async (req: Request, res: Response) => { // Get user from database const user = await db.get( - 'SELECT id, email, password, name, role, employee_type as employeeType, contract_type as contractType, can_work_alone as canWorkAlone, is_active as isActive FROM employees WHERE email = ? AND is_active = 1', + 'SELECT id, email, password, firstname, lastname, role, employee_type as employeeType, contract_type as contractType, can_work_alone as canWorkAlone, is_active as isActive FROM employees WHERE email = ? AND is_active = 1', [email] ); @@ -117,7 +116,7 @@ export const getCurrentUser = async (req: Request, res: Response) => { } const user = await db.get( - 'SELECT id, email, name, role, employee_type as employeeType, contract_type as contractType, can_work_alone as canWorkAlone, is_active as isActive FROM employees WHERE id = ? AND is_active = 1', + 'SELECT id, email, firstname, lastname, role, employee_type as employeeType, contract_type as contractType, can_work_alone as canWorkAlone, is_active as isActive FROM employees WHERE id = ? AND is_active = 1', [jwtUser.userId] ); @@ -163,10 +162,10 @@ export const validateToken = async (req: Request, res: Response) => { export const register = async (req: Request, res: Response) => { try { - const { email, password, name, role = 'user' } = req.body as RegisterRequest; + const { email, password, firstname, lastname, role = 'user' } = req.body as RegisterRequest; // Validate required fields - if (!email || !password || !name) { + if (!email || !password || !firstname || !lastname) { return res.status(400).json({ error: 'E-Mail, Passwort und Name sind erforderlich' }); @@ -188,11 +187,11 @@ export const register = async (req: Request, res: Response) => { const hashedPassword = await bcrypt.hash(password, 10); // Insert user - const result = await db.run( - `INSERT INTO employees (id, email, password, name, role, employee_type, contract_type, can_work_alone, is_active) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [uuidv4(), email, hashedPassword, name, role, 'experienced', 'small', false, 1] - ); + const result = await db.run( + `INSERT INTO employees (id, email, password, firstname, lastname, role, employee_type, contract_type, can_work_alone, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [uuidv4(), email, hashedPassword, firstname, lastname, role, 'experienced', 'small', false, 1] + ); if (!result.lastID) { throw new Error('Benutzer konnte nicht erstellt werden'); diff --git a/backend/src/controllers/employeeController.ts b/backend/src/controllers/employeeController.ts index 5ef86ef..fcc99b1 100644 --- a/backend/src/controllers/employeeController.ts +++ b/backend/src/controllers/employeeController.ts @@ -15,7 +15,7 @@ export const getEmployees = async (req: AuthRequest, res: Response): Promise(` SELECT - id, email, name, role, is_active as isActive, + id, email, firstname, lastname, role, is_active as isActive, employee_type as employeeType, contract_type as contractType, can_work_alone as canWorkAlone, @@ -78,15 +78,16 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise => { try { const { id } = req.params; - const { name, role, isActive, employeeType, contractType, canWorkAlone } = req.body; // Statt isSufficientlyIndependent - + const { firstname, lastname, role, isActive, employeeType, contractType, canWorkAlone } = req.body; console.log('📝 Update Employee Request:', { id, name, role, isActive, employeeType, contractType, canWorkAlone }); // Check if employee exists @@ -168,14 +169,15 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise(` - SELECT * FROM employee_availability - WHERE employee_id = ? - ORDER BY day_of_week, time_slot_id + SELECT ea.*, s.day_of_week, s.time_slot_id + FROM employee_availability ea + JOIN shifts s ON ea.shift_id = s.id + WHERE ea.employee_id = ? + ORDER BY s.day_of_week, s.time_slot_id `, [employeeId]); //console.log('✅ Successfully got availabilities from employee:', availabilities); @@ -334,14 +338,13 @@ export const updateAvailabilities = async (req: AuthRequest, res: Response): Pro for (const availability of availabilities) { const availabilityId = uuidv4(); await db.run( - `INSERT INTO employee_availability (id, employee_id, plan_id, day_of_week, time_slot_id, preference_level, notes) - VALUES (?, ?, ?, ?, ?, ?, ?)`, + `INSERT INTO employee_availability (id, employee_id, plan_id, shift_id, preference_level, notes) + VALUES (?, ?, ?, ?, ?, ?)`, [ availabilityId, employeeId, planId, - availability.dayOfWeek, - availability.timeSlotId, + availability.shiftId, availability.preferenceLevel, availability.notes || null ] diff --git a/backend/src/controllers/setupController.ts b/backend/src/controllers/setupController.ts index c2312c3..2ed106a 100644 --- a/backend/src/controllers/setupController.ts +++ b/backend/src/controllers/setupController.ts @@ -4,7 +4,6 @@ import bcrypt from 'bcrypt'; import { v4 as uuidv4 } from 'uuid'; import { randomUUID } from 'crypto'; import { db } from '../services/databaseService.js'; -//import { initializeDefaultTemplates } from './shiftPlanController.js'; export const checkSetupStatus = async (req: Request, res: Response): Promise => { try { @@ -44,14 +43,14 @@ export const setupAdmin = async (req: Request, res: Response): Promise => return; } - const { password, name } = req.body; + const { password, firstname, lastname } = req.body; const email = 'admin@instandhaltung.de'; console.log('👤 Creating admin with data:', { name, email }); // Validation - if (!password || !name) { - res.status(400).json({ error: 'Passwort und Name sind erforderlich' }); + if (!password || !firstname || !lastname) { + res.status(400).json({ error: 'Passwort, Vorname und Nachname sind erforderlich' }); return; } @@ -73,9 +72,9 @@ export const setupAdmin = async (req: Request, res: Response): Promise => try { // Create admin user await db.run( - `INSERT INTO employees (id, email, password, name, role, is_active, employee_type, contract_type) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - [adminId, email, hashedPassword, name, 'admin', 1, 'manager', 'large'] + `INSERT INTO employees (id, email, password, firstname, lastname, role, employee_type, contract_type, can_work_alone, is_active) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [adminId, email, hashedPassword, firstname, lastname, 'admin', 'manager', 'large', true, 1] ); console.log('✅ Admin user created successfully'); diff --git a/backend/src/controllers/shiftPlanController.ts b/backend/src/controllers/shiftPlanController.ts index 25d60d3..6a2c37f 100644 --- a/backend/src/controllers/shiftPlanController.ts +++ b/backend/src/controllers/shiftPlanController.ts @@ -12,7 +12,7 @@ import { createPlanFromPreset, TEMPLATE_PRESETS } from '../models/defaults/shift async function getPlanWithDetails(planId: string) { const plan = await db.get(` - SELECT sp.*, e.name as created_by_name + SELECT sp.*, e.firstname || ' ' || e.lastname as created_by_name FROM shift_plans sp LEFT JOIN employees e ON sp.created_by = e.id WHERE sp.id = ? @@ -69,7 +69,7 @@ async function getPlanWithDetails(planId: string) { export const getShiftPlans = async (req: Request, res: Response): Promise => { try { const plans = await db.all(` - SELECT sp.*, e.name as created_by_name + SELECT sp.*, e.firstname || ' ' || e.lastname as created_by_name FROM shift_plans sp LEFT JOIN employees e ON sp.created_by = e.id ORDER BY sp.created_at DESC @@ -94,7 +94,7 @@ export const getShiftPlan = async (req: Request, res: Response): Promise = const { id } = req.params; const plan = await db.get(` - SELECT sp.*, e.name as created_by_name + SELECT sp.*, e.firstname || ' ' || e.lastname as created_by_name FROM shift_plans sp LEFT JOIN employees e ON sp.created_by = e.id WHERE sp.id = ? @@ -555,7 +555,7 @@ export const deleteShiftPlan = async (req: Request, res: Response): Promise { const plan = await db.get(` - SELECT sp.*, e.name as created_by_name + SELECT sp.*, e.firstname || ' ' || e.lastname as created_by_name FROM shift_plans sp LEFT JOIN employees e ON sp.created_by = e.id WHERE sp.id = ? diff --git a/backend/src/database/schema.sql b/backend/src/database/schema.sql index e94daf0..8160ec3 100644 --- a/backend/src/database/schema.sql +++ b/backend/src/database/schema.sql @@ -3,8 +3,8 @@ CREATE TABLE IF NOT EXISTS employees ( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, password TEXT NOT NULL, - name TEXT NOT NULL, - role TEXT CHECK(role IN ('admin', 'user', 'maintenance')) 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, can_work_alone BOOLEAN DEFAULT FALSE, @@ -13,6 +13,18 @@ CREATE TABLE IF NOT EXISTS employees ( 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 +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) +); + -- Shift plans table CREATE TABLE IF NOT EXISTS shift_plans ( id TEXT PRIMARY KEY, @@ -83,14 +95,13 @@ CREATE TABLE IF NOT EXISTS employee_availability ( id TEXT PRIMARY KEY, employee_id TEXT NOT NULL, plan_id TEXT NOT NULL, - day_of_week INTEGER NOT NULL CHECK (day_of_week >= 1 AND day_of_week <= 7), - time_slot_id TEXT NOT NULL, + shift_id TEXT NOT NULL, preference_level INTEGER CHECK(preference_level IN (1, 2, 3)) NOT NULL, notes TEXT, FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE, FOREIGN KEY (plan_id) REFERENCES shift_plans(id) ON DELETE CASCADE, - FOREIGN KEY (time_slot_id) REFERENCES time_slots(id) ON DELETE CASCADE, - UNIQUE(employee_id, plan_id, day_of_week, time_slot_id) + FOREIGN KEY (shift_id) REFERENCES shifts(id) ON DELETE CASCADE, + UNIQUE(employee_id, plan_id, shift_id) ); -- Performance indexes diff --git a/backend/src/models/Employee.ts b/backend/src/models/Employee.ts index 68aa0e1..325b1b6 100644 --- a/backend/src/models/Employee.ts +++ b/backend/src/models/Employee.ts @@ -2,7 +2,8 @@ export interface Employee { id: string; email: string; - name: string; + firstname: string; + lastname: string; role: 'admin' | 'maintenance' | 'user'; employeeType: 'manager' | 'trainee' | 'experienced'; contractType: 'small' | 'large'; @@ -15,7 +16,8 @@ export interface Employee { export interface CreateEmployeeRequest { email: string; password: string; - name: string; + firstname: string; + lastname: string; role: 'admin' | 'maintenance' | 'user'; employeeType: 'manager' | 'trainee' | 'experienced'; contractType: 'small' | 'large'; @@ -23,7 +25,8 @@ export interface CreateEmployeeRequest { } export interface UpdateEmployeeRequest { - name?: string; + firstname?: string; + lastname?: string; role?: 'admin' | 'maintenance' | 'user'; employeeType?: 'manager' | 'trainee' | 'experienced'; contractType?: 'small' | 'large'; @@ -39,8 +42,7 @@ export interface EmployeeAvailability { id: string; employeeId: string; planId: string; - dayOfWeek: number; // 1=Monday, 7=Sunday - timeSlotId: 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 notes?: string; } @@ -72,4 +74,14 @@ export interface ManagerSelfAssignmentRequest { export interface EmployeeWithAvailabilities extends Employee { availabilities: EmployeeAvailability[]; +} + +// Additional types for the new roles system +export interface Role { + role: 'admin' | 'user' | 'maintenance'; +} + +export interface EmployeeRole { + employeeId: string; + role: 'admin' | 'user' | 'maintenance'; } \ No newline at end of file diff --git a/backend/src/models/ShiftPlan.ts b/backend/src/models/ShiftPlan.ts index 50d089d..932a2ea 100644 --- a/backend/src/models/ShiftPlan.ts +++ b/backend/src/models/ShiftPlan.ts @@ -50,16 +50,6 @@ export interface ShiftAssignment { assignedBy: string; } -export interface EmployeeAvailability { - id: string; - employeeId: string; - planId: string; - dayOfWeek: number; - timeSlotId: string; - preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable - notes?: string; -} - // Request/Response DTOs export interface CreateShiftPlanRequest { name: string; @@ -95,10 +85,6 @@ export interface AssignEmployeeRequest { scheduledShiftId: string; } -export interface UpdateAvailabilityRequest { - planId: string; - availabilities: Omit[]; -} export interface UpdateRequiredEmployeesRequest { requiredEmployees: number; diff --git a/backend/src/routes/scheduling.ts b/backend/src/routes/scheduling.ts index b74b058..320e07a 100644 --- a/backend/src/routes/scheduling.ts +++ b/backend/src/routes/scheduling.ts @@ -39,7 +39,7 @@ router.post('/generate-schedule', async (req, res) => { assignments: Object.keys(result.assignments).length, violations: result.violations.length }); - + res.json(result); } catch (error) { console.error('Scheduling failed:', error); diff --git a/backend/src/services/SchedulingService.ts b/backend/src/services/SchedulingService.ts index aafabc4..15c8d6a 100644 --- a/backend/src/services/SchedulingService.ts +++ b/backend/src/services/SchedulingService.ts @@ -84,38 +84,42 @@ export class SchedulingService { } private generateScheduledShiftsFromTemplate(shiftPlan: ShiftPlan): any[] { - const shifts: any[] = []; - - if (!shiftPlan.startDate || !shiftPlan.shifts) { - return shifts; - } - - const startDate = new Date(shiftPlan.startDate); - - // Generate shifts for one week (Monday to Sunday) - for (let dayOffset = 0; dayOffset < 7; dayOffset++) { - 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 dayShifts = shiftPlan.shifts.filter(shift => shift.dayOfWeek === dayOfWeek); - - dayShifts.forEach(shift => { - shifts.push({ - id: `generated_${currentDate.toISOString().split('T')[0]}_${shift.timeSlotId}`, - date: currentDate.toISOString().split('T')[0], - timeSlotId: shift.timeSlotId, - requiredEmployees: shift.requiredEmployees, - minWorkers: 1, - maxWorkers: 2, - isPriority: false - }); - }); - } - - console.log("Created shifts for one week. Amount: ", shifts.length); - + const shifts: any[] = []; + + if (!shiftPlan || !shiftPlan.startDate) { return shifts; + } + + const startDate = new Date(shiftPlan.startDate); + + // Generate shifts for one week (Monday to Sunday) + for (let dayOffset = 0; dayOffset < 7; dayOffset++) { + 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 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}`; + + shifts.push({ + id: shiftId, // This matches what frontend expects + date: currentDate.toISOString().split('T')[0], + timeSlotId: shift.timeSlotId, + requiredEmployees: shift.requiredEmployees, + minWorkers: 1, + maxWorkers: 2, + isPriority: false + }); + + console.log(`✅ Generated shift: ${shiftId} for day ${dayOfWeek}, timeSlot ${shift.timeSlotId}`); + }); + } + + console.log("Created shifts for one week. Amount: ", shifts.length); + return shifts; } private prepareAvailabilities(availabilities: Availability[], shiftPlan: ShiftPlan): any[] { diff --git a/backend/src/workers/scheduler-worker.ts b/backend/src/workers/scheduler-worker.ts index 9a4f8f9..f46bebb 100644 --- a/backend/src/workers/scheduler-worker.ts +++ b/backend/src/workers/scheduler-worker.ts @@ -18,15 +18,16 @@ 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 = activeEmployees.filter(emp => emp.employeeType === 'trainee'); - const experienced = activeEmployees.filter(emp => emp.employeeType === 'experienced'); + const trainees = nonManagerEmployees.filter(emp => emp.employeeType === 'trainee'); + const experienced = nonManagerEmployees.filter(emp => emp.employeeType === 'experienced'); - console.log(`Building model with ${activeEmployees.length} employees, ${shifts.length} shifts`); + console.log(`Building model with ${nonManagerEmployees.length} employees, ${shifts.length} shifts`); console.log(`Available shifts per week: ${shifts.length}`); // 1. Create assignment variables for all possible assignments - activeEmployees.forEach((employee: any) => { + nonManagerEmployees.forEach((employee: any) => { shifts.forEach((shift: any) => { const varName = `assign_${employee.id}_${shift.id}`; model.addVariable(varName, 'bool'); @@ -34,7 +35,7 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void { }); // 2. Availability constraints - activeEmployees.forEach((employee: any) => { + nonManagerEmployees.forEach((employee: any) => { shifts.forEach((shift: any) => { const availability = availabilities.find( (a: any) => a.employeeId === employee.id && a.shiftId === shift.id @@ -53,7 +54,7 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void { // 3. Max 1 shift per day per employee const shiftsByDate = groupShiftsByDate(shifts); - activeEmployees.forEach((employee: any) => { + nonManagerEmployees.forEach((employee: any) => { Object.entries(shiftsByDate).forEach(([date, dayShifts]) => { const dayAssignmentVars = (dayShifts as any[]).map( (shift: any) => `assign_${employee.id}_${shift.id}` @@ -70,7 +71,7 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void { // 4. Shift staffing constraints shifts.forEach((shift: any) => { - const assignmentVars = activeEmployees.map( + const assignmentVars = nonManagerEmployees.map( (emp: any) => `assign_${emp.id}_${shift.id}` ); @@ -115,11 +116,42 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void { }); }); - // 6. Contract type constraints + // 6. Employees who cannot work alone constraint + const employeesWhoCantWorkAlone = nonManagerEmployees.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 => + emp.id !== employee.id && emp.isActive + ); + + if (otherEmployees.length === 0) { + // No other employees available, this employee cannot work this shift + model.addConstraint( + `${employeeVar} == 0`, + `No other employees available for ${employee.name} in shift ${shift.id}` + ); + } else { + const otherEmployeeVars = otherEmployees.map(emp => + `assign_${emp.id}_${shift.id}` + ); + + // Constraint: if this employee works, at least one other must work + model.addConstraint( + `${employeeVar} <= ${otherEmployeeVars.join(' + ')}`, + `${employee.name} cannot work alone in ${shift.id}` + ); + } + }); + }); + + // 7. Contract type constraints const totalShifts = shifts.length; console.log(`Total available shifts: ${totalShifts}`); - activeEmployees.forEach((employee: any) => { + nonManagerEmployees.forEach((employee: any) => { const contractType = employee.contractType || 'large'; // EXACT SHIFTS PER WEEK @@ -146,11 +178,11 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void { } }); - // 7. Objective: Maximize preferred assignments with soft constraints + // 8. Objective: Maximize preferred assignments with soft constraints let objectiveExpression = ''; let softConstraintPenalty = ''; - activeEmployees.forEach((employee: any) => { + nonManagerEmployees.forEach((employee: any) => { shifts.forEach((shift: any) => { const varName = `assign_${employee.id}_${shift.id}`; const availability = availabilities.find( @@ -192,23 +224,13 @@ function groupShiftsByDate(shifts: any[]): Record { function extractAssignmentsFromSolution(solution: any, employees: any[], shifts: any[]): any { const assignments: any = {}; - const employeeAssignments: any = {}; - console.log('=== SOLUTION DEBUG INFO ==='); - console.log('Solution success:', solution.success); - console.log('Raw assignments from Python:', solution.assignments?.length || 0); - console.log('Variables in solution:', Object.keys(solution.variables || {}).length); - - // Initialize assignments object - shifts.forEach((shift: any) => { - assignments[shift.id] = []; - }); - - employees.forEach((employee: any) => { - employeeAssignments[employee.id] = 0; + console.log('🔍 DEBUG: Available shifts with new ID pattern:'); + shifts.forEach(shift => { + console.log(` - ${shift.id} (Day: ${shift.id.split('-')[0]}, TimeSlot: ${shift.id.split('-')[1]})`); }); - // Python-parsed assignments + // Your existing assignment extraction logic... if (solution.assignments && solution.assignments.length > 0) { console.log('Using Python-parsed assignments (cleaner)'); @@ -216,32 +238,28 @@ function extractAssignmentsFromSolution(solution: any, employees: any[], shifts: const shiftId = assignment.shiftId; const employeeId = assignment.employeeId; - if (shiftId && employeeId && assignments[shiftId]) { + if (shiftId && employeeId) { + if (!assignments[shiftId]) { + assignments[shiftId] = []; + } // Check if this assignment already exists to avoid duplicates if (!assignments[shiftId].includes(employeeId)) { assignments[shiftId].push(employeeId); - employeeAssignments[employeeId]++; } } }); - } + } - // Log results - console.log('=== ASSIGNMENT RESULTS ==='); - employees.forEach((employee: any) => { - console.log(` ${employee.name}: ${employeeAssignments[employee.id]} shifts`); + // 🆕 ADD: Enhanced logging with employee names + console.log('🎯 FINAL ASSIGNMENTS WITH EMPLOYEE :'); + Object.entries(assignments).forEach(([shiftId, employeeIds]) => { + const employeeNames = (employeeIds as string[]).map(empId => { + const employee = employees.find(emp => emp.id === empId); + return employee ? employee.id : 'Unknown'; + }); + console.log(` 📅 ${shiftId}: ${employeeNames.join(', ')}`); }); - let totalAssignments = 0; - shifts.forEach((shift: any) => { - const count = assignments[shift.id]?.length || 0; - totalAssignments += count; - console.log(` Shift ${shift.id}: ${count} employees`); - }); - - console.log(`Total assignments: ${totalAssignments}`); - console.log('=========================='); - return assignments; } @@ -277,6 +295,20 @@ function detectViolations(assignments: any, employees: any[], shifts: any[]): st } }); + // Check for employees working alone who shouldn't + shifts.forEach((shift: any) => { + const assignedEmployees = assignments[shift.id] || []; + + if (assignedEmployees.length === 1) { + const singleEmployeeId = assignedEmployees[0]; + const singleEmployee = employeeMap.get(singleEmployeeId); + + if (singleEmployee && !singleEmployee.canWorkAlone) { + violations.push(`EMPLOYEE_ALONE: ${singleEmployee.name} is working alone in shift ${shift.id} but cannot work alone`); + } + } + }); + // Check for multiple shifts per day per employee const shiftsByDate = groupShiftsByDate(shifts); employees.forEach((employee: any) => { @@ -297,6 +329,35 @@ function detectViolations(assignments: any, employees: any[], shifts: any[]): st return violations; } +function assignManagersToShifts(assignments: any, managers: any[], shifts: any[], availabilities: any[]): any { + const managersToAssign = managers.filter(emp => emp.isActive && emp.employeeType === 'manager'); + + console.log(`Assigning ${managersToAssign.length} managers to shifts based on availability=1`); + + managersToAssign.forEach((manager: any) => { + shifts.forEach((shift: any) => { + const availability = availabilities.find( + (a: any) => a.employeeId === manager.id && a.shiftId === shift.id + ); + + // Assign manager if they have availability=1 (preferred) + if (availability?.preferenceLevel === 1) { + if (!assignments[shift.id]) { + assignments[shift.id] = []; + } + + // 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)`); + } + } + }); + }); + + return assignments; +} + async function runScheduling() { const data: WorkerData = workerData; const startTime = Date.now(); @@ -313,6 +374,8 @@ async function runScheduling() { } console.log(`Optimizing ${data.shifts.length} shifts for ${data.employees.length} employees`); + + const nonManagerEmployees = data.employees.filter(emp => emp.isActive && emp.employeeType !== 'manager'); const model = new CPModel(); buildSchedulingModel(model, data); @@ -340,26 +403,38 @@ async function runScheduling() { ]; if (solution.success) { - // Extract assignments from solution - assignments = extractAssignmentsFromSolution(solution, data.employees, data.shifts); + // Extract assignments from solution (non-managers only) + assignments = extractAssignmentsFromSolution(solution, nonManagerEmployees, data.shifts); - // Only detect violations if we actually have assignments + // 🆕 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 if (Object.keys(assignments).length > 0) { - violations = detectViolations(assignments, data.employees, data.shifts); + violations = detectViolations(assignments, nonManagerEmployees, data.shifts); } else { violations.push('NO_ASSIGNMENTS: Solver reported success but produced no assignments'); - console.warn('Solver reported success but produced no assignments. Solution:', solution); } - // Add assignment statistics + // Update resolution report + if (violations.length === 0) { + resolutionReport.push('✅ No constraint violations detected for non-manager employees'); + } else { + resolutionReport.push(`⚠️ Found ${violations.length} violations for non-manager employees:`); + violations.forEach(violation => { + resolutionReport.push(` - ${violation}`); + }); + } + + // Add assignment statistics (including managers) const totalAssignments = Object.values(assignments).reduce((sum: number, shiftAssignments: any) => sum + shiftAssignments.length, 0 ); - resolutionReport.push(`📊 Total assignments: ${totalAssignments}`); + resolutionReport.push(`📊 Total assignments: ${totalAssignments} (including managers)`); } else { - violations.push('SCHEDULING_FAILED: No feasible solution found'); - resolutionReport.push('❌ No feasible solution could be found'); + violations.push('SCHEDULING_FAILED: No feasible solution found for non-manager employees'); + resolutionReport.push('❌ No feasible solution could be found for non-manager employees'); } parentPort?.postMessage({ diff --git a/frontend/src/pages/Employees/components/AvailabilityManager.tsx b/frontend/src/pages/Employees/components/AvailabilityManager.tsx index 0761a86..7ca78a9 100644 --- a/frontend/src/pages/Employees/components/AvailabilityManager.tsx +++ b/frontend/src/pages/Employees/components/AvailabilityManager.tsx @@ -12,9 +12,11 @@ interface AvailabilityManagerProps { } // Local interface extensions -interface ExtendedTimeSlot extends TimeSlot { +interface ExtendedShift extends Shift { + timeSlotName?: string; + startTime?: string; + endTime?: string; displayName?: string; - source?: string; } interface Availability extends EmployeeAvailability { @@ -68,20 +70,42 @@ const AvailabilityManager: React.FC = ({ return time.substring(0, 5); }; - // Create a data structure that maps days to their actual time slots + // Create a data structure that maps days to their shifts with time slot info const getTimetableData = () => { if (!selectedPlan || !selectedPlan.shifts || !selectedPlan.timeSlots) { - return { days: [], timeSlotsByDay: {} }; + return { days: [], shiftsByDay: {} }; } - // Group shifts by day + // Create a map for quick time slot lookups + const timeSlotMap = new Map(selectedPlan.timeSlots.map(ts => [ts.id, ts])); + + // Group shifts by day and enhance with time slot info const shiftsByDay = selectedPlan.shifts.reduce((acc, shift) => { if (!acc[shift.dayOfWeek]) { acc[shift.dayOfWeek] = []; } - acc[shift.dayOfWeek].push(shift); + + const timeSlot = timeSlotMap.get(shift.timeSlotId); + const enhancedShift: ExtendedShift = { + ...shift, + timeSlotName: timeSlot?.name, + startTime: timeSlot?.startTime, + endTime: timeSlot?.endTime, + displayName: timeSlot ? `${timeSlot.name} (${formatTime(timeSlot.startTime)}-${formatTime(timeSlot.endTime)})` : shift.id + }; + + acc[shift.dayOfWeek].push(enhancedShift); return acc; - }, {} as Record); + }, {} as Record); + + // Sort shifts within each day by start time + Object.keys(shiftsByDay).forEach(day => { + shiftsByDay[parseInt(day)].sort((a, b) => { + const timeA = a.startTime || ''; + const timeB = b.startTime || ''; + return timeA.localeCompare(timeB); + }); + }); // Get unique days that have shifts const days = Array.from(new Set(selectedPlan.shifts.map(shift => shift.dayOfWeek))) @@ -90,24 +114,7 @@ const AvailabilityManager: React.FC = ({ return daysOfWeek.find(day => day.id === dayId) || { id: dayId, name: `Tag ${dayId}` }; }); - // For each day, get the time slots that actually have shifts - const timeSlotsByDay: Record = {}; - - days.forEach(day => { - const shiftsForDay = shiftsByDay[day.id] || []; - const timeSlotIdsForDay = new Set(shiftsForDay.map(shift => shift.timeSlotId)); - - timeSlotsByDay[day.id] = selectedPlan.timeSlots - .filter(timeSlot => timeSlotIdsForDay.has(timeSlot.id)) - .map(timeSlot => ({ - ...timeSlot, - displayName: `${timeSlot.name} (${formatTime(timeSlot.startTime)}-${formatTime(timeSlot.endTime)})`, - source: `Plan: ${selectedPlan.name}` - })) - .sort((a, b) => a.startTime.localeCompare(b.startTime)); - }); - - return { days, timeSlotsByDay }; + return { days, shiftsByDay }; }; const loadData = async () => { @@ -137,7 +144,6 @@ const AvailabilityManager: React.FC = ({ // 3. Select first plan with actual shifts if available if (plans.length > 0) { - // Find a plan that actually has shifts and time slots const planWithShifts = plans.find(plan => plan.shifts && plan.shifts.length > 0 && plan.timeSlots && plan.timeSlots.length > 0 @@ -146,7 +152,6 @@ const AvailabilityManager: React.FC = ({ setSelectedPlanId(planWithShifts.id); console.log('✅ SCHICHTPLAN AUSGEWÄHLT:', planWithShifts.name); - // Load the selected plan to get its actual used time slots and days await loadSelectedPlan(); } @@ -170,8 +175,7 @@ const AvailabilityManager: React.FC = ({ name: plan.name, timeSlotsCount: plan.timeSlots?.length || 0, shiftsCount: plan.shifts?.length || 0, - usedDays: Array.from(new Set(plan.shifts?.map(s => s.dayOfWeek) || [])).sort(), - usedTimeSlots: Array.from(new Set(plan.shifts?.map(s => s.timeSlotId) || [])).length + usedDays: Array.from(new Set(plan.shifts?.map(s => s.dayOfWeek) || [])).sort() }); } catch (err: any) { console.error('❌ FEHLER BEIM LADEN DES SCHICHTPLANS:', err); @@ -179,14 +183,11 @@ const AvailabilityManager: React.FC = ({ } }; - const handleAvailabilityLevelChange = (dayId: number, timeSlotId: string, level: AvailabilityLevel) => { - console.log(`🔄 ÄNDERE VERFÜGBARKEIT: Tag ${dayId}, Slot ${timeSlotId}, Level ${level}`); + const handleAvailabilityLevelChange = (shiftId: string, level: AvailabilityLevel) => { + console.log(`🔄 ÄNDERE VERFÜGBARKEIT: Shift ${shiftId}, Level ${level}`); setAvailabilities(prev => { - const existingIndex = prev.findIndex(avail => - avail.dayOfWeek === dayId && - avail.timeSlotId === timeSlotId - ); + const existingIndex = prev.findIndex(avail => avail.shiftId === shiftId); if (existingIndex >= 0) { // Update existing availability @@ -198,13 +199,20 @@ const AvailabilityManager: React.FC = ({ }; return updated; } else { - // Create new availability + // Create new availability using shiftId directly + const shift = selectedPlan?.shifts?.find(s => s.id === shiftId); + if (!shift) { + console.error('❌ Shift nicht gefunden:', shiftId); + return prev; + } + const newAvailability: Availability = { - id: `temp-${dayId}-${timeSlotId}-${Date.now()}`, + id: `temp-${shiftId}-${Date.now()}`, employeeId: employee.id, - planId: selectedPlanId || '', - dayOfWeek: dayId, - timeSlotId: timeSlotId, + planId: selectedPlanId, + shiftId: shiftId, // Use shiftId directly + dayOfWeek: shift.dayOfWeek, // Keep for backward compatibility if needed + timeSlotId: shift.timeSlotId, // Keep for backward compatibility if needed preferenceLevel: level, isAvailable: level !== 3 }; @@ -213,21 +221,16 @@ const AvailabilityManager: React.FC = ({ }); }; - const getAvailabilityForDayAndSlot = (dayId: number, timeSlotId: string): AvailabilityLevel => { - const availability = availabilities.find(avail => - avail.dayOfWeek === dayId && - avail.timeSlotId === timeSlotId - ); - - const result = availability?.preferenceLevel || 3; - return result; + const getAvailabilityForShift = (shiftId: string): AvailabilityLevel => { + const availability = availabilities.find(avail => avail.shiftId === shiftId); + return availability?.preferenceLevel || 3; }; - // Update the timetable rendering to use the new data structure + // Update the timetable rendering to use shifts directly const renderTimetable = () => { - const { days, timeSlotsByDay } = getTimetableData(); + const { days, shiftsByDay } = getTimetableData(); - if (days.length === 0 || Object.keys(timeSlotsByDay).length === 0) { + if (days.length === 0 || Object.keys(shiftsByDay).length === 0) { return (
= ({ ); } - // Get all unique time slots across all days for row headers - const allTimeSlotIds = new Set(); + // Get all unique shifts across all days for row headers + const allShifts: ExtendedShift[] = []; + const shiftIds = new Set(); + days.forEach(day => { - timeSlotsByDay[day.id]?.forEach(timeSlot => { - allTimeSlotIds.add(timeSlot.id); + shiftsByDay[day.id]?.forEach(shift => { + if (!shiftIds.has(shift.id)) { + shiftIds.add(shift.id); + allShifts.push(shift); + } }); }); - const allTimeSlots = Array.from(allTimeSlotIds) - .map(id => selectedPlan?.timeSlots?.find(ts => ts.id === id)) - .filter(Boolean) - .map(timeSlot => ({ - ...timeSlot!, - displayName: `${timeSlot!.name} (${formatTime(timeSlot!.startTime)}-${formatTime(timeSlot!.endTime)})`, - source: `Plan: ${selectedPlan!.name}` - })) - .sort((a, b) => a.startTime.localeCompare(b.startTime)); + // Sort shifts by time slot start time + allShifts.sort((a, b) => { + const timeA = a.startTime || ''; + const timeB = b.startTime || ''; + return timeA.localeCompare(timeB); + }); return (
= ({ }}> Verfügbarkeit definieren
- {allTimeSlots.length} Schichttypen • {days.length} Tage • Nur tatsächlich im Plan verwendete Schichten + {allShifts.length} Schichten • {days.length} Tage • Direkte Shift-ID Zuordnung
@@ -294,7 +299,7 @@ const AvailabilityManager: React.FC = ({ textAlign: 'left', border: '1px solid #dee2e6', fontWeight: 'bold', - minWidth: '100px' + minWidth: '150px' }}> Schicht (Zeit) @@ -312,9 +317,9 @@ const AvailabilityManager: React.FC = ({ - {allTimeSlots.map((timeSlot, timeIndex) => ( - ( + = ({ fontWeight: '500', backgroundColor: '#f8f9fa' }}> - {timeSlot.displayName} + {shift.displayName}
- {timeSlot.source} + Shift-ID: {shift.id.substring(0, 8)}...
{days.map(weekday => { - // Check if this time slot exists for this day - const timeSlotForDay = timeSlotsByDay[weekday.id]?.find(ts => ts.id === timeSlot.id); + // Check if this shift exists for this day + const shiftForDay = shiftsByDay[weekday.id]?.find(s => s.id === shift.id); - if (!timeSlotForDay) { + if (!shiftForDay) { return ( = ({ ); } - const currentLevel = getAvailabilityForDayAndSlot(weekday.id, timeSlot.id); + const currentLevel = getAvailabilityForShift(shift.id); const levelConfig = availabilityLevels.find(l => l.level === currentLevel); return ( @@ -360,7 +365,7 @@ const AvailabilityManager: React.FC = ({ value={currentLevel} onChange={(e) => { const newLevel = parseInt(e.target.value) as AvailabilityLevel; - handleAvailabilityLevelChange(weekday.id, timeSlot.id, newLevel); + handleAvailabilityLevelChange(shift.id, newLevel); }} style={{ padding: '8px 12px', @@ -410,12 +415,12 @@ const AvailabilityManager: React.FC = ({ return; } - const { days, timeSlotsByDay } = getTimetableData(); + const { days, shiftsByDay } = getTimetableData(); // Filter availabilities to only include those with actual shifts const validAvailabilities = availabilities.filter(avail => { - const timeSlotsForDay = timeSlotsByDay[avail.dayOfWeek] || []; - return timeSlotsForDay.some(slot => slot.id === avail.timeSlotId); + // Check if this shiftId exists in the current plan + return selectedPlan?.shifts?.some(shift => shift.id === avail.shiftId); }); if (validAvailabilities.length === 0) { @@ -423,13 +428,14 @@ const AvailabilityManager: React.FC = ({ return; } - // Convert to the format expected by the API + // Convert to the format expected by the API - using shiftId directly const requestData = { planId: selectedPlanId, availabilities: validAvailabilities.map(avail => ({ planId: selectedPlanId, - dayOfWeek: avail.dayOfWeek, - timeSlotId: avail.timeSlotId, + shiftId: avail.shiftId, // Use shiftId directly + dayOfWeek: avail.dayOfWeek, // Keep for backward compatibility + timeSlotId: avail.timeSlotId, // Keep for backward compatibility preferenceLevel: avail.preferenceLevel, notes: avail.notes })) @@ -457,14 +463,14 @@ const AvailabilityManager: React.FC = ({ ); } - const { days, timeSlotsByDay } = getTimetableData(); - const allTimeSlotIds = new Set(); + const { days, shiftsByDay } = getTimetableData(); + const allShiftIds = new Set(); days.forEach(day => { - timeSlotsByDay[day.id]?.forEach(timeSlot => { - allTimeSlotIds.add(timeSlot.id); + shiftsByDay[day.id]?.forEach(shift => { + allShiftIds.add(shift.id); }); }); - const timeSlotsCount = allTimeSlotIds.size; + const shiftsCount = allShiftIds.size; return (
= ({ borderBottom: '2px solid #f0f0f0', paddingBottom: '15px' }}> - 📅 Verfügbarkeit verwalten + 📅 Verfügbarkeit verwalten (Shift-ID basiert) {/* Debug-Info */}

- {timeSlotsCount === 0 ? '❌ PROBLEM: Keine Zeit-Slots gefunden' : '✅ Plan-Daten geladen'} + {shiftsCount === 0 ? '❌ PROBLEM: Keine Shifts gefunden' : '✅ Plan-Daten geladen'}

Ausgewählter Plan: {selectedPlan?.name || 'Keiner'}
-
Verwendete Zeit-Slots: {timeSlotsCount}
+
Einzigartige Shifts: {shiftsCount}
Verwendete Tage: {days.length} ({days.map(d => d.name).join(', ')})
Gesamte Shifts im Plan: {selectedPlan?.shifts?.length || 0}
+
Methode: Direkte Shift-ID Zuordnung
- - {selectedPlan && selectedPlan.shifts && ( -
- Shifts im Plan: - {selectedPlan.shifts.map((shift, index) => ( -
- • Tag {shift.dayOfWeek}: {shift.timeSlotId} ({shift.requiredEmployees} Personen) -
- ))} -
- )}
{/* Employee Info */} @@ -524,7 +520,7 @@ const AvailabilityManager: React.FC = ({ {employee.name}

- Legen Sie die Verfügbarkeit für {employee.name} fest. + Legen Sie die Verfügbarkeit für {employee.name} fest (basierend auf Shift-IDs).

@@ -608,7 +604,7 @@ const AvailabilityManager: React.FC = ({ {shiftPlans.map(plan => ( ))} @@ -617,7 +613,7 @@ const AvailabilityManager: React.FC = ({ {selectedPlan && (
Plan: {selectedPlan.name}
-
Zeit-Slots: {selectedPlan.timeSlots?.length || 0}
+
Shifts: {selectedPlan.shifts?.length || 0}
Status: {selectedPlan.status}
)} @@ -651,14 +647,14 @@ const AvailabilityManager: React.FC = ({