mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 06:55:45 +01:00
reworked scheduling
This commit is contained in:
1829
frontend/src/services/scheduling.ts
Normal file
1829
frontend/src/services/scheduling.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,84 +0,0 @@
|
||||
// frontend/src/services/scheduling/dataAdapter.ts
|
||||
import { Employee, EmployeeAvailability } from '../../models/Employee';
|
||||
import { ScheduledShift } from '../../models/ShiftPlan';
|
||||
import { SchedulingEmployee, SchedulingShift } from './types';
|
||||
|
||||
export function transformToSchedulingData(
|
||||
employees: Employee[],
|
||||
scheduledShifts: ScheduledShift[],
|
||||
availabilities: EmployeeAvailability[]
|
||||
): {
|
||||
schedulingEmployees: SchedulingEmployee[];
|
||||
schedulingShifts: SchedulingShift[];
|
||||
managerShifts: string[];
|
||||
} {
|
||||
|
||||
// Create employee availability map
|
||||
const availabilityMap = new Map<string, Map<string, number>>();
|
||||
|
||||
availabilities.forEach(avail => {
|
||||
if (!availabilityMap.has(avail.employeeId)) {
|
||||
availabilityMap.set(avail.employeeId, new Map());
|
||||
}
|
||||
|
||||
// Create a unique key for each shift pattern (dayOfWeek + timeSlotId)
|
||||
const shiftKey = `${avail.dayOfWeek}-${avail.timeSlotId}`;
|
||||
availabilityMap.get(avail.employeeId)!.set(shiftKey, avail.preferenceLevel);
|
||||
});
|
||||
|
||||
// Transform employees
|
||||
const schedulingEmployees: SchedulingEmployee[] = employees.map(emp => {
|
||||
// Map roles
|
||||
let role: 'manager' | 'erfahren' | 'neu';
|
||||
if (emp.role === 'admin') role = 'manager';
|
||||
else if (emp.employeeType === 'experienced') role = 'erfahren';
|
||||
else role = 'neu';
|
||||
|
||||
// Map contract
|
||||
const contract = emp.contractType === 'small' ? 1 : 2;
|
||||
|
||||
return {
|
||||
id: emp.id,
|
||||
name: emp.name,
|
||||
role,
|
||||
contract,
|
||||
availability: availabilityMap.get(emp.id) || new Map(),
|
||||
assignedCount: 0,
|
||||
originalData: emp
|
||||
};
|
||||
});
|
||||
|
||||
// Transform shifts and identify manager shifts
|
||||
const schedulingShifts: SchedulingShift[] = scheduledShifts.map(scheduledShift => ({
|
||||
id: scheduledShift.id,
|
||||
requiredEmployees: scheduledShift.requiredEmployees,
|
||||
originalData: scheduledShift
|
||||
}));
|
||||
|
||||
// Identify manager shifts (shifts where manager has availability 1 or 2)
|
||||
const manager = schedulingEmployees.find(emp => emp.role === 'manager');
|
||||
const managerShifts: string[] = [];
|
||||
|
||||
if (manager) {
|
||||
scheduledShifts.forEach(scheduledShift => {
|
||||
const dayOfWeek = getDayOfWeek(scheduledShift.date);
|
||||
const shiftKey = `${dayOfWeek}-${scheduledShift.timeSlotId}`;
|
||||
const preference = manager.availability.get(shiftKey);
|
||||
|
||||
if (preference === 1 || preference === 2) {
|
||||
managerShifts.push(scheduledShift.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
schedulingEmployees,
|
||||
schedulingShifts,
|
||||
managerShifts
|
||||
};
|
||||
}
|
||||
|
||||
function getDayOfWeek(dateString: string): number {
|
||||
const date = new Date(dateString);
|
||||
return date.getDay() === 0 ? 7 : date.getDay();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// frontend/src/services/scheduling/index.ts
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
export * from './repairFunctions';
|
||||
export * from './shiftScheduler';
|
||||
export * from './dataAdapter';
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,472 +0,0 @@
|
||||
// frontend/src/services/scheduling/shiftScheduler.ts
|
||||
import {
|
||||
SchedulingEmployee,
|
||||
SchedulingShift,
|
||||
Assignment,
|
||||
SchedulingResult,
|
||||
SchedulingConstraints,
|
||||
RepairContext
|
||||
} from './types';
|
||||
import {
|
||||
canAssign,
|
||||
candidateScore,
|
||||
onlyNeuAssigned,
|
||||
assignEmployee,
|
||||
hasErfahrener,
|
||||
wouldBeAloneIfAdded,
|
||||
isManagerShiftWithOnlyNew,
|
||||
isManagerAlone,
|
||||
hasExperiencedAloneNotAllowed
|
||||
} from './utils';
|
||||
import {
|
||||
attemptMoveErfahrenerTo,
|
||||
attemptUnassignOrSwap,
|
||||
attemptAddErfahrenerToShift,
|
||||
attemptFillFromPool,
|
||||
resolveTwoExperiencedInShift,
|
||||
attemptMoveExperiencedToManagerShift,
|
||||
attemptSwapForExperienced,
|
||||
resolveOverstaffedExperienced,
|
||||
prioritizeWarningsWithPool,
|
||||
resolveExperiencedAloneNotAllowed,
|
||||
checkAllProblemsResolved,
|
||||
createDetailedResolutionReport
|
||||
} from './repairFunctions';
|
||||
|
||||
// Phase A: Regular employee scheduling (without manager)
|
||||
function phaseAPlan(
|
||||
shifts: SchedulingShift[],
|
||||
employees: SchedulingEmployee[],
|
||||
constraints: SchedulingConstraints
|
||||
): { assignments: Assignment; warnings: string[] } {
|
||||
const assignments: Assignment = {};
|
||||
const warnings: string[] = [];
|
||||
const employeeMap = new Map(employees.map(emp => [emp.id, emp]));
|
||||
|
||||
// Initialize assignments
|
||||
shifts.forEach(shift => {
|
||||
assignments[shift.id] = [];
|
||||
});
|
||||
|
||||
// Helper function to find best candidate
|
||||
function findBestCandidate(candidates: SchedulingEmployee[], shiftId: string): SchedulingEmployee | null {
|
||||
if (candidates.length === 0) return null;
|
||||
|
||||
return candidates.reduce((best, current) => {
|
||||
const bestScore = candidateScore(best, shiftId);
|
||||
const currentScore = candidateScore(current, shiftId);
|
||||
return currentScore < bestScore ? current : best;
|
||||
});
|
||||
}
|
||||
|
||||
// 1) Basic coverage: at least 1 person per shift, prefer experienced
|
||||
for (const shift of shifts) {
|
||||
const candidates = employees.filter(emp => canAssign(emp, shift.id));
|
||||
|
||||
if (candidates.length === 0) {
|
||||
warnings.push(`No available employees for shift ${shift.id}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Prefer erfahrene candidates
|
||||
const erfahrenCandidates = candidates.filter(emp => emp.role === 'erfahren');
|
||||
const bestCandidate = findBestCandidate(
|
||||
erfahrenCandidates.length > 0 ? erfahrenCandidates : candidates,
|
||||
shift.id
|
||||
);
|
||||
|
||||
if (bestCandidate) {
|
||||
assignEmployee(bestCandidate, shift.id, assignments);
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Prevent 'neu alone' if constraint enabled
|
||||
if (constraints.enforceNoTraineeAlone) {
|
||||
for (const shift of shifts) {
|
||||
if (onlyNeuAssigned(assignments[shift.id], employeeMap)) {
|
||||
const erfahrenCandidates = employees.filter(emp =>
|
||||
emp.role === 'erfahren' && canAssign(emp, shift.id)
|
||||
);
|
||||
|
||||
if (erfahrenCandidates.length > 0) {
|
||||
const bestCandidate = findBestCandidate(erfahrenCandidates, shift.id);
|
||||
if (bestCandidate) {
|
||||
assignEmployee(bestCandidate, shift.id, assignments);
|
||||
}
|
||||
} else {
|
||||
// Try repair
|
||||
if (!attemptMoveErfahrenerTo(shift.id, assignments, employeeMap, shifts)) {
|
||||
warnings.push(`Cannot prevent neu-alone in shift ${shift.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3) Fill up to target employees per shift
|
||||
const targetPerShift = constraints.targetEmployeesPerShift || 2;
|
||||
|
||||
for (const shift of shifts) {
|
||||
while (assignments[shift.id].length < targetPerShift) {
|
||||
const candidates = employees.filter(emp =>
|
||||
canAssign(emp, shift.id) &&
|
||||
!assignments[shift.id].includes(emp.id) &&
|
||||
!wouldBeAloneIfAdded(emp, shift.id, assignments, employeeMap)
|
||||
);
|
||||
|
||||
if (candidates.length === 0) break;
|
||||
|
||||
const bestCandidate = findBestCandidate(candidates, shift.id);
|
||||
if (bestCandidate) {
|
||||
assignEmployee(bestCandidate, shift.id, assignments);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { assignments, warnings };
|
||||
}
|
||||
|
||||
// Phase B: Insert manager and ensure "experienced with manager"
|
||||
function phaseBInsertManager(
|
||||
assignments: Assignment,
|
||||
manager: SchedulingEmployee | undefined,
|
||||
managerShifts: string[],
|
||||
employees: SchedulingEmployee[],
|
||||
nonManagerShifts: SchedulingShift[],
|
||||
constraints: SchedulingConstraints
|
||||
): { assignments: Assignment; warnings: string[] } {
|
||||
const warnings: string[] = [];
|
||||
const employeeMap = new Map(employees.map(emp => [emp.id, emp]));
|
||||
|
||||
if (!manager) return { assignments, warnings };
|
||||
|
||||
console.log(`🎯 Phase B: Processing ${managerShifts.length} manager shifts`);
|
||||
|
||||
for (const shiftId of managerShifts) {
|
||||
let _shift = nonManagerShifts.find(s => s.id === shiftId) || { id: shiftId, requiredEmployees: 2 };
|
||||
|
||||
// Assign manager to his chosen shifts
|
||||
if (!assignments[shiftId].includes(manager.id)) {
|
||||
assignments[shiftId].push(manager.id);
|
||||
manager.assignedCount++;
|
||||
console.log(`✅ Assigned manager to shift ${shiftId}`);
|
||||
}
|
||||
|
||||
// Rule: if manager present, MUST have at least one ERFAHRENER
|
||||
if (constraints.enforceExperiencedWithChef) {
|
||||
const hasExperienced = hasErfahrener(assignments[shiftId], employeeMap);
|
||||
|
||||
if (!hasExperienced) {
|
||||
console.log(`⚠️ Manager shift ${shiftId} missing experienced employee`);
|
||||
|
||||
// Strategy 1: Try to add an experienced employee directly
|
||||
const erfahrenCandidates = employees.filter(emp =>
|
||||
emp.role === 'erfahren' &&
|
||||
canAssign(emp, shiftId) &&
|
||||
!assignments[shiftId].includes(emp.id)
|
||||
);
|
||||
|
||||
if (erfahrenCandidates.length > 0) {
|
||||
// Find best candidate using scoring
|
||||
const bestCandidate = erfahrenCandidates.reduce((best, current) => {
|
||||
const bestScore = candidateScore(best, shiftId);
|
||||
const currentScore = candidateScore(current, shiftId);
|
||||
return currentScore < bestScore ? current : best;
|
||||
});
|
||||
|
||||
assignEmployee(bestCandidate, shiftId, assignments);
|
||||
console.log(`✅ Added experienced ${bestCandidate.id} to manager shift ${shiftId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strategy 2: Try to swap with another shift
|
||||
if (attemptSwapForExperienced(shiftId, assignments, employeeMap, nonManagerShifts)) {
|
||||
console.log(`✅ Swapped experienced into manager shift ${shiftId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strategy 3: Try to move experienced from overloaded shift
|
||||
if (attemptMoveExperiencedToManagerShift(shiftId, assignments, employeeMap, nonManagerShifts)) {
|
||||
console.log(`✅ Moved experienced to manager shift ${shiftId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Final fallback: Check if we can at least add ANY employee (not just experienced)
|
||||
const anyCandidates = employees.filter(emp =>
|
||||
emp.role !== 'manager' &&
|
||||
canAssign(emp, shiftId) &&
|
||||
!assignments[shiftId].includes(emp.id) &&
|
||||
!wouldBeAloneIfAdded(emp, shiftId, assignments, employeeMap)
|
||||
);
|
||||
|
||||
if (anyCandidates.length > 0) {
|
||||
const bestCandidate = anyCandidates.reduce((best, current) => {
|
||||
const bestScore = candidateScore(best, shiftId);
|
||||
const currentScore = candidateScore(current, shiftId);
|
||||
return currentScore < bestScore ? current : best;
|
||||
});
|
||||
|
||||
assignEmployee(bestCandidate, shiftId, assignments);
|
||||
warnings.push(`Manager shift ${shiftId} has non-experienced backup: ${bestCandidate.name}`);
|
||||
console.log(`⚠️ Added non-experienced backup to manager shift ${shiftId}`);
|
||||
} else {
|
||||
warnings.push(`Manager alone in shift ${shiftId} - no available employees`);
|
||||
console.log(`❌ Cannot fix manager alone in shift ${shiftId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { assignments, warnings };
|
||||
}
|
||||
|
||||
// Phase C: Repair and validate
|
||||
export function enhancedPhaseCRepairValidate(
|
||||
assignments: Assignment,
|
||||
employees: SchedulingEmployee[],
|
||||
shifts: SchedulingShift[],
|
||||
managerShifts: string[],
|
||||
constraints: SchedulingConstraints
|
||||
): { assignments: Assignment; violations: string[]; resolutionReport: string[]; allProblemsResolved: boolean } {
|
||||
|
||||
const repairContext: RepairContext = {
|
||||
lockedShifts: new Set<string>(),
|
||||
unassignedPool: [],
|
||||
warnings: [],
|
||||
violations: []
|
||||
};
|
||||
|
||||
const employeeMap = new Map(employees.map(emp => [emp.id, emp]));
|
||||
const manager = employees.find(emp => emp.role === 'manager');
|
||||
|
||||
console.log('🔄 Starting Enhanced Phase C: Detailed Repair & Validation');
|
||||
|
||||
// 1. Manager-Schutzregel
|
||||
managerShifts.forEach(shiftId => {
|
||||
repairContext.lockedShifts.add(shiftId);
|
||||
repairContext.warnings.push(`Schicht ${shiftId} als Manager-Schicht gesperrt`);
|
||||
});
|
||||
|
||||
// 2. Überbesetzte erfahrene Mitarbeiter identifizieren und in Pool verschieben
|
||||
resolveOverstaffedExperienced(assignments, employeeMap, shifts, repairContext);
|
||||
|
||||
// 3. Erfahrene Mitarbeiter, die nicht alleine arbeiten dürfen
|
||||
resolveExperiencedAloneNotAllowed(assignments, employeeMap, shifts, repairContext);
|
||||
|
||||
// 4. Doppel-Erfahrene-Strafe (nur für Nicht-Manager-Schichten)
|
||||
shifts.forEach(shift => {
|
||||
if (!managerShifts.includes(shift.id)) {
|
||||
resolveTwoExperiencedInShift(shift.id, assignments, employeeMap, repairContext);
|
||||
}
|
||||
});
|
||||
|
||||
// 5. Priorisierte Zuweisung von Pool-Mitarbeitern zu Schichten mit Warnungen
|
||||
prioritizeWarningsWithPool(assignments, employeeMap, shifts, managerShifts, repairContext);
|
||||
|
||||
// 6. Standard-Validation
|
||||
shifts.forEach(shift => {
|
||||
const assignment = assignments[shift.id] || [];
|
||||
|
||||
// Leere Schichten beheben
|
||||
if (assignment.length === 0) {
|
||||
if (!attemptFillFromPool(shift.id, assignments, employeeMap, repairContext, managerShifts)) {
|
||||
repairContext.violations.push({
|
||||
type: 'EmptyShift',
|
||||
shiftId: shift.id,
|
||||
severity: 'error',
|
||||
message: `Leere Schicht: ${shift.id}`
|
||||
});
|
||||
repairContext.warnings.push(`Konnte leere Schicht ${shift.id} nicht beheben`);
|
||||
}
|
||||
}
|
||||
|
||||
// Neu-allein Schichten beheben (nur für Nicht-Manager-Schichten)
|
||||
if (constraints.enforceNoTraineeAlone &&
|
||||
!managerShifts.includes(shift.id) &&
|
||||
onlyNeuAssigned(assignment, employeeMap)) {
|
||||
if (!attemptAddErfahrenerToShift(shift.id, assignments, employeeMap, shifts)) {
|
||||
repairContext.violations.push({
|
||||
type: 'NeuAlone',
|
||||
shiftId: shift.id,
|
||||
severity: 'error',
|
||||
message: `Nur neue Mitarbeiter in Schicht: ${shift.id}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Erfahrene-allein Prüfung (erneut nach Reparaturen)
|
||||
const experiencedAloneCheck = hasExperiencedAloneNotAllowed(assignment, employeeMap);
|
||||
if (experiencedAloneCheck.hasViolation && !repairContext.lockedShifts.has(shift.id)) {
|
||||
const emp = employeeMap.get(experiencedAloneCheck.employeeId!);
|
||||
repairContext.violations.push({
|
||||
type: 'ExperiencedAloneNotAllowed',
|
||||
shiftId: shift.id,
|
||||
employeeId: experiencedAloneCheck.employeeId,
|
||||
severity: 'error',
|
||||
message: `Erfahrener Mitarbeiter ${emp?.name || experiencedAloneCheck.employeeId} arbeitet allein in Schicht ${shift.id}`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 7. Vertragsüberschreitungen beheben
|
||||
employees.forEach(emp => {
|
||||
if (emp.role !== 'manager' && emp.assignedCount > emp.contract) {
|
||||
if (!attemptUnassignOrSwap(emp.id, assignments, employeeMap, shifts)) {
|
||||
repairContext.violations.push({
|
||||
type: 'ContractExceeded',
|
||||
employeeId: emp.id,
|
||||
severity: 'error',
|
||||
message: `Vertragslimit überschritten für: ${emp.name}`
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 8. Nachbesserung: Manager-Schichten prüfen
|
||||
managerShifts.forEach(shiftId => {
|
||||
const assignment = assignments[shiftId] || [];
|
||||
|
||||
// Manager allein
|
||||
if (isManagerAlone(assignment, manager?.id)) {
|
||||
repairContext.violations.push({
|
||||
type: 'ManagerAlone',
|
||||
shiftId: shiftId,
|
||||
severity: 'error',
|
||||
message: `Manager allein in Schicht ${shiftId}`
|
||||
});
|
||||
}
|
||||
|
||||
// Manager + nur Neue
|
||||
if (isManagerShiftWithOnlyNew(assignment, employeeMap, manager?.id)) {
|
||||
repairContext.violations.push({
|
||||
type: 'ManagerWithOnlyNew',
|
||||
shiftId: shiftId,
|
||||
severity: 'warning',
|
||||
message: `Manager mit nur Neuen in Schicht ${shiftId}`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Erstelle finale Violations-Liste
|
||||
const uniqueViolations = repairContext.violations.filter((v, index, self) =>
|
||||
index === self.findIndex(t =>
|
||||
t.type === v.type &&
|
||||
t.shiftId === v.shiftId &&
|
||||
t.employeeId === v.employeeId
|
||||
)
|
||||
);
|
||||
|
||||
const uniqueWarnings = repairContext.warnings.filter((warning, index, array) =>
|
||||
array.indexOf(warning) === index
|
||||
);
|
||||
|
||||
const finalViolations = [
|
||||
...uniqueViolations
|
||||
.filter(v => v.severity === 'error')
|
||||
.map(v => `ERROR: ${v.message}`),
|
||||
...uniqueViolations
|
||||
.filter(v => v.severity === 'warning')
|
||||
.map(v => `WARNING: ${v.message}`),
|
||||
...uniqueWarnings.map(w => `INFO: ${w}`)
|
||||
];
|
||||
|
||||
// 9. DETAILLIERTER REPARATUR-BERICHT
|
||||
const resolutionReport = createDetailedResolutionReport(
|
||||
assignments,
|
||||
employeeMap,
|
||||
shifts,
|
||||
managerShifts,
|
||||
repairContext
|
||||
);
|
||||
|
||||
// Bestimme ob alle kritischen Probleme behoben wurden
|
||||
const criticalProblems = uniqueViolations.filter(v => v.severity === 'error');
|
||||
const allProblemsResolved = criticalProblems.length === 0;
|
||||
|
||||
console.log('📊 Enhanced Phase C completed:', {
|
||||
totalActions: uniqueWarnings.length,
|
||||
criticalProblems: criticalProblems.length,
|
||||
warnings: uniqueViolations.filter(v => v.severity === 'warning').length,
|
||||
allProblemsResolved
|
||||
});
|
||||
|
||||
return {
|
||||
assignments,
|
||||
violations: finalViolations,
|
||||
resolutionReport,
|
||||
allProblemsResolved
|
||||
};
|
||||
}
|
||||
|
||||
export function scheduleWithManager(
|
||||
shifts: SchedulingShift[],
|
||||
employees: SchedulingEmployee[],
|
||||
managerShifts: string[],
|
||||
constraints: SchedulingConstraints
|
||||
): SchedulingResult & { resolutionReport?: string[]; allProblemsResolved?: boolean } {
|
||||
|
||||
const assignments: Assignment = {};
|
||||
const allViolations: string[] = [];
|
||||
|
||||
// Initialisiere Zuweisungen
|
||||
shifts.forEach(shift => {
|
||||
assignments[shift.id] = [];
|
||||
});
|
||||
|
||||
// Finde Manager
|
||||
const manager = employees.find(emp => emp.role === 'manager');
|
||||
|
||||
// Filtere Manager und Nicht-Manager-Schichten für Phase A
|
||||
const nonManagerEmployees = employees.filter(emp => emp.role !== 'manager');
|
||||
const nonManagerShifts = shifts.filter(shift => !managerShifts.includes(shift.id));
|
||||
|
||||
console.log('🔄 Starting Phase A: Regular employee scheduling');
|
||||
|
||||
// Phase A: Reguläre Planung
|
||||
const phaseAResult = phaseAPlan(nonManagerShifts, nonManagerEmployees, constraints);
|
||||
Object.assign(assignments, phaseAResult.assignments);
|
||||
|
||||
console.log('🔄 Starting Phase B: Enhanced Manager insertion');
|
||||
|
||||
// Phase B: Erweiterte Manager-Einfügung
|
||||
const phaseBResult = phaseBInsertManager(
|
||||
assignments,
|
||||
manager,
|
||||
managerShifts,
|
||||
employees,
|
||||
nonManagerShifts,
|
||||
constraints
|
||||
);
|
||||
|
||||
console.log('🔄 Starting Enhanced Phase C: Smart Repair & Validation');
|
||||
|
||||
// Phase C: Erweiterte Reparatur und Validierung mit Pool-Verwaltung
|
||||
const phaseCResult = enhancedPhaseCRepairValidate(assignments, employees, shifts, managerShifts, constraints);
|
||||
|
||||
// Verwende Array.filter für uniqueIssues
|
||||
const uniqueIssues = phaseCResult.violations.filter((issue, index, array) =>
|
||||
array.indexOf(issue) === index
|
||||
);
|
||||
|
||||
// Erfolg basiert jetzt auf allProblemsResolved statt nur auf ERRORs
|
||||
const success = phaseCResult.allProblemsResolved;
|
||||
|
||||
console.log('📊 Enhanced scheduling with pool management completed:', {
|
||||
assignments: Object.keys(assignments).filter(k => assignments[k].length > 0).length,
|
||||
totalShifts: shifts.length,
|
||||
totalIssues: uniqueIssues.length,
|
||||
errors: uniqueIssues.filter(v => v.includes('ERROR:')).length,
|
||||
warnings: uniqueIssues.filter(v => v.includes('WARNING:')).length,
|
||||
allProblemsResolved: success
|
||||
});
|
||||
|
||||
return {
|
||||
assignments,
|
||||
violations: uniqueIssues,
|
||||
success: success,
|
||||
resolutionReport: phaseCResult.resolutionReport,
|
||||
allProblemsResolved: success
|
||||
};
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
// frontend/src/services/scheduling/types.ts
|
||||
import { ScheduledShift } from "../../models/ShiftPlan";
|
||||
|
||||
export interface SchedulingEmployee {
|
||||
id: string;
|
||||
name: string;
|
||||
role: 'manager' | 'erfahren' | 'neu';
|
||||
contract: number; // max assignments per week
|
||||
availability: Map<string, number>; // shiftId -> preferenceLevel (1,2,3)
|
||||
assignedCount: number;
|
||||
originalData: any; // reference to original employee data
|
||||
}
|
||||
|
||||
export interface SchedulingShift {
|
||||
id: string;
|
||||
requiredEmployees: number;
|
||||
isManagerShift?: boolean;
|
||||
originalData: any; // reference to original shift data
|
||||
}
|
||||
|
||||
export interface Assignment {
|
||||
[shiftId: string]: string[]; // employee IDs
|
||||
}
|
||||
|
||||
export interface SchedulingConstraints {
|
||||
enforceNoTraineeAlone: boolean;
|
||||
enforceExperiencedWithChef: boolean;
|
||||
maxRepairAttempts: number;
|
||||
}
|
||||
|
||||
export interface SchedulingResult {
|
||||
assignments: Assignment;
|
||||
violations: string[];
|
||||
success: boolean;
|
||||
resolutionReport?: string[];
|
||||
allProblemsResolved?: boolean;
|
||||
}
|
||||
|
||||
export interface AssignmentResult {
|
||||
assignments: { [shiftId: string]: string[] };
|
||||
violations: string[];
|
||||
success: boolean;
|
||||
pattern: WeeklyPattern;
|
||||
resolutionReport?: string[];
|
||||
allProblemsResolved?: boolean;
|
||||
}
|
||||
|
||||
export interface WeeklyPattern {
|
||||
weekShifts: ScheduledShift[];
|
||||
assignments: { [shiftId: string]: string[] };
|
||||
weekNumber: number;
|
||||
}
|
||||
|
||||
export interface SchedulingConstraints {
|
||||
enforceNoTraineeAlone: boolean;
|
||||
enforceExperiencedWithChef: boolean;
|
||||
maxRepairAttempts: number;
|
||||
targetEmployeesPerShift?: number; // New: flexible target
|
||||
}
|
||||
|
||||
export interface Violation {
|
||||
type: 'EmptyShift' | 'NeuAlone' | 'ContractExceeded' | 'ManagerWithoutExperienced' |
|
||||
'TwoExperiencedInShift' | 'ManagerAlone' | 'ManagerWithOnlyNew' | 'ExperiencedAloneNotAllowed';
|
||||
shiftId?: string;
|
||||
employeeId?: string;
|
||||
severity: 'error' | 'warning';
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface RepairContext {
|
||||
lockedShifts: Set<string>;
|
||||
unassignedPool: string[];
|
||||
warnings: string[];
|
||||
violations: Violation[];
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
// frontend/src/services/scheduling/utils.ts
|
||||
import { SchedulingEmployee, SchedulingShift, Assignment } from './types';
|
||||
|
||||
// Scoring system
|
||||
export function getAvailabilityScore(preferenceLevel: number): number {
|
||||
switch (preferenceLevel) {
|
||||
case 1: return 2; // preferred
|
||||
case 2: return 1; // available
|
||||
case 3: return -9999; // unavailable
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export function canAssign(emp: SchedulingEmployee, shiftId: string): boolean {
|
||||
if (emp.availability.get(shiftId) === 3) return false;
|
||||
if (emp.role === 'manager') return false; // Phase A: ignore manager
|
||||
return emp.assignedCount < emp.contract;
|
||||
}
|
||||
|
||||
export function candidateScore(emp: SchedulingEmployee, shiftId: string): number {
|
||||
const availability = emp.availability.get(shiftId) || 3;
|
||||
const baseScore = -getAvailabilityScore(availability); // prefer higher availability scores
|
||||
const loadPenalty = emp.assignedCount * 0.5; // fairness: penalize already assigned
|
||||
const rolePenalty = emp.role === 'erfahren' ? 0 : 0.5; // prefer experienced
|
||||
|
||||
return baseScore + loadPenalty + rolePenalty;
|
||||
}
|
||||
|
||||
export function onlyNeuAssigned(assignment: string[], employees: Map<string, SchedulingEmployee>): boolean {
|
||||
if (assignment.length === 0) return false;
|
||||
return assignment.every(empId => {
|
||||
const emp = employees.get(empId);
|
||||
return emp?.role === 'neu';
|
||||
});
|
||||
}
|
||||
|
||||
export function assignEmployee(emp: SchedulingEmployee, shiftId: string, assignments: Assignment): void {
|
||||
if (!assignments[shiftId]) {
|
||||
assignments[shiftId] = [];
|
||||
}
|
||||
assignments[shiftId].push(emp.id);
|
||||
emp.assignedCount++;
|
||||
}
|
||||
|
||||
export function unassignEmployee(emp: SchedulingEmployee, shiftId: string, assignments: Assignment): void {
|
||||
if (assignments[shiftId]) {
|
||||
assignments[shiftId] = assignments[shiftId].filter(id => id !== emp.id);
|
||||
emp.assignedCount--;
|
||||
}
|
||||
}
|
||||
|
||||
export function hasErfahrener(assignment: string[], employees: Map<string, SchedulingEmployee>): boolean {
|
||||
return assignment.some(empId => {
|
||||
const emp = employees.get(empId);
|
||||
return emp?.role === 'erfahren';
|
||||
});
|
||||
}
|
||||
|
||||
export function wouldBeAloneIfAdded(
|
||||
candidate: SchedulingEmployee,
|
||||
shiftId: string,
|
||||
assignments: Assignment,
|
||||
employees: Map<string, SchedulingEmployee>
|
||||
): boolean {
|
||||
const currentAssignment = assignments[shiftId] || [];
|
||||
|
||||
// If adding to empty shift and candidate is neu, they would be alone
|
||||
if (currentAssignment.length === 0 && candidate.role === 'neu') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If all current assignments are neu and candidate is neu, they would be alone together
|
||||
if (onlyNeuAssigned(currentAssignment, employees) && candidate.role === 'neu') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function findViolations(
|
||||
assignments: Assignment,
|
||||
employees: Map<string, SchedulingEmployee>,
|
||||
shifts: SchedulingShift[],
|
||||
managerShifts: string[] = []
|
||||
): { type: string; shiftId?: string; employeeId?: string; severity: string }[] {
|
||||
const violations: any[] = [];
|
||||
const employeeMap = employees;
|
||||
|
||||
// Check each shift
|
||||
shifts.forEach(shift => {
|
||||
const assignment = assignments[shift.id] || [];
|
||||
|
||||
// Empty shift violation
|
||||
if (assignment.length === 0) {
|
||||
violations.push({
|
||||
type: 'EmptyShift',
|
||||
shiftId: shift.id,
|
||||
severity: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
// Neu alone violation
|
||||
if (onlyNeuAssigned(assignment, employeeMap)) {
|
||||
violations.push({
|
||||
type: 'NeuAlone',
|
||||
shiftId: shift.id,
|
||||
severity: 'error'
|
||||
});
|
||||
}
|
||||
|
||||
// Manager without experienced (for manager shifts)
|
||||
if (managerShifts.includes(shift.id)) {
|
||||
const hasManager = assignment.some(empId => {
|
||||
const emp = employeeMap.get(empId);
|
||||
return emp?.role === 'manager';
|
||||
});
|
||||
|
||||
if (hasManager && !hasErfahrener(assignment, employeeMap)) {
|
||||
violations.push({
|
||||
type: 'ManagerWithoutExperienced',
|
||||
shiftId: shift.id,
|
||||
severity: 'warning' // Could be warning instead of error
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check employee contracts
|
||||
employeeMap.forEach((emp, empId) => {
|
||||
if (emp.role !== 'manager' && emp.assignedCount > emp.contract) {
|
||||
violations.push({
|
||||
type: 'ContractExceeded',
|
||||
employeeId: empId,
|
||||
severity: 'error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
export function canRemove(
|
||||
empId: string,
|
||||
shiftId: string,
|
||||
lockedShifts: Set<string>,
|
||||
assignments: Assignment,
|
||||
employees: Map<string, SchedulingEmployee>
|
||||
): boolean {
|
||||
// Wenn Schicht gesperrt ist, kann niemand entfernt werden
|
||||
if (lockedShifts.has(shiftId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const emp = employees.get(empId);
|
||||
if (!emp) return false;
|
||||
|
||||
// Überprüfe ob Entfernen neue Verletzungen verursachen würde
|
||||
const currentAssignment = assignments[shiftId] || [];
|
||||
const wouldBeEmpty = currentAssignment.length <= 1;
|
||||
const wouldBeNeuAlone = wouldBeEmpty && emp.role === 'erfahren';
|
||||
const wouldBeExperiencedAlone = wouldBeExperiencedAloneIfRemoved(empId, shiftId, assignments, employees);
|
||||
|
||||
return !wouldBeEmpty && !wouldBeNeuAlone && !wouldBeExperiencedAlone;
|
||||
}
|
||||
|
||||
export function countExperiencedCanWorkAlone(
|
||||
assignment: string[],
|
||||
employees: Map<string, SchedulingEmployee>
|
||||
): string[] {
|
||||
return assignment.filter(empId => {
|
||||
const emp = employees.get(empId);
|
||||
return emp?.role === 'erfahren' && emp.originalData?.canWorkAlone;
|
||||
});
|
||||
}
|
||||
|
||||
export function isManagerShiftWithOnlyNew(
|
||||
assignment: string[],
|
||||
employees: Map<string, SchedulingEmployee>,
|
||||
managerId?: string
|
||||
): boolean {
|
||||
if (!managerId || !assignment.includes(managerId)) return false;
|
||||
|
||||
const nonManagerEmployees = assignment.filter(id => id !== managerId);
|
||||
return onlyNeuAssigned(nonManagerEmployees, employees);
|
||||
}
|
||||
|
||||
export function isManagerAlone(
|
||||
assignment: string[],
|
||||
managerId?: string
|
||||
): boolean {
|
||||
return assignment.length === 1 && assignment[0] === managerId;
|
||||
}
|
||||
|
||||
export function hasExperiencedAloneNotAllowed(
|
||||
assignment: string[],
|
||||
employees: Map<string, SchedulingEmployee>
|
||||
): { hasViolation: boolean; employeeId?: string } {
|
||||
if (assignment.length !== 1) return { hasViolation: false };
|
||||
|
||||
const empId = assignment[0];
|
||||
const emp = employees.get(empId);
|
||||
|
||||
if (emp && emp.role === 'erfahren' && !emp.originalData?.canWorkAlone) {
|
||||
return { hasViolation: true, employeeId: empId };
|
||||
}
|
||||
|
||||
return { hasViolation: false };
|
||||
}
|
||||
|
||||
export function isExperiencedCanWorkAlone(emp: SchedulingEmployee): boolean {
|
||||
return emp.role === 'erfahren' && emp.originalData?.canWorkAlone === true;
|
||||
}
|
||||
|
||||
export function wouldBeExperiencedAloneIfRemoved(
|
||||
empId: string,
|
||||
shiftId: string,
|
||||
assignments: Assignment,
|
||||
employees: Map<string, SchedulingEmployee>
|
||||
): boolean {
|
||||
const assignment = assignments[shiftId] || [];
|
||||
if (assignment.length <= 1) return false;
|
||||
|
||||
const remainingAssignment = assignment.filter(id => id !== empId);
|
||||
if (remainingAssignment.length !== 1) return false;
|
||||
|
||||
const remainingEmp = employees.get(remainingAssignment[0]);
|
||||
return remainingEmp?.role === 'erfahren' && !remainingEmp.originalData?.canWorkAlone;
|
||||
}
|
||||
@@ -2,13 +2,13 @@
|
||||
import { ShiftPlan, ScheduledShift } from '../models/ShiftPlan';
|
||||
import { Employee, EmployeeAvailability } from '../models/Employee';
|
||||
import { authService } from './authService';
|
||||
import { scheduleWithManager } from './scheduling/shiftScheduler';
|
||||
import { transformToSchedulingData } from './scheduling/dataAdapter';
|
||||
import { AssignmentResult, WeeklyPattern } from './scheduling/types';
|
||||
import { IntelligentShiftScheduler, AssignmentResult, WeeklyPattern } from './scheduling';
|
||||
import { isScheduledShift } from '../models/helpers';
|
||||
|
||||
const API_BASE_URL = 'http://localhost:3002/api/scheduled-shifts';
|
||||
|
||||
|
||||
|
||||
// Helper function to get auth headers
|
||||
const getAuthHeaders = () => {
|
||||
const token = localStorage.getItem('token');
|
||||
@@ -21,7 +21,7 @@ const getAuthHeaders = () => {
|
||||
export class ShiftAssignmentService {
|
||||
async updateScheduledShift(id: string, updates: { assignedEmployees: string[] }): Promise<void> {
|
||||
try {
|
||||
console.log('🔄 Updating scheduled shift via API:', { id, updates });
|
||||
//console.log('🔄 Updating scheduled shift via API:', { id, updates });
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/${id}`, {
|
||||
method: 'PUT',
|
||||
@@ -141,65 +141,64 @@ export class ShiftAssignmentService {
|
||||
constraints: any = {}
|
||||
): Promise<AssignmentResult> {
|
||||
|
||||
console.log('🔄 Starting enhanced scheduling algorithm...');
|
||||
console.log('🧠 Starting intelligent scheduling for FIRST WEEK ONLY...');
|
||||
|
||||
// Get defined shifts for the first week
|
||||
const definedShifts = await this.getDefinedShifts(shiftPlan);
|
||||
const firstWeekShifts = this.getFirstWeekShifts(definedShifts);
|
||||
// Load all scheduled shifts
|
||||
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||
|
||||
console.log('📊 First week analysis:', {
|
||||
totalShifts: definedShifts.length,
|
||||
firstWeekShifts: firstWeekShifts.length,
|
||||
employees: employees.length
|
||||
});
|
||||
if (scheduledShifts.length === 0) {
|
||||
return {
|
||||
assignments: {},
|
||||
violations: ['❌ KRITISCH: Keine Schichten verfügbar für die Zuordnung'],
|
||||
success: false,
|
||||
resolutionReport: ['🚨 ABBRUCH: Keine Schichten im Plan verfügbar']
|
||||
};
|
||||
}
|
||||
|
||||
// Transform data for scheduling algorithm
|
||||
const { schedulingEmployees, schedulingShifts, managerShifts } = transformToSchedulingData(
|
||||
// Set cache for scheduler
|
||||
IntelligentShiftScheduler.scheduledShiftsCache.set(shiftPlan.id, scheduledShifts);
|
||||
|
||||
// 🔥 RUN SCHEDULING FOR FIRST WEEK ONLY
|
||||
const schedulingResult = await IntelligentShiftScheduler.generateOptimalSchedule(
|
||||
shiftPlan,
|
||||
employees.filter(emp => emp.isActive),
|
||||
firstWeekShifts,
|
||||
availabilities
|
||||
availabilities,
|
||||
constraints
|
||||
);
|
||||
|
||||
console.log('🎯 Transformed data for scheduling:', {
|
||||
employees: schedulingEmployees.length,
|
||||
shifts: schedulingShifts.length,
|
||||
managerShifts: managerShifts.length
|
||||
// Get first week shifts for pattern
|
||||
const firstWeekShifts = this.getFirstWeekShifts(scheduledShifts);
|
||||
|
||||
console.log('🔄 Creating weekly pattern from FIRST WEEK:', {
|
||||
firstWeekShifts: firstWeekShifts.length,
|
||||
allShifts: scheduledShifts.length,
|
||||
patternAssignments: Object.keys(schedulingResult.assignments).length
|
||||
});
|
||||
|
||||
// Run the enhanced scheduling algorithm with better constraints
|
||||
const schedulingResult = scheduleWithManager(
|
||||
schedulingShifts,
|
||||
schedulingEmployees,
|
||||
managerShifts,
|
||||
{
|
||||
enforceNoTraineeAlone: constraints.enforceNoTraineeAlone ?? true,
|
||||
enforceExperiencedWithChef: constraints.enforceExperiencedWithChef ?? true,
|
||||
maxRepairAttempts: constraints.maxRepairAttempts ?? 50,
|
||||
targetEmployeesPerShift: constraints.targetEmployeesPerShift ?? 2 // Flexible target
|
||||
}
|
||||
);
|
||||
|
||||
console.log('📊 Enhanced scheduling completed:', {
|
||||
assignments: Object.keys(schedulingResult.assignments).length,
|
||||
violations: schedulingResult.violations.length,
|
||||
success: schedulingResult.success
|
||||
});
|
||||
|
||||
// Apply weekly pattern to all shifts
|
||||
const weeklyPattern: WeeklyPattern = {
|
||||
weekShifts: firstWeekShifts,
|
||||
assignments: schedulingResult.assignments,
|
||||
assignments: schedulingResult.assignments, // 🔥 Diese enthalten nur erste Woche
|
||||
weekNumber: 1
|
||||
};
|
||||
|
||||
const allAssignments = this.applyWeeklyPattern(definedShifts, weeklyPattern);
|
||||
// 🔥 APPLY PATTERN TO ALL WEEKS
|
||||
const allAssignments = this.applyWeeklyPattern(scheduledShifts, weeklyPattern);
|
||||
|
||||
console.log('✅ Pattern applied to all weeks:', {
|
||||
firstWeekAssignments: Object.keys(schedulingResult.assignments).length,
|
||||
allWeeksAssignments: Object.keys(allAssignments).length
|
||||
});
|
||||
|
||||
// Clean cache
|
||||
IntelligentShiftScheduler.scheduledShiftsCache.delete(shiftPlan.id);
|
||||
|
||||
return {
|
||||
assignments: allAssignments,
|
||||
assignments: allAssignments, // 🔥 Diese enthalten alle Wochen
|
||||
violations: schedulingResult.violations,
|
||||
success: schedulingResult.violations.length === 0,
|
||||
success: schedulingResult.success,
|
||||
pattern: weeklyPattern,
|
||||
resolutionReport: schedulingResult.resolutionReport // Füge diese Zeile hinzu
|
||||
resolutionReport: schedulingResult.resolutionReport,
|
||||
qualityMetrics: schedulingResult.qualityMetrics
|
||||
};
|
||||
}
|
||||
|
||||
@@ -336,29 +335,48 @@ export class ShiftAssignmentService {
|
||||
|
||||
const assignments: { [shiftId: string]: string[] } = {};
|
||||
|
||||
// Group all shifts by week
|
||||
const shiftsByWeek = this.groupShiftsByWeek(allShifts);
|
||||
// Group all shifts by week AND day-timeSlot combination
|
||||
const shiftsByPatternKey = new Map<string, ScheduledShift[]>();
|
||||
|
||||
console.log('📅 Applying weekly pattern to', Object.keys(shiftsByWeek).length, 'weeks');
|
||||
|
||||
// For each week, apply the pattern from week 1
|
||||
Object.entries(shiftsByWeek).forEach(([weekKey, weekShifts]) => {
|
||||
const weekNumber = parseInt(weekKey);
|
||||
allShifts.forEach(shift => {
|
||||
const dayOfWeek = this.getDayOfWeek(shift.date);
|
||||
const patternKey = `${dayOfWeek}-${shift.timeSlotId}`;
|
||||
|
||||
weekShifts.forEach(shift => {
|
||||
// Find the corresponding shift in the weekly pattern
|
||||
const patternShift = this.findMatchingPatternShift(shift, weeklyPattern.weekShifts);
|
||||
|
||||
if (patternShift) {
|
||||
// Use the same assignment as the pattern shift
|
||||
assignments[shift.id] = [...weeklyPattern.assignments[patternShift.id]];
|
||||
} else {
|
||||
// No matching pattern shift, leave empty
|
||||
assignments[shift.id] = [];
|
||||
}
|
||||
});
|
||||
if (!shiftsByPatternKey.has(patternKey)) {
|
||||
shiftsByPatternKey.set(patternKey, []);
|
||||
}
|
||||
shiftsByPatternKey.get(patternKey)!.push(shift);
|
||||
});
|
||||
|
||||
console.log('📊 Pattern application analysis:');
|
||||
console.log('- Unique pattern keys:', shiftsByPatternKey.size);
|
||||
console.log('- Pattern keys:', Array.from(shiftsByPatternKey.keys()));
|
||||
|
||||
// For each shift in all weeks, find the matching pattern shift
|
||||
allShifts.forEach(shift => {
|
||||
const dayOfWeek = this.getDayOfWeek(shift.date);
|
||||
//const patternKey = `${dayOfWeek}-${shift.timeSlotId}`;
|
||||
const patternKey = `${shift.timeSlotId}`;
|
||||
|
||||
// Find the pattern shift for this day-timeSlot combination
|
||||
const patternShift = weeklyPattern.weekShifts.find(patternShift => {
|
||||
const patternDayOfWeek = this.getDayOfWeek(patternShift.date);
|
||||
return patternDayOfWeek === dayOfWeek &&
|
||||
patternShift.timeSlotId === shift.timeSlotId;
|
||||
});
|
||||
|
||||
if (patternShift && weeklyPattern.assignments[patternShift.id]) {
|
||||
assignments[shift.id] = [...weeklyPattern.assignments[patternShift.id]];
|
||||
} else {
|
||||
assignments[shift.id] = [];
|
||||
console.warn(`❌ No pattern found for shift: ${patternKey}`);
|
||||
}
|
||||
});
|
||||
|
||||
// DEBUG: Check assignment coverage
|
||||
const assignedShifts = Object.values(assignments).filter(a => a.length > 0).length;
|
||||
console.log(`📊 Assignment coverage: ${assignedShifts}/${allShifts.length} shifts assigned`);
|
||||
|
||||
return assignments;
|
||||
}
|
||||
|
||||
|
||||
@@ -128,26 +128,6 @@ export const shiftPlanService = {
|
||||
}
|
||||
},
|
||||
|
||||
async revertToDraft(id: string): Promise<ShiftPlan> {
|
||||
const response = await fetch(`${API_BASE}/${id}/revert-to-draft`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authService.getAuthHeaders()
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
authService.logout();
|
||||
throw new Error('Nicht authorisiert - bitte erneut anmelden');
|
||||
}
|
||||
throw new Error('Fehler beim Zurücksetzen des Schichtplans');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
// Get specific template or plan
|
||||
getTemplate: async (id: string): Promise<ShiftPlan> => {
|
||||
const response = await fetch(`${API_BASE}/${id}`, {
|
||||
@@ -219,4 +199,29 @@ export const shiftPlanService = {
|
||||
description: preset.description
|
||||
}));
|
||||
},
|
||||
|
||||
async clearAssignments(planId: string): Promise<void> {
|
||||
try {
|
||||
console.log('🔄 Clearing assignments for plan:', planId);
|
||||
|
||||
const response = await fetch(`${API_BASE}/${planId}/clear-assignments`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...authService.getAuthHeaders()
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
|
||||
throw new Error(errorData.error || `Failed to clear assignments: ${response.status}`);
|
||||
}
|
||||
|
||||
console.log('✅ Assignments cleared successfully');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error clearing assignments:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user