logic working; notifying does not

This commit is contained in:
2025-10-14 13:19:37 +02:00
parent 36f64cced8
commit a2aaa12c4c
6 changed files with 1505 additions and 197 deletions

View File

@@ -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`
}); });
} }
@@ -641,78 +659,95 @@ const ShiftPlanView: React.FC = () => {
{/* Assignment Preview Modal */} {/* Assignment Preview Modal */}
{showAssignmentPreview && assignmentResult && ( {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={{ <div style={{
position: 'fixed', backgroundColor: 'white',
top: 0, borderRadius: '8px',
left: 0, padding: '30px',
right: 0, maxWidth: '800px',
bottom: 0, maxHeight: '80vh',
backgroundColor: 'rgba(0,0,0,0.5)', overflow: 'auto'
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 1000
}}> }}>
<div style={{ <h2>Wochenmuster-Zuordnung</h2>
backgroundColor: 'white',
borderRadius: '8px', {/* Reparatur-Bericht anzeigen */}
padding: '30px', {assignmentResult.resolutionReport && (
maxWidth: '800px', <div style={{
maxHeight: '80vh', backgroundColor: '#e8f4fd',
overflow: 'auto' border: '1px solid #b8d4f0',
}}> borderRadius: '4px',
<h2>Wochenmuster-Zuordnung</h2> padding: '15px',
marginBottom: '20px',
{/* Show weekly pattern info */} fontSize: '14px'
{assignmentResult.pattern && ( }}>
<div style={{ <h4 style={{ color: '#2c3e50', marginTop: 0 }}>Reparatur-Bericht</h4>
backgroundColor: '#e8f4fd', <div style={{ maxHeight: '200px', overflow: 'auto' }}>
border: '1px solid #b8d4f0', {assignmentResult.resolutionReport.map((line, index) => (
borderRadius: '4px', <div key={index} style={{
padding: '15px', color: line.includes('✅') ? '#2ecc71' : line.includes('❌') ? '#e74c3c' : '#2c3e50',
marginBottom: '20px' fontFamily: 'monospace',
}}> fontSize: '12px',
<h4 style={{ color: '#2c3e50', marginTop: 0 }}>Wochenmuster erstellt</h4> marginBottom: '2px'
<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 {line}
und dieses für alle {Math.ceil(Object.keys(assignmentResult.assignments).length / assignmentResult.pattern.weekShifts.length)} Wochen im Plan wiederholt. </div>
</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>
</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> </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' }}> <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'}

View File

@@ -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 };
} }

View File

@@ -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) { }
for (const shiftId of managerShifts) {
const shift = shifts.find(s => s.id === shiftId);
if (!shift) continue;
// Assign manager to his chosen shifts // Phase B: Insert manager and ensure "experienced with manager"
if (!assignments[shiftId].includes(manager.id)) { function phaseBInsertManager(
if (manager.availability.get(shiftId) === 3) { assignments: Assignment,
violations.push(`Manager assigned to shift he marked unavailable: ${shiftId}`); manager: SchedulingEmployee | undefined,
} managerShifts: string[],
assignEmployee(manager, shiftId, assignments); 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 (!manager) return { assignments, warnings };
if (!hasErfahrener(assignments[shiftId], employeeMap)) {
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 => 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) => {
assignEmployee(bestCandidate, shiftId, assignments); const bestScore = candidateScore(best, shiftId);
continue; 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 // Strategy 2: Try to swap with another shift
if (!attemptSwapBringErfahrener(shiftId, assignments, employeeMap, shifts) && if (attemptSwapForExperienced(shiftId, assignments, employeeMap, nonManagerShifts)) {
!attemptComplexRepairForManagerShift(shiftId, assignments, employeeMap, shifts)) { console.log(`✅ Swapped experienced into manager shift ${shiftId}`);
violations.push(`Cannot satisfy manager+erfahren requirement for 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 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}`);
}
}
for (const emp of employees) { // Phase C: Repair and validate
if (emp.role !== 'manager' && emp.assignedCount > emp.contract) { export function enhancedPhaseCRepairValidate(
violations.push(`Contract exceeded for employee: ${emp.id}`); 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 { 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
}; };
} }

View File

@@ -22,27 +22,54 @@ 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 {
weekShifts: ScheduledShift[]; weekShifts: ScheduledShift[];
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[];
} }

View File

@@ -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); return true;
} }
// Erfahren employees are always allowed // If all current assignments are neu and candidate is neu, they would be alone together
return true; 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;
} }

View File

@@ -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[],