mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 15:05:45 +01:00
logic working; notifying does not
This commit is contained in:
@@ -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 && (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 1000
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 1000
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
padding: '30px',
|
||||
maxWidth: '800px',
|
||||
maxHeight: '80vh',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
<div style={{
|
||||
backgroundColor: 'white',
|
||||
borderRadius: '8px',
|
||||
padding: '30px',
|
||||
maxWidth: '800px',
|
||||
maxHeight: '80vh',
|
||||
overflow: 'auto'
|
||||
}}>
|
||||
<h2>Wochenmuster-Zuordnung</h2>
|
||||
|
||||
{/* Show weekly pattern info */}
|
||||
{assignmentResult.pattern && (
|
||||
<div style={{
|
||||
backgroundColor: '#e8f4fd',
|
||||
border: '1px solid #b8d4f0',
|
||||
borderRadius: '4px',
|
||||
padding: '15px',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
<h4 style={{ color: '#2c3e50', marginTop: 0 }}>Wochenmuster erstellt</h4>
|
||||
<p style={{ margin: 0, color: '#2c3e50' }}>
|
||||
Der Algorithmus hat ein Muster für <strong>{assignmentResult.pattern.weekShifts.length} Schichten</strong> 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.
|
||||
</p>
|
||||
<div style={{ marginTop: '10px', fontSize: '14px' }}>
|
||||
<strong>Wochenmuster-Statistik:</strong>
|
||||
<div>- Schichten pro Woche: {assignmentResult.pattern.weekShifts.length}</div>
|
||||
<div>- Zuweisungen pro Woche: {Object.values(assignmentResult.pattern.assignments).flat().length}</div>
|
||||
<div>- Gesamtzuweisungen: {Object.values(assignmentResult.assignments).flat().length}</div>
|
||||
</div>
|
||||
<h2>Wochenmuster-Zuordnung</h2>
|
||||
|
||||
{/* Reparatur-Bericht anzeigen */}
|
||||
{assignmentResult.resolutionReport && (
|
||||
<div style={{
|
||||
backgroundColor: '#e8f4fd',
|
||||
border: '1px solid #b8d4f0',
|
||||
borderRadius: '4px',
|
||||
padding: '15px',
|
||||
marginBottom: '20px',
|
||||
fontSize: '14px'
|
||||
}}>
|
||||
<h4 style={{ color: '#2c3e50', marginTop: 0 }}>Reparatur-Bericht</h4>
|
||||
<div style={{ maxHeight: '200px', overflow: 'auto' }}>
|
||||
{assignmentResult.resolutionReport.map((line, index) => (
|
||||
<div key={index} style={{
|
||||
color: line.includes('✅') ? '#2ecc71' : line.includes('❌') ? '#e74c3c' : '#2c3e50',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '12px',
|
||||
marginBottom: '2px'
|
||||
}}>
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{assignmentResult.violations.length > 0 && (
|
||||
<div style={{
|
||||
backgroundColor: '#fff3cd',
|
||||
border: '1px solid #ffeaa7',
|
||||
borderRadius: '4px',
|
||||
padding: '15px',
|
||||
marginBottom: '20px'
|
||||
}}>
|
||||
<h4 style={{ color: '#856404', marginTop: 0 }}>Warnungen:</h4>
|
||||
<ul style={{ margin: 0, paddingLeft: '20px' }}>
|
||||
{assignmentResult.violations.map((violation, index) => (
|
||||
<li key={index} style={{ color: '#856404', marginBottom: '5px' }}>
|
||||
{violation}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{assignmentResult && (
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h4>Zusammenfassung:</h4>
|
||||
{assignmentResult.allProblemsResolved ? (
|
||||
<p style={{ color: '#2ecc71', fontWeight: 'bold' }}>
|
||||
✅ Alle kritischen Probleme behoben! Der Plan kann veröffentlicht werden.
|
||||
</p>
|
||||
) : (
|
||||
<div>
|
||||
<p style={{ color: '#e74c3c', fontWeight: 'bold' }}>
|
||||
❌ Es gibt kritische Probleme die behoben werden müssen:
|
||||
</p>
|
||||
<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>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', gap: '10px', justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
@@ -731,14 +766,14 @@ const ShiftPlanView: React.FC = () => {
|
||||
|
||||
<button
|
||||
onClick={handlePublish}
|
||||
disabled={publishing || !assignmentResult.success}
|
||||
disabled={publishing || !assignmentResult.allProblemsResolved}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: assignmentResult.success ? '#2ecc71' : '#95a5a6',
|
||||
backgroundColor: assignmentResult.allProblemsResolved ? '#2ecc71' : '#95a5a6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: assignmentResult.success ? 'pointer' : 'not-allowed'
|
||||
cursor: assignmentResult.allProblemsResolved ? 'pointer' : 'not-allowed'
|
||||
}}
|
||||
>
|
||||
{publishing ? 'Veröffentliche...' : 'Veröffentlichen'}
|
||||
|
||||
@@ -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<string, SchedulingEmployee>,
|
||||
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<string, SchedulingEmployee>,
|
||||
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<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,
|
||||
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<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}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
};
|
||||
}
|
||||
@@ -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<string>;
|
||||
unassignedPool: string[];
|
||||
warnings: string[];
|
||||
violations: Violation[];
|
||||
}
|
||||
@@ -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<string, SchedulingEmployee>): 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<string, SchedulingEmployee>): boolean {
|
||||
return assignment.some(empId => {
|
||||
const emp = employees.get(empId);
|
||||
@@ -40,19 +56,173 @@ export function hasErfahrener(assignment: string[], employees: Map<string, Sched
|
||||
});
|
||||
}
|
||||
|
||||
export function respectsNewRuleIfAdded(
|
||||
emp: SchedulingEmployee,
|
||||
shiftId: string,
|
||||
assignments: Assignment,
|
||||
export function wouldBeAloneIfAdded(
|
||||
candidate: SchedulingEmployee,
|
||||
shiftId: string,
|
||||
assignments: Assignment,
|
||||
employees: Map<string, SchedulingEmployee>
|
||||
): 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<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 = {}
|
||||
): Promise<AssignmentResult> {
|
||||
|
||||
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[],
|
||||
|
||||
Reference in New Issue
Block a user