mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 06:55:45 +01:00
logic working; notifying does not
This commit is contained in:
@@ -167,11 +167,29 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
setAssignmentResult(result);
|
setAssignmentResult(result);
|
||||||
setShowAssignmentPreview(true);
|
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({
|
showNotification({
|
||||||
type: 'warning',
|
type: 'success',
|
||||||
title: 'Warnung',
|
title: 'Erfolg',
|
||||||
message: `Automatische Zuordnung hat ${result.violations.length} Probleme gefunden.`
|
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`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -663,56 +681,73 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
}}>
|
}}>
|
||||||
<h2>Wochenmuster-Zuordnung</h2>
|
<h2>Wochenmuster-Zuordnung</h2>
|
||||||
|
|
||||||
{/* Show weekly pattern info */}
|
{/* Reparatur-Bericht anzeigen */}
|
||||||
{assignmentResult.pattern && (
|
{assignmentResult.resolutionReport && (
|
||||||
<div style={{
|
<div style={{
|
||||||
backgroundColor: '#e8f4fd',
|
backgroundColor: '#e8f4fd',
|
||||||
border: '1px solid #b8d4f0',
|
border: '1px solid #b8d4f0',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
padding: '15px',
|
padding: '15px',
|
||||||
marginBottom: '20px'
|
marginBottom: '20px',
|
||||||
|
fontSize: '14px'
|
||||||
}}>
|
}}>
|
||||||
<h4 style={{ color: '#2c3e50', marginTop: 0 }}>Wochenmuster erstellt</h4>
|
<h4 style={{ color: '#2c3e50', marginTop: 0 }}>Reparatur-Bericht</h4>
|
||||||
<p style={{ margin: 0, color: '#2c3e50' }}>
|
<div style={{ maxHeight: '200px', overflow: 'auto' }}>
|
||||||
Der Algorithmus hat ein Muster für <strong>{assignmentResult.pattern.weekShifts.length} Schichten</strong> in der ersten Woche erstellt
|
{assignmentResult.resolutionReport.map((line, index) => (
|
||||||
und dieses für alle {Math.ceil(Object.keys(assignmentResult.assignments).length / assignmentResult.pattern.weekShifts.length)} Wochen im Plan wiederholt.
|
<div key={index} style={{
|
||||||
</p>
|
color: line.includes('✅') ? '#2ecc71' : line.includes('❌') ? '#e74c3c' : '#2c3e50',
|
||||||
<div style={{ marginTop: '10px', fontSize: '14px' }}>
|
fontFamily: 'monospace',
|
||||||
<strong>Wochenmuster-Statistik:</strong>
|
fontSize: '12px',
|
||||||
<div>- Schichten pro Woche: {assignmentResult.pattern.weekShifts.length}</div>
|
marginBottom: '2px'
|
||||||
<div>- Zuweisungen pro Woche: {Object.values(assignmentResult.pattern.assignments).flat().length}</div>
|
}}>
|
||||||
<div>- Gesamtzuweisungen: {Object.values(assignmentResult.assignments).flat().length}</div>
|
{line}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{assignmentResult.violations.length > 0 && (
|
{assignmentResult && (
|
||||||
<div style={{
|
<div style={{ marginBottom: '20px' }}>
|
||||||
backgroundColor: '#fff3cd',
|
<h4>Zusammenfassung:</h4>
|
||||||
border: '1px solid #ffeaa7',
|
{assignmentResult.allProblemsResolved ? (
|
||||||
borderRadius: '4px',
|
<p style={{ color: '#2ecc71', fontWeight: 'bold' }}>
|
||||||
padding: '15px',
|
✅ Alle kritischen Probleme behoben! Der Plan kann veröffentlicht werden.
|
||||||
marginBottom: '20px'
|
</p>
|
||||||
}}>
|
) : (
|
||||||
<h4 style={{ color: '#856404', marginTop: 0 }}>Warnungen:</h4>
|
<div>
|
||||||
<ul style={{ margin: 0, paddingLeft: '20px' }}>
|
<p style={{ color: '#e74c3c', fontWeight: 'bold' }}>
|
||||||
{assignmentResult.violations.map((violation, index) => (
|
❌ Es gibt kritische Probleme die behoben werden müssen:
|
||||||
<li key={index} style={{ color: '#856404', marginBottom: '5px' }}>
|
</p>
|
||||||
{violation}
|
<ul>
|
||||||
|
{assignmentResult.violations
|
||||||
|
.filter(v => v.includes('❌ KRITISCH:'))
|
||||||
|
.map((violation, index) => (
|
||||||
|
<li key={index} style={{ color: '#e74c3c', fontSize: '14px' }}>
|
||||||
|
{violation.replace('❌ KRITISCH: ', '')}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{assignmentResult.violations.some(v => v.includes('⚠️')) && (
|
||||||
|
<div style={{ marginTop: '10px' }}>
|
||||||
|
<p style={{ color: '#f39c12', fontWeight: 'bold' }}>
|
||||||
|
⚠️ Warnungen (beeinflussen nicht die Veröffentlichung):
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
{assignmentResult.violations
|
||||||
|
.filter(v => v.includes('⚠️'))
|
||||||
|
.map((warning, index) => (
|
||||||
|
<li key={index} style={{ color: '#f39c12', fontSize: '14px' }}>
|
||||||
|
{warning.replace('⚠️ WARNHINWEIS: ', '')}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={{ marginBottom: '20px' }}>
|
|
||||||
<h4>Zusammenfassung:</h4>
|
|
||||||
<p>
|
|
||||||
{assignmentResult.success
|
|
||||||
? '✅ Alle Schichten können zugeordnet werden!'
|
|
||||||
: '⚠️ Es gibt Probleme bei der Zuordnung die manuell behoben werden müssen.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '10px', justifyContent: 'flex-end' }}>
|
<div style={{ display: 'flex', gap: '10px', justifyContent: 'flex-end' }}>
|
||||||
<button
|
<button
|
||||||
@@ -731,14 +766,14 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handlePublish}
|
onClick={handlePublish}
|
||||||
disabled={publishing || !assignmentResult.success}
|
disabled={publishing || !assignmentResult.allProblemsResolved}
|
||||||
style={{
|
style={{
|
||||||
padding: '8px 16px',
|
padding: '8px 16px',
|
||||||
backgroundColor: assignmentResult.success ? '#2ecc71' : '#95a5a6',
|
backgroundColor: assignmentResult.allProblemsResolved ? '#2ecc71' : '#95a5a6',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
cursor: assignmentResult.success ? 'pointer' : 'not-allowed'
|
cursor: assignmentResult.allProblemsResolved ? 'pointer' : 'not-allowed'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{publishing ? 'Veröffentliche...' : 'Veröffentlichen'}
|
{publishing ? 'Veröffentliche...' : 'Veröffentlichen'}
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
// frontend/src/services/scheduling/repairFunctions.ts
|
// frontend/src/services/scheduling/repairFunctions.ts
|
||||||
import { SchedulingEmployee, SchedulingShift, Assignment } from './types';
|
import { SchedulingEmployee, SchedulingShift, Assignment, RepairContext } from './types';
|
||||||
import { canAssign, assignEmployee, hasErfahrener } from './utils';
|
import { canAssign, assignEmployee, unassignEmployee,
|
||||||
|
hasErfahrener, candidateScore,
|
||||||
|
countExperiencedCanWorkAlone,
|
||||||
|
isManagerAlone, isManagerShiftWithOnlyNew,
|
||||||
|
onlyNeuAssigned, canRemove,
|
||||||
|
wouldBeAloneIfAdded,
|
||||||
|
hasExperiencedAloneNotAllowed
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
export function attemptRepairMoveErfahrener(
|
export function attemptMoveErfahrenerTo(
|
||||||
targetShiftId: string,
|
targetShiftId: string,
|
||||||
assignments: Assignment,
|
assignments: Assignment,
|
||||||
employees: Map<string, SchedulingEmployee>,
|
employees: Map<string, SchedulingEmployee>,
|
||||||
allShifts: SchedulingShift[]
|
allShifts: SchedulingShift[]
|
||||||
): boolean {
|
): boolean {
|
||||||
|
|
||||||
for (const shift of allShifts) {
|
for (const shift of allShifts) {
|
||||||
|
if (shift.id === targetShiftId) continue;
|
||||||
|
|
||||||
const currentAssignment = assignments[shift.id] || [];
|
const currentAssignment = assignments[shift.id] || [];
|
||||||
|
|
||||||
// Skip if this shift doesn't have multiple employees
|
// 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
|
// Check if employee can be moved to target shift
|
||||||
if (canAssign(emp, targetShiftId)) {
|
if (canAssign(emp, targetShiftId)) {
|
||||||
// Remove from current shift
|
// Remove from current shift
|
||||||
assignments[shift.id] = currentAssignment.filter(id => id !== empId);
|
unassignEmployee(emp, shift.id, assignments);
|
||||||
emp.assignedCount--;
|
|
||||||
|
|
||||||
// Add to target shift
|
// Add to target shift
|
||||||
assignEmployee(emp, targetShiftId, assignments);
|
assignEmployee(emp, targetShiftId, assignments);
|
||||||
@@ -63,9 +70,13 @@ export function attemptSwapBringErfahrener(
|
|||||||
|
|
||||||
// Check if target employee can go to the other shift
|
// Check if target employee can go to the other shift
|
||||||
if (canAssign(targetEmp, shift.id)) {
|
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
|
// Perform swap
|
||||||
assignments[shift.id] = currentAssignment.filter(id => id !== erfahrenerId).concat(targetEmpId);
|
assignments[shift.id] = tempCurrentAssignment;
|
||||||
assignments[targetShiftId] = targetAssignment.filter(id => id !== targetEmpId).concat(erfahrenerId);
|
assignments[targetShiftId] = tempTargetAssignment;
|
||||||
|
|
||||||
erfahrener.assignedCount++;
|
erfahrener.assignedCount++;
|
||||||
targetEmp.assignedCount--;
|
targetEmp.assignedCount--;
|
||||||
@@ -80,13 +91,776 @@ export function attemptSwapBringErfahrener(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function attemptComplexRepairForManagerShift(
|
export function attemptLocalFixNeuAlone(
|
||||||
targetShiftId: string,
|
shiftId: string,
|
||||||
assignments: Assignment,
|
assignments: Assignment,
|
||||||
employees: Map<string, SchedulingEmployee>,
|
employees: Map<string, SchedulingEmployee>,
|
||||||
allShifts: SchedulingShift[]
|
allShifts: SchedulingShift[]
|
||||||
): boolean {
|
): boolean {
|
||||||
// Try multiple repair strategies
|
// Try to move an erfahrener to this shift
|
||||||
return attemptSwapBringErfahrener(targetShiftId, assignments, employees, allShifts) ||
|
if (attemptMoveErfahrenerTo(shiftId, assignments, employees, allShifts)) {
|
||||||
attemptRepairMoveErfahrener(targetShiftId, 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<string, SchedulingEmployee>,
|
||||||
|
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<string, SchedulingEmployee>,
|
||||||
|
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<string, SchedulingEmployee>,
|
||||||
|
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<string, SchedulingEmployee>,
|
||||||
|
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<string, SchedulingEmployee>,
|
||||||
|
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<string, SchedulingEmployee>,
|
||||||
|
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<string, SchedulingEmployee>,
|
||||||
|
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<string, SchedulingEmployee>,
|
||||||
|
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<string, SchedulingEmployee>,
|
||||||
|
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<string, SchedulingEmployee>,
|
||||||
|
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<string, SchedulingEmployee>,
|
||||||
|
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<string, SchedulingEmployee>,
|
||||||
|
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<string, SchedulingEmployee>,
|
||||||
|
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<string, SchedulingEmployee>,
|
||||||
|
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 };
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,8 @@ import {
|
|||||||
SchedulingShift,
|
SchedulingShift,
|
||||||
Assignment,
|
Assignment,
|
||||||
SchedulingResult,
|
SchedulingResult,
|
||||||
SchedulingConstraints
|
SchedulingConstraints,
|
||||||
|
RepairContext
|
||||||
} from './types';
|
} from './types';
|
||||||
import {
|
import {
|
||||||
canAssign,
|
canAssign,
|
||||||
@@ -12,23 +13,38 @@ import {
|
|||||||
onlyNeuAssigned,
|
onlyNeuAssigned,
|
||||||
assignEmployee,
|
assignEmployee,
|
||||||
hasErfahrener,
|
hasErfahrener,
|
||||||
respectsNewRuleIfAdded
|
wouldBeAloneIfAdded,
|
||||||
|
findViolations,
|
||||||
|
isManagerShiftWithOnlyNew,
|
||||||
|
isManagerAlone,
|
||||||
|
hasExperiencedAloneNotAllowed
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import {
|
import {
|
||||||
attemptRepairMoveErfahrener,
|
attemptMoveErfahrenerTo,
|
||||||
attemptSwapBringErfahrener,
|
attemptSwapBringErfahrener,
|
||||||
attemptComplexRepairForManagerShift
|
attemptLocalFixNeuAlone,
|
||||||
|
attemptUnassignOrSwap,
|
||||||
|
attemptFillFromOverallocated,
|
||||||
|
attemptAddErfahrenerToShift,
|
||||||
|
attemptFillFromPool,
|
||||||
|
checkManagerShiftRules,
|
||||||
|
resolveTwoExperiencedInShift,
|
||||||
|
attemptMoveExperiencedToManagerShift,
|
||||||
|
attemptSwapForExperienced,
|
||||||
|
resolveOverstaffedExperienced,
|
||||||
|
prioritizeWarningsWithPool,
|
||||||
|
resolveExperiencedAloneNotAllowed,
|
||||||
|
checkAllProblemsResolved
|
||||||
} from './repairFunctions';
|
} from './repairFunctions';
|
||||||
|
|
||||||
export function scheduleWithManager(
|
// Phase A: Regular employee scheduling (without manager)
|
||||||
|
function phaseAPlan(
|
||||||
shifts: SchedulingShift[],
|
shifts: SchedulingShift[],
|
||||||
employees: SchedulingEmployee[],
|
employees: SchedulingEmployee[],
|
||||||
managerShifts: string[], // shift IDs where manager should be assigned
|
|
||||||
constraints: SchedulingConstraints
|
constraints: SchedulingConstraints
|
||||||
): SchedulingResult {
|
): { assignments: Assignment; warnings: string[] } {
|
||||||
|
|
||||||
const assignments: Assignment = {};
|
const assignments: Assignment = {};
|
||||||
const violations: string[] = [];
|
const warnings: string[] = [];
|
||||||
const employeeMap = new Map(employees.map(emp => [emp.id, emp]));
|
const employeeMap = new Map(employees.map(emp => [emp.id, emp]));
|
||||||
|
|
||||||
// Initialize assignments
|
// Initialize assignments
|
||||||
@@ -36,9 +52,6 @@ export function scheduleWithManager(
|
|||||||
assignments[shift.id] = [];
|
assignments[shift.id] = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find manager
|
|
||||||
const manager = employees.find(emp => emp.role === 'manager');
|
|
||||||
|
|
||||||
// Helper function to find best candidate
|
// Helper function to find best candidate
|
||||||
function findBestCandidate(candidates: SchedulingEmployee[], shiftId: string): SchedulingEmployee | null {
|
function findBestCandidate(candidates: SchedulingEmployee[], shiftId: string): SchedulingEmployee | null {
|
||||||
if (candidates.length === 0) return null;
|
if (candidates.length === 0) return null;
|
||||||
@@ -46,29 +59,16 @@ export function scheduleWithManager(
|
|||||||
return candidates.reduce((best, current) => {
|
return candidates.reduce((best, current) => {
|
||||||
const bestScore = candidateScore(best, shiftId);
|
const bestScore = candidateScore(best, shiftId);
|
||||||
const currentScore = candidateScore(current, shiftId);
|
const currentScore = candidateScore(current, shiftId);
|
||||||
|
return currentScore < bestScore ? current : best;
|
||||||
// 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;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// PHASE A: Regular employee scheduling (IGNORE ManagerShifts)
|
// 1) Basic coverage: at least 1 person per shift, prefer experienced
|
||||||
const nonManagerShifts = shifts.filter(shift => !managerShifts.includes(shift.id));
|
for (const shift of shifts) {
|
||||||
|
const candidates = employees.filter(emp => canAssign(emp, 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)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (candidates.length === 0) {
|
if (candidates.length === 0) {
|
||||||
violations.push(`No available employees for shift ${shift.id}`);
|
warnings.push(`No available employees for shift ${shift.id}`);
|
||||||
continue;
|
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) {
|
if (constraints.enforceNoTraineeAlone) {
|
||||||
for (const shift of nonManagerShifts) {
|
for (const shift of shifts) {
|
||||||
if (onlyNeuAssigned(assignments[shift.id], employeeMap)) {
|
if (onlyNeuAssigned(assignments[shift.id], employeeMap)) {
|
||||||
const erfahrenCandidates = employees.filter(emp =>
|
const erfahrenCandidates = employees.filter(emp =>
|
||||||
emp.role === 'erfahren' &&
|
emp.role === 'erfahren' && canAssign(emp, shift.id)
|
||||||
canAssign(emp, shift.id)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (erfahrenCandidates.length > 0) {
|
if (erfahrenCandidates.length > 0) {
|
||||||
@@ -100,22 +99,23 @@ export function scheduleWithManager(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Try repair
|
// Try repair
|
||||||
if (!attemptRepairMoveErfahrener(shift.id, assignments, employeeMap, shifts)) {
|
if (!attemptMoveErfahrenerTo(shift.id, assignments, employeeMap, shifts)) {
|
||||||
violations.push(`Cannot prevent neu-alone in shift ${shift.id}`);
|
warnings.push(`Cannot prevent neu-alone in shift ${shift.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Goal: up to required employees per shift
|
// 3) Fill up to target employees per shift
|
||||||
for (const shift of nonManagerShifts) {
|
const targetPerShift = constraints.targetEmployeesPerShift || 2;
|
||||||
while (assignments[shift.id].length < shift.requiredEmployees) {
|
|
||||||
|
for (const shift of shifts) {
|
||||||
|
while (assignments[shift.id].length < targetPerShift) {
|
||||||
const candidates = employees.filter(emp =>
|
const candidates = employees.filter(emp =>
|
||||||
emp.role !== 'manager' &&
|
|
||||||
canAssign(emp, shift.id) &&
|
canAssign(emp, shift.id) &&
|
||||||
!assignments[shift.id].includes(emp.id) &&
|
!assignments[shift.id].includes(emp.id) &&
|
||||||
respectsNewRuleIfAdded(emp, shift.id, assignments, employeeMap)
|
!wouldBeAloneIfAdded(emp, shift.id, assignments, employeeMap)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (candidates.length === 0) break;
|
if (candidates.length === 0) break;
|
||||||
@@ -129,63 +129,362 @@ export function scheduleWithManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PHASE B: Manager shifts
|
return { assignments, warnings };
|
||||||
if (manager) {
|
}
|
||||||
|
|
||||||
|
// 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) {
|
for (const shiftId of managerShifts) {
|
||||||
const shift = shifts.find(s => s.id === shiftId);
|
const shift = nonManagerShifts.find(s => s.id === shiftId) || { id: shiftId, requiredEmployees: 2 };
|
||||||
if (!shift) continue;
|
|
||||||
|
|
||||||
// Assign manager to his chosen shifts
|
// Assign manager to his chosen shifts
|
||||||
if (!assignments[shiftId].includes(manager.id)) {
|
if (!assignments[shiftId].includes(manager.id)) {
|
||||||
if (manager.availability.get(shiftId) === 3) {
|
assignments[shiftId].push(manager.id);
|
||||||
violations.push(`Manager assigned to shift he marked unavailable: ${shiftId}`);
|
manager.assignedCount++;
|
||||||
}
|
console.log(`✅ Assigned manager to shift ${shiftId}`);
|
||||||
assignEmployee(manager, shiftId, assignments);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rule: if manager present, MUST be at least one ERFAHRENER in same shift
|
// Rule: if manager present, MUST have at least one ERFAHRENER
|
||||||
if (!hasErfahrener(assignments[shiftId], employeeMap)) {
|
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 =>
|
const erfahrenCandidates = employees.filter(emp =>
|
||||||
emp.role === 'erfahren' &&
|
emp.role === 'erfahren' &&
|
||||||
canAssign(emp, shiftId)
|
canAssign(emp, shiftId) &&
|
||||||
|
!assignments[shiftId].includes(emp.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (erfahrenCandidates.length > 0) {
|
if (erfahrenCandidates.length > 0) {
|
||||||
const bestCandidate = findBestCandidate(erfahrenCandidates, shiftId);
|
// Find best candidate using scoring
|
||||||
if (bestCandidate) {
|
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);
|
assignEmployee(bestCandidate, shiftId, assignments);
|
||||||
|
console.log(`✅ Added experienced ${bestCandidate.id} to manager shift ${shiftId}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Strategy 2: Try to swap with another shift
|
||||||
|
if (attemptSwapForExperienced(shiftId, assignments, employeeMap, nonManagerShifts)) {
|
||||||
|
console.log(`✅ Swapped experienced into manager shift ${shiftId}`);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try repairs
|
// Strategy 3: Try to move experienced from overloaded shift
|
||||||
if (!attemptSwapBringErfahrener(shiftId, assignments, employeeMap, shifts) &&
|
if (attemptMoveExperiencedToManagerShift(shiftId, assignments, employeeMap, nonManagerShifts)) {
|
||||||
!attemptComplexRepairForManagerShift(shiftId, assignments, employeeMap, shifts)) {
|
console.log(`✅ Moved experienced to manager shift ${shiftId}`);
|
||||||
violations.push(`Cannot satisfy manager+erfahren requirement for 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
|
return { assignments, warnings };
|
||||||
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}`);
|
// 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: 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}`
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const emp of employees) {
|
// 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 (emp.role !== 'manager' && emp.assignedCount > emp.contract) {
|
||||||
violations.push(`Contract exceeded for employee: ${emp.id}`);
|
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 {
|
return {
|
||||||
assignments,
|
assignments,
|
||||||
violations,
|
violations: finalViolations,
|
||||||
success: violations.length === 0
|
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
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -22,23 +22,27 @@ export interface Assignment {
|
|||||||
[shiftId: string]: string[]; // employee IDs
|
[shiftId: string]: string[]; // employee IDs
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SchedulingResult {
|
|
||||||
assignments: Assignment;
|
|
||||||
violations: string[];
|
|
||||||
success: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SchedulingConstraints {
|
export interface SchedulingConstraints {
|
||||||
enforceNoTraineeAlone: boolean;
|
enforceNoTraineeAlone: boolean;
|
||||||
enforceExperiencedWithChef: boolean;
|
enforceExperiencedWithChef: boolean;
|
||||||
maxRepairAttempts: number;
|
maxRepairAttempts: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SchedulingResult {
|
||||||
|
assignments: Assignment;
|
||||||
|
violations: string[];
|
||||||
|
success: boolean;
|
||||||
|
resolutionReport?: string[];
|
||||||
|
allProblemsResolved?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AssignmentResult {
|
export interface AssignmentResult {
|
||||||
assignments: { [shiftId: string]: string[] };
|
assignments: { [shiftId: string]: string[] };
|
||||||
violations: string[];
|
violations: string[];
|
||||||
success: boolean;
|
success: boolean;
|
||||||
pattern: WeeklyPattern;
|
pattern: WeeklyPattern;
|
||||||
|
resolutionReport?: string[];
|
||||||
|
allProblemsResolved?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WeeklyPattern {
|
export interface WeeklyPattern {
|
||||||
@@ -46,3 +50,26 @@ export interface WeeklyPattern {
|
|||||||
assignments: { [shiftId: string]: string[] };
|
assignments: { [shiftId: string]: string[] };
|
||||||
weekNumber: number;
|
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,20 +1,29 @@
|
|||||||
// frontend/src/services/scheduling/utils.ts
|
// frontend/src/services/scheduling/utils.ts
|
||||||
import { SchedulingEmployee, SchedulingShift, Assignment } from './types';
|
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 {
|
export function canAssign(emp: SchedulingEmployee, shiftId: string): boolean {
|
||||||
if (emp.availability.get(shiftId) === 3) return false;
|
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;
|
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 availability = emp.availability.get(shiftId) || 3;
|
||||||
const rolePriority = emp.role === 'erfahren' ? 0 : 1;
|
const baseScore = -getAvailabilityScore(availability); // prefer higher availability scores
|
||||||
return [availability, emp.assignedCount, rolePriority];
|
const loadPenalty = emp.assignedCount * 0.5; // fairness: penalize already assigned
|
||||||
}
|
const rolePenalty = emp.role === 'erfahren' ? 0 : 0.5; // prefer experienced
|
||||||
|
|
||||||
export function rolePriority(role: string): number {
|
return baseScore + loadPenalty + rolePenalty;
|
||||||
return role === 'erfahren' ? 0 : 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onlyNeuAssigned(assignment: string[], employees: Map<string, SchedulingEmployee>): boolean {
|
export function onlyNeuAssigned(assignment: string[], employees: Map<string, SchedulingEmployee>): boolean {
|
||||||
@@ -33,6 +42,13 @@ export function assignEmployee(emp: SchedulingEmployee, shiftId: string, assignm
|
|||||||
emp.assignedCount++;
|
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 {
|
export function hasErfahrener(assignment: string[], employees: Map<string, SchedulingEmployee>): boolean {
|
||||||
return assignment.some(empId => {
|
return assignment.some(empId => {
|
||||||
const emp = employees.get(empId);
|
const emp = employees.get(empId);
|
||||||
@@ -40,19 +56,173 @@ export function hasErfahrener(assignment: string[], employees: Map<string, Sched
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function respectsNewRuleIfAdded(
|
export function wouldBeAloneIfAdded(
|
||||||
emp: SchedulingEmployee,
|
candidate: SchedulingEmployee,
|
||||||
shiftId: string,
|
shiftId: string,
|
||||||
assignments: Assignment,
|
assignments: Assignment,
|
||||||
employees: Map<string, SchedulingEmployee>
|
employees: Map<string, SchedulingEmployee>
|
||||||
): boolean {
|
): boolean {
|
||||||
const currentAssignment = assignments[shiftId] || [];
|
const currentAssignment = assignments[shiftId] || [];
|
||||||
|
|
||||||
if (emp.role === 'neu') {
|
// If adding to empty shift and candidate is neu, they would be alone
|
||||||
// Neu employee can only be added if there's already an erfahrener
|
if (currentAssignment.length === 0 && candidate.role === 'neu') {
|
||||||
return hasErfahrener(currentAssignment, employees);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Erfahren employees are always allowed
|
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
@@ -127,7 +127,7 @@ export class ShiftAssignmentService {
|
|||||||
constraints: any = {}
|
constraints: any = {}
|
||||||
): Promise<AssignmentResult> {
|
): Promise<AssignmentResult> {
|
||||||
|
|
||||||
console.log('🔄 Starting new scheduling algorithm...');
|
console.log('🔄 Starting enhanced scheduling algorithm...');
|
||||||
|
|
||||||
// Get defined shifts for the first week
|
// Get defined shifts for the first week
|
||||||
const definedShifts = this.getDefinedShifts(shiftPlan);
|
const definedShifts = this.getDefinedShifts(shiftPlan);
|
||||||
@@ -152,7 +152,7 @@ export class ShiftAssignmentService {
|
|||||||
managerShifts: managerShifts.length
|
managerShifts: managerShifts.length
|
||||||
});
|
});
|
||||||
|
|
||||||
// Run the scheduling algorithm
|
// Run the enhanced scheduling algorithm with better constraints
|
||||||
const schedulingResult = scheduleWithManager(
|
const schedulingResult = scheduleWithManager(
|
||||||
schedulingShifts,
|
schedulingShifts,
|
||||||
schedulingEmployees,
|
schedulingEmployees,
|
||||||
@@ -160,11 +160,12 @@ export class ShiftAssignmentService {
|
|||||||
{
|
{
|
||||||
enforceNoTraineeAlone: constraints.enforceNoTraineeAlone ?? true,
|
enforceNoTraineeAlone: constraints.enforceNoTraineeAlone ?? true,
|
||||||
enforceExperiencedWithChef: constraints.enforceExperiencedWithChef ?? 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,
|
assignments: Object.keys(schedulingResult.assignments).length,
|
||||||
violations: schedulingResult.violations.length,
|
violations: schedulingResult.violations.length,
|
||||||
success: schedulingResult.success
|
success: schedulingResult.success
|
||||||
@@ -183,10 +184,12 @@ export class ShiftAssignmentService {
|
|||||||
assignments: allAssignments,
|
assignments: allAssignments,
|
||||||
violations: schedulingResult.violations,
|
violations: schedulingResult.violations,
|
||||||
success: schedulingResult.violations.length === 0,
|
success: schedulingResult.violations.length === 0,
|
||||||
pattern: weeklyPattern
|
pattern: weeklyPattern,
|
||||||
|
resolutionReport: schedulingResult.resolutionReport // Füge diese Zeile hinzu
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static async createWeeklyPattern(
|
private static async createWeeklyPattern(
|
||||||
definedShifts: ScheduledShift[],
|
definedShifts: ScheduledShift[],
|
||||||
employees: Employee[],
|
employees: Employee[],
|
||||||
|
|||||||
Reference in New Issue
Block a user