backend working

This commit is contained in:
2025-10-19 00:15:17 +02:00
parent 372b9b2f20
commit 970651c021
11 changed files with 1060 additions and 162 deletions

View File

@@ -1,7 +1,33 @@
// backend/src/types/scheduling.ts // backend/src/models/scheduling.ts
import { Employee } from './Employee.js'; import { Employee } from './Employee.js';
import { ShiftPlan } from './ShiftPlan.js'; import { ShiftPlan } from './ShiftPlan.js';
// Add the missing type definitions
export interface Availability {
id: string;
employeeId: string;
planId: string;
dayOfWeek: number; // 1=Monday, 7=Sunday
timeSlotId: string;
preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable
notes?: string;
}
export interface Constraint {
type: string;
severity: 'hard' | 'soft';
parameters: {
maxShiftsPerDay?: number;
minEmployeesPerShift?: number;
maxEmployeesPerShift?: number;
enforceTraineeSupervision?: boolean;
contractHoursLimit?: boolean;
maxHoursPerWeek?: number;
[key: string]: any;
};
weight?: number; // For soft constraints
}
export interface ScheduleRequest { export interface ScheduleRequest {
shiftPlan: ShiftPlan; shiftPlan: ShiftPlan;
employees: Employee[]; employees: Employee[];
@@ -30,6 +56,7 @@ export interface Violation {
message: string; message: string;
involvedEmployees?: string[]; involvedEmployees?: string[];
shiftId?: string; shiftId?: string;
details?: any;
} }
export interface SolverOptions { export interface SolverOptions {
@@ -48,4 +75,48 @@ export interface Solution {
variablesCreated: number; variablesCreated: number;
optimal: boolean; optimal: boolean;
}; };
}
// Additional helper types for the scheduling system
export interface SchedulingConfig {
maxRepairAttempts: number;
targetEmployeesPerShift: number;
enforceNoTraineeAlone: boolean;
enforceExperiencedWithChef: boolean;
preferEmployeePreferences: boolean;
}
export interface AssignmentResult {
assignments: { [shiftId: string]: string[] }; // shiftId -> employeeIds
violations: string[];
resolutionReport: string[];
success: boolean;
statistics?: {
totalAssignments: number;
preferredAssignments: number;
availableAssignments: number;
coverageRate: number;
violationCount: number;
};
}
export interface EmployeeAvailabilitySummary {
employeeId: string;
employeeName: string;
preferredSlots: number;
availableSlots: number;
unavailableSlots: number;
totalSlots: number;
}
export interface ShiftRequirement {
shiftId: string;
timeSlotId: string;
dayOfWeek: number;
date?: string;
requiredEmployees: number;
minEmployees: number;
maxEmployees: number;
assignedEmployees: string[];
isPriority: boolean;
} }

View File

@@ -0,0 +1,247 @@
# backend/python-scripts/scheduling_solver.py
from ortools.sat.python import cp_model
import json
import sys
from typing import List, Dict, Any, Tuple
from collections import defaultdict
import math
from datetime import datetime, timedelta
class ScheduleOptimizer:
def __init__(self):
self.model = cp_model.CpModel()
self.solver = cp_model.CpSolver()
self.assignments = {}
self.violations = []
self.resolution_report = []
# Solver parameters for better performance
self.solver.parameters.max_time_in_seconds = 100 # 100 seconds timeout
self.solver.parameters.num_search_workers = 8
self.solver.parameters.log_search_progress = True
def generateOptimalSchedule(self, shiftPlan, employees, availabilities, constraints):
"""
Hauptalgorithmus für die Schichtplanoptimierung
"""
try:
self.resolution_report.append("🚀 Starting scheduling optimization...")
# Prepare data
scheduled_shifts = self.prepareShifts(shiftPlan)
first_week_shifts = self.getFirstWeekShifts(scheduled_shifts)
# Separate employee types
managers = self.filterEmployees(employees, 'manager')
workers = self.filterEmployees(employees, lambda e: e.get('employeeType') != 'manager')
experienced = self.filterEmployees(employees, lambda e: e.get('role') != 'admin' and e.get('employeeType') == 'experienced')
trainees = self.filterEmployees(employees, lambda e: e.get('role') != 'admin' and e.get('employeeType') == 'trainee')
self.resolution_report.append(f"📊 Employee counts: {len(managers)} managers, {len(experienced)} experienced, {len(trainees)} trainees")
# Create experienced x trainees hashmap
hashmap_experienced_trainees = self.createTraineePartners(workers, availabilities)
# Optimize distribution
optimized_assignments = self.optimizeDistribution(
trainees, experienced, managers, availabilities, constraints, first_week_shifts
)
# Validation
final_violations = self.detectAllViolations(
optimized_assignments, employees, availabilities, constraints, first_week_shifts
)
self.violations.extend(final_violations)
# Fix violations
fixed_assignments = self.fixViolations(
optimized_assignments, employees, availabilities, constraints, first_week_shifts, maxIterations=20
)
# Add managers to priority shifts
final_assignments = self.assignManagersToPriority(
managers, fixed_assignments, availabilities, first_week_shifts
)
# Final validation
final_violations = self.detectAllViolations(
final_assignments, employees, availabilities, constraints, first_week_shifts
)
success = (self.countCriticalViolations(final_violations) == 0)
self.resolution_report.append(f"✅ Scheduling completed: {success}")
return {
'assignments': self.formatAssignments(final_assignments),
'violations': final_violations,
'success': success,
'resolution_report': self.resolution_report
}
except Exception as error:
self.resolution_report.append(f"❌ Error: {str(error)}")
return self.errorResult(error)
def optimizeDistribution(self, trainees, experienced, managers, availabilities, constraints, shifts):
"""
Optimiert die Verteilung der Schichten unter Berücksichtigung aller Constraints
"""
# Reset model for new optimization
self.model = cp_model.CpModel()
assignments = {}
all_employees = experienced + trainees
self.resolution_report.append(f"🔧 Building model with {len(shifts)} shifts and {len(all_employees)} employees")
# Create assignment variables
for employee in all_employees:
employee_id = employee['id']
assignments[employee_id] = {}
for shift in shifts:
shift_id = shift['id']
availability = self.getAvailability(employee_id, shift_id, availabilities)
if availability in [1, 2]: # Only create variables for available shifts
var_name = f"assign_{employee_id}_{shift_id}"
assignments[employee_id][shift_id] = self.model.NewBoolVar(var_name)
# Constraint: Max 1 shift per day per employee
shifts_by_day = self.groupShiftsByDay(shifts)
for employee in all_employees:
employee_id = employee['id']
for day, day_shifts in shifts_by_day.items():
shift_vars = []
for shift in day_shifts:
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) <= 1)
# Constraint: Each shift has required employees
for shift in shifts:
shift_id = shift['id']
shift_vars = []
for employee in all_employees:
employee_id = employee['id']
if shift_id in assignments.get(employee_id, {}):
shift_vars.append(assignments[employee_id][shift_id])
if shift_vars:
min_workers = shift.get('minWorkers', 1)
max_workers = shift.get('maxWorkers', 3)
self.model.Add(sum(shift_vars) >= min_workers)
self.model.Add(sum(shift_vars) <= max_workers)
# Constraint: Trainees cannot work alone
for shift in shifts:
shift_id = shift['id']
trainee_vars = []
experienced_vars = []
for trainee in trainees:
trainee_id = trainee['id']
if shift_id in assignments.get(trainee_id, {}):
trainee_vars.append(assignments[trainee_id][shift_id])
for exp in experienced:
exp_id = exp['id']
if shift_id in assignments.get(exp_id, {}):
experienced_vars.append(assignments[exp_id][shift_id])
if trainee_vars and experienced_vars:
# 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)
# Constraint: Contract hours limits
for employee in all_employees:
employee_id = employee['id']
contract_type = employee.get('contractType', 'large')
max_hours = 40 if contract_type == 'large' else 20
total_hours_var = 0
for shift in shifts:
shift_id = shift['id']
if shift_id in assignments.get(employee_id, {}):
# Assume 8 hours per shift (adjust based on your time slots)
shift_hours = 8
total_hours_var += assignments[employee_id][shift_id] * shift_hours
self.model.Add(total_hours_var <= max_hours)
# Objective: Maximize preferred assignments
objective_terms = []
for employee in all_employees:
employee_id = employee['id']
for shift in shifts:
shift_id = shift['id']
if shift_id in assignments.get(employee_id, {}):
availability = self.getAvailability(employee_id, shift_id, availabilities)
if availability == 1: # Preferred
objective_terms.append(assignments[employee_id][shift_id] * 10)
elif availability == 2: # Available
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)
self.model.Maximize(sum(objective_terms))
# Solve the model
self.resolution_report.append("🎯 Solving optimization model...")
status = self.solver.Solve(self.model)
if status == cp_model.OPTIMAL:
self.resolution_report.append("✅ Optimal solution found!")
elif status == cp_model.FEASIBLE:
self.resolution_report.append("⚠️ Feasible solution found (may not be optimal)")
else:
self.resolution_report.append("❌ No solution found")
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
return self.extractAssignments(assignments, all_employees, shifts)
else:
return {}
def formatAssignments(self, assignments):
"""Format assignments for frontend consumption"""
formatted = {}
for shift_id, employee_ids in assignments.items():
formatted[shift_id] = employee_ids
return formatted
def prepareShifts(self, shiftPlan):
"""Prepare shifts for optimization"""
if 'shifts' in shiftPlan:
return shiftPlan['shifts']
return []
# ... (keep the other helper methods from your original code)
# detectAllViolations, fixViolations, createTraineePartners, etc.
# Main execution for Python script
if __name__ == "__main__":
try:
# Read input from stdin
input_data = json.loads(sys.stdin.read())
optimizer = ScheduleOptimizer()
result = optimizer.generateOptimalSchedule(
input_data.get('shiftPlan', {}),
input_data.get('employees', []),
input_data.get('availabilities', []),
input_data.get('constraints', {})
)
print(json.dumps(result))
except Exception as e:
error_result = {
'assignments': {},
'violations': [f'Error: {str(e)}'],
'success': False,
'resolution_report': [f'Critical error: {str(e)}'],
'error': str(e)
}
print(json.dumps(error_result))

View File

@@ -1,4 +1,3 @@
// routes/scheduling.ts
import express from 'express'; import express from 'express';
import { SchedulingService } from '../services/SchedulingService.js'; import { SchedulingService } from '../services/SchedulingService.js';
@@ -8,18 +7,56 @@ router.post('/generate-schedule', async (req, res) => {
try { try {
const { shiftPlan, employees, availabilities, constraints } = req.body; const { shiftPlan, employees, availabilities, constraints } = req.body;
console.log('Received scheduling request:', {
shiftPlan: shiftPlan?.name,
employeeCount: employees?.length,
availabilityCount: availabilities?.length,
constraintCount: constraints?.length
});
// Validate required data
if (!shiftPlan || !employees || !availabilities) {
return res.status(400).json({
error: 'Missing required data',
details: {
shiftPlan: !!shiftPlan,
employees: !!employees,
availabilities: !!availabilities
}
});
}
const scheduler = new SchedulingService(); const scheduler = new SchedulingService();
const result = await scheduler.generateOptimalSchedule({ const result = await scheduler.generateOptimalSchedule({
shiftPlan, shiftPlan,
employees, employees,
availabilities, availabilities,
constraints constraints: constraints || []
});
console.log('Scheduling completed:', {
success: result.success,
assignments: Object.keys(result.assignments).length,
violations: result.violations.length
}); });
res.json(result); res.json(result);
} catch (error) { } catch (error) {
res.status(500).json({ error: 'Scheduling failed', details: error }); console.error('Scheduling failed:', error);
res.status(500).json({
error: 'Scheduling failed',
details: error instanceof Error ? error.message : 'Unknown error'
});
} }
}); });
// Health check for scheduling service
router.get('/health', (req, res) => {
res.json({
status: 'ok',
service: 'scheduling',
timestamp: new Date().toISOString()
});
});
export default router; export default router;

View File

@@ -1,47 +1,167 @@
// src/services/schedulingService.ts // backend/src/services/SchedulingService.ts
import { Worker } from 'worker_threads'; import { Worker } from 'worker_threads';
import path from 'path'; import path from 'path';
import { Employee, ShiftPlan } from '../models/Employee.js'; import { fileURLToPath } from 'url';
import { Employee, EmployeeAvailability } from '../models/Employee.js';
import { ShiftPlan, ScheduledShift } from '../models/ShiftPlan.js';
import { ScheduleRequest, ScheduleResult, Availability, Constraint } from '../models/scheduling.js';
export interface ScheduleRequest { const __filename = fileURLToPath(import.meta.url);
shiftPlan: ShiftPlan; const __dirname = path.dirname(__filename);
employees: Employee[];
availabilities: Availability[];
constraints: Constraint[];
}
export interface ScheduleResult {
assignments: Assignment[];
violations: Violation[];
success: boolean;
resolutionReport: string[];
processingTime: number;
}
export class SchedulingService { export class SchedulingService {
async generateOptimalSchedule(request: ScheduleRequest): Promise<ScheduleResult> { async generateOptimalSchedule(request: ScheduleRequest): Promise<ScheduleResult> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const worker = new Worker(path.resolve(__dirname, '../workers/scheduler-worker.js'), { const workerPath = path.resolve(__dirname, '../workers/scheduler-worker.js');
workerData: request
});
// Timeout nach 110 Sekunden const worker = new Worker(workerPath, {
workerData: this.prepareWorkerData(request)
});
// Timeout after 110 seconds
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
worker.terminate(); worker.terminate();
reject(new Error('Scheduling timeout after 110 seconds')); reject(new Error('Scheduling timeout after 110 seconds'));
}, 110000); }, 110000);
worker.on('message', (result: ScheduleResult) => { worker.on('message', (result: ScheduleResult) => {
clearTimeout(timeout); clearTimeout(timeout);
resolve(result); resolve(result);
}); });
worker.on('error', reject); worker.on('error', (error) => {
clearTimeout(timeout);
reject(error);
});
worker.on('exit', (code) => { worker.on('exit', (code) => {
clearTimeout(timeout);
if (code !== 0) { if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`)); reject(new Error(`Worker stopped with exit code ${code}`));
} }
}); });
}); });
} }
private prepareWorkerData(request: ScheduleRequest): any {
const { shiftPlan, employees, availabilities, constraints } = request;
// Convert scheduled shifts to a format the worker can use
const shifts = this.prepareShifts(shiftPlan);
// Prepare availabilities in worker-friendly format
const workerAvailabilities = this.prepareAvailabilities(availabilities, shiftPlan);
return {
shiftPlan: {
id: shiftPlan.id,
name: shiftPlan.name,
startDate: shiftPlan.startDate,
endDate: shiftPlan.endDate,
status: shiftPlan.status
},
employees: employees.filter(emp => emp.isActive),
shifts,
availabilities: workerAvailabilities,
constraints: this.prepareConstraints(constraints)
};
}
private prepareShifts(shiftPlan: ShiftPlan): any[] {
if (!shiftPlan.scheduledShifts || shiftPlan.scheduledShifts.length === 0) {
// Generate scheduled shifts from template
return this.generateScheduledShiftsFromTemplate(shiftPlan);
}
return shiftPlan.scheduledShifts.map(shift => ({
id: shift.id,
date: shift.date,
timeSlotId: shift.timeSlotId,
requiredEmployees: shift.requiredEmployees,
minWorkers: 1,
maxWorkers: 3,
isPriority: false
}));
}
private generateScheduledShiftsFromTemplate(shiftPlan: ShiftPlan): any[] {
const shifts: any[] = [];
if (!shiftPlan.startDate || !shiftPlan.endDate || !shiftPlan.shifts) {
return shifts;
}
const startDate = new Date(shiftPlan.startDate);
const endDate = new Date(shiftPlan.endDate);
// Generate shifts for each day in the date range
for (let date = new Date(startDate); date <= endDate; date.setDate(date.getDate() + 1)) {
const dayOfWeek = date.getDay() === 0 ? 7 : date.getDay(); // Convert to 1-7 (Mon-Sun)
const dayShifts = shiftPlan.shifts.filter(shift => shift.dayOfWeek === dayOfWeek);
dayShifts.forEach(shift => {
shifts.push({
id: `generated_${date.toISOString().split('T')[0]}_${shift.timeSlotId}`,
date: date.toISOString().split('T')[0],
timeSlotId: shift.timeSlotId,
requiredEmployees: shift.requiredEmployees,
minWorkers: 1,
maxWorkers: 3,
isPriority: false
});
});
}
return shifts;
}
private prepareAvailabilities(availabilities: Availability[], shiftPlan: ShiftPlan): any[] {
// Convert availability format to worker-friendly format
return availabilities.map(avail => ({
employeeId: avail.employeeId,
shiftId: this.findShiftIdForAvailability(avail, shiftPlan),
availability: avail.preferenceLevel // 1, 2, 3
}));
}
private findShiftIdForAvailability(availability: Availability, shiftPlan: ShiftPlan): string {
// Find the corresponding scheduled shift ID for this availability
if (shiftPlan.scheduledShifts) {
const scheduledShift = shiftPlan.scheduledShifts.find(shift =>
shift.timeSlotId === availability.timeSlotId &&
this.getDayOfWeekFromDate(shift.date) === availability.dayOfWeek
);
if (scheduledShift) {
return scheduledShift.id;
}
}
// Fallback: generate a consistent ID
return `shift_${availability.dayOfWeek}_${availability.timeSlotId}`;
}
private getDayOfWeekFromDate(dateString: string): number {
const date = new Date(dateString);
return date.getDay() === 0 ? 7 : date.getDay();
}
private prepareConstraints(constraints: Constraint[]): any {
const defaultConstraints = {
maxShiftsPerDay: 1,
minEmployeesPerShift: 1,
maxEmployeesPerShift: 3,
enforceTraineeSupervision: true,
contractHoursLimits: true
};
return {
...defaultConstraints,
...constraints.reduce((acc, constraint) => {
acc[constraint.type] = constraint.parameters;
return acc;
}, {} as any)
};
}
} }

View File

@@ -1,35 +1,37 @@
// backend/src/workers/scheduler-worker.ts // backend/src/workers/scheduler-worker.ts
import { parentPort, workerData } from 'worker_threads'; import { parentPort, workerData } from 'worker_threads';
import { CPModel, CPSolver } from './cp-sat-wrapper.js'; import { CPModel, CPSolver } from './cp-sat-wrapper.js';
import { ShiftPlan} from '../models/ShiftPlan.js'; import { ShiftPlan } from '../models/ShiftPlan.js';
import { Employee, } from '../models/Employee.js'; import { Employee, EmployeeAvailability } from '../models/Employee.js';
import { Availability, Constraint } from '../models/scheduling.js';
interface WorkerData { interface WorkerData {
shiftPlan: ShiftPlan; shiftPlan: ShiftPlan;
employees: Employee[]; employees: Employee[];
availabilities: any[]; availabilities: Availability[];
constraints: any[]; constraints: Constraint[];
shifts: any[];
} }
function buildSchedulingModel(model: CPModel, data: WorkerData): void { function buildSchedulingModel(model: CPModel, data: WorkerData): void {
const { employees, shifts, availabilities, constraints } = data; const { employees, shifts, availabilities, constraints } = data;
// 1. Entscheidungsvariablen erstellen // 1. Entscheidungsvariablen erstellen
employees.forEach(employee => { employees.forEach((employee: any) => {
shifts.forEach(shift => { shifts.forEach((shift: any) => {
const varName = `assign_${employee.id}_${shift.id}`; const varName = `assign_${employee.id}_${shift.id}`;
model.addVariable(varName, 'bool'); model.addVariable(varName, 'bool');
}); });
}); });
// 2. Verfügbarkeits-Constraints // 2. Verfügbarkeits-Constraints
employees.forEach(employee => { employees.forEach((employee: any) => {
shifts.forEach(shift => { shifts.forEach((shift: any) => {
const availability = availabilities.find( const availability = availabilities.find(
a => a.employeeId === employee.id && a.shiftId === shift.id (a: any) => a.employeeId === employee.id && a.shiftId === shift.id
); );
if (availability?.availability === 3) { if (availability?.preferenceLevel === 3) {
const varName = `assign_${employee.id}_${shift.id}`; const varName = `assign_${employee.id}_${shift.id}`;
model.addConstraint(`${varName} == 0`, `Availability constraint for ${employee.name}`); model.addConstraint(`${varName} == 0`, `Availability constraint for ${employee.name}`);
} }
@@ -37,90 +39,211 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
}); });
// 3. Schicht-Besetzungs-Constraints // 3. Schicht-Besetzungs-Constraints
shifts.forEach(shift => { shifts.forEach((shift: any) => {
const assignmentVars = employees.map( const assignmentVars = employees.map(
emp => `assign_${emp.id}_${shift.id}` (emp: any) => `assign_${emp.id}_${shift.id}`
); );
model.addConstraint( if (assignmentVars.length > 0) {
`${assignmentVars.join(' + ')} >= ${shift.minWorkers}`, model.addConstraint(
`Min workers for shift ${shift.id}` `${assignmentVars.join(' + ')} >= ${shift.minWorkers || 1}`,
); `Min workers for shift ${shift.id}`
);
model.addConstraint(
`${assignmentVars.join(' + ')} <= ${shift.maxWorkers}`, model.addConstraint(
`Max workers for shift ${shift.id}` `${assignmentVars.join(' + ')} <= ${shift.maxWorkers || 3}`,
); `Max workers for shift ${shift.id}`
);
}
}); });
// 4. Keine zwei Schichten pro Tag pro Employee // 4. Keine zwei Schichten pro Tag pro Employee
employees.forEach(employee => { employees.forEach((employee: any) => {
const shiftsByDate = groupShiftsByDate(shifts); const shiftsByDate = groupShiftsByDate(shifts);
Object.entries(shiftsByDate).forEach(([date, dayShifts]) => { Object.entries(shiftsByDate).forEach(([date, dayShifts]) => {
const dayAssignmentVars = dayShifts.map( const dayAssignmentVars = (dayShifts as any[]).map(
(shift: any) => `assign_${employee.id}_${shift.id}` (shift: any) => `assign_${employee.id}_${shift.id}`
); );
model.addConstraint( if (dayAssignmentVars.length > 0) {
`${dayAssignmentVars.join(' + ')} <= 1`, model.addConstraint(
`Max one shift per day for ${employee.name} on ${date}` `${dayAssignmentVars.join(' + ')} <= 1`,
); `Max one shift per day for ${employee.name} on ${date}`
}); );
});
// 5. Trainee-Überwachungs-Constraints
const trainees = employees.filter(emp => emp.employeeType === 'trainee');
const experienced = employees.filter(emp => emp.employeeType === 'experienced');
trainees.forEach(trainee => {
shifts.forEach(shift => {
const traineeVar = `assign_${trainee.id}_${shift.id}`;
const experiencedVars = experienced.map(exp => `assign_${exp.id}_${shift.id}`);
model.addConstraint(
`${traineeVar} <= ${experiencedVars.join(' + ')}`,
`Trainee ${trainee.name} requires supervision in shift ${shift.id}`
);
});
});
// 6. Ziel: Verfügbarkeits-Score maximieren
let objectiveExpression = '';
employees.forEach(employee => {
shifts.forEach(shift => {
const availability = availabilities.find(
a => a.employeeId === employee.id && a.shiftId === shift.id
);
if (availability) {
const score = availability.availability === 1 ? 3 :
availability.availability === 2 ? 1 : 0;
const varName = `assign_${employee.id}_${shift.id}`;
objectiveExpression += objectiveExpression ? ` + ${score} * ${varName}` : `${score} * ${varName}`;
} }
}); });
}); });
model.maximize(objectiveExpression); // 5. Trainee-Überwachungs-Constraints
const trainees = employees.filter((emp: any) => emp.employeeType === 'trainee');
const experienced = employees.filter((emp: any) => emp.employeeType === 'experienced');
trainees.forEach((trainee: any) => {
shifts.forEach((shift: any) => {
const traineeVar = `assign_${trainee.id}_${shift.id}`;
const experiencedVars = experienced.map((exp: any) => `assign_${exp.id}_${shift.id}`);
if (experiencedVars.length > 0) {
model.addConstraint(
`${traineeVar} <= ${experiencedVars.join(' + ')}`,
`Trainee ${trainee.name} requires supervision in shift ${shift.id}`
);
}
});
});
// 6. Contract Hours Constraints
employees.forEach((employee: any) => {
const contractHours = employee.contractType === 'small' ? 20 : 40;
const shiftHoursVars: string[] = [];
shifts.forEach((shift: any) => {
const shiftHours = 8; // Assuming 8 hours per shift
const varName = `assign_${employee.id}_${shift.id}`;
shiftHoursVars.push(`${shiftHours} * ${varName}`);
});
if (shiftHoursVars.length > 0) {
model.addConstraint(
`${shiftHoursVars.join(' + ')} <= ${contractHours}`,
`Contract hours limit for ${employee.name}`
);
}
});
// 7. Ziel: Verfügbarkeits-Score maximieren
let objectiveExpression = '';
employees.forEach((employee: any) => {
shifts.forEach((shift: any) => {
const availability = availabilities.find(
(a: any) => a.employeeId === employee.id && a.shiftId === shift.id
);
if (availability) {
const score = availability.preferenceLevel === 1 ? 10 :
availability.preferenceLevel === 2 ? 5 :
-100; // Heavy penalty for assigning unavailable shifts
const varName = `assign_${employee.id}_${shift.id}`;
if (objectiveExpression) {
objectiveExpression += ` + ${score} * ${varName}`;
} else {
objectiveExpression = `${score} * ${varName}`;
}
}
});
});
if (objectiveExpression) {
model.maximize(objectiveExpression);
}
} }
function groupShiftsByDate(shifts: any[]): Record<string, any[]> { function groupShiftsByDate(shifts: any[]): Record<string, any[]> {
return shifts.reduce((groups, shift) => { return shifts.reduce((groups: Record<string, any[]>, shift: any) => {
const date = shift.date.split('T')[0]; const date = shift.date?.split('T')[0] || 'unknown';
if (!groups[date]) groups[date] = []; if (!groups[date]) groups[date] = [];
groups[date].push(shift); groups[date].push(shift);
return groups; return groups;
}, {}); }, {});
} }
function extractAssignmentsFromSolution(solution: any, employees: any[], shifts: any[]): any {
const assignments: any = {};
// Initialize assignments object with shift IDs
shifts.forEach((shift: any) => {
assignments[shift.id] = [];
});
// Extract assignments from solution variables
employees.forEach((employee: any) => {
shifts.forEach((shift: any) => {
const varName = `assign_${employee.id}_${shift.id}`;
const isAssigned = solution.variables?.[varName] === 1;
if (isAssigned) {
if (!assignments[shift.id]) {
assignments[shift.id] = [];
}
assignments[shift.id].push(employee.id);
}
});
});
return assignments;
}
function detectViolations(assignments: any, employees: any[], shifts: any[]): string[] {
const violations: string[] = [];
const employeeMap = new Map(employees.map((emp: any) => [emp.id, emp]));
// Check for understaffed shifts
shifts.forEach((shift: any) => {
const assignedCount = assignments[shift.id]?.length || 0;
const minRequired = shift.minWorkers || 1;
if (assignedCount < minRequired) {
violations.push(`UNDERSTAFFED: Shift ${shift.id} has ${assignedCount} employees but requires ${minRequired}`);
}
});
// Check for trainee supervision
shifts.forEach((shift: any) => {
const assignedEmployees = assignments[shift.id] || [];
const hasTrainee = assignedEmployees.some((empId: string) => {
const emp = employeeMap.get(empId);
return emp?.employeeType === 'trainee';
});
const hasExperienced = assignedEmployees.some((empId: string) => {
const emp = employeeMap.get(empId);
return emp?.employeeType === 'experienced';
});
if (hasTrainee && !hasExperienced) {
violations.push(`TRAINEE_UNSUPERVISED: Shift ${shift.id} has trainee but no experienced employee`);
}
});
// Check for multiple shifts per day per employee
const shiftsByDate = groupShiftsByDate(shifts);
employees.forEach((employee: any) => {
Object.entries(shiftsByDate).forEach(([date, dayShifts]) => {
let shiftsAssigned = 0;
dayShifts.forEach((shift: any) => {
if (assignments[shift.id]?.includes(employee.id)) {
shiftsAssigned++;
}
});
if (shiftsAssigned > 1) {
violations.push(`MULTIPLE_SHIFTS: ${employee.name} has ${shiftsAssigned} shifts on ${date}`);
}
});
});
return violations;
}
async function runScheduling() { async function runScheduling() {
const data: WorkerData = workerData; const data: WorkerData = workerData;
const startTime = Date.now();
try { try {
console.log('Starting scheduling optimization...'); console.log('Starting scheduling optimization...');
const startTime = Date.now();
// Validate input data
if (!data.shifts || data.shifts.length === 0) {
throw new Error('No shifts provided for scheduling');
}
if (!data.employees || data.employees.length === 0) {
throw new Error('No employees provided for scheduling');
}
console.log(`Optimizing ${data.shifts.length} shifts for ${data.employees.length} employees`);
const model = new CPModel(); const model = new CPModel();
buildSchedulingModel(model, data); buildSchedulingModel(model, data);
@@ -132,34 +255,80 @@ async function runScheduling() {
}); });
const solution = await solver.solve(model); const solution = await solver.solve(model);
solution.processingTime = Date.now() - startTime; const processingTime = Date.now() - startTime;
console.log(`Scheduling completed in ${solution.processingTime}ms`); console.log(`Scheduling completed in ${processingTime}ms`);
console.log(`Solution success: ${solution.success}`);
let assignments = {};
let violations: string[] = [];
let resolutionReport: string[] = [
`Solved in ${processingTime}ms`,
`Variables: ${solution.metadata?.variablesCreated || 'unknown'}`,
`Constraints: ${solution.metadata?.constraintsAdded || 'unknown'}`,
`Optimal: ${solution.metadata?.optimal || false}`,
`Status: ${solution.success ? 'SUCCESS' : 'FAILED'}`
];
if (solution.success) {
// Extract assignments from solution
assignments = extractAssignmentsFromSolution(solution, data.employees, data.shifts);
// Detect violations
violations = detectViolations(assignments, data.employees, data.shifts);
if (violations.length === 0) {
resolutionReport.push('✅ No constraint violations detected');
} else {
resolutionReport.push(`⚠️ Found ${violations.length} violations:`);
violations.forEach(violation => {
resolutionReport.push(` - ${violation}`);
});
}
// Add assignment statistics
const totalAssignments = Object.values(assignments).reduce((sum: number, shiftAssignments: any) =>
sum + shiftAssignments.length, 0
);
resolutionReport.push(`📊 Total assignments: ${totalAssignments}`);
} else {
violations.push('SCHEDULING_FAILED: No feasible solution found');
resolutionReport.push('❌ No feasible solution could be found');
}
parentPort?.postMessage({ parentPort?.postMessage({
assignments: solution.assignments, assignments,
violations: solution.violations, violations,
success: solution.success, success: solution.success && violations.length === 0,
resolutionReport: [ resolutionReport,
`Solved in ${solution.processingTime}ms`, processingTime
`Variables: ${solution.metadata.variablesCreated}`,
`Constraints: ${solution.metadata.constraintsAdded}`,
`Optimal: ${solution.metadata.optimal}`
],
processingTime: solution.processingTime
}); });
} catch (error) { } catch (error) {
console.error('Scheduling worker error:', error); console.error('Scheduling worker error:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
parentPort?.postMessage({ parentPort?.postMessage({
error: error.message, error: errorMessage,
success: false, success: false,
assignments: [], assignments: {},
violations: [], violations: [`ERROR: ${errorMessage}`],
resolutionReport: [`Error: ${error.message}`], resolutionReport: [`Error: ${errorMessage}`],
processingTime: 0 processingTime: Date.now() - startTime
}); });
} }
} }
// Handle graceful shutdown
process.on('SIGTERM', () => {
console.log('Scheduling worker received SIGTERM, shutting down...');
process.exit(0);
});
process.on('SIGINT', () => {
console.log('Scheduling worker received SIGINT, shutting down...');
process.exit(0);
});
runScheduling(); runScheduling();

View File

@@ -1,43 +1,141 @@
// src/components/Scheduler.tsx
import React from 'react'; import React from 'react';
import { useScheduling } from '../hooks/useScheduling'; import { useScheduling } from '../services/scheduling/useScheduling';
import { ScheduleRequest } from '../models/scheduling';
interface Props { interface SchedulerProps {
scheduleRequest: ScheduleRequest; scheduleRequest: ScheduleRequest;
onScheduleGenerated?: (result: any) => void;
} }
export const Scheduler: React.FC<Props> = ({ scheduleRequest }) => { export const Scheduler: React.FC<SchedulerProps> = ({
scheduleRequest,
onScheduleGenerated
}) => {
const { generateSchedule, loading, error, result } = useScheduling(); const { generateSchedule, loading, error, result } = useScheduling();
const handleGenerateSchedule = async () => { const handleGenerateSchedule = async () => {
try { try {
await generateSchedule(scheduleRequest); const scheduleResult = await generateSchedule(scheduleRequest);
if (onScheduleGenerated) {
onScheduleGenerated(scheduleResult);
}
} catch (err) { } catch (err) {
// Error handling console.error('Scheduling failed:', err);
} }
}; };
return ( return (
<div> <div style={{ padding: '20px', border: '1px solid #e0e0e0', borderRadius: '8px' }}>
<h3>Automatic Schedule Generation</h3>
<button <button
onClick={handleGenerateSchedule} onClick={handleGenerateSchedule}
disabled={loading} disabled={loading}
style={{
padding: '12px 24px',
backgroundColor: loading ? '#95a5a6' : '#3498db',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: loading ? 'not-allowed' : 'pointer',
fontSize: '16px',
fontWeight: 'bold'
}}
> >
{loading ? 'Generating Schedule...' : 'Generate Optimal Schedule'} {loading ? '🔄 Generating Schedule...' : '🚀 Generate Optimal Schedule'}
</button> </button>
{loading && ( {loading && (
<div> <div style={{ marginTop: '15px' }}>
<progress max="100" value="70" /> <div style={{
<p>Optimizing schedule... (max 2 minutes)</p> width: '100%',
height: '8px',
backgroundColor: '#ecf0f1',
borderRadius: '4px',
overflow: 'hidden'
}}>
<div style={{
width: '70%',
height: '100%',
backgroundColor: '#3498db',
animation: 'pulse 2s infinite',
borderRadius: '4px'
}} />
</div>
<p style={{ color: '#7f8c8d', fontSize: '14px', marginTop: '8px' }}>
Optimizing schedule... (max 2 minutes)
</p>
</div> </div>
)} )}
{error && <div className="error">{error}</div>} {error && (
<div style={{
marginTop: '15px',
padding: '12px',
backgroundColor: '#f8d7da',
border: '1px solid #f5c6cb',
borderRadius: '4px',
color: '#721c24'
}}>
<strong>Error:</strong> {error}
</div>
)}
{result && ( {result && (
<ScheduleResultView result={result} /> <div style={{ marginTop: '20px' }}>
<ScheduleResultView result={result} />
</div>
)} )}
</div> </div>
); );
}; };
const ScheduleResultView: React.FC<{ result: any }> = ({ result }) => {
return (
<div style={{
padding: '15px',
backgroundColor: result.success ? '#d4edda' : '#f8d7da',
border: `1px solid ${result.success ? '#c3e6cb' : '#f5c6cb'}`,
borderRadius: '4px'
}}>
<h4 style={{
color: result.success ? '#155724' : '#721c24',
marginTop: 0
}}>
{result.success ? '✅ Schedule Generated Successfully' : '❌ Schedule Generation Failed'}
</h4>
<div style={{ marginBottom: '10px' }}>
<strong>Assignments:</strong> {Object.keys(result.assignments || {}).length} shifts assigned
</div>
<div style={{ marginBottom: '10px' }}>
<strong>Violations:</strong> {result.violations?.length || 0}
</div>
{result.resolution_report && result.resolution_report.length > 0 && (
<details style={{ marginTop: '10px' }}>
<summary style={{ cursor: 'pointer', fontWeight: 'bold' }}>
Resolution Report
</summary>
<div style={{
marginTop: '10px',
maxHeight: '200px',
overflow: 'auto',
fontSize: '12px',
fontFamily: 'monospace',
backgroundColor: 'rgba(0,0,0,0.05)',
padding: '10px',
borderRadius: '4px'
}}>
{result.resolution_report.map((line: string, index: number) => (
<div key={index} style={{ marginBottom: '2px' }}>{line}</div>
))}
</div>
</details>
)}
</div>
);
};
export default Scheduler;

View File

@@ -1,37 +0,0 @@
// frontend/src/services/scheduling/scheduling.ts
import { useState, useCallback } from 'react';
import { ScheduleRequest, ScheduleResult } from '../types/scheduling';
export const useScheduling = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<ScheduleResult | null>(null);
const generateSchedule = useCallback(async (request: ScheduleRequest) => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/scheduling/generate-schedule', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request)
});
if (!response.ok) throw new Error('Scheduling request failed');
const data: ScheduleResult = await response.json();
setResult(data);
return data;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
}, []);
return { generateSchedule, loading, error, result };
};

View File

@@ -0,0 +1,122 @@
// backend/src/models/scheduling.ts
import { Employee } from './Employee.js';
import { ShiftPlan } from './ShiftPlan.js';
// Add the missing type definitions
export interface Availability {
id: string;
employeeId: string;
planId: string;
dayOfWeek: number; // 1=Monday, 7=Sunday
timeSlotId: string;
preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable
notes?: string;
}
export interface Constraint {
type: string;
severity: 'hard' | 'soft';
parameters: {
maxShiftsPerDay?: number;
minEmployeesPerShift?: number;
maxEmployeesPerShift?: number;
enforceTraineeSupervision?: boolean;
contractHoursLimit?: boolean;
maxHoursPerWeek?: number;
[key: string]: any;
};
weight?: number; // For soft constraints
}
export interface ScheduleRequest {
shiftPlan: ShiftPlan;
employees: Employee[];
availabilities: Availability[];
constraints: Constraint[];
}
export interface ScheduleResult {
assignments: Assignment[];
violations: Violation[];
success: boolean;
resolutionReport: string[];
processingTime: number;
}
export interface Assignment {
shiftId: string;
employeeId: string;
assignedAt: Date;
score: number; // Qualität der Zuweisung (1-100)
}
export interface Violation {
type: string;
severity: 'critical' | 'warning';
message: string;
involvedEmployees?: string[];
shiftId?: string;
details?: any;
}
export interface SolverOptions {
maxTimeInSeconds: number;
numSearchWorkers: number;
logSearchProgress: boolean;
}
export interface Solution {
assignments: Assignment[];
violations: Violation[];
success: boolean;
metadata: {
solveTime: number;
constraintsAdded: number;
variablesCreated: number;
optimal: boolean;
};
}
// Additional helper types for the scheduling system
export interface SchedulingConfig {
maxRepairAttempts: number;
targetEmployeesPerShift: number;
enforceNoTraineeAlone: boolean;
enforceExperiencedWithChef: boolean;
preferEmployeePreferences: boolean;
}
export interface AssignmentResult {
assignments: { [shiftId: string]: string[] }; // shiftId -> employeeIds
violations: string[];
resolutionReport: string[];
success: boolean;
statistics?: {
totalAssignments: number;
preferredAssignments: number;
availableAssignments: number;
coverageRate: number;
violationCount: number;
};
}
export interface EmployeeAvailabilitySummary {
employeeId: string;
employeeName: string;
preferredSlots: number;
availableSlots: number;
unavailableSlots: number;
totalSlots: number;
}
export interface ShiftRequirement {
shiftId: string;
timeSlotId: string;
dayOfWeek: number;
date?: string;
requiredEmployees: number;
minEmployees: number;
maxEmployees: number;
assignedEmployees: string[];
isPriority: boolean;
}

View File

@@ -5,7 +5,7 @@ import { useAuth } from '../../contexts/AuthContext';
import { shiftPlanService } from '../../services/shiftPlanService'; import { shiftPlanService } from '../../services/shiftPlanService';
import { employeeService } from '../../services/employeeService'; import { employeeService } from '../../services/employeeService';
import { shiftAssignmentService, ShiftAssignmentService } from '../../services/shiftAssignmentService'; import { shiftAssignmentService, ShiftAssignmentService } from '../../services/shiftAssignmentService';
import { IntelligentShiftScheduler, SchedulingResult, AssignmentResult } from '../../hooks/useScheduling'; import { IntelligentShiftScheduler, SchedulingResult, AssignmentResult } from '../../services/scheduling/useScheduling';
import { ShiftPlan, TimeSlot, ScheduledShift } from '../../models/ShiftPlan'; import { ShiftPlan, TimeSlot, ScheduledShift } from '../../models/ShiftPlan';
import { Employee, EmployeeAvailability } from '../../models/Employee'; import { Employee, EmployeeAvailability } from '../../models/Employee';
import { useNotification } from '../../contexts/NotificationContext'; import { useNotification } from '../../contexts/NotificationContext';

View File

@@ -0,0 +1,71 @@
import { useState, useCallback } from 'react';
import { ScheduleRequest, ScheduleResult } from '../../models/scheduling';
export const useScheduling = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<ScheduleResult | null>(null);
const generateSchedule = useCallback(async (request: ScheduleRequest) => {
setLoading(true);
setError(null);
try {
console.log('📤 Sending scheduling request:', {
shiftPlan: request.shiftPlan.name,
employees: request.employees.length,
availabilities: request.availabilities.length,
constraints: request.constraints.length
});
const response = await fetch('/api/scheduling/generate-schedule', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Scheduling request failed: ${response.status} ${errorText}`);
}
const data: ScheduleResult = await response.json();
console.log('📥 Received scheduling result:', {
success: data.success,
assignments: Object.keys(data.assignments).length,
violations: data.violations.length,
processingTime: data.processingTime
});
setResult(data);
return data;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown scheduling error';
console.error('❌ Scheduling error:', errorMessage);
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
}, []);
const reset = useCallback(() => {
setLoading(false);
setError(null);
setResult(null);
}, []);
return {
generateSchedule,
loading,
error,
result,
reset
};
};
// Export for backward compatibility
export default useScheduling;

View File

@@ -2,7 +2,7 @@
import { ShiftPlan, ScheduledShift } from '../models/ShiftPlan'; import { ShiftPlan, ScheduledShift } from '../models/ShiftPlan';
import { Employee, EmployeeAvailability } from '../models/Employee'; import { Employee, EmployeeAvailability } from '../models/Employee';
import { authService } from './authService'; import { authService } from './authService';
import { IntelligentShiftScheduler, AssignmentResult, WeeklyPattern } from '../hooks/useScheduling'; //import { IntelligentShiftScheduler, AssignmentResult, WeeklyPattern } from './scheduling/useScheduling';
import { isScheduledShift } from '../models/helpers'; import { isScheduledShift } from '../models/helpers';
const API_BASE_URL = 'http://localhost:3002/api/scheduled-shifts'; const API_BASE_URL = 'http://localhost:3002/api/scheduled-shifts';