diff --git a/backend/package.json b/backend/package.json index 53a3f40..ea9eb4b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -3,9 +3,10 @@ "version": "1.0.0", "type": "module", "scripts": { - "dev": "npx tsx src/server.ts", + "dev": "npm run build && npx tsx src/server.ts", "build": "tsc", - "start": "node dist/server.js" + "start": "node dist/server.js", + "prestart": "npm run build" }, "dependencies": { "@types/bcrypt": "^6.0.0", @@ -25,6 +26,7 @@ "@types/jsonwebtoken": "^9.0.2", "@types/uuid": "^9.0.2", "ts-node": "^10.9.0", - "typescript": "^5.0.0" + "typescript": "^5.0.0", + "tsx": "^4.0.0" } } diff --git a/backend/src/python-scripts/scheduling_solver.py b/backend/src/python-scripts/scheduling_solver.py index 70c61f4..b544173 100644 --- a/backend/src/python-scripts/scheduling_solver.py +++ b/backend/src/python-scripts/scheduling_solver.py @@ -153,6 +153,54 @@ class ScheduleOptimizer: # If any trainee is assigned, at least one experienced must be assigned for trainee_var in trainee_vars: self.model.Add(sum(experienced_vars) >= 1).OnlyEnforceIf(trainee_var) + + # Prevent shifts with only one worker unless that worker can work alone + for shift in shifts: + shift_id = shift['id'] + + # Create a variable for "this shift has exactly one worker" + shift_has_one_worker = self.model.NewBoolVar(f'shift_{shift_id}_one_worker') + shift_assignment_count = sum(assignments[emp['id']].get(shift_id, 0) + for emp in all_employees + if shift_id in assignments.get(emp['id'], {})) + + # Link the count to the boolean variable + self.model.Add(shift_assignment_count == 1).OnlyEnforceIf(shift_has_one_worker) + self.model.Add(shift_assignment_count != 1).OnlyEnforceIf(shift_has_one_worker.Not()) + + # Create a variable for "this shift has someone who cannot work alone" + has_cannot_work_alone = self.model.NewBoolVar(f'shift_{shift_id}_cannot_work_alone') + cannot_work_alone_vars = [] + for employee in all_employees: + employee_id = employee['id'] + if shift_id in assignments.get(employee_id, {}): + is_experienced = employee.get('employeeType') == 'experienced' + can_work_alone = employee.get('canWorkAlone', False) + if not (is_experienced and can_work_alone): + cannot_work_alone_vars.append(assignments[employee_id][shift_id]) + + if cannot_work_alone_vars: + self.model.Add(sum(cannot_work_alone_vars) >= 1).OnlyEnforceIf(has_cannot_work_alone) + self.model.Add(sum(cannot_work_alone_vars) == 0).OnlyEnforceIf(has_cannot_work_alone.Not()) + + # Constraint: If shift has one worker, it cannot have someone who cannot work alone + self.model.AddImplication(shift_has_one_worker, has_cannot_work_alone.Not()) + + # Exact shifts per contract type + for employee in all_employees: + employee_id = employee['id'] + contract_type = employee.get('contractType', 'large') + exact_shifts = 5 if contract_type == 'small' else 10 + + shift_vars = [] + for shift in shifts: + shift_id = shift['id'] + if shift_id in assignments.get(employee_id, {}): + shift_vars.append(assignments[employee_id][shift_id]) + + if shift_vars: + self.model.Add(sum(shift_vars) == exact_shifts) + self.resolution_report.append(f"📋 Employee {employee_id}: {exact_shifts} shifts ({contract_type} contract)") # Constraint: Contract hours limits for employee in all_employees: @@ -184,7 +232,7 @@ class ScheduleOptimizer: objective_terms.append(assignments[employee_id][shift_id] * 5) # Penalize unavailable assignments (shouldn't happen due to constraints) else: - objective_terms.append(assignments[employee_id][shift_id] * -100) + objective_terms.append(assignments[employee_id][shift_id] * -1000) self.model.Maximize(sum(objective_terms)) @@ -204,6 +252,40 @@ class ScheduleOptimizer: else: return {} + def groupShiftsByDay(self, shifts): + """Group shifts by date""" + shifts_by_day = defaultdict(list) + for shift in shifts: + date = shift.get('date', 'unknown') + shifts_by_day[date].append(shift) + return shifts_by_day + + def getAvailability(self, employee_id, shift_id, availabilities): + """Get availability level for employee and shift""" + for avail in availabilities: + if avail.get('employeeId') == employee_id and avail.get('shiftId') == shift_id: + return avail.get('availability', 2) # Default to available + return 2 # Default to available if no preference specified + + def extractAssignments(self, assignments, employees, shifts): + """Extract assignments from solution""" + result_assignments = {} + + # Initialize with empty lists + for shift in shifts: + result_assignments[shift['id']] = [] + + # Fill with assigned employees + for employee in employees: + employee_id = employee['id'] + for shift in shifts: + shift_id = shift['id'] + if (shift_id in assignments.get(employee_id, {}) and + self.solver.Value(assignments[employee_id][shift_id]) == 1): + result_assignments[shift_id].append(employee_id) + + return result_assignments + def formatAssignments(self, assignments): """Format assignments for frontend consumption""" formatted = {} @@ -217,8 +299,102 @@ class ScheduleOptimizer: return shiftPlan['shifts'] return [] - # ... (keep the other helper methods from your original code) - # detectAllViolations, fixViolations, createTraineePartners, etc. + def filterEmployees(self, employees, condition): + """Filter employees based on condition""" + if callable(condition): + return [emp for emp in employees if condition(emp)] + elif isinstance(condition, str): + return [emp for emp in employees if emp.get('employeeType') == condition] + return [] + + def getFirstWeekShifts(self, shifts): + """Get shifts for the first week (simplified)""" + # For simplicity, return all shifts or implement week filtering logic + return shifts + + def createTraineePartners(self, workers, availabilities): + """Create trainee-experienced partnerships based on availability""" + # Simplified implementation - return empty dict for now + return {} + + def detectAllViolations(self, assignments, employees, availabilities, constraints, shifts): + """Detect all constraint violations""" + violations = [] + employee_map = {emp['id']: emp for emp in employees} + + # Check for understaffed shifts + for shift in shifts: + shift_id = shift['id'] + assigned_count = len(assignments.get(shift_id, [])) + min_required = shift.get('minWorkers', 1) + + if assigned_count < min_required: + violations.append(f"UNDERSTAFFED: Shift {shift_id} has {assigned_count} employees but requires {min_required}") + + # Check for trainee supervision + for shift in shifts: + shift_id = shift['id'] + assigned_employees = assignments.get(shift_id, []) + has_trainee = any(employee_map.get(emp_id, {}).get('employeeType') == 'trainee' for emp_id in assigned_employees) + has_experienced = any(employee_map.get(emp_id, {}).get('employeeType') == 'experienced' for emp_id in assigned_employees) + + if has_trainee and not has_experienced: + violations.append(f"TRAINEE_UNSUPERVISED: Shift {shift_id} has trainee but no experienced employee") + + # Check for multiple shifts per day + shifts_by_day = self.groupShiftsByDay(shifts) + for employee in employees: + employee_id = employee['id'] + for date, day_shifts in shifts_by_day.items(): + shifts_assigned = 0 + for shift in day_shifts: + if employee_id in assignments.get(shift['id'], []): + shifts_assigned += 1 + + if shifts_assigned > 1: + violations.append(f"MULTIPLE_SHIFTS: {employee.get('name', employee_id)} has {shifts_assigned} shifts on {date}") + + # Check contract type constraints + for employee in employees: + employee_id = employee['id'] + contract_type = employee.get('contractType', 'large') + expected_shifts = 5 if contract_type == 'small' else 10 + + total_shifts = 0 + for shift_assignments in assignments.values(): + if employee_id in shift_assignments: + total_shifts += 1 + + if total_shifts != expected_shifts: + violations.append(f"CONTRACT_VIOLATION: {employee.get('name', employee_id)} has {total_shifts} shifts but should have exactly {expected_shifts} ({contract_type} contract)") + + return violations + + def fixViolations(self, assignments, employees, availabilities, constraints, shifts, maxIterations=20): + """Fix violations in assignments""" + # Simplified implementation - return original assignments + # In a real implementation, this would iteratively fix violations + return assignments + + def assignManagersToPriority(self, managers, assignments, availabilities, shifts): + """Assign managers to priority shifts""" + # Simplified implementation - return original assignments + return assignments + + def countCriticalViolations(self, violations): + """Count critical violations""" + critical_keywords = ['UNDERSTAFFED', 'TRAINEE_UNSUPERVISED', 'CONTRACT_VIOLATION'] + return sum(1 for violation in violations if any(keyword in violation for keyword in critical_keywords)) + + def errorResult(self, error): + """Return error result""" + return { + 'assignments': {}, + 'violations': [f'Error: {str(error)}'], + 'success': False, + 'resolution_report': [f'Critical error: {str(error)}'], + 'error': str(error) + } # Main execution for Python script if __name__ == "__main__": diff --git a/backend/src/server.ts b/backend/src/server.ts index 4c3080e..39e6cba 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -26,10 +26,6 @@ app.use('/api/shift-plans', shiftPlanRoutes); app.use('/api/scheduled-shifts', scheduledShifts); app.use('/api/scheduling', schedulingRoutes); -app.listen(PORT, () => { - console.log(`Scheduling server running on port ${PORT}`); -}); - // Error handling middleware should come after routes app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => { console.error('Unhandled error:', err); diff --git a/backend/src/services/SchedulingService.ts b/backend/src/services/SchedulingService.ts index a448e82..cd658ec 100644 --- a/backend/src/services/SchedulingService.ts +++ b/backend/src/services/SchedulingService.ts @@ -164,4 +164,4 @@ export class SchedulingService { }, {} as any) }; } -} \ No newline at end of file +} diff --git a/backend/src/workers/schedular-worker.ts b/backend/src/workers/scheduler-worker.ts similarity index 91% rename from backend/src/workers/schedular-worker.ts rename to backend/src/workers/scheduler-worker.ts index 575b117..054adcf 100644 --- a/backend/src/workers/schedular-worker.ts +++ b/backend/src/workers/scheduler-worker.ts @@ -3,7 +3,7 @@ import { parentPort, workerData } from 'worker_threads'; import { CPModel, CPSolver } from './cp-sat-wrapper.js'; import { ShiftPlan, Shift } from '../models/ShiftPlan.js'; import { Employee, EmployeeAvailability } from '../models/Employee.js'; -import { Availability, Constraint } from '../models/scheduling.js'; +import { Availability, Constraint, Violation, SolverOptions, Solution, Assignment } from '../models/scheduling.js'; interface WorkerData { shiftPlan: ShiftPlan; @@ -13,6 +13,7 @@ interface WorkerData { shifts: Shift[]; } + function buildSchedulingModel(model: CPModel, data: WorkerData): void { const { employees, shifts, availabilities, constraints } = data; @@ -93,26 +94,36 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void { }); }); - // 6. Contract Hours Constraints + // 6. Employee cant workalone employees.forEach((employee: any) => { - const contractHours = employee.contractType === 'small' ? 20 : 40; - const shiftHoursVars: string[] = []; + if (employee.employeeType === 'experienced' && employee.canWorkAlone) { + shifts.forEach((shift: any) => { + const varName = `assign_${employee.id}_${shift.id}`; + // Allow this employee to work alone (no additional constraint needed) + // This is more about not preventing single assignments + }); + } + }); + + // 7. Contract Type Shifts Constraint + employees.forEach((employee: any) => { + const exactShiftsPerWeek = employee.contractType === 'small' ? 5 : 10; // Example: exactly 5 shifts for small, 10 for large + const shiftVars: string[] = []; shifts.forEach((shift: any) => { - const shiftHours = 8; // Assuming 8 hours per shift const varName = `assign_${employee.id}_${shift.id}`; - shiftHoursVars.push(`${shiftHours} * ${varName}`); + shiftVars.push(varName); }); - if (shiftHoursVars.length > 0) { + if (shiftVars.length > 0) { model.addConstraint( - `${shiftHoursVars.join(' + ')} <= ${contractHours}`, - `Contract hours limit for ${employee.name}` + `${shiftVars.join(' + ')} == ${exactShiftsPerWeek}`, + `Exact shifts per week for ${employee.name} (${employee.contractType} contract)` ); } }); - // 7. Ziel: Verfügbarkeits-Score maximieren + // 8. Ziel: Verfügbarkeits-Score maximieren let objectiveExpression = ''; employees.forEach((employee: any) => { shifts.forEach((shift: any) => { diff --git a/frontend/src/services/shiftAssignmentService.ts b/frontend/src/services/shiftAssignmentService.ts index 9016583..7df5433 100644 --- a/frontend/src/services/shiftAssignmentService.ts +++ b/frontend/src/services/shiftAssignmentService.ts @@ -2,10 +2,9 @@ import { ShiftPlan, ScheduledShift } from '../models/ShiftPlan'; import { Employee, EmployeeAvailability } from '../models/Employee'; import { authService } from './authService'; -//import { IntelligentShiftScheduler, AssignmentResult, WeeklyPattern } from './scheduling/useScheduling'; -import { AssignmentResult } from '../models/scheduling'; +import { AssignmentResult, ScheduleRequest } from '../models/scheduling'; -const API_BASE_URL = 'http://localhost:3002/api/scheduled-shifts'; +const API_BASE_URL = 'http://localhost:3002/api'; @@ -23,7 +22,7 @@ export class ShiftAssignmentService { try { //console.log('🔄 Updating scheduled shift via API:', { id, updates }); - const response = await fetch(`${API_BASE_URL}/${id}`, { + const response = await fetch(`${API_BASE_URL}/scheduled-shifts/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', @@ -72,7 +71,7 @@ export class ShiftAssignmentService { async getScheduledShift(id: string): Promise { try { - const response = await fetch(`${API_BASE_URL}/${id}`, { + const response = await fetch(`${API_BASE_URL}/scheduled-shifts/${id}`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } @@ -103,7 +102,7 @@ export class ShiftAssignmentService { // New method to get all scheduled shifts for a plan async getScheduledShiftsForPlan(planId: string): Promise { try { - const response = await fetch(`${API_BASE_URL}/plan/${planId}`, { + const response = await fetch(`${API_BASE_URL}/scheduled-shifts/plan/${planId}`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } @@ -134,23 +133,43 @@ export class ShiftAssignmentService { } } + private async callSchedulingAPI(request: ScheduleRequest): Promise { + const response = await fetch(`${API_BASE_URL}/scheduling/generate-schedule`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...authService.getAuthHeaders() + }, + body: JSON.stringify(request) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Scheduling failed'); + } + + return response.json(); + } + async assignShifts( shiftPlan: ShiftPlan, employees: Employee[], availabilities: EmployeeAvailability[], constraints: any = {} ): Promise { + console.log('🧠 Starting scheduling optimization...'); - console.log('🧠 Starting intelligent scheduling for FIRST WEEK ONLY...'); - - - - return { - assignments: , - violations: , - success: , - resolutionReport: , + const scheduleRequest: ScheduleRequest = { + shiftPlan, + employees, + availabilities: availabilities.map(avail => ({ + ...avail, + preferenceLevel: avail.preferenceLevel as 1 | 2 | 3 + })), + constraints: Array.isArray(constraints) ? constraints : [] }; + + return await this.callSchedulingAPI(scheduleRequest); } }