mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-11-30 22:45:46 +01:00
backend working
This commit is contained in:
@@ -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 {
|
||||||
@@ -49,3 +76,47 @@ export interface Solution {
|
|||||||
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;
|
||||||
|
}
|
||||||
247
backend/src/python-scripts/scheduling_solver.py
Normal file
247
backend/src/python-scripts/scheduling_solver.py
Normal 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))
|
||||||
@@ -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;
|
||||||
@@ -1,31 +1,24 @@
|
|||||||
// 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
|
|
||||||
|
const worker = new Worker(workerPath, {
|
||||||
|
workerData: this.prepareWorkerData(request)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Timeout nach 110 Sekunden
|
// 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'));
|
||||||
@@ -36,12 +29,139 @@ export class SchedulingService {
|
|||||||
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)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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(
|
model.addConstraint(
|
||||||
`${assignmentVars.join(' + ')} <= ${shift.maxWorkers}`,
|
`${assignmentVars.join(' + ')} <= ${shift.maxWorkers || 3}`,
|
||||||
`Max workers for shift ${shift.id}`
|
`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();
|
||||||
@@ -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;
|
||||||
@@ -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 };
|
|
||||||
};
|
|
||||||
122
frontend/src/models/scheduling.ts
Normal file
122
frontend/src/models/scheduling.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
|||||||
71
frontend/src/services/scheduling/useScheduling.ts
Normal file
71
frontend/src/services/scheduling/useScheduling.ts
Normal 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;
|
||||||
@@ -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';
|
||||||
|
|||||||
Reference in New Issue
Block a user