reworked scheduling

This commit is contained in:
2025-10-18 01:55:04 +02:00
parent b86040dc04
commit f705a83cd4
26 changed files with 3222 additions and 3193 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,84 +0,0 @@
// frontend/src/services/scheduling/dataAdapter.ts
import { Employee, EmployeeAvailability } from '../../models/Employee';
import { ScheduledShift } from '../../models/ShiftPlan';
import { SchedulingEmployee, SchedulingShift } from './types';
export function transformToSchedulingData(
employees: Employee[],
scheduledShifts: ScheduledShift[],
availabilities: EmployeeAvailability[]
): {
schedulingEmployees: SchedulingEmployee[];
schedulingShifts: SchedulingShift[];
managerShifts: string[];
} {
// Create employee availability map
const availabilityMap = new Map<string, Map<string, number>>();
availabilities.forEach(avail => {
if (!availabilityMap.has(avail.employeeId)) {
availabilityMap.set(avail.employeeId, new Map());
}
// Create a unique key for each shift pattern (dayOfWeek + timeSlotId)
const shiftKey = `${avail.dayOfWeek}-${avail.timeSlotId}`;
availabilityMap.get(avail.employeeId)!.set(shiftKey, avail.preferenceLevel);
});
// Transform employees
const schedulingEmployees: SchedulingEmployee[] = employees.map(emp => {
// Map roles
let role: 'manager' | 'erfahren' | 'neu';
if (emp.role === 'admin') role = 'manager';
else if (emp.employeeType === 'experienced') role = 'erfahren';
else role = 'neu';
// Map contract
const contract = emp.contractType === 'small' ? 1 : 2;
return {
id: emp.id,
name: emp.name,
role,
contract,
availability: availabilityMap.get(emp.id) || new Map(),
assignedCount: 0,
originalData: emp
};
});
// Transform shifts and identify manager shifts
const schedulingShifts: SchedulingShift[] = scheduledShifts.map(scheduledShift => ({
id: scheduledShift.id,
requiredEmployees: scheduledShift.requiredEmployees,
originalData: scheduledShift
}));
// Identify manager shifts (shifts where manager has availability 1 or 2)
const manager = schedulingEmployees.find(emp => emp.role === 'manager');
const managerShifts: string[] = [];
if (manager) {
scheduledShifts.forEach(scheduledShift => {
const dayOfWeek = getDayOfWeek(scheduledShift.date);
const shiftKey = `${dayOfWeek}-${scheduledShift.timeSlotId}`;
const preference = manager.availability.get(shiftKey);
if (preference === 1 || preference === 2) {
managerShifts.push(scheduledShift.id);
}
});
}
return {
schedulingEmployees,
schedulingShifts,
managerShifts
};
}
function getDayOfWeek(dateString: string): number {
const date = new Date(dateString);
return date.getDay() === 0 ? 7 : date.getDay();
}

View File

@@ -1,6 +0,0 @@
// frontend/src/services/scheduling/index.ts
export * from './types';
export * from './utils';
export * from './repairFunctions';
export * from './shiftScheduler';
export * from './dataAdapter';

File diff suppressed because it is too large Load Diff

View File

@@ -1,472 +0,0 @@
// frontend/src/services/scheduling/shiftScheduler.ts
import {
SchedulingEmployee,
SchedulingShift,
Assignment,
SchedulingResult,
SchedulingConstraints,
RepairContext
} from './types';
import {
canAssign,
candidateScore,
onlyNeuAssigned,
assignEmployee,
hasErfahrener,
wouldBeAloneIfAdded,
isManagerShiftWithOnlyNew,
isManagerAlone,
hasExperiencedAloneNotAllowed
} from './utils';
import {
attemptMoveErfahrenerTo,
attemptUnassignOrSwap,
attemptAddErfahrenerToShift,
attemptFillFromPool,
resolveTwoExperiencedInShift,
attemptMoveExperiencedToManagerShift,
attemptSwapForExperienced,
resolveOverstaffedExperienced,
prioritizeWarningsWithPool,
resolveExperiencedAloneNotAllowed,
checkAllProblemsResolved,
createDetailedResolutionReport
} from './repairFunctions';
// Phase A: Regular employee scheduling (without manager)
function phaseAPlan(
shifts: SchedulingShift[],
employees: SchedulingEmployee[],
constraints: SchedulingConstraints
): { assignments: Assignment; warnings: string[] } {
const assignments: Assignment = {};
const warnings: string[] = [];
const employeeMap = new Map(employees.map(emp => [emp.id, emp]));
// Initialize assignments
shifts.forEach(shift => {
assignments[shift.id] = [];
});
// Helper function to find best candidate
function findBestCandidate(candidates: SchedulingEmployee[], shiftId: string): SchedulingEmployee | null {
if (candidates.length === 0) return null;
return candidates.reduce((best, current) => {
const bestScore = candidateScore(best, shiftId);
const currentScore = candidateScore(current, shiftId);
return currentScore < bestScore ? current : best;
});
}
// 1) Basic coverage: at least 1 person per shift, prefer experienced
for (const shift of shifts) {
const candidates = employees.filter(emp => canAssign(emp, shift.id));
if (candidates.length === 0) {
warnings.push(`No available employees for shift ${shift.id}`);
continue;
}
// Prefer erfahrene candidates
const erfahrenCandidates = candidates.filter(emp => emp.role === 'erfahren');
const bestCandidate = findBestCandidate(
erfahrenCandidates.length > 0 ? erfahrenCandidates : candidates,
shift.id
);
if (bestCandidate) {
assignEmployee(bestCandidate, shift.id, assignments);
}
}
// 2) Prevent 'neu alone' if constraint enabled
if (constraints.enforceNoTraineeAlone) {
for (const shift of shifts) {
if (onlyNeuAssigned(assignments[shift.id], employeeMap)) {
const erfahrenCandidates = employees.filter(emp =>
emp.role === 'erfahren' && canAssign(emp, shift.id)
);
if (erfahrenCandidates.length > 0) {
const bestCandidate = findBestCandidate(erfahrenCandidates, shift.id);
if (bestCandidate) {
assignEmployee(bestCandidate, shift.id, assignments);
}
} else {
// Try repair
if (!attemptMoveErfahrenerTo(shift.id, assignments, employeeMap, shifts)) {
warnings.push(`Cannot prevent neu-alone in shift ${shift.id}`);
}
}
}
}
}
// 3) Fill up to target employees per shift
const targetPerShift = constraints.targetEmployeesPerShift || 2;
for (const shift of shifts) {
while (assignments[shift.id].length < targetPerShift) {
const candidates = employees.filter(emp =>
canAssign(emp, shift.id) &&
!assignments[shift.id].includes(emp.id) &&
!wouldBeAloneIfAdded(emp, shift.id, assignments, employeeMap)
);
if (candidates.length === 0) break;
const bestCandidate = findBestCandidate(candidates, shift.id);
if (bestCandidate) {
assignEmployee(bestCandidate, shift.id, assignments);
} else {
break;
}
}
}
return { assignments, warnings };
}
// Phase B: Insert manager and ensure "experienced with manager"
function phaseBInsertManager(
assignments: Assignment,
manager: SchedulingEmployee | undefined,
managerShifts: string[],
employees: SchedulingEmployee[],
nonManagerShifts: SchedulingShift[],
constraints: SchedulingConstraints
): { assignments: Assignment; warnings: string[] } {
const warnings: string[] = [];
const employeeMap = new Map(employees.map(emp => [emp.id, emp]));
if (!manager) return { assignments, warnings };
console.log(`🎯 Phase B: Processing ${managerShifts.length} manager shifts`);
for (const shiftId of managerShifts) {
let _shift = nonManagerShifts.find(s => s.id === shiftId) || { id: shiftId, requiredEmployees: 2 };
// Assign manager to his chosen shifts
if (!assignments[shiftId].includes(manager.id)) {
assignments[shiftId].push(manager.id);
manager.assignedCount++;
console.log(`✅ Assigned manager to shift ${shiftId}`);
}
// Rule: if manager present, MUST have at least one ERFAHRENER
if (constraints.enforceExperiencedWithChef) {
const hasExperienced = hasErfahrener(assignments[shiftId], employeeMap);
if (!hasExperienced) {
console.log(`⚠️ Manager shift ${shiftId} missing experienced employee`);
// Strategy 1: Try to add an experienced employee directly
const erfahrenCandidates = employees.filter(emp =>
emp.role === 'erfahren' &&
canAssign(emp, shiftId) &&
!assignments[shiftId].includes(emp.id)
);
if (erfahrenCandidates.length > 0) {
// Find best candidate using scoring
const bestCandidate = erfahrenCandidates.reduce((best, current) => {
const bestScore = candidateScore(best, shiftId);
const currentScore = candidateScore(current, shiftId);
return currentScore < bestScore ? current : best;
});
assignEmployee(bestCandidate, shiftId, assignments);
console.log(`✅ Added experienced ${bestCandidate.id} to manager shift ${shiftId}`);
continue;
}
// Strategy 2: Try to swap with another shift
if (attemptSwapForExperienced(shiftId, assignments, employeeMap, nonManagerShifts)) {
console.log(`✅ Swapped experienced into manager shift ${shiftId}`);
continue;
}
// Strategy 3: Try to move experienced from overloaded shift
if (attemptMoveExperiencedToManagerShift(shiftId, assignments, employeeMap, nonManagerShifts)) {
console.log(`✅ Moved experienced to manager shift ${shiftId}`);
continue;
}
// Final fallback: Check if we can at least add ANY employee (not just experienced)
const anyCandidates = employees.filter(emp =>
emp.role !== 'manager' &&
canAssign(emp, shiftId) &&
!assignments[shiftId].includes(emp.id) &&
!wouldBeAloneIfAdded(emp, shiftId, assignments, employeeMap)
);
if (anyCandidates.length > 0) {
const bestCandidate = anyCandidates.reduce((best, current) => {
const bestScore = candidateScore(best, shiftId);
const currentScore = candidateScore(current, shiftId);
return currentScore < bestScore ? current : best;
});
assignEmployee(bestCandidate, shiftId, assignments);
warnings.push(`Manager shift ${shiftId} has non-experienced backup: ${bestCandidate.name}`);
console.log(`⚠️ Added non-experienced backup to manager shift ${shiftId}`);
} else {
warnings.push(`Manager alone in shift ${shiftId} - no available employees`);
console.log(`❌ Cannot fix manager alone in shift ${shiftId}`);
}
}
}
}
return { assignments, warnings };
}
// Phase C: Repair and validate
export function enhancedPhaseCRepairValidate(
assignments: Assignment,
employees: SchedulingEmployee[],
shifts: SchedulingShift[],
managerShifts: string[],
constraints: SchedulingConstraints
): { assignments: Assignment; violations: string[]; resolutionReport: string[]; allProblemsResolved: boolean } {
const repairContext: RepairContext = {
lockedShifts: new Set<string>(),
unassignedPool: [],
warnings: [],
violations: []
};
const employeeMap = new Map(employees.map(emp => [emp.id, emp]));
const manager = employees.find(emp => emp.role === 'manager');
console.log('🔄 Starting Enhanced Phase C: Detailed Repair & Validation');
// 1. Manager-Schutzregel
managerShifts.forEach(shiftId => {
repairContext.lockedShifts.add(shiftId);
repairContext.warnings.push(`Schicht ${shiftId} als Manager-Schicht gesperrt`);
});
// 2. Überbesetzte erfahrene Mitarbeiter identifizieren und in Pool verschieben
resolveOverstaffedExperienced(assignments, employeeMap, shifts, repairContext);
// 3. Erfahrene Mitarbeiter, die nicht alleine arbeiten dürfen
resolveExperiencedAloneNotAllowed(assignments, employeeMap, shifts, repairContext);
// 4. Doppel-Erfahrene-Strafe (nur für Nicht-Manager-Schichten)
shifts.forEach(shift => {
if (!managerShifts.includes(shift.id)) {
resolveTwoExperiencedInShift(shift.id, assignments, employeeMap, repairContext);
}
});
// 5. Priorisierte Zuweisung von Pool-Mitarbeitern zu Schichten mit Warnungen
prioritizeWarningsWithPool(assignments, employeeMap, shifts, managerShifts, repairContext);
// 6. Standard-Validation
shifts.forEach(shift => {
const assignment = assignments[shift.id] || [];
// Leere Schichten beheben
if (assignment.length === 0) {
if (!attemptFillFromPool(shift.id, assignments, employeeMap, repairContext, managerShifts)) {
repairContext.violations.push({
type: 'EmptyShift',
shiftId: shift.id,
severity: 'error',
message: `Leere Schicht: ${shift.id}`
});
repairContext.warnings.push(`Konnte leere Schicht ${shift.id} nicht beheben`);
}
}
// Neu-allein Schichten beheben (nur für Nicht-Manager-Schichten)
if (constraints.enforceNoTraineeAlone &&
!managerShifts.includes(shift.id) &&
onlyNeuAssigned(assignment, employeeMap)) {
if (!attemptAddErfahrenerToShift(shift.id, assignments, employeeMap, shifts)) {
repairContext.violations.push({
type: 'NeuAlone',
shiftId: shift.id,
severity: 'error',
message: `Nur neue Mitarbeiter in Schicht: ${shift.id}`
});
}
}
// Erfahrene-allein Prüfung (erneut nach Reparaturen)
const experiencedAloneCheck = hasExperiencedAloneNotAllowed(assignment, employeeMap);
if (experiencedAloneCheck.hasViolation && !repairContext.lockedShifts.has(shift.id)) {
const emp = employeeMap.get(experiencedAloneCheck.employeeId!);
repairContext.violations.push({
type: 'ExperiencedAloneNotAllowed',
shiftId: shift.id,
employeeId: experiencedAloneCheck.employeeId,
severity: 'error',
message: `Erfahrener Mitarbeiter ${emp?.name || experiencedAloneCheck.employeeId} arbeitet allein in Schicht ${shift.id}`
});
}
});
// 7. Vertragsüberschreitungen beheben
employees.forEach(emp => {
if (emp.role !== 'manager' && emp.assignedCount > emp.contract) {
if (!attemptUnassignOrSwap(emp.id, assignments, employeeMap, shifts)) {
repairContext.violations.push({
type: 'ContractExceeded',
employeeId: emp.id,
severity: 'error',
message: `Vertragslimit überschritten für: ${emp.name}`
});
}
}
});
// 8. Nachbesserung: Manager-Schichten prüfen
managerShifts.forEach(shiftId => {
const assignment = assignments[shiftId] || [];
// Manager allein
if (isManagerAlone(assignment, manager?.id)) {
repairContext.violations.push({
type: 'ManagerAlone',
shiftId: shiftId,
severity: 'error',
message: `Manager allein in Schicht ${shiftId}`
});
}
// Manager + nur Neue
if (isManagerShiftWithOnlyNew(assignment, employeeMap, manager?.id)) {
repairContext.violations.push({
type: 'ManagerWithOnlyNew',
shiftId: shiftId,
severity: 'warning',
message: `Manager mit nur Neuen in Schicht ${shiftId}`
});
}
});
// Erstelle finale Violations-Liste
const uniqueViolations = repairContext.violations.filter((v, index, self) =>
index === self.findIndex(t =>
t.type === v.type &&
t.shiftId === v.shiftId &&
t.employeeId === v.employeeId
)
);
const uniqueWarnings = repairContext.warnings.filter((warning, index, array) =>
array.indexOf(warning) === index
);
const finalViolations = [
...uniqueViolations
.filter(v => v.severity === 'error')
.map(v => `ERROR: ${v.message}`),
...uniqueViolations
.filter(v => v.severity === 'warning')
.map(v => `WARNING: ${v.message}`),
...uniqueWarnings.map(w => `INFO: ${w}`)
];
// 9. DETAILLIERTER REPARATUR-BERICHT
const resolutionReport = createDetailedResolutionReport(
assignments,
employeeMap,
shifts,
managerShifts,
repairContext
);
// Bestimme ob alle kritischen Probleme behoben wurden
const criticalProblems = uniqueViolations.filter(v => v.severity === 'error');
const allProblemsResolved = criticalProblems.length === 0;
console.log('📊 Enhanced Phase C completed:', {
totalActions: uniqueWarnings.length,
criticalProblems: criticalProblems.length,
warnings: uniqueViolations.filter(v => v.severity === 'warning').length,
allProblemsResolved
});
return {
assignments,
violations: finalViolations,
resolutionReport,
allProblemsResolved
};
}
export function scheduleWithManager(
shifts: SchedulingShift[],
employees: SchedulingEmployee[],
managerShifts: string[],
constraints: SchedulingConstraints
): SchedulingResult & { resolutionReport?: string[]; allProblemsResolved?: boolean } {
const assignments: Assignment = {};
const allViolations: string[] = [];
// Initialisiere Zuweisungen
shifts.forEach(shift => {
assignments[shift.id] = [];
});
// Finde Manager
const manager = employees.find(emp => emp.role === 'manager');
// Filtere Manager und Nicht-Manager-Schichten für Phase A
const nonManagerEmployees = employees.filter(emp => emp.role !== 'manager');
const nonManagerShifts = shifts.filter(shift => !managerShifts.includes(shift.id));
console.log('🔄 Starting Phase A: Regular employee scheduling');
// Phase A: Reguläre Planung
const phaseAResult = phaseAPlan(nonManagerShifts, nonManagerEmployees, constraints);
Object.assign(assignments, phaseAResult.assignments);
console.log('🔄 Starting Phase B: Enhanced Manager insertion');
// Phase B: Erweiterte Manager-Einfügung
const phaseBResult = phaseBInsertManager(
assignments,
manager,
managerShifts,
employees,
nonManagerShifts,
constraints
);
console.log('🔄 Starting Enhanced Phase C: Smart Repair & Validation');
// Phase C: Erweiterte Reparatur und Validierung mit Pool-Verwaltung
const phaseCResult = enhancedPhaseCRepairValidate(assignments, employees, shifts, managerShifts, constraints);
// Verwende Array.filter für uniqueIssues
const uniqueIssues = phaseCResult.violations.filter((issue, index, array) =>
array.indexOf(issue) === index
);
// Erfolg basiert jetzt auf allProblemsResolved statt nur auf ERRORs
const success = phaseCResult.allProblemsResolved;
console.log('📊 Enhanced scheduling with pool management completed:', {
assignments: Object.keys(assignments).filter(k => assignments[k].length > 0).length,
totalShifts: shifts.length,
totalIssues: uniqueIssues.length,
errors: uniqueIssues.filter(v => v.includes('ERROR:')).length,
warnings: uniqueIssues.filter(v => v.includes('WARNING:')).length,
allProblemsResolved: success
});
return {
assignments,
violations: uniqueIssues,
success: success,
resolutionReport: phaseCResult.resolutionReport,
allProblemsResolved: success
};
}

View File

@@ -1,75 +0,0 @@
// frontend/src/services/scheduling/types.ts
import { ScheduledShift } from "../../models/ShiftPlan";
export interface SchedulingEmployee {
id: string;
name: string;
role: 'manager' | 'erfahren' | 'neu';
contract: number; // max assignments per week
availability: Map<string, number>; // shiftId -> preferenceLevel (1,2,3)
assignedCount: number;
originalData: any; // reference to original employee data
}
export interface SchedulingShift {
id: string;
requiredEmployees: number;
isManagerShift?: boolean;
originalData: any; // reference to original shift data
}
export interface Assignment {
[shiftId: string]: string[]; // employee IDs
}
export interface SchedulingConstraints {
enforceNoTraineeAlone: boolean;
enforceExperiencedWithChef: boolean;
maxRepairAttempts: number;
}
export interface SchedulingResult {
assignments: Assignment;
violations: string[];
success: boolean;
resolutionReport?: string[];
allProblemsResolved?: boolean;
}
export interface AssignmentResult {
assignments: { [shiftId: string]: string[] };
violations: string[];
success: boolean;
pattern: WeeklyPattern;
resolutionReport?: string[];
allProblemsResolved?: boolean;
}
export interface WeeklyPattern {
weekShifts: ScheduledShift[];
assignments: { [shiftId: string]: string[] };
weekNumber: number;
}
export interface SchedulingConstraints {
enforceNoTraineeAlone: boolean;
enforceExperiencedWithChef: boolean;
maxRepairAttempts: number;
targetEmployeesPerShift?: number; // New: flexible target
}
export interface Violation {
type: 'EmptyShift' | 'NeuAlone' | 'ContractExceeded' | 'ManagerWithoutExperienced' |
'TwoExperiencedInShift' | 'ManagerAlone' | 'ManagerWithOnlyNew' | 'ExperiencedAloneNotAllowed';
shiftId?: string;
employeeId?: string;
severity: 'error' | 'warning';
message: string;
}
export interface RepairContext {
lockedShifts: Set<string>;
unassignedPool: string[];
warnings: string[];
violations: Violation[];
}

View File

@@ -1,228 +0,0 @@
// frontend/src/services/scheduling/utils.ts
import { SchedulingEmployee, SchedulingShift, Assignment } from './types';
// Scoring system
export function getAvailabilityScore(preferenceLevel: number): number {
switch (preferenceLevel) {
case 1: return 2; // preferred
case 2: return 1; // available
case 3: return -9999; // unavailable
default: return 0;
}
}
export function canAssign(emp: SchedulingEmployee, shiftId: string): boolean {
if (emp.availability.get(shiftId) === 3) return false;
if (emp.role === 'manager') return false; // Phase A: ignore manager
return emp.assignedCount < emp.contract;
}
export function candidateScore(emp: SchedulingEmployee, shiftId: string): number {
const availability = emp.availability.get(shiftId) || 3;
const baseScore = -getAvailabilityScore(availability); // prefer higher availability scores
const loadPenalty = emp.assignedCount * 0.5; // fairness: penalize already assigned
const rolePenalty = emp.role === 'erfahren' ? 0 : 0.5; // prefer experienced
return baseScore + loadPenalty + rolePenalty;
}
export function onlyNeuAssigned(assignment: string[], employees: Map<string, SchedulingEmployee>): boolean {
if (assignment.length === 0) return false;
return assignment.every(empId => {
const emp = employees.get(empId);
return emp?.role === 'neu';
});
}
export function assignEmployee(emp: SchedulingEmployee, shiftId: string, assignments: Assignment): void {
if (!assignments[shiftId]) {
assignments[shiftId] = [];
}
assignments[shiftId].push(emp.id);
emp.assignedCount++;
}
export function unassignEmployee(emp: SchedulingEmployee, shiftId: string, assignments: Assignment): void {
if (assignments[shiftId]) {
assignments[shiftId] = assignments[shiftId].filter(id => id !== emp.id);
emp.assignedCount--;
}
}
export function hasErfahrener(assignment: string[], employees: Map<string, SchedulingEmployee>): boolean {
return assignment.some(empId => {
const emp = employees.get(empId);
return emp?.role === 'erfahren';
});
}
export function wouldBeAloneIfAdded(
candidate: SchedulingEmployee,
shiftId: string,
assignments: Assignment,
employees: Map<string, SchedulingEmployee>
): boolean {
const currentAssignment = assignments[shiftId] || [];
// If adding to empty shift and candidate is neu, they would be alone
if (currentAssignment.length === 0 && candidate.role === 'neu') {
return true;
}
// If all current assignments are neu and candidate is neu, they would be alone together
if (onlyNeuAssigned(currentAssignment, employees) && candidate.role === 'neu') {
return true;
}
return false;
}
export function findViolations(
assignments: Assignment,
employees: Map<string, SchedulingEmployee>,
shifts: SchedulingShift[],
managerShifts: string[] = []
): { type: string; shiftId?: string; employeeId?: string; severity: string }[] {
const violations: any[] = [];
const employeeMap = employees;
// Check each shift
shifts.forEach(shift => {
const assignment = assignments[shift.id] || [];
// Empty shift violation
if (assignment.length === 0) {
violations.push({
type: 'EmptyShift',
shiftId: shift.id,
severity: 'error'
});
}
// Neu alone violation
if (onlyNeuAssigned(assignment, employeeMap)) {
violations.push({
type: 'NeuAlone',
shiftId: shift.id,
severity: 'error'
});
}
// Manager without experienced (for manager shifts)
if (managerShifts.includes(shift.id)) {
const hasManager = assignment.some(empId => {
const emp = employeeMap.get(empId);
return emp?.role === 'manager';
});
if (hasManager && !hasErfahrener(assignment, employeeMap)) {
violations.push({
type: 'ManagerWithoutExperienced',
shiftId: shift.id,
severity: 'warning' // Could be warning instead of error
});
}
}
});
// Check employee contracts
employeeMap.forEach((emp, empId) => {
if (emp.role !== 'manager' && emp.assignedCount > emp.contract) {
violations.push({
type: 'ContractExceeded',
employeeId: empId,
severity: 'error'
});
}
});
return violations;
}
export function canRemove(
empId: string,
shiftId: string,
lockedShifts: Set<string>,
assignments: Assignment,
employees: Map<string, SchedulingEmployee>
): boolean {
// Wenn Schicht gesperrt ist, kann niemand entfernt werden
if (lockedShifts.has(shiftId)) {
return false;
}
const emp = employees.get(empId);
if (!emp) return false;
// Überprüfe ob Entfernen neue Verletzungen verursachen würde
const currentAssignment = assignments[shiftId] || [];
const wouldBeEmpty = currentAssignment.length <= 1;
const wouldBeNeuAlone = wouldBeEmpty && emp.role === 'erfahren';
const wouldBeExperiencedAlone = wouldBeExperiencedAloneIfRemoved(empId, shiftId, assignments, employees);
return !wouldBeEmpty && !wouldBeNeuAlone && !wouldBeExperiencedAlone;
}
export function countExperiencedCanWorkAlone(
assignment: string[],
employees: Map<string, SchedulingEmployee>
): string[] {
return assignment.filter(empId => {
const emp = employees.get(empId);
return emp?.role === 'erfahren' && emp.originalData?.canWorkAlone;
});
}
export function isManagerShiftWithOnlyNew(
assignment: string[],
employees: Map<string, SchedulingEmployee>,
managerId?: string
): boolean {
if (!managerId || !assignment.includes(managerId)) return false;
const nonManagerEmployees = assignment.filter(id => id !== managerId);
return onlyNeuAssigned(nonManagerEmployees, employees);
}
export function isManagerAlone(
assignment: string[],
managerId?: string
): boolean {
return assignment.length === 1 && assignment[0] === managerId;
}
export function hasExperiencedAloneNotAllowed(
assignment: string[],
employees: Map<string, SchedulingEmployee>
): { hasViolation: boolean; employeeId?: string } {
if (assignment.length !== 1) return { hasViolation: false };
const empId = assignment[0];
const emp = employees.get(empId);
if (emp && emp.role === 'erfahren' && !emp.originalData?.canWorkAlone) {
return { hasViolation: true, employeeId: empId };
}
return { hasViolation: false };
}
export function isExperiencedCanWorkAlone(emp: SchedulingEmployee): boolean {
return emp.role === 'erfahren' && emp.originalData?.canWorkAlone === true;
}
export function wouldBeExperiencedAloneIfRemoved(
empId: string,
shiftId: string,
assignments: Assignment,
employees: Map<string, SchedulingEmployee>
): boolean {
const assignment = assignments[shiftId] || [];
if (assignment.length <= 1) return false;
const remainingAssignment = assignment.filter(id => id !== empId);
if (remainingAssignment.length !== 1) return false;
const remainingEmp = employees.get(remainingAssignment[0]);
return remainingEmp?.role === 'erfahren' && !remainingEmp.originalData?.canWorkAlone;
}

View File

@@ -2,13 +2,13 @@
import { ShiftPlan, ScheduledShift } from '../models/ShiftPlan';
import { Employee, EmployeeAvailability } from '../models/Employee';
import { authService } from './authService';
import { scheduleWithManager } from './scheduling/shiftScheduler';
import { transformToSchedulingData } from './scheduling/dataAdapter';
import { AssignmentResult, WeeklyPattern } from './scheduling/types';
import { IntelligentShiftScheduler, AssignmentResult, WeeklyPattern } from './scheduling';
import { isScheduledShift } from '../models/helpers';
const API_BASE_URL = 'http://localhost:3002/api/scheduled-shifts';
// Helper function to get auth headers
const getAuthHeaders = () => {
const token = localStorage.getItem('token');
@@ -21,7 +21,7 @@ const getAuthHeaders = () => {
export class ShiftAssignmentService {
async updateScheduledShift(id: string, updates: { assignedEmployees: string[] }): Promise<void> {
try {
console.log('🔄 Updating scheduled shift via API:', { id, updates });
//console.log('🔄 Updating scheduled shift via API:', { id, updates });
const response = await fetch(`${API_BASE_URL}/${id}`, {
method: 'PUT',
@@ -141,65 +141,64 @@ export class ShiftAssignmentService {
constraints: any = {}
): Promise<AssignmentResult> {
console.log('🔄 Starting enhanced scheduling algorithm...');
console.log('🧠 Starting intelligent scheduling for FIRST WEEK ONLY...');
// Get defined shifts for the first week
const definedShifts = await this.getDefinedShifts(shiftPlan);
const firstWeekShifts = this.getFirstWeekShifts(definedShifts);
// Load all scheduled shifts
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
console.log('📊 First week analysis:', {
totalShifts: definedShifts.length,
firstWeekShifts: firstWeekShifts.length,
employees: employees.length
});
if (scheduledShifts.length === 0) {
return {
assignments: {},
violations: ['❌ KRITISCH: Keine Schichten verfügbar für die Zuordnung'],
success: false,
resolutionReport: ['🚨 ABBRUCH: Keine Schichten im Plan verfügbar']
};
}
// Transform data for scheduling algorithm
const { schedulingEmployees, schedulingShifts, managerShifts } = transformToSchedulingData(
// Set cache for scheduler
IntelligentShiftScheduler.scheduledShiftsCache.set(shiftPlan.id, scheduledShifts);
// 🔥 RUN SCHEDULING FOR FIRST WEEK ONLY
const schedulingResult = await IntelligentShiftScheduler.generateOptimalSchedule(
shiftPlan,
employees.filter(emp => emp.isActive),
firstWeekShifts,
availabilities
availabilities,
constraints
);
console.log('🎯 Transformed data for scheduling:', {
employees: schedulingEmployees.length,
shifts: schedulingShifts.length,
managerShifts: managerShifts.length
// Get first week shifts for pattern
const firstWeekShifts = this.getFirstWeekShifts(scheduledShifts);
console.log('🔄 Creating weekly pattern from FIRST WEEK:', {
firstWeekShifts: firstWeekShifts.length,
allShifts: scheduledShifts.length,
patternAssignments: Object.keys(schedulingResult.assignments).length
});
// Run the enhanced scheduling algorithm with better constraints
const schedulingResult = scheduleWithManager(
schedulingShifts,
schedulingEmployees,
managerShifts,
{
enforceNoTraineeAlone: constraints.enforceNoTraineeAlone ?? true,
enforceExperiencedWithChef: constraints.enforceExperiencedWithChef ?? true,
maxRepairAttempts: constraints.maxRepairAttempts ?? 50,
targetEmployeesPerShift: constraints.targetEmployeesPerShift ?? 2 // Flexible target
}
);
console.log('📊 Enhanced scheduling completed:', {
assignments: Object.keys(schedulingResult.assignments).length,
violations: schedulingResult.violations.length,
success: schedulingResult.success
});
// Apply weekly pattern to all shifts
const weeklyPattern: WeeklyPattern = {
weekShifts: firstWeekShifts,
assignments: schedulingResult.assignments,
assignments: schedulingResult.assignments, // 🔥 Diese enthalten nur erste Woche
weekNumber: 1
};
const allAssignments = this.applyWeeklyPattern(definedShifts, weeklyPattern);
// 🔥 APPLY PATTERN TO ALL WEEKS
const allAssignments = this.applyWeeklyPattern(scheduledShifts, weeklyPattern);
console.log('✅ Pattern applied to all weeks:', {
firstWeekAssignments: Object.keys(schedulingResult.assignments).length,
allWeeksAssignments: Object.keys(allAssignments).length
});
// Clean cache
IntelligentShiftScheduler.scheduledShiftsCache.delete(shiftPlan.id);
return {
assignments: allAssignments,
assignments: allAssignments, // 🔥 Diese enthalten alle Wochen
violations: schedulingResult.violations,
success: schedulingResult.violations.length === 0,
success: schedulingResult.success,
pattern: weeklyPattern,
resolutionReport: schedulingResult.resolutionReport // Füge diese Zeile hinzu
resolutionReport: schedulingResult.resolutionReport,
qualityMetrics: schedulingResult.qualityMetrics
};
}
@@ -336,29 +335,48 @@ export class ShiftAssignmentService {
const assignments: { [shiftId: string]: string[] } = {};
// Group all shifts by week
const shiftsByWeek = this.groupShiftsByWeek(allShifts);
// Group all shifts by week AND day-timeSlot combination
const shiftsByPatternKey = new Map<string, ScheduledShift[]>();
console.log('📅 Applying weekly pattern to', Object.keys(shiftsByWeek).length, 'weeks');
// For each week, apply the pattern from week 1
Object.entries(shiftsByWeek).forEach(([weekKey, weekShifts]) => {
const weekNumber = parseInt(weekKey);
allShifts.forEach(shift => {
const dayOfWeek = this.getDayOfWeek(shift.date);
const patternKey = `${dayOfWeek}-${shift.timeSlotId}`;
weekShifts.forEach(shift => {
// Find the corresponding shift in the weekly pattern
const patternShift = this.findMatchingPatternShift(shift, weeklyPattern.weekShifts);
if (patternShift) {
// Use the same assignment as the pattern shift
assignments[shift.id] = [...weeklyPattern.assignments[patternShift.id]];
} else {
// No matching pattern shift, leave empty
assignments[shift.id] = [];
}
});
if (!shiftsByPatternKey.has(patternKey)) {
shiftsByPatternKey.set(patternKey, []);
}
shiftsByPatternKey.get(patternKey)!.push(shift);
});
console.log('📊 Pattern application analysis:');
console.log('- Unique pattern keys:', shiftsByPatternKey.size);
console.log('- Pattern keys:', Array.from(shiftsByPatternKey.keys()));
// For each shift in all weeks, find the matching pattern shift
allShifts.forEach(shift => {
const dayOfWeek = this.getDayOfWeek(shift.date);
//const patternKey = `${dayOfWeek}-${shift.timeSlotId}`;
const patternKey = `${shift.timeSlotId}`;
// Find the pattern shift for this day-timeSlot combination
const patternShift = weeklyPattern.weekShifts.find(patternShift => {
const patternDayOfWeek = this.getDayOfWeek(patternShift.date);
return patternDayOfWeek === dayOfWeek &&
patternShift.timeSlotId === shift.timeSlotId;
});
if (patternShift && weeklyPattern.assignments[patternShift.id]) {
assignments[shift.id] = [...weeklyPattern.assignments[patternShift.id]];
} else {
assignments[shift.id] = [];
console.warn(`❌ No pattern found for shift: ${patternKey}`);
}
});
// DEBUG: Check assignment coverage
const assignedShifts = Object.values(assignments).filter(a => a.length > 0).length;
console.log(`📊 Assignment coverage: ${assignedShifts}/${allShifts.length} shifts assigned`);
return assignments;
}

View File

@@ -128,26 +128,6 @@ export const shiftPlanService = {
}
},
async revertToDraft(id: string): Promise<ShiftPlan> {
const response = await fetch(`${API_BASE}/${id}/revert-to-draft`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
}
});
if (!response.ok) {
if (response.status === 401) {
authService.logout();
throw new Error('Nicht authorisiert - bitte erneut anmelden');
}
throw new Error('Fehler beim Zurücksetzen des Schichtplans');
}
return response.json();
},
// Get specific template or plan
getTemplate: async (id: string): Promise<ShiftPlan> => {
const response = await fetch(`${API_BASE}/${id}`, {
@@ -219,4 +199,29 @@ export const shiftPlanService = {
description: preset.description
}));
},
async clearAssignments(planId: string): Promise<void> {
try {
console.log('🔄 Clearing assignments for plan:', planId);
const response = await fetch(`${API_BASE}/${planId}/clear-assignments`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...authService.getAuthHeaders()
}
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(errorData.error || `Failed to clear assignments: ${response.status}`);
}
console.log('✅ Assignments cleared successfully');
} catch (error) {
console.error('❌ Error clearing assignments:', error);
throw error;
}
},
};