diff --git a/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx b/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx
index 0f3b1fc..72f4cb2 100644
--- a/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx
+++ b/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx
@@ -167,11 +167,29 @@ const ShiftPlanView: React.FC = () => {
setAssignmentResult(result);
setShowAssignmentPreview(true);
- if (!result.success) {
+ // Zeige Reparatur-Bericht in der Konsole
+ if (result.resolutionReport) {
+ console.log('🔧 Reparatur-Bericht:');
+ result.resolutionReport.forEach(line => console.log(line));
+ }
+
+ // Verwende allProblemsResolved für die Erfolgsmeldung
+ if (result.allProblemsResolved) {
showNotification({
- type: 'warning',
- title: 'Warnung',
- message: `Automatische Zuordnung hat ${result.violations.length} Probleme gefunden.`
+ type: 'success',
+ title: 'Erfolg',
+ message: 'Alle kritischen Probleme wurden behoben! Der Schichtplan kann veröffentlicht werden.'
+ });
+ } else {
+ const criticalCount = result.violations.filter(v => v.includes('❌ KRITISCH:')).length;
+ const warningCount = result.violations.filter(v => v.includes('⚠️')).length;
+
+ showNotification({
+ type: warningCount > 0 ? 'warning' : 'error',
+ title: criticalCount > 0 ? 'Kritische Probleme' : 'Warnungen',
+ message: criticalCount > 0
+ ? `${criticalCount} kritische Probleme müssen behoben werden`
+ : `${warningCount} Warnungen - Plan kann trotzdem veröffentlicht werden`
});
}
@@ -641,78 +659,95 @@ const ShiftPlanView: React.FC = () => {
{/* Assignment Preview Modal */}
{showAssignmentPreview && assignmentResult && (
+
-
-
Wochenmuster-Zuordnung
-
- {/* Show weekly pattern info */}
- {assignmentResult.pattern && (
-
-
Wochenmuster erstellt
-
- Der Algorithmus hat ein Muster für {assignmentResult.pattern.weekShifts.length} Schichten in der ersten Woche erstellt
- und dieses für alle {Math.ceil(Object.keys(assignmentResult.assignments).length / assignmentResult.pattern.weekShifts.length)} Wochen im Plan wiederholt.
-
-
-
Wochenmuster-Statistik:
-
- Schichten pro Woche: {assignmentResult.pattern.weekShifts.length}
-
- Zuweisungen pro Woche: {Object.values(assignmentResult.pattern.assignments).flat().length}
-
- Gesamtzuweisungen: {Object.values(assignmentResult.assignments).flat().length}
-
+
Wochenmuster-Zuordnung
+
+ {/* Reparatur-Bericht anzeigen */}
+ {assignmentResult.resolutionReport && (
+
+
Reparatur-Bericht
+
+ {assignmentResult.resolutionReport.map((line, index) => (
+
+ {line}
+
+ ))}
- )}
-
- {assignmentResult.violations.length > 0 && (
-
-
Warnungen:
-
- {assignmentResult.violations.map((violation, index) => (
-
- {violation}
-
- ))}
-
-
- )}
-
-
-
Zusammenfassung:
-
- {assignmentResult.success
- ? '✅ Alle Schichten können zugeordnet werden!'
- : '⚠️ Es gibt Probleme bei der Zuordnung die manuell behoben werden müssen.'}
-
+ )}
+
+ {assignmentResult && (
+
+
Zusammenfassung:
+ {assignmentResult.allProblemsResolved ? (
+
+ ✅ Alle kritischen Probleme behoben! Der Plan kann veröffentlicht werden.
+
+ ) : (
+
+
+ ❌ Es gibt kritische Probleme die behoben werden müssen:
+
+
+ {assignmentResult.violations
+ .filter(v => v.includes('❌ KRITISCH:'))
+ .map((violation, index) => (
+
+ {violation.replace('❌ KRITISCH: ', '')}
+
+ ))}
+
+ {assignmentResult.violations.some(v => v.includes('⚠️')) && (
+
+
+ ⚠️ Warnungen (beeinflussen nicht die Veröffentlichung):
+
+
+ {assignmentResult.violations
+ .filter(v => v.includes('⚠️'))
+ .map((warning, index) => (
+
+ {warning.replace('⚠️ WARNHINWEIS: ', '')}
+
+ ))}
+
+
+ )}
+
+ )}
+
+ )}
{
{publishing ? 'Veröffentliche...' : 'Veröffentlichen'}
diff --git a/frontend/src/services/scheduling/repairFunctions.ts b/frontend/src/services/scheduling/repairFunctions.ts
index bc44142..1a36af0 100644
--- a/frontend/src/services/scheduling/repairFunctions.ts
+++ b/frontend/src/services/scheduling/repairFunctions.ts
@@ -1,15 +1,23 @@
// frontend/src/services/scheduling/repairFunctions.ts
-import { SchedulingEmployee, SchedulingShift, Assignment } from './types';
-import { canAssign, assignEmployee, hasErfahrener } from './utils';
+import { SchedulingEmployee, SchedulingShift, Assignment, RepairContext } from './types';
+import { canAssign, assignEmployee, unassignEmployee,
+ hasErfahrener, candidateScore,
+ countExperiencedCanWorkAlone,
+ isManagerAlone, isManagerShiftWithOnlyNew,
+ onlyNeuAssigned, canRemove,
+ wouldBeAloneIfAdded,
+ hasExperiencedAloneNotAllowed
+} from './utils';
-export function attemptRepairMoveErfahrener(
+export function attemptMoveErfahrenerTo(
targetShiftId: string,
assignments: Assignment,
employees: Map,
allShifts: SchedulingShift[]
): boolean {
-
for (const shift of allShifts) {
+ if (shift.id === targetShiftId) continue;
+
const currentAssignment = assignments[shift.id] || [];
// Skip if this shift doesn't have multiple employees
@@ -22,8 +30,7 @@ export function attemptRepairMoveErfahrener(
// Check if employee can be moved to target shift
if (canAssign(emp, targetShiftId)) {
// Remove from current shift
- assignments[shift.id] = currentAssignment.filter(id => id !== empId);
- emp.assignedCount--;
+ unassignEmployee(emp, shift.id, assignments);
// Add to target shift
assignEmployee(emp, targetShiftId, assignments);
@@ -63,9 +70,13 @@ export function attemptSwapBringErfahrener(
// Check if target employee can go to the other shift
if (canAssign(targetEmp, shift.id)) {
+ // Check if swap would create new violations
+ const tempTargetAssignment = targetAssignment.filter(id => id !== targetEmpId).concat(erfahrenerId);
+ const tempCurrentAssignment = currentAssignment.filter(id => id !== erfahrenerId).concat(targetEmpId);
+
// Perform swap
- assignments[shift.id] = currentAssignment.filter(id => id !== erfahrenerId).concat(targetEmpId);
- assignments[targetShiftId] = targetAssignment.filter(id => id !== targetEmpId).concat(erfahrenerId);
+ assignments[shift.id] = tempCurrentAssignment;
+ assignments[targetShiftId] = tempTargetAssignment;
erfahrener.assignedCount++;
targetEmp.assignedCount--;
@@ -80,13 +91,776 @@ export function attemptSwapBringErfahrener(
return false;
}
-export function attemptComplexRepairForManagerShift(
- targetShiftId: string,
+export function attemptLocalFixNeuAlone(
+ shiftId: string,
assignments: Assignment,
employees: Map,
allShifts: SchedulingShift[]
): boolean {
- // Try multiple repair strategies
- return attemptSwapBringErfahrener(targetShiftId, assignments, employees, allShifts) ||
- attemptRepairMoveErfahrener(targetShiftId, assignments, employees, allShifts);
+ // Try to move an erfahrener to this shift
+ if (attemptMoveErfahrenerTo(shiftId, assignments, employees, allShifts)) {
+ return true;
+ }
+
+ // Try to swap with another shift
+ if (attemptSwapBringErfahrener(shiftId, assignments, employees, allShifts)) {
+ return true;
+ }
+
+ return false;
+}
+
+export function attemptUnassignOrSwap(
+ employeeId: string,
+ assignments: Assignment,
+ employees: Map,
+ allShifts: SchedulingShift[]
+): boolean {
+ const employee = employees.get(employeeId);
+ if (!employee) return false;
+
+ // Find shifts where this employee is assigned
+ const assignedShifts = allShifts.filter(shift =>
+ assignments[shift.id]?.includes(employeeId)
+ );
+
+ // Try to remove from shifts where they're least needed
+ for (const shift of assignedShifts.sort((a, b) => {
+ const aCount = assignments[a.id]?.length || 0;
+ const bCount = assignments[b.id]?.length || 0;
+ return bCount - aCount; // Remove from shifts with most employees first
+ })) {
+ // Check if removal would cause new violations
+ const wouldBeEmpty = (assignments[shift.id]?.length || 0) <= 1;
+ const wouldBeNeuAlone = wouldBeEmpty && employee.role === 'erfahren';
+
+ if (!wouldBeEmpty && !wouldBeNeuAlone) {
+ unassignEmployee(employee, shift.id, assignments);
+ console.log(`📉 Unassigned ${employeeId} from shift ${shift.id} to fix contract`);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+export function attemptFillFromOverallocated(
+ shiftId: string,
+ assignments: Assignment,
+ employees: Map,
+ allShifts: SchedulingShift[]
+): boolean {
+ // Find employees who are underutilized and available for this shift
+ const availableEmployees = Array.from(employees.values())
+ .filter(emp =>
+ canAssign(emp, shiftId) &&
+ emp.assignedCount < emp.contract
+ )
+ .sort((a, b) => candidateScore(a, shiftId) - candidateScore(b, shiftId));
+
+ if (availableEmployees.length > 0) {
+ const bestCandidate = availableEmployees[0];
+ assignEmployee(bestCandidate, shiftId, assignments);
+ console.log(`📈 Filled empty shift ${shiftId} with ${bestCandidate.id}`);
+ return true;
+ }
+
+ return false;
+}
+
+export function resolveTwoExperiencedInShift(
+ shiftId: string,
+ assignments: Assignment,
+ employees: Map,
+ repairContext: RepairContext
+): boolean {
+ const assignment = assignments[shiftId] || [];
+ const experiencedCanWorkAlone = countExperiencedCanWorkAlone(assignment, employees);
+
+ if (experiencedCanWorkAlone.length > 1) {
+ // Finde den erfahrenen Mitarbeiter mit den meisten Zuweisungen
+ const worstCandidate = experiencedCanWorkAlone.reduce((worst, current) => {
+ const worstEmp = employees.get(worst);
+ const currentEmp = employees.get(current);
+ if (!worstEmp || !currentEmp) return worst;
+ return currentEmp.assignedCount > worstEmp.assignedCount ? current : worst;
+ });
+
+ const worstEmp = employees.get(worstCandidate);
+ if (worstEmp && canRemove(worstCandidate, shiftId, repairContext.lockedShifts, assignments, employees)) {
+ // Entferne den Mitarbeiter mit den meisten Zuweisungen
+ unassignEmployee(worstEmp, shiftId, assignments);
+ repairContext.unassignedPool.push(worstCandidate);
+
+ repairContext.warnings.push(`Zwei erfahrene in Schicht ${shiftId} - aufgelöst`);
+ console.log(`🔧 Resolved two experienced in shift ${shiftId}, removed ${worstCandidate}`);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+export function attemptFillFromPool(
+ shiftId: string,
+ assignments: Assignment,
+ employees: Map,
+ repairContext: RepairContext,
+ managerShifts?: string[]
+): boolean {
+ if (assignments[shiftId] && assignments[shiftId].length > 0) return false;
+
+ const isManagerShift = managerShifts?.includes(shiftId) || false;
+
+ // Durchsuche den Pool nach geeigneten Mitarbeitern
+ for (let i = 0; i < repairContext.unassignedPool.length; i++) {
+ const empId = repairContext.unassignedPool[i];
+ const emp = employees.get(empId);
+
+ if (emp && canAssign(emp, shiftId)) {
+ // Für Manager-Schichten: bevorzuge erfahrene Mitarbeiter
+ if (isManagerShift && emp.role !== 'erfahren') {
+ continue;
+ }
+
+ // Prüfe ob Zuweisung neue Probleme verursachen würde
+ if (wouldBeAloneIfAdded(emp, shiftId, assignments, employees)) {
+ continue;
+ }
+
+ assignEmployee(emp, shiftId, assignments);
+ repairContext.unassignedPool.splice(i, 1); // Aus Pool entfernen
+
+ console.log(`📈 Filled ${isManagerShift ? 'manager ' : ''}shift ${shiftId} from pool with ${empId}`);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+export function attemptAddErfahrenerToShift(
+ shiftId: string,
+ assignments: Assignment,
+ employees: Map,
+ allShifts: SchedulingShift[]
+): boolean {
+ const assignment = assignments[shiftId] || [];
+
+ if (!onlyNeuAssigned(assignment, employees)) return false;
+
+ // Suche verfügbaren erfahrenen Mitarbeiter
+ const erfahrenCandidates = Array.from(employees.values()).filter(emp =>
+ emp.role === 'erfahren' &&
+ canAssign(emp, shiftId) &&
+ !assignment.includes(emp.id)
+ );
+
+ if (erfahrenCandidates.length > 0) {
+ // Wähle den besten Kandidaten basierend auf Score
+ 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 erfahrener ${bestCandidate.id} to neu-alone shift ${shiftId}`);
+ return true;
+ }
+
+ return false;
+}
+
+export function checkManagerShiftRules(
+ shiftId: string,
+ assignments: Assignment,
+ employees: Map,
+ managerId: string | undefined,
+ repairContext: RepairContext
+): void {
+ const assignment = assignments[shiftId] || [];
+
+ if (!managerId) return;
+
+ // Manager allein in Schicht
+ if (isManagerAlone(assignment, managerId)) {
+ repairContext.warnings.push(`Manager allein in Schicht ${shiftId}`);
+ repairContext.violations.push({
+ type: 'ManagerAlone',
+ shiftId,
+ severity: 'warning',
+ message: `Manager arbeitet allein in Schicht ${shiftId}`
+ });
+ }
+
+ // Manager + nur Neue ohne Erfahrene
+ if (isManagerShiftWithOnlyNew(assignment, employees, managerId)) {
+ repairContext.warnings.push(`Manager + Neue(r) ohne Erfahrene in Schicht ${shiftId}`);
+ repairContext.violations.push({
+ type: 'ManagerWithOnlyNew',
+ shiftId,
+ severity: 'warning',
+ message: `Manager arbeitet nur mit Neuen in Schicht ${shiftId}`
+ });
+ }
+}
+
+export function attemptSwapForExperienced(
+ managerShiftId: string,
+ assignments: Assignment,
+ employees: Map,
+ allShifts: SchedulingShift[]
+): boolean {
+ const managerAssignment = assignments[managerShiftId] || [];
+ const manager = managerAssignment.find(empId => employees.get(empId)?.role === 'manager');
+ if (!manager) return false;
+
+ // Look for shifts with experienced employees that could swap
+ for (const otherShift of allShifts) {
+ if (otherShift.id === managerShiftId) continue;
+
+ const otherAssignment = assignments[otherShift.id] || [];
+ const experiencedInOther = otherAssignment.filter(empId => {
+ const emp = employees.get(empId);
+ return emp?.role === 'erfahren';
+ });
+
+ for (const experiencedId of experiencedInOther) {
+ const experiencedEmp = employees.get(experiencedId);
+ if (!experiencedEmp) continue;
+
+ // Check if experienced can work in manager shift
+ if (!canAssign(experiencedEmp, managerShiftId)) continue;
+
+ // Find someone from manager shift (non-manager) who can swap to other shift
+ const nonManagerInManagerShift = managerAssignment.filter(id => id !== manager);
+
+ for (const swapCandidateId of nonManagerInManagerShift) {
+ const swapCandidate = employees.get(swapCandidateId);
+ if (!swapCandidate) continue;
+
+ // Check if swap candidate can work in other shift
+ if (canAssign(swapCandidate, otherShift.id)) {
+ // Perform the swap
+ assignments[managerShiftId] = managerAssignment.filter(id => id !== swapCandidateId).concat(experiencedId);
+ assignments[otherShift.id] = otherAssignment.filter(id => id !== experiencedId).concat(swapCandidateId);
+
+ experiencedEmp.assignedCount++;
+ swapCandidate.assignedCount--;
+
+ console.log(`🔄 Swapped ${experiencedId} into manager shift for ${swapCandidateId}`);
+ return true;
+ }
+ }
+
+ // If no swap candidate, try just moving the experienced if other shift won't be empty
+ if (otherAssignment.length > 1) {
+ assignments[managerShiftId].push(experiencedId);
+ assignments[otherShift.id] = otherAssignment.filter(id => id !== experiencedId);
+
+ experiencedEmp.assignedCount++;
+ console.log(`➡️ Moved ${experiencedId} to manager shift from ${otherShift.id}`);
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+// New function to move experienced to manager shift
+export function attemptMoveExperiencedToManagerShift(
+ managerShiftId: string,
+ assignments: Assignment,
+ employees: Map,
+ allShifts: SchedulingShift[]
+): boolean {
+ // Find shifts with multiple experienced employees
+ for (const otherShift of allShifts) {
+ if (otherShift.id === managerShiftId) continue;
+
+ const otherAssignment = assignments[otherShift.id] || [];
+ const experiencedEmployees = otherAssignment.filter(empId => {
+ const emp = employees.get(empId);
+ return emp?.role === 'erfahren';
+ });
+
+ // If this shift has multiple experienced, we can move one
+ if (experiencedEmployees.length > 1) {
+ for (const experiencedId of experiencedEmployees) {
+ const experiencedEmp = employees.get(experiencedId);
+ if (!experiencedEmp) continue;
+
+ if (canAssign(experiencedEmp, managerShiftId)) {
+ // Move the experienced employee
+ assignments[managerShiftId].push(experiencedId);
+ assignments[otherShift.id] = otherAssignment.filter(id => id !== experiencedId);
+
+ experiencedEmp.assignedCount++;
+ console.log(`🎯 Moved experienced ${experiencedId} from overloaded shift to manager shift`);
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+export function resolveOverstaffedExperienced(
+ assignments: Assignment,
+ employees: Map,
+ shifts: SchedulingShift[],
+ repairContext: RepairContext
+): void {
+ const experiencedShifts: { shiftId: string, experiencedCount: number, canWorkAloneCount: number }[] = [];
+
+ // Analysiere alle Schichten auf erfahrene Mitarbeiter
+ shifts.forEach(shift => {
+ const assignment = assignments[shift.id] || [];
+ const experiencedEmployees = assignment.filter(empId => {
+ const emp = employees.get(empId);
+ return emp?.role === 'erfahren';
+ });
+
+ const canWorkAloneCount = experiencedEmployees.filter(empId => {
+ const emp = employees.get(empId);
+ return emp?.originalData?.canWorkAlone;
+ }).length;
+
+ if (experiencedEmployees.length > 0) {
+ experiencedShifts.push({
+ shiftId: shift.id,
+ experiencedCount: experiencedEmployees.length,
+ canWorkAloneCount
+ });
+ }
+ });
+
+ // Sortiere Schichten nach Anzahl der erfahrenen Mitarbeiter (absteigend)
+ experiencedShifts.sort((a, b) => b.experiencedCount - a.experiencedCount);
+
+ // Behebe Schichten mit mehr als 1 erfahrenem Mitarbeiter
+ experiencedShifts.forEach(shiftInfo => {
+ if (shiftInfo.experiencedCount > 1 && !repairContext.lockedShifts.has(shiftInfo.shiftId)) {
+ const assignment = assignments[shiftInfo.shiftId] || [];
+ const experiencedInShift = assignment.filter(empId => {
+ const emp = employees.get(empId);
+ return emp?.role === 'erfahren';
+ });
+
+ // Entferne überschüssige erfahrene Mitarbeiter (behalte mindestens 1)
+ const excessCount = shiftInfo.experiencedCount - 1;
+ const toRemove = experiencedInShift.slice(0, excessCount);
+
+ toRemove.forEach(empId => {
+ const emp = employees.get(empId);
+ if (emp && canRemove(empId, shiftInfo.shiftId, repairContext.lockedShifts, assignments, employees)) {
+ unassignEmployee(emp, shiftInfo.shiftId, assignments);
+ repairContext.unassignedPool.push(empId);
+
+ repairContext.warnings.push(`Überzählige erfahrene in Schicht ${shiftInfo.shiftId} entfernt: ${emp.name}`);
+ console.log(`📉 Removed excess experienced ${empId} from shift ${shiftInfo.shiftId}`);
+ }
+ });
+ }
+ });
+}
+
+// Neue Funktion: Behebe erfahrene Mitarbeiter, die nicht alleine arbeiten dürfen
+export function resolveExperiencedAloneNotAllowed(
+ assignments: Assignment,
+ employees: Map,
+ shifts: SchedulingShift[],
+ repairContext: RepairContext
+): void {
+ shifts.forEach(shift => {
+ if (repairContext.lockedShifts.has(shift.id)) return;
+
+ const assignment = assignments[shift.id] || [];
+ const violation = hasExperiencedAloneNotAllowed(assignment, employees);
+
+ if (violation.hasViolation) {
+ console.log(`⚠️ Experienced alone not allowed in shift ${shift.id}, employee: ${violation.employeeId}`);
+
+ // Versuche einen weiteren Mitarbeiter hinzuzufügen
+ const availableEmployees = Array.from(employees.values()).filter(emp =>
+ emp.id !== violation.employeeId &&
+ canAssign(emp, shift.id) &&
+ !assignment.includes(emp.id) &&
+ !wouldBeAloneIfAdded(emp, shift.id, assignments, employees)
+ );
+
+ if (availableEmployees.length > 0) {
+ // Wähle den besten Kandidaten
+ const bestCandidate = availableEmployees.reduce((best, current) => {
+ const bestScore = candidateScore(best, shift.id);
+ const currentScore = candidateScore(current, shift.id);
+ return currentScore < bestScore ? current : best;
+ });
+
+ assignEmployee(bestCandidate, shift.id, assignments);
+ repairContext.warnings.push(`Zusätzlicher Mitarbeiter zu Schicht ${shift.id} hinzugefügt, um erfahrenen nicht allein zu lassen`);
+ console.log(`✅ Added ${bestCandidate.id} to shift ${shift.id} to prevent experienced alone`);
+ return;
+ }
+
+ // Wenn Hinzufügen nicht möglich, versuche zu tauschen
+ if (attemptSwapForExperiencedAlone(shift.id, violation.employeeId!, assignments, employees, shifts, repairContext)) {
+ return;
+ }
+
+ // Wenn nichts funktioniert, füge Verletzung hinzu
+ const emp = employees.get(violation.employeeId!);
+ repairContext.violations.push({
+ type: 'ExperiencedAloneNotAllowed',
+ shiftId: shift.id,
+ employeeId: violation.employeeId,
+ severity: 'error',
+ message: `Erfahrener Mitarbeiter ${emp?.name || violation.employeeId} arbeitet allein, darf aber nicht alleine arbeiten`
+ });
+ }
+ });
+}
+
+// Neue Funktion: Tausche für experienced-alone Problem
+export function attemptSwapForExperiencedAlone(
+ shiftId: string,
+ experiencedEmpId: string,
+ assignments: Assignment,
+ employees: Map,
+ allShifts: SchedulingShift[],
+ repairContext: RepairContext
+): boolean {
+ const experiencedEmp = employees.get(experiencedEmpId);
+ if (!experiencedEmp) return false;
+
+ // Suche nach Schichten mit mehreren Mitarbeitern, wo wir tauschen können
+ for (const otherShift of allShifts) {
+ if (otherShift.id === shiftId || repairContext.lockedShifts.has(otherShift.id)) continue;
+
+ const otherAssignment = assignments[otherShift.id] || [];
+
+ // Suche nach einem Mitarbeiter, der alleine arbeiten darf
+ for (const otherEmpId of otherAssignment) {
+ const otherEmp = employees.get(otherEmpId);
+ if (!otherEmp) continue;
+
+ // Prüfe ob dieser Mitarbeiter alleine arbeiten darf oder erfahren ist
+ const canWorkAlone = otherEmp.role === 'manager' ||
+ (otherEmp.role === 'erfahren' && otherEmp.originalData?.canWorkAlone) ||
+ otherEmp.role === 'neu';
+
+ if (canWorkAlone) {
+ // Prüfe ob Tausch möglich ist
+ if (canAssign(experiencedEmp, otherShift.id) && canAssign(otherEmp, shiftId)) {
+ // Prüfe ob Tausch neue Probleme verursachen würde
+ const wouldCauseProblemsInTarget = wouldBeAloneIfAdded(experiencedEmp, otherShift.id, assignments, employees);
+ const wouldCauseProblemsInSource = wouldBeAloneIfAdded(otherEmp, shiftId, assignments, employees);
+
+ if (!wouldCauseProblemsInTarget && !wouldCauseProblemsInSource) {
+ // Führe Tausch durch
+ assignments[shiftId] = [otherEmpId];
+ assignments[otherShift.id] = otherAssignment.filter(id => id !== otherEmpId).concat(experiencedEmpId);
+
+ experiencedEmp.assignedCount++;
+ otherEmp.assignedCount--;
+
+ console.log(`🔄 Swapped ${experiencedEmpId} with ${otherEmpId} to resolve experienced-alone`);
+ repairContext.warnings.push(`Getauscht: ${experiencedEmp.name} mit ${otherEmp.name} um allein-Arbeiten zu vermeiden`);
+ return true;
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+// Erweiterte prioritizeWarningsWithPool Funktion
+export function prioritizeWarningsWithPool(
+ assignments: Assignment,
+ employees: Map,
+ shifts: SchedulingShift[],
+ managerShifts: string[],
+ repairContext: RepairContext
+): void {
+ if (repairContext.unassignedPool.length === 0) return;
+
+ // Identifiziere Schichten mit Warnungen (priorisierte Reihenfolge)
+ const warningShifts: { shiftId: string, priority: number, reason: string }[] = [];
+
+ shifts.forEach(shift => {
+ const assignment = assignments[shift.id] || [];
+ const manager = Array.from(employees.values()).find(emp => emp.role === 'manager');
+
+ // 1. Höchste Priorität: Experienced alone not allowed
+ const experiencedAloneCheck = hasExperiencedAloneNotAllowed(assignment, employees);
+ if (experiencedAloneCheck.hasViolation) {
+ warningShifts.push({
+ shiftId: shift.id,
+ priority: 1,
+ reason: 'Erfahrener arbeitet allein (nicht erlaubt)'
+ });
+ }
+ // 2. Hohe Priorität: Manager allein
+ else if (managerShifts.includes(shift.id) && isManagerAlone(assignment, manager?.id)) {
+ warningShifts.push({
+ shiftId: shift.id,
+ priority: 2,
+ reason: 'Manager allein'
+ });
+ }
+ // 3. Hohe Priorität: Manager + nur Neue
+ else if (managerShifts.includes(shift.id) && isManagerShiftWithOnlyNew(assignment, employees, manager?.id)) {
+ warningShifts.push({
+ shiftId: shift.id,
+ priority: 3,
+ reason: 'Manager mit nur Neuen'
+ });
+ }
+ // 4. Mittlere Priorität: Nur Neue (nicht-Manager Schichten)
+ else if (!managerShifts.includes(shift.id) && onlyNeuAssigned(assignment, employees)) {
+ warningShifts.push({
+ shiftId: shift.id,
+ priority: 4,
+ reason: 'Nur Neue in Schicht'
+ });
+ }
+ // 5. Niedrige Priorität: Unterbesetzte Schichten
+ else {
+ const shiftObj = shifts.find(s => s.id === shift.id);
+ if (shiftObj && assignment.length < shiftObj.requiredEmployees) {
+ warningShifts.push({
+ shiftId: shift.id,
+ priority: 5,
+ reason: 'Unterbesetzt'
+ });
+ }
+ }
+ });
+
+ // Sortiere nach Priorität
+ warningShifts.sort((a, b) => a.priority - b.priority);
+
+ console.log(`🎯 Found ${warningShifts.length} warning shifts to prioritize`);
+
+ // Weise Pool-Mitarbeiter priorisiert zu
+ warningShifts.forEach(warningShift => {
+ if (repairContext.unassignedPool.length === 0) return;
+
+ const shiftId = warningShift.shiftId;
+ const assignment = assignments[shiftId] || [];
+ const isManagerShift = managerShifts.includes(shiftId);
+ const isExperiencedAlone = warningShift.priority === 1;
+
+ // Für Manager-Schichten und experienced-alone: bevorzuge erfahrene Mitarbeiter
+ let candidates = [...repairContext.unassignedPool];
+
+ if (isManagerShift || isExperiencedAlone) {
+ candidates = candidates.filter(empId => {
+ const emp = employees.get(empId);
+ return emp?.role === 'erfahren';
+ });
+ }
+
+ // Finde den besten Kandidaten für diese Schicht
+ const bestCandidate = candidates.reduce((best: string | null, current) => {
+ if (!best) return current;
+
+ const bestEmp = employees.get(best);
+ const currentEmp = employees.get(current);
+
+ if (!bestEmp || !currentEmp) return best;
+
+ const bestScore = candidateScore(bestEmp, shiftId);
+ const currentScore = candidateScore(currentEmp, shiftId);
+
+ return currentScore < bestScore ? current : best;
+ }, null);
+
+ if (bestCandidate) {
+ const emp = employees.get(bestCandidate);
+ if (emp && canAssign(emp, shiftId)) {
+ // Prüfe ob Zuweisung neue Probleme verursachen würde
+ const wouldCauseProblems = wouldBeAloneIfAdded(emp, shiftId, assignments, employees);
+
+ if (!wouldCauseProblems) {
+ assignEmployee(emp, shiftId, assignments);
+
+ // Entferne aus Pool
+ const poolIndex = repairContext.unassignedPool.indexOf(bestCandidate);
+ if (poolIndex > -1) {
+ repairContext.unassignedPool.splice(poolIndex, 1);
+ }
+
+ console.log(`🎯 Assigned ${bestCandidate} from pool to ${warningShift.reason} shift ${shiftId}`);
+ repairContext.warnings.push(`Pool-Mitarbeiter ${emp.name} zugewiesen zu ${warningShift.reason} Schicht`);
+ }
+ }
+ }
+ });
+}
+
+export function checkResolvedWarnings(
+ assignments: Assignment,
+ employees: Map,
+ shifts: SchedulingShift[],
+ managerShifts: string[],
+ initialWarnings: string[]
+): { resolved: string[]; remaining: string[] } {
+ const resolved: string[] = [];
+ const remaining: string[] = [];
+ const manager = Array.from(employees.values()).find(emp => emp.role === 'manager');
+
+ // Prüfe jede anfängliche Warnung
+ initialWarnings.forEach(warning => {
+ let isResolved = true;
+
+ // Extrahiere die Shift-ID aus der Warnung
+ const shiftIdMatch = warning.match(/Schicht\s+([a-f0-9-]+)/);
+ if (shiftIdMatch) {
+ const shiftId = shiftIdMatch[1];
+ const assignment = assignments[shiftId] || [];
+
+ // Prüfe basierend auf Warnungstyp
+ if (warning.includes('Manager allein')) {
+ isResolved = !isManagerAlone(assignment, manager?.id);
+ } else if (warning.includes('Manager mit nur Neuen')) {
+ isResolved = !isManagerShiftWithOnlyNew(assignment, employees, manager?.id);
+ } else if (warning.includes('Zwei erfahrene in Schicht')) {
+ const experiencedCanWorkAlone = countExperiencedCanWorkAlone(assignment, employees);
+ isResolved = experiencedCanWorkAlone.length <= 1;
+ } else if (warning.includes('Überzählige erfahrene')) {
+ // Diese Warnung zeigt eine durchgeführte Aktion an, nicht ein bestehendes Problem
+ isResolved = true;
+ } else if (warning.includes('Erfahrener arbeitet allein')) {
+ const violation = hasExperiencedAloneNotAllowed(assignment, employees);
+ isResolved = !violation.hasViolation;
+ }
+ } else {
+ // Für Warnungen ohne spezifische Shift-ID
+ isResolved = !warning.includes('nicht repariert');
+ }
+
+ if (isResolved) {
+ resolved.push(`✅ BEHOBEN: ${warning}`);
+ } else {
+ remaining.push(`❌ VERBLEIBEND: ${warning}`);
+ }
+ });
+
+ return { resolved, remaining };
+}
+
+// Erweiterte Funktion zur Überprüfung aller Probleme
+export function checkAllProblemsResolved(
+ assignments: Assignment,
+ employees: Map,
+ shifts: SchedulingShift[],
+ managerShifts: string[],
+ finalViolations: string[]
+): { resolved: string[]; remaining: string[]; allResolved: boolean } {
+ const resolved: string[] = [];
+ const remaining: string[] = [];
+ const manager = Array.from(employees.values()).find(emp => emp.role === 'manager');
+
+ // Prüfe jede finale Violation
+ finalViolations.forEach(violation => {
+ // IGNORIERE Aktionen - das sind keine Probleme
+ if (violation.includes('Pool-Mitarbeiter') && violation.includes('zugewiesen')) {
+ resolved.push(`✅ AKTION: ${violation.replace('WARNING: ', '')}`);
+ return; // Überspringe diese Meldung
+ }
+
+ if (violation.startsWith('ERROR:')) {
+ // ... bestehende ERROR-Logik ...
+ } else if (violation.startsWith('WARNING:')) {
+ const warningText = violation.replace('WARNING: ', '');
+ const shiftIdMatch = warningText.match(/Schicht\s+([a-f0-9-]+)/);
+
+ if (shiftIdMatch) {
+ const shiftId = shiftIdMatch[1];
+ const assignment = assignments[shiftId] || [];
+ let isResolved = false;
+
+ // Prüfe basierend auf Warnungstyp
+ if (warningText.includes('Manager allein')) {
+ isResolved = !isManagerAlone(assignment, manager?.id);
+ } else if (warningText.includes('Manager mit nur Neuen')) {
+ isResolved = !isManagerShiftWithOnlyNew(assignment, employees, manager?.id);
+ } else if (warningText.includes('Zwei erfahrene in Schicht')) {
+ const experiencedCanWorkAlone = countExperiencedCanWorkAlone(assignment, employees);
+ isResolved = experiencedCanWorkAlone.length <= 1;
+ } else if (warningText.includes('Erfahrener arbeitet allein')) {
+ const violationCheck = hasExperiencedAloneNotAllowed(assignment, employees);
+ isResolved = !violationCheck.hasViolation;
+ } else {
+ // Für allgemeine Warnungen
+ isResolved = true;
+ }
+
+ if (isResolved) {
+ resolved.push(`✅ BEHOBEN: ${warningText}`);
+ } else {
+ remaining.push(`⚠️ WARNHINWEIS: ${warningText}`);
+ }
+ } else {
+ // Für Warnungen ohne Shift-ID
+ remaining.push(`⚠️ WARNHINWEIS: ${warningText}`);
+ }
+ }
+ });
+
+ // Zusätzliche Prüfung auf KRITISCHE Probleme (die ein Publizieren verhindern)
+ const criticalProblems: string[] = [];
+
+ shifts.forEach(shift => {
+ const assignment = assignments[shift.id] || [];
+
+ // KRITISCH: Manager allein
+ if (managerShifts.includes(shift.id) && isManagerAlone(assignment, manager?.id)) {
+ criticalProblems.push(`Manager allein in Schicht ${shift.id}`);
+ }
+
+ // KRITISCH: Erfahrene allein (nicht erlaubt)
+ const experiencedAloneCheck = hasExperiencedAloneNotAllowed(assignment, employees);
+ if (experiencedAloneCheck.hasViolation) {
+ const emp = employees.get(experiencedAloneCheck.employeeId!);
+ criticalProblems.push(`Erfahrener ${emp?.name} arbeitet allein in Schicht ${shift.id}`);
+ }
+
+ // KRITISCH: Leere Schichten
+ if (assignment.length === 0) {
+ criticalProblems.push(`Leere Schicht ${shift.id}`);
+ }
+
+ // KRITISCH: Nur Neue in normalen Schichten
+ if (!managerShifts.includes(shift.id) && onlyNeuAssigned(assignment, employees)) {
+ criticalProblems.push(`Nur Neue in Schicht ${shift.id}`);
+ }
+
+ // KRITISCH: Vertragsüberschreitungen
+ employees.forEach(emp => {
+ if (emp.role !== 'manager' && emp.assignedCount > emp.contract) {
+ criticalProblems.push(`Vertragslimit überschritten für ${emp.name}`);
+ }
+ });
+
+ // NICHT KRITISCH: Manager mit nur Neuen (nur Warnung)
+ // -> wird nicht in criticalProblems aufgenommen
+ });
+
+ // Füge kritische Probleme zu remaining hinzu
+ criticalProblems.forEach(problem => {
+ if (!remaining.some(r => r.includes(problem))) {
+ remaining.push(`❌ KRITISCH: ${problem}`);
+ }
+ });
+
+ const allResolved = criticalProblems.length === 0;
+
+ return { resolved, remaining, allResolved };
}
\ No newline at end of file
diff --git a/frontend/src/services/scheduling/shiftScheduler.ts b/frontend/src/services/scheduling/shiftScheduler.ts
index 25acc0f..aff6580 100644
--- a/frontend/src/services/scheduling/shiftScheduler.ts
+++ b/frontend/src/services/scheduling/shiftScheduler.ts
@@ -4,7 +4,8 @@ import {
SchedulingShift,
Assignment,
SchedulingResult,
- SchedulingConstraints
+ SchedulingConstraints,
+ RepairContext
} from './types';
import {
canAssign,
@@ -12,23 +13,38 @@ import {
onlyNeuAssigned,
assignEmployee,
hasErfahrener,
- respectsNewRuleIfAdded
+ wouldBeAloneIfAdded,
+ findViolations,
+ isManagerShiftWithOnlyNew,
+ isManagerAlone,
+ hasExperiencedAloneNotAllowed
} from './utils';
import {
- attemptRepairMoveErfahrener,
+ attemptMoveErfahrenerTo,
attemptSwapBringErfahrener,
- attemptComplexRepairForManagerShift
+ attemptLocalFixNeuAlone,
+ attemptUnassignOrSwap,
+ attemptFillFromOverallocated,
+ attemptAddErfahrenerToShift,
+ attemptFillFromPool,
+ checkManagerShiftRules,
+ resolveTwoExperiencedInShift,
+ attemptMoveExperiencedToManagerShift,
+ attemptSwapForExperienced,
+ resolveOverstaffedExperienced,
+ prioritizeWarningsWithPool,
+ resolveExperiencedAloneNotAllowed,
+ checkAllProblemsResolved
} from './repairFunctions';
-export function scheduleWithManager(
+// Phase A: Regular employee scheduling (without manager)
+function phaseAPlan(
shifts: SchedulingShift[],
employees: SchedulingEmployee[],
- managerShifts: string[], // shift IDs where manager should be assigned
constraints: SchedulingConstraints
-): SchedulingResult {
-
+): { assignments: Assignment; warnings: string[] } {
const assignments: Assignment = {};
- const violations: string[] = [];
+ const warnings: string[] = [];
const employeeMap = new Map(employees.map(emp => [emp.id, emp]));
// Initialize assignments
@@ -36,9 +52,6 @@ export function scheduleWithManager(
assignments[shift.id] = [];
});
- // Find manager
- const manager = employees.find(emp => emp.role === 'manager');
-
// Helper function to find best candidate
function findBestCandidate(candidates: SchedulingEmployee[], shiftId: string): SchedulingEmployee | null {
if (candidates.length === 0) return null;
@@ -46,29 +59,16 @@ export function scheduleWithManager(
return candidates.reduce((best, current) => {
const bestScore = candidateScore(best, shiftId);
const currentScore = candidateScore(current, shiftId);
-
- // Compare arrays lexicographically
- for (let i = 0; i < bestScore.length; i++) {
- if (currentScore[i] < bestScore[i]) return current;
- if (currentScore[i] > bestScore[i]) return best;
- }
- return best;
+ return currentScore < bestScore ? current : best;
});
}
- // PHASE A: Regular employee scheduling (IGNORE ManagerShifts)
- const nonManagerShifts = shifts.filter(shift => !managerShifts.includes(shift.id));
-
- // 1) Basic coverage: at least 1 person per shift (prefer erfahrene)
- for (const shift of nonManagerShifts) {
- const candidates = employees.filter(emp =>
- emp.role !== 'manager' &&
- canAssign(emp, shift.id) &&
- respectsNewRuleIfAdded(emp, shift.id, assignments, employeeMap)
- );
-
+ // 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) {
- violations.push(`No available employees for shift ${shift.id}`);
+ warnings.push(`No available employees for shift ${shift.id}`);
continue;
}
@@ -84,13 +84,12 @@ export function scheduleWithManager(
}
}
- // 2) Ensure: no neu working alone (Phase A)
+ // 2) Prevent 'neu alone' if constraint enabled
if (constraints.enforceNoTraineeAlone) {
- for (const shift of nonManagerShifts) {
+ for (const shift of shifts) {
if (onlyNeuAssigned(assignments[shift.id], employeeMap)) {
const erfahrenCandidates = employees.filter(emp =>
- emp.role === 'erfahren' &&
- canAssign(emp, shift.id)
+ emp.role === 'erfahren' && canAssign(emp, shift.id)
);
if (erfahrenCandidates.length > 0) {
@@ -100,22 +99,23 @@ export function scheduleWithManager(
}
} else {
// Try repair
- if (!attemptRepairMoveErfahrener(shift.id, assignments, employeeMap, shifts)) {
- violations.push(`Cannot prevent neu-alone in shift ${shift.id}`);
+ if (!attemptMoveErfahrenerTo(shift.id, assignments, employeeMap, shifts)) {
+ warnings.push(`Cannot prevent neu-alone in shift ${shift.id}`);
}
}
}
}
}
- // 3) Goal: up to required employees per shift
- for (const shift of nonManagerShifts) {
- while (assignments[shift.id].length < shift.requiredEmployees) {
+ // 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 =>
- emp.role !== 'manager' &&
canAssign(emp, shift.id) &&
!assignments[shift.id].includes(emp.id) &&
- respectsNewRuleIfAdded(emp, shift.id, assignments, employeeMap)
+ !wouldBeAloneIfAdded(emp, shift.id, assignments, employeeMap)
);
if (candidates.length === 0) break;
@@ -129,63 +129,362 @@ export function scheduleWithManager(
}
}
- // PHASE B: Manager shifts
- if (manager) {
- for (const shiftId of managerShifts) {
- const shift = shifts.find(s => s.id === shiftId);
- if (!shift) continue;
+ return { assignments, warnings };
+}
- // Assign manager to his chosen shifts
- if (!assignments[shiftId].includes(manager.id)) {
- if (manager.availability.get(shiftId) === 3) {
- violations.push(`Manager assigned to shift he marked unavailable: ${shiftId}`);
- }
- assignEmployee(manager, shiftId, assignments);
- }
+// 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]));
- // Rule: if manager present, MUST be at least one ERFAHRENER in same shift
- if (!hasErfahrener(assignments[shiftId], employeeMap)) {
+ if (!manager) return { assignments, warnings };
+
+ console.log(`🎯 Phase B: Processing ${managerShifts.length} manager shifts`);
+
+ for (const shiftId of managerShifts) {
+ const 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)
+ canAssign(emp, shiftId) &&
+ !assignments[shiftId].includes(emp.id)
);
if (erfahrenCandidates.length > 0) {
- const bestCandidate = findBestCandidate(erfahrenCandidates, shiftId);
- if (bestCandidate) {
- assignEmployee(bestCandidate, shiftId, assignments);
- continue;
- }
+ // 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;
}
- // Try repairs
- if (!attemptSwapBringErfahrener(shiftId, assignments, employeeMap, shifts) &&
- !attemptComplexRepairForManagerShift(shiftId, assignments, employeeMap, shifts)) {
- violations.push(`Cannot satisfy manager+erfahren requirement for shift ${shiftId}`);
+ // 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}`);
}
}
}
}
- // FINAL: Validate constraints
- for (const shift of shifts) {
- if (assignments[shift.id].length === 0) {
- violations.push(`Empty shift after scheduling: ${shift.id}`);
- }
- if (constraints.enforceNoTraineeAlone && onlyNeuAssigned(assignments[shift.id], employeeMap)) {
- violations.push(`Neu alone after full scheduling: ${shift.id}`);
- }
- }
+ return { assignments, warnings };
+}
- for (const emp of employees) {
- if (emp.role !== 'manager' && emp.assignedCount > emp.contract) {
- violations.push(`Contract exceeded for employee: ${emp.id}`);
+// 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(),
+ 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: Smart Repair & Validation');
+
+ // 1. Manager-Schutzregel
+ managerShifts.forEach(shiftId => {
+ repairContext.lockedShifts.add(shiftId);
+ });
+
+ // 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}`
+ });
+ }
+ }
+
+ // 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, darf aber nicht alleine arbeiten`
+ });
+ }
+ });
+
+ // 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 -> KRITISCH (error)
+ if (isManagerAlone(assignment, manager?.id)) {
+ repairContext.violations.push({
+ type: 'ManagerAlone',
+ shiftId: shiftId,
+ severity: 'error', // KRITISCH
+ message: `Manager allein in Schicht ${shiftId}`
+ });
+ }
+
+ // Manager + nur Neue -> NUR WARNUNG (warning)
+ if (isManagerShiftWithOnlyNew(assignment, employeeMap, manager?.id)) {
+ repairContext.violations.push({
+ type: 'ManagerWithOnlyNew',
+ shiftId: shiftId,
+ severity: 'warning', // NUR WARNUNG
+ 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 = [
+ // Nur ERROR-Violations als ERROR markieren
+ ...uniqueViolations
+ .filter(v => v.severity === 'error')
+ .map(v => `ERROR: ${v.message}`),
+
+ // WARNING-Violations als WARNING markieren
+ ...uniqueViolations
+ .filter(v => v.severity === 'warning')
+ .map(v => `WARNING: ${v.message}`),
+
+ // Andere Warnungen als INFO (Aktionen)
+ ...uniqueWarnings.map(w => `INFO: ${w}`)
+ ];
+
+ // 9. FINALE ÜBERPRÜFUNG: Prüfe ob alle kritischen Probleme gelöst wurden
+ const resolutionCheck = checkAllProblemsResolved(
+ assignments,
+ employeeMap,
+ shifts,
+ managerShifts,
+ finalViolations
+ );
+
+ const resolutionReport = [
+ '=== REPARATUR-BERICHT ===',
+ `Aufgelöste Probleme: ${resolutionCheck.resolved.length}`,
+ `Verbleibende Probleme: ${resolutionCheck.remaining.length}`,
+ `Alle kritischen Probleme behoben: ${resolutionCheck.allResolved ? '✅ JA' : '❌ NEIN'}`,
+ '',
+ '--- AUFGELÖSTE PROBLEME ---',
+ ...(resolutionCheck.resolved.length > 0 ? resolutionCheck.resolved : ['Keine']),
+ '',
+ '--- VERBLEIBENDE PROBLEME ---',
+ ...(resolutionCheck.remaining.length > 0 ? resolutionCheck.remaining : ['Keine']),
+ '',
+ '=== ENDE BERICHT ==='
+ ];
+
+ console.log('📊 Enhanced Phase C completed:', {
+ poolSize: repairContext.unassignedPool.length,
+ violations: uniqueViolations.length,
+ warnings: uniqueWarnings.length,
+ allProblemsResolved: resolutionCheck.allResolved
+ });
return {
assignments,
- violations,
- success: violations.length === 0
+ violations: finalViolations,
+ resolutionReport,
+ allProblemsResolved: resolutionCheck.allResolved
+ };
+}
+
+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
};
}
\ No newline at end of file
diff --git a/frontend/src/services/scheduling/types.ts b/frontend/src/services/scheduling/types.ts
index 0c2f417..7d293ab 100644
--- a/frontend/src/services/scheduling/types.ts
+++ b/frontend/src/services/scheduling/types.ts
@@ -22,27 +22,54 @@ export interface Assignment {
[shiftId: string]: string[]; // employee IDs
}
-export interface SchedulingResult {
- assignments: Assignment;
- violations: string[];
- success: boolean;
-}
-
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;
+ unassignedPool: string[];
+ warnings: string[];
+ violations: Violation[];
}
\ No newline at end of file
diff --git a/frontend/src/services/scheduling/utils.ts b/frontend/src/services/scheduling/utils.ts
index efbc50c..7c73eac 100644
--- a/frontend/src/services/scheduling/utils.ts
+++ b/frontend/src/services/scheduling/utils.ts
@@ -1,20 +1,29 @@
// 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 true;
+ if (emp.role === 'manager') return false; // Phase A: ignore manager
return emp.assignedCount < emp.contract;
}
-export function candidateScore(emp: SchedulingEmployee, shiftId: string): [number, number, number] {
+export function candidateScore(emp: SchedulingEmployee, shiftId: string): number {
const availability = emp.availability.get(shiftId) || 3;
- const rolePriority = emp.role === 'erfahren' ? 0 : 1;
- return [availability, emp.assignedCount, rolePriority];
-}
-
-export function rolePriority(role: string): number {
- return role === 'erfahren' ? 0 : 1;
+ 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): boolean {
@@ -33,6 +42,13 @@ export function assignEmployee(emp: SchedulingEmployee, shiftId: string, assignm
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): boolean {
return assignment.some(empId => {
const emp = employees.get(empId);
@@ -40,19 +56,173 @@ export function hasErfahrener(assignment: string[], employees: Map
): boolean {
const currentAssignment = assignments[shiftId] || [];
- if (emp.role === 'neu') {
- // Neu employee can only be added if there's already an erfahrener
- return hasErfahrener(currentAssignment, employees);
+ // If adding to empty shift and candidate is neu, they would be alone
+ if (currentAssignment.length === 0 && candidate.role === 'neu') {
+ return true;
}
- // Erfahren employees are always allowed
- 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,
+ 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,
+ assignments: Assignment,
+ employees: Map
+): 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[] {
+ return assignment.filter(empId => {
+ const emp = employees.get(empId);
+ return emp?.role === 'erfahren' && emp.originalData?.canWorkAlone;
+ });
+}
+
+export function isManagerShiftWithOnlyNew(
+ assignment: string[],
+ employees: Map,
+ 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
+): { 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
+): 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;
}
\ No newline at end of file
diff --git a/frontend/src/services/shiftAssignmentService.ts b/frontend/src/services/shiftAssignmentService.ts
index 74ed8ff..279c25f 100644
--- a/frontend/src/services/shiftAssignmentService.ts
+++ b/frontend/src/services/shiftAssignmentService.ts
@@ -127,7 +127,7 @@ export class ShiftAssignmentService {
constraints: any = {}
): Promise {
- console.log('🔄 Starting new scheduling algorithm...');
+ console.log('🔄 Starting enhanced scheduling algorithm...');
// Get defined shifts for the first week
const definedShifts = this.getDefinedShifts(shiftPlan);
@@ -152,7 +152,7 @@ export class ShiftAssignmentService {
managerShifts: managerShifts.length
});
- // Run the scheduling algorithm
+ // Run the enhanced scheduling algorithm with better constraints
const schedulingResult = scheduleWithManager(
schedulingShifts,
schedulingEmployees,
@@ -160,11 +160,12 @@ export class ShiftAssignmentService {
{
enforceNoTraineeAlone: constraints.enforceNoTraineeAlone ?? true,
enforceExperiencedWithChef: constraints.enforceExperiencedWithChef ?? true,
- maxRepairAttempts: constraints.maxRepairAttempts ?? 50
+ maxRepairAttempts: constraints.maxRepairAttempts ?? 50,
+ targetEmployeesPerShift: constraints.targetEmployeesPerShift ?? 2 // Flexible target
}
);
- console.log('📊 Scheduling completed:', {
+ console.log('📊 Enhanced scheduling completed:', {
assignments: Object.keys(schedulingResult.assignments).length,
violations: schedulingResult.violations.length,
success: schedulingResult.success
@@ -183,10 +184,12 @@ export class ShiftAssignmentService {
assignments: allAssignments,
violations: schedulingResult.violations,
success: schedulingResult.violations.length === 0,
- pattern: weeklyPattern
+ pattern: weeklyPattern,
+ resolutionReport: schedulingResult.resolutionReport // Füge diese Zeile hinzu
};
}
+
private static async createWeeklyPattern(
definedShifts: ScheduledShift[],
employees: Employee[],