mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-11-30 22:45:46 +01:00
cp running
This commit is contained in:
@@ -17,51 +17,43 @@ interface WorkerData {
|
||||
function buildSchedulingModel(model: CPModel, data: WorkerData): void {
|
||||
const { employees, shifts, availabilities, constraints } = data;
|
||||
|
||||
// 1. Entscheidungsvariablen erstellen
|
||||
employees.forEach((employee: any) => {
|
||||
// Filter employees to only include active ones
|
||||
const activeEmployees = employees.filter(emp => emp.isActive);
|
||||
const trainees = activeEmployees.filter(emp => emp.employeeType === 'trainee');
|
||||
const experienced = activeEmployees.filter(emp => emp.employeeType === 'experienced');
|
||||
|
||||
console.log(`Building model with ${activeEmployees.length} employees, ${shifts.length} shifts`);
|
||||
console.log(`Available shifts per week: ${shifts.length}`);
|
||||
|
||||
// 1. Create assignment variables for all possible assignments
|
||||
activeEmployees.forEach((employee: any) => {
|
||||
shifts.forEach((shift: any) => {
|
||||
const varName = `assign_${employee.id}_${shift.id}`;
|
||||
model.addVariable(varName, 'bool');
|
||||
});
|
||||
});
|
||||
|
||||
// 2. Verfügbarkeits-Constraints
|
||||
employees.forEach((employee: any) => {
|
||||
|
||||
// 2. Availability constraints
|
||||
activeEmployees.forEach((employee: any) => {
|
||||
shifts.forEach((shift: any) => {
|
||||
const availability = availabilities.find(
|
||||
(a: any) => a.employeeId === employee.id && a.shiftId === shift.id
|
||||
);
|
||||
|
||||
// Hard constraint: never assign when preference level is 3 (unavailable)
|
||||
if (availability?.preferenceLevel === 3) {
|
||||
const varName = `assign_${employee.id}_${shift.id}`;
|
||||
model.addConstraint(`${varName} == 0`, `Availability constraint for ${employee.name}`);
|
||||
model.addConstraint(
|
||||
`${varName} == 0`,
|
||||
`Hard availability constraint for ${employee.name} in shift ${shift.id}`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 3. Schicht-Besetzungs-Constraints
|
||||
shifts.forEach((shift: any) => {
|
||||
const assignmentVars = employees.map(
|
||||
(emp: any) => `assign_${emp.id}_${shift.id}`
|
||||
);
|
||||
|
||||
if (assignmentVars.length > 0) {
|
||||
model.addConstraint(
|
||||
`${assignmentVars.join(' + ')} >= ${shift.minWorkers || 1}`,
|
||||
`Min workers for shift ${shift.id}`
|
||||
);
|
||||
|
||||
model.addConstraint(
|
||||
`${assignmentVars.join(' + ')} <= ${shift.maxWorkers || 3}`,
|
||||
`Max workers for shift ${shift.id}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Keine zwei Schichten pro Tag pro Employee
|
||||
employees.forEach((employee: any) => {
|
||||
const shiftsByDate = groupShiftsByDate(shifts);
|
||||
|
||||
|
||||
// 3. Max 1 shift per day per employee
|
||||
const shiftsByDate = groupShiftsByDate(shifts);
|
||||
activeEmployees.forEach((employee: any) => {
|
||||
Object.entries(shiftsByDate).forEach(([date, dayShifts]) => {
|
||||
const dayAssignmentVars = (dayShifts as any[]).map(
|
||||
(shift: any) => `assign_${employee.id}_${shift.id}`
|
||||
@@ -75,79 +67,126 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 5. Trainee-Überwachungs-Constraints
|
||||
const trainees = employees.filter((emp: any) => emp.employeeType === 'trainee');
|
||||
const experienced = employees.filter((emp: any) => emp.employeeType === 'experienced');
|
||||
|
||||
|
||||
// 4. Shift staffing constraints (RELAXED)
|
||||
shifts.forEach((shift: any) => {
|
||||
const assignmentVars = activeEmployees.map(
|
||||
(emp: any) => `assign_${emp.id}_${shift.id}`
|
||||
);
|
||||
|
||||
if (assignmentVars.length > 0) {
|
||||
// Minimum workers - make this a soft constraint if possible
|
||||
const minWorkers = Math.max(shift.minWorkers || 1, 1);
|
||||
model.addConstraint(
|
||||
`${assignmentVars.join(' + ')} >= ${minWorkers}`,
|
||||
`Min workers for shift ${shift.id}`
|
||||
);
|
||||
|
||||
// Maximum workers
|
||||
const maxWorkers = shift.maxWorkers || 3;
|
||||
model.addConstraint(
|
||||
`${assignmentVars.join(' + ')} <= ${maxWorkers}`,
|
||||
`Max workers for shift ${shift.id}`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 5. Trainee supervision constraints
|
||||
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}`);
|
||||
const experiencedVars = experienced.map((exp: any) =>
|
||||
`assign_${exp.id}_${shift.id}`
|
||||
);
|
||||
|
||||
if (experiencedVars.length > 0) {
|
||||
// If trainee works, at least one experienced must work
|
||||
model.addConstraint(
|
||||
`${traineeVar} <= ${experiencedVars.join(' + ')}`,
|
||||
`Trainee ${trainee.name} requires supervision in shift ${shift.id}`
|
||||
);
|
||||
} else {
|
||||
// If no experienced available, trainee cannot work this shift
|
||||
model.addConstraint(
|
||||
`${traineeVar} == 0`,
|
||||
`No experienced staff for trainee ${trainee.name} in shift ${shift.id}`
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 6. Contract type constraints
|
||||
const totalShifts = shifts.length;
|
||||
console.log(`Total available shifts: ${totalShifts}`);
|
||||
|
||||
// 6. Employee cant workalone
|
||||
employees.forEach((employee: any) => {
|
||||
if (employee.employeeType === 'experienced' && employee.canWorkAlone) {
|
||||
shifts.forEach((shift: any) => {
|
||||
const varName = `assign_${employee.id}_${shift.id}`;
|
||||
// Allow this employee to work alone (no additional constraint needed)
|
||||
// This is more about not preventing single assignments
|
||||
});
|
||||
activeEmployees.forEach((employee: any) => {
|
||||
const contractType = employee.contractType || 'large';
|
||||
|
||||
// ADJUSTMENT: Make contract constraints feasible with available shifts
|
||||
let minShifts, maxShifts;
|
||||
|
||||
if (contractType === 'small') {
|
||||
// Small contract: 1 shifts
|
||||
minShifts = 1;
|
||||
maxShifts = Math.min(1, totalShifts);
|
||||
} else {
|
||||
// Large contract: 2 shifts (2)
|
||||
minShifts = 2;
|
||||
maxShifts = Math.min(2, totalShifts);
|
||||
}
|
||||
|
||||
const shiftVars = shifts.map(
|
||||
(shift: any) => `assign_${employee.id}_${shift.id}`
|
||||
);
|
||||
|
||||
if (shiftVars.length > 0) {
|
||||
// Use range instead of exact number
|
||||
model.addConstraint(
|
||||
`${shiftVars.join(' + ')} == ${minShifts}`,
|
||||
`Expected shifts for ${employee.name} (${contractType} contract)`
|
||||
);
|
||||
|
||||
model.addConstraint(
|
||||
`${shiftVars.join(' + ')} <= ${maxShifts}`,
|
||||
`Max shifts for ${employee.name} (${contractType} contract)`
|
||||
);
|
||||
|
||||
console.log(`Employee ${employee.name}: ${minShifts}-${maxShifts} shifts (${contractType})`);
|
||||
}
|
||||
});
|
||||
|
||||
// 7. Contract Type Shifts Constraint
|
||||
employees.forEach((employee: any) => {
|
||||
const exactShiftsPerWeek = employee.contractType === 'small' ? 5 : 10; // Example: exactly 5 shifts for small, 10 for large
|
||||
const shiftVars: string[] = [];
|
||||
|
||||
// 7. Objective: Maximize preferred assignments with soft constraints
|
||||
let objectiveExpression = '';
|
||||
let softConstraintPenalty = '';
|
||||
|
||||
activeEmployees.forEach((employee: any) => {
|
||||
shifts.forEach((shift: any) => {
|
||||
const varName = `assign_${employee.id}_${shift.id}`;
|
||||
shiftVars.push(varName);
|
||||
});
|
||||
|
||||
if (shiftVars.length > 0) {
|
||||
model.addConstraint(
|
||||
`${shiftVars.join(' + ')} == ${exactShiftsPerWeek}`,
|
||||
`Exact shifts per week for ${employee.name} (${employee.contractType} contract)`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// 8. 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
|
||||
);
|
||||
|
||||
let score = 0;
|
||||
if (availability) {
|
||||
const score = availability.preferenceLevel === 1 ? 10 :
|
||||
availability.preferenceLevel === 2 ? 5 :
|
||||
-1000; // Heavy penalty for assigning unavailable shifts
|
||||
|
||||
const varName = `assign_${employee.id}_${shift.id}`;
|
||||
if (objectiveExpression) {
|
||||
objectiveExpression += ` + ${score} * ${varName}`;
|
||||
} else {
|
||||
objectiveExpression = `${score} * ${varName}`;
|
||||
}
|
||||
score = availability.preferenceLevel === 1 ? 10 :
|
||||
availability.preferenceLevel === 2 ? 5 :
|
||||
-10000; // Very heavy penalty for unavailable
|
||||
} else {
|
||||
// No availability info - slight preference to assign
|
||||
score = 1;
|
||||
}
|
||||
|
||||
if (objectiveExpression) {
|
||||
objectiveExpression += ` + ${score} * ${varName}`;
|
||||
} else {
|
||||
objectiveExpression = `${score} * ${varName}`;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (objectiveExpression) {
|
||||
model.maximize(objectiveExpression);
|
||||
console.log('Objective function set with preference optimization');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,27 +201,94 @@ function groupShiftsByDate(shifts: any[]): Record<string, any[]> {
|
||||
|
||||
function extractAssignmentsFromSolution(solution: any, employees: any[], shifts: any[]): any {
|
||||
const assignments: any = {};
|
||||
const employeeAssignments: any = {};
|
||||
|
||||
// Initialize assignments object with shift IDs
|
||||
console.log('=== SOLUTION DEBUG INFO ===');
|
||||
console.log('Solution success:', solution.success);
|
||||
console.log('Raw assignments from Python:', solution.assignments?.length || 0);
|
||||
console.log('Variables in solution:', Object.keys(solution.variables || {}).length);
|
||||
|
||||
// Initialize assignments object
|
||||
shifts.forEach((shift: any) => {
|
||||
assignments[shift.id] = [];
|
||||
});
|
||||
|
||||
// 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] = [];
|
||||
employeeAssignments[employee.id] = 0;
|
||||
});
|
||||
|
||||
// METHOD 1: Try to use raw variables from solution
|
||||
if (solution.variables) {
|
||||
console.log('Using variable-based assignment extraction');
|
||||
|
||||
Object.entries(solution.variables).forEach(([varName, value]) => {
|
||||
if (varName.startsWith('assign_') && value === 1) {
|
||||
const parts = varName.split('_');
|
||||
if (parts.length >= 3) {
|
||||
const employeeId = parts[1];
|
||||
const shiftId = parts.slice(2).join('_');
|
||||
|
||||
// Find the actual shift ID (handle generated IDs)
|
||||
const actualShift = shifts.find(s =>
|
||||
s.id === shiftId ||
|
||||
`assign_${employeeId}_${s.id}` === varName
|
||||
);
|
||||
|
||||
if (actualShift) {
|
||||
if (!assignments[actualShift.id]) {
|
||||
assignments[actualShift.id] = [];
|
||||
}
|
||||
assignments[actualShift.id].push(employeeId);
|
||||
employeeAssignments[employeeId]++;
|
||||
}
|
||||
}
|
||||
assignments[shift.id].push(employee.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// METHOD 2: Fallback to parsed assignments from Python
|
||||
if (solution.assignments && solution.assignments.length > 0) {
|
||||
console.log('Using Python-parsed assignments');
|
||||
|
||||
solution.assignments.forEach((assignment: any) => {
|
||||
const shiftId = assignment.shiftId;
|
||||
const employeeId = assignment.employeeId;
|
||||
|
||||
if (shiftId && employeeId) {
|
||||
if (!assignments[shiftId]) {
|
||||
assignments[shiftId] = [];
|
||||
}
|
||||
assignments[shiftId].push(employeeId);
|
||||
employeeAssignments[employeeId]++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// METHOD 3: Debug - log all variables to see what's available
|
||||
if (Object.keys(assignments).length === 0 && solution.variables) {
|
||||
console.log('Debug: First 10 variables from solution:');
|
||||
const varNames = Object.keys(solution.variables).slice(0, 10);
|
||||
varNames.forEach(varName => {
|
||||
console.log(` ${varName} = ${solution.variables[varName]}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Log results
|
||||
console.log('=== ASSIGNMENT RESULTS ===');
|
||||
employees.forEach((employee: any) => {
|
||||
console.log(` ${employee.name}: ${employeeAssignments[employee.id]} shifts`);
|
||||
});
|
||||
|
||||
let totalAssignments = 0;
|
||||
shifts.forEach((shift: any) => {
|
||||
const count = assignments[shift.id]?.length || 0;
|
||||
totalAssignments += count;
|
||||
console.log(` Shift ${shift.id}: ${count} employees`);
|
||||
});
|
||||
|
||||
console.log(`Total assignments: ${totalAssignments}`);
|
||||
console.log('==========================');
|
||||
|
||||
return assignments;
|
||||
}
|
||||
|
||||
@@ -244,7 +350,6 @@ async function runScheduling() {
|
||||
try {
|
||||
console.log('Starting scheduling optimization...');
|
||||
|
||||
|
||||
// Validate input data
|
||||
if (!data.shifts || data.shifts.length === 0) {
|
||||
throw new Error('No shifts provided for scheduling');
|
||||
@@ -285,16 +390,12 @@ async function runScheduling() {
|
||||
// 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');
|
||||
// Only detect violations if we actually have assignments
|
||||
if (Object.keys(assignments).length > 0) {
|
||||
violations = detectViolations(assignments, data.employees, data.shifts);
|
||||
} else {
|
||||
resolutionReport.push(`⚠️ Found ${violations.length} violations:`);
|
||||
violations.forEach(violation => {
|
||||
resolutionReport.push(` - ${violation}`);
|
||||
});
|
||||
violations.push('NO_ASSIGNMENTS: Solver reported success but produced no assignments');
|
||||
console.warn('Solver reported success but produced no assignments. Solution:', solution);
|
||||
}
|
||||
|
||||
// Add assignment statistics
|
||||
|
||||
Reference in New Issue
Block a user