mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-11-30 22:45:46 +01:00
seperated files for scheduling logic
This commit is contained in:
@@ -4,7 +4,8 @@ import { useParams, useNavigate } from 'react-router-dom';
|
|||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import { shiftPlanService } from '../../services/shiftPlanService';
|
import { shiftPlanService } from '../../services/shiftPlanService';
|
||||||
import { employeeService } from '../../services/employeeService';
|
import { employeeService } from '../../services/employeeService';
|
||||||
import { shiftAssignmentService, ShiftAssignmentService, AssignmentResult } from '../../services/shiftAssignmentService';
|
import { shiftAssignmentService, ShiftAssignmentService } from '../../services/shiftAssignmentService';
|
||||||
|
import { AssignmentResult } from '../../services/scheduling/types';
|
||||||
import { ShiftPlan, TimeSlot } from '../../models/ShiftPlan';
|
import { ShiftPlan, TimeSlot } from '../../models/ShiftPlan';
|
||||||
import { Employee, EmployeeAvailability } from '../../models/Employee';
|
import { Employee, EmployeeAvailability } from '../../models/Employee';
|
||||||
import { useNotification } from '../../contexts/NotificationContext';
|
import { useNotification } from '../../contexts/NotificationContext';
|
||||||
|
|||||||
84
frontend/src/services/scheduling/dataAdapter.ts
Normal file
84
frontend/src/services/scheduling/dataAdapter.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
// 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();
|
||||||
|
}
|
||||||
6
frontend/src/services/scheduling/index.ts
Normal file
6
frontend/src/services/scheduling/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// frontend/src/services/scheduling/index.ts
|
||||||
|
export * from './types';
|
||||||
|
export * from './utils';
|
||||||
|
export * from './repairFunctions';
|
||||||
|
export * from './shiftScheduler';
|
||||||
|
export * from './dataAdapter';
|
||||||
92
frontend/src/services/scheduling/repairFunctions.ts
Normal file
92
frontend/src/services/scheduling/repairFunctions.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
// frontend/src/services/scheduling/repairFunctions.ts
|
||||||
|
import { SchedulingEmployee, SchedulingShift, Assignment } from './types';
|
||||||
|
import { canAssign, assignEmployee, hasErfahrener } from './utils';
|
||||||
|
|
||||||
|
export function attemptRepairMoveErfahrener(
|
||||||
|
targetShiftId: string,
|
||||||
|
assignments: Assignment,
|
||||||
|
employees: Map<string, SchedulingEmployee>,
|
||||||
|
allShifts: SchedulingShift[]
|
||||||
|
): boolean {
|
||||||
|
|
||||||
|
for (const shift of allShifts) {
|
||||||
|
const currentAssignment = assignments[shift.id] || [];
|
||||||
|
|
||||||
|
// Skip if this shift doesn't have multiple employees
|
||||||
|
if (currentAssignment.length <= 1) continue;
|
||||||
|
|
||||||
|
for (const empId of currentAssignment) {
|
||||||
|
const emp = employees.get(empId);
|
||||||
|
if (!emp || emp.role !== 'erfahren') continue;
|
||||||
|
|
||||||
|
// Check if employee can be moved to target shift
|
||||||
|
if (canAssign(emp, targetShiftId)) {
|
||||||
|
// Remove from current shift
|
||||||
|
assignments[shift.id] = currentAssignment.filter(id => id !== empId);
|
||||||
|
emp.assignedCount--;
|
||||||
|
|
||||||
|
// Add to target shift
|
||||||
|
assignEmployee(emp, targetShiftId, assignments);
|
||||||
|
console.log(`🔧 Repaired: Moved erfahrener ${emp.id} to shift ${targetShiftId}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function attemptSwapBringErfahrener(
|
||||||
|
targetShiftId: string,
|
||||||
|
assignments: Assignment,
|
||||||
|
employees: Map<string, SchedulingEmployee>,
|
||||||
|
allShifts: SchedulingShift[]
|
||||||
|
): boolean {
|
||||||
|
const targetAssignment = assignments[targetShiftId] || [];
|
||||||
|
|
||||||
|
for (const shift of allShifts) {
|
||||||
|
if (shift.id === targetShiftId) continue;
|
||||||
|
|
||||||
|
const currentAssignment = assignments[shift.id] || [];
|
||||||
|
|
||||||
|
for (const erfahrenerId of currentAssignment) {
|
||||||
|
const erfahrener = employees.get(erfahrenerId);
|
||||||
|
if (!erfahrener || erfahrener.role !== 'erfahren') continue;
|
||||||
|
|
||||||
|
// Check if erfahrener can go to target shift
|
||||||
|
if (!canAssign(erfahrener, targetShiftId)) continue;
|
||||||
|
|
||||||
|
// Find someone from target shift who can swap
|
||||||
|
for (const targetEmpId of targetAssignment) {
|
||||||
|
const targetEmp = employees.get(targetEmpId);
|
||||||
|
if (!targetEmp) continue;
|
||||||
|
|
||||||
|
// Check if target employee can go to the other shift
|
||||||
|
if (canAssign(targetEmp, shift.id)) {
|
||||||
|
// Perform swap
|
||||||
|
assignments[shift.id] = currentAssignment.filter(id => id !== erfahrenerId).concat(targetEmpId);
|
||||||
|
assignments[targetShiftId] = targetAssignment.filter(id => id !== targetEmpId).concat(erfahrenerId);
|
||||||
|
|
||||||
|
erfahrener.assignedCount++;
|
||||||
|
targetEmp.assignedCount--;
|
||||||
|
|
||||||
|
console.log(`🔄 Swapped: ${erfahrener.id} <-> ${targetEmp.id}`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function attemptComplexRepairForManagerShift(
|
||||||
|
targetShiftId: string,
|
||||||
|
assignments: Assignment,
|
||||||
|
employees: Map<string, SchedulingEmployee>,
|
||||||
|
allShifts: SchedulingShift[]
|
||||||
|
): boolean {
|
||||||
|
// Try multiple repair strategies
|
||||||
|
return attemptSwapBringErfahrener(targetShiftId, assignments, employees, allShifts) ||
|
||||||
|
attemptRepairMoveErfahrener(targetShiftId, assignments, employees, allShifts);
|
||||||
|
}
|
||||||
191
frontend/src/services/scheduling/shiftScheduler.ts
Normal file
191
frontend/src/services/scheduling/shiftScheduler.ts
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
// frontend/src/services/scheduling/shiftScheduler.ts
|
||||||
|
import {
|
||||||
|
SchedulingEmployee,
|
||||||
|
SchedulingShift,
|
||||||
|
Assignment,
|
||||||
|
SchedulingResult,
|
||||||
|
SchedulingConstraints
|
||||||
|
} from './types';
|
||||||
|
import {
|
||||||
|
canAssign,
|
||||||
|
candidateScore,
|
||||||
|
onlyNeuAssigned,
|
||||||
|
assignEmployee,
|
||||||
|
hasErfahrener,
|
||||||
|
respectsNewRuleIfAdded
|
||||||
|
} from './utils';
|
||||||
|
import {
|
||||||
|
attemptRepairMoveErfahrener,
|
||||||
|
attemptSwapBringErfahrener,
|
||||||
|
attemptComplexRepairForManagerShift
|
||||||
|
} from './repairFunctions';
|
||||||
|
|
||||||
|
export function scheduleWithManager(
|
||||||
|
shifts: SchedulingShift[],
|
||||||
|
employees: SchedulingEmployee[],
|
||||||
|
managerShifts: string[], // shift IDs where manager should be assigned
|
||||||
|
constraints: SchedulingConstraints
|
||||||
|
): SchedulingResult {
|
||||||
|
|
||||||
|
const assignments: Assignment = {};
|
||||||
|
const violations: string[] = [];
|
||||||
|
const employeeMap = new Map(employees.map(emp => [emp.id, emp]));
|
||||||
|
|
||||||
|
// Initialize assignments
|
||||||
|
shifts.forEach(shift => {
|
||||||
|
assignments[shift.id] = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find manager
|
||||||
|
const manager = employees.find(emp => emp.role === 'manager');
|
||||||
|
|
||||||
|
// Helper function to find best candidate
|
||||||
|
function findBestCandidate(candidates: SchedulingEmployee[], shiftId: string): SchedulingEmployee | null {
|
||||||
|
if (candidates.length === 0) return null;
|
||||||
|
|
||||||
|
return candidates.reduce((best, current) => {
|
||||||
|
const bestScore = candidateScore(best, shiftId);
|
||||||
|
const currentScore = candidateScore(current, shiftId);
|
||||||
|
|
||||||
|
// Compare arrays lexicographically
|
||||||
|
for (let i = 0; i < bestScore.length; i++) {
|
||||||
|
if (currentScore[i] < bestScore[i]) return current;
|
||||||
|
if (currentScore[i] > bestScore[i]) return best;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHASE A: Regular employee scheduling (IGNORE ManagerShifts)
|
||||||
|
const nonManagerShifts = shifts.filter(shift => !managerShifts.includes(shift.id));
|
||||||
|
|
||||||
|
// 1) Basic coverage: at least 1 person per shift (prefer erfahrene)
|
||||||
|
for (const shift of nonManagerShifts) {
|
||||||
|
const candidates = employees.filter(emp =>
|
||||||
|
emp.role !== 'manager' &&
|
||||||
|
canAssign(emp, shift.id) &&
|
||||||
|
respectsNewRuleIfAdded(emp, shift.id, assignments, employeeMap)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (candidates.length === 0) {
|
||||||
|
violations.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) Ensure: no neu working alone (Phase A)
|
||||||
|
if (constraints.enforceNoTraineeAlone) {
|
||||||
|
for (const shift of nonManagerShifts) {
|
||||||
|
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 (!attemptRepairMoveErfahrener(shift.id, assignments, employeeMap, shifts)) {
|
||||||
|
violations.push(`Cannot prevent neu-alone in shift ${shift.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Goal: up to required employees per shift
|
||||||
|
for (const shift of nonManagerShifts) {
|
||||||
|
while (assignments[shift.id].length < shift.requiredEmployees) {
|
||||||
|
const candidates = employees.filter(emp =>
|
||||||
|
emp.role !== 'manager' &&
|
||||||
|
canAssign(emp, shift.id) &&
|
||||||
|
!assignments[shift.id].includes(emp.id) &&
|
||||||
|
respectsNewRuleIfAdded(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHASE B: Manager shifts
|
||||||
|
if (manager) {
|
||||||
|
for (const shiftId of managerShifts) {
|
||||||
|
const shift = shifts.find(s => s.id === shiftId);
|
||||||
|
if (!shift) continue;
|
||||||
|
|
||||||
|
// Assign manager to his chosen shifts
|
||||||
|
if (!assignments[shiftId].includes(manager.id)) {
|
||||||
|
if (manager.availability.get(shiftId) === 3) {
|
||||||
|
violations.push(`Manager assigned to shift he marked unavailable: ${shiftId}`);
|
||||||
|
}
|
||||||
|
assignEmployee(manager, shiftId, assignments);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule: if manager present, MUST be at least one ERFAHRENER in same shift
|
||||||
|
if (!hasErfahrener(assignments[shiftId], employeeMap)) {
|
||||||
|
const erfahrenCandidates = employees.filter(emp =>
|
||||||
|
emp.role === 'erfahren' &&
|
||||||
|
canAssign(emp, shiftId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (erfahrenCandidates.length > 0) {
|
||||||
|
const bestCandidate = findBestCandidate(erfahrenCandidates, shiftId);
|
||||||
|
if (bestCandidate) {
|
||||||
|
assignEmployee(bestCandidate, shiftId, assignments);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try repairs
|
||||||
|
if (!attemptSwapBringErfahrener(shiftId, assignments, employeeMap, shifts) &&
|
||||||
|
!attemptComplexRepairForManagerShift(shiftId, assignments, employeeMap, shifts)) {
|
||||||
|
violations.push(`Cannot satisfy manager+erfahren requirement for shift ${shiftId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FINAL: Validate constraints
|
||||||
|
for (const shift of shifts) {
|
||||||
|
if (assignments[shift.id].length === 0) {
|
||||||
|
violations.push(`Empty shift after scheduling: ${shift.id}`);
|
||||||
|
}
|
||||||
|
if (constraints.enforceNoTraineeAlone && onlyNeuAssigned(assignments[shift.id], employeeMap)) {
|
||||||
|
violations.push(`Neu alone after full scheduling: ${shift.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const emp of employees) {
|
||||||
|
if (emp.role !== 'manager' && emp.assignedCount > emp.contract) {
|
||||||
|
violations.push(`Contract exceeded for employee: ${emp.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
assignments,
|
||||||
|
violations,
|
||||||
|
success: violations.length === 0
|
||||||
|
};
|
||||||
|
}
|
||||||
48
frontend/src/services/scheduling/types.ts
Normal file
48
frontend/src/services/scheduling/types.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// 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 SchedulingResult {
|
||||||
|
assignments: Assignment;
|
||||||
|
violations: string[];
|
||||||
|
success: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SchedulingConstraints {
|
||||||
|
enforceNoTraineeAlone: boolean;
|
||||||
|
enforceExperiencedWithChef: boolean;
|
||||||
|
maxRepairAttempts: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssignmentResult {
|
||||||
|
assignments: { [shiftId: string]: string[] };
|
||||||
|
violations: string[];
|
||||||
|
success: boolean;
|
||||||
|
pattern: WeeklyPattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WeeklyPattern {
|
||||||
|
weekShifts: ScheduledShift[];
|
||||||
|
assignments: { [shiftId: string]: string[] };
|
||||||
|
weekNumber: number;
|
||||||
|
}
|
||||||
58
frontend/src/services/scheduling/utils.ts
Normal file
58
frontend/src/services/scheduling/utils.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// frontend/src/services/scheduling/utils.ts
|
||||||
|
import { SchedulingEmployee, SchedulingShift, Assignment } from './types';
|
||||||
|
|
||||||
|
export function canAssign(emp: SchedulingEmployee, shiftId: string): boolean {
|
||||||
|
if (emp.availability.get(shiftId) === 3) return false;
|
||||||
|
if (emp.role === 'manager') return true;
|
||||||
|
return emp.assignedCount < emp.contract;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function candidateScore(emp: SchedulingEmployee, shiftId: string): [number, number, number] {
|
||||||
|
const availability = emp.availability.get(shiftId) || 3;
|
||||||
|
const rolePriority = emp.role === 'erfahren' ? 0 : 1;
|
||||||
|
return [availability, emp.assignedCount, rolePriority];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rolePriority(role: string): number {
|
||||||
|
return role === 'erfahren' ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 hasErfahrener(assignment: string[], employees: Map<string, SchedulingEmployee>): boolean {
|
||||||
|
return assignment.some(empId => {
|
||||||
|
const emp = employees.get(empId);
|
||||||
|
return emp?.role === 'erfahren';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function respectsNewRuleIfAdded(
|
||||||
|
emp: SchedulingEmployee,
|
||||||
|
shiftId: string,
|
||||||
|
assignments: Assignment,
|
||||||
|
employees: Map<string, SchedulingEmployee>
|
||||||
|
): boolean {
|
||||||
|
const currentAssignment = assignments[shiftId] || [];
|
||||||
|
|
||||||
|
if (emp.role === 'neu') {
|
||||||
|
// Neu employee can only be added if there's already an erfahrener
|
||||||
|
return hasErfahrener(currentAssignment, employees);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erfahren employees are always allowed
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -2,22 +2,12 @@
|
|||||||
import { ShiftPlan, ScheduledShift } from '../models/ShiftPlan';
|
import { ShiftPlan, ScheduledShift } from '../models/ShiftPlan';
|
||||||
import { Employee, EmployeeAvailability } from '../models/Employee';
|
import { Employee, EmployeeAvailability } from '../models/Employee';
|
||||||
import { authService } from './authService';
|
import { authService } from './authService';
|
||||||
|
import { scheduleWithManager } from './scheduling/shiftScheduler';
|
||||||
|
import { transformToSchedulingData } from './scheduling/dataAdapter';
|
||||||
|
import { AssignmentResult, WeeklyPattern } from './scheduling/types';
|
||||||
|
|
||||||
const API_BASE_URL = 'http://localhost:3002/api/scheduled-shifts';
|
const API_BASE_URL = 'http://localhost:3002/api/scheduled-shifts';
|
||||||
|
|
||||||
export interface AssignmentResult {
|
|
||||||
assignments: { [shiftId: string]: string[] };
|
|
||||||
violations: string[];
|
|
||||||
success: boolean;
|
|
||||||
pattern: WeeklyPattern;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WeeklyPattern {
|
|
||||||
weekShifts: ScheduledShift[];
|
|
||||||
assignments: { [shiftId: string]: string[] };
|
|
||||||
weekNumber: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to get auth headers
|
// Helper function to get auth headers
|
||||||
const getAuthHeaders = () => {
|
const getAuthHeaders = () => {
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
@@ -137,41 +127,62 @@ export class ShiftAssignmentService {
|
|||||||
constraints: any = {}
|
constraints: any = {}
|
||||||
): Promise<AssignmentResult> {
|
): Promise<AssignmentResult> {
|
||||||
|
|
||||||
console.log('🔄 Starting weekly pattern assignment...');
|
console.log('🔄 Starting new scheduling algorithm...');
|
||||||
|
|
||||||
// Get defined shifts
|
// Get defined shifts for the first week
|
||||||
const definedShifts = this.getDefinedShifts(shiftPlan);
|
const definedShifts = this.getDefinedShifts(shiftPlan);
|
||||||
const activeEmployees = employees.filter(emp => emp.isActive);
|
const firstWeekShifts = this.getFirstWeekShifts(definedShifts);
|
||||||
|
|
||||||
console.log('📊 Plan analysis:');
|
console.log('📊 First week analysis:', {
|
||||||
console.log('- Total shifts in plan:', definedShifts.length);
|
totalShifts: definedShifts.length,
|
||||||
console.log('- Active employees:', activeEmployees.length);
|
firstWeekShifts: firstWeekShifts.length,
|
||||||
|
employees: employees.length
|
||||||
|
});
|
||||||
|
|
||||||
// STRATEGY: Create weekly pattern and repeat
|
// Transform data for scheduling algorithm
|
||||||
const weeklyPattern = await this.createWeeklyPattern(
|
const { schedulingEmployees, schedulingShifts, managerShifts } = transformToSchedulingData(
|
||||||
definedShifts,
|
employees.filter(emp => emp.isActive),
|
||||||
activeEmployees,
|
firstWeekShifts,
|
||||||
availabilities,
|
availabilities
|
||||||
constraints.enforceNoTraineeAlone
|
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('🎯 Weekly pattern created for', weeklyPattern.weekShifts.length, 'shifts');
|
console.log('🎯 Transformed data for scheduling:', {
|
||||||
|
employees: schedulingEmployees.length,
|
||||||
|
shifts: schedulingShifts.length,
|
||||||
|
managerShifts: managerShifts.length
|
||||||
|
});
|
||||||
|
|
||||||
// Apply pattern to all weeks in the plan
|
// Run the scheduling algorithm
|
||||||
const assignments = this.applyWeeklyPattern(definedShifts, weeklyPattern);
|
const schedulingResult = scheduleWithManager(
|
||||||
|
schedulingShifts,
|
||||||
const violations = this.findViolations(assignments, activeEmployees, definedShifts, constraints.enforceNoTraineeAlone);
|
schedulingEmployees,
|
||||||
|
managerShifts,
|
||||||
|
{
|
||||||
|
enforceNoTraineeAlone: constraints.enforceNoTraineeAlone ?? true,
|
||||||
|
enforceExperiencedWithChef: constraints.enforceExperiencedWithChef ?? true,
|
||||||
|
maxRepairAttempts: constraints.maxRepairAttempts ?? 50
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
console.log('📊 Weekly pattern assignment completed:');
|
console.log('📊 Scheduling completed:', {
|
||||||
console.log('- Pattern shifts:', weeklyPattern.weekShifts.length);
|
assignments: Object.keys(schedulingResult.assignments).length,
|
||||||
console.log('- Total plan shifts:', definedShifts.length);
|
violations: schedulingResult.violations.length,
|
||||||
console.log('- Assignments made:', Object.values(assignments).flat().length);
|
success: schedulingResult.success
|
||||||
console.log('- Violations:', violations.length);
|
});
|
||||||
|
|
||||||
|
// Apply weekly pattern to all shifts
|
||||||
|
const weeklyPattern: WeeklyPattern = {
|
||||||
|
weekShifts: firstWeekShifts,
|
||||||
|
assignments: schedulingResult.assignments,
|
||||||
|
weekNumber: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
const allAssignments = this.applyWeeklyPattern(definedShifts, weeklyPattern);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assignments,
|
assignments: allAssignments,
|
||||||
violations,
|
violations: schedulingResult.violations,
|
||||||
success: violations.length === 0,
|
success: schedulingResult.violations.length === 0,
|
||||||
pattern: weeklyPattern
|
pattern: weeklyPattern
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user