mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 15:05:45 +01:00
updated frorntend to backend
This commit is contained in:
@@ -3,9 +3,10 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "npx tsx src/server.ts",
|
"dev": "npm run build && npx tsx src/server.ts",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "node dist/server.js"
|
"start": "node dist/server.js",
|
||||||
|
"prestart": "npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
"@types/jsonwebtoken": "^9.0.2",
|
"@types/jsonwebtoken": "^9.0.2",
|
||||||
"@types/uuid": "^9.0.2",
|
"@types/uuid": "^9.0.2",
|
||||||
"ts-node": "^10.9.0",
|
"ts-node": "^10.9.0",
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0",
|
||||||
|
"tsx": "^4.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,6 +154,54 @@ class ScheduleOptimizer:
|
|||||||
for trainee_var in trainee_vars:
|
for trainee_var in trainee_vars:
|
||||||
self.model.Add(sum(experienced_vars) >= 1).OnlyEnforceIf(trainee_var)
|
self.model.Add(sum(experienced_vars) >= 1).OnlyEnforceIf(trainee_var)
|
||||||
|
|
||||||
|
# Prevent shifts with only one worker unless that worker can work alone
|
||||||
|
for shift in shifts:
|
||||||
|
shift_id = shift['id']
|
||||||
|
|
||||||
|
# Create a variable for "this shift has exactly one worker"
|
||||||
|
shift_has_one_worker = self.model.NewBoolVar(f'shift_{shift_id}_one_worker')
|
||||||
|
shift_assignment_count = sum(assignments[emp['id']].get(shift_id, 0)
|
||||||
|
for emp in all_employees
|
||||||
|
if shift_id in assignments.get(emp['id'], {}))
|
||||||
|
|
||||||
|
# Link the count to the boolean variable
|
||||||
|
self.model.Add(shift_assignment_count == 1).OnlyEnforceIf(shift_has_one_worker)
|
||||||
|
self.model.Add(shift_assignment_count != 1).OnlyEnforceIf(shift_has_one_worker.Not())
|
||||||
|
|
||||||
|
# Create a variable for "this shift has someone who cannot work alone"
|
||||||
|
has_cannot_work_alone = self.model.NewBoolVar(f'shift_{shift_id}_cannot_work_alone')
|
||||||
|
cannot_work_alone_vars = []
|
||||||
|
for employee in all_employees:
|
||||||
|
employee_id = employee['id']
|
||||||
|
if shift_id in assignments.get(employee_id, {}):
|
||||||
|
is_experienced = employee.get('employeeType') == 'experienced'
|
||||||
|
can_work_alone = employee.get('canWorkAlone', False)
|
||||||
|
if not (is_experienced and can_work_alone):
|
||||||
|
cannot_work_alone_vars.append(assignments[employee_id][shift_id])
|
||||||
|
|
||||||
|
if cannot_work_alone_vars:
|
||||||
|
self.model.Add(sum(cannot_work_alone_vars) >= 1).OnlyEnforceIf(has_cannot_work_alone)
|
||||||
|
self.model.Add(sum(cannot_work_alone_vars) == 0).OnlyEnforceIf(has_cannot_work_alone.Not())
|
||||||
|
|
||||||
|
# Constraint: If shift has one worker, it cannot have someone who cannot work alone
|
||||||
|
self.model.AddImplication(shift_has_one_worker, has_cannot_work_alone.Not())
|
||||||
|
|
||||||
|
# Exact shifts per contract type
|
||||||
|
for employee in all_employees:
|
||||||
|
employee_id = employee['id']
|
||||||
|
contract_type = employee.get('contractType', 'large')
|
||||||
|
exact_shifts = 5 if contract_type == 'small' else 10
|
||||||
|
|
||||||
|
shift_vars = []
|
||||||
|
for shift in shifts:
|
||||||
|
shift_id = shift['id']
|
||||||
|
if shift_id in assignments.get(employee_id, {}):
|
||||||
|
shift_vars.append(assignments[employee_id][shift_id])
|
||||||
|
|
||||||
|
if shift_vars:
|
||||||
|
self.model.Add(sum(shift_vars) == exact_shifts)
|
||||||
|
self.resolution_report.append(f"📋 Employee {employee_id}: {exact_shifts} shifts ({contract_type} contract)")
|
||||||
|
|
||||||
# Constraint: Contract hours limits
|
# Constraint: Contract hours limits
|
||||||
for employee in all_employees:
|
for employee in all_employees:
|
||||||
employee_id = employee['id']
|
employee_id = employee['id']
|
||||||
@@ -184,7 +232,7 @@ class ScheduleOptimizer:
|
|||||||
objective_terms.append(assignments[employee_id][shift_id] * 5)
|
objective_terms.append(assignments[employee_id][shift_id] * 5)
|
||||||
# Penalize unavailable assignments (shouldn't happen due to constraints)
|
# Penalize unavailable assignments (shouldn't happen due to constraints)
|
||||||
else:
|
else:
|
||||||
objective_terms.append(assignments[employee_id][shift_id] * -100)
|
objective_terms.append(assignments[employee_id][shift_id] * -1000)
|
||||||
|
|
||||||
self.model.Maximize(sum(objective_terms))
|
self.model.Maximize(sum(objective_terms))
|
||||||
|
|
||||||
@@ -204,6 +252,40 @@ class ScheduleOptimizer:
|
|||||||
else:
|
else:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
def groupShiftsByDay(self, shifts):
|
||||||
|
"""Group shifts by date"""
|
||||||
|
shifts_by_day = defaultdict(list)
|
||||||
|
for shift in shifts:
|
||||||
|
date = shift.get('date', 'unknown')
|
||||||
|
shifts_by_day[date].append(shift)
|
||||||
|
return shifts_by_day
|
||||||
|
|
||||||
|
def getAvailability(self, employee_id, shift_id, availabilities):
|
||||||
|
"""Get availability level for employee and shift"""
|
||||||
|
for avail in availabilities:
|
||||||
|
if avail.get('employeeId') == employee_id and avail.get('shiftId') == shift_id:
|
||||||
|
return avail.get('availability', 2) # Default to available
|
||||||
|
return 2 # Default to available if no preference specified
|
||||||
|
|
||||||
|
def extractAssignments(self, assignments, employees, shifts):
|
||||||
|
"""Extract assignments from solution"""
|
||||||
|
result_assignments = {}
|
||||||
|
|
||||||
|
# Initialize with empty lists
|
||||||
|
for shift in shifts:
|
||||||
|
result_assignments[shift['id']] = []
|
||||||
|
|
||||||
|
# Fill with assigned employees
|
||||||
|
for employee in employees:
|
||||||
|
employee_id = employee['id']
|
||||||
|
for shift in shifts:
|
||||||
|
shift_id = shift['id']
|
||||||
|
if (shift_id in assignments.get(employee_id, {}) and
|
||||||
|
self.solver.Value(assignments[employee_id][shift_id]) == 1):
|
||||||
|
result_assignments[shift_id].append(employee_id)
|
||||||
|
|
||||||
|
return result_assignments
|
||||||
|
|
||||||
def formatAssignments(self, assignments):
|
def formatAssignments(self, assignments):
|
||||||
"""Format assignments for frontend consumption"""
|
"""Format assignments for frontend consumption"""
|
||||||
formatted = {}
|
formatted = {}
|
||||||
@@ -217,8 +299,102 @@ class ScheduleOptimizer:
|
|||||||
return shiftPlan['shifts']
|
return shiftPlan['shifts']
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# ... (keep the other helper methods from your original code)
|
def filterEmployees(self, employees, condition):
|
||||||
# detectAllViolations, fixViolations, createTraineePartners, etc.
|
"""Filter employees based on condition"""
|
||||||
|
if callable(condition):
|
||||||
|
return [emp for emp in employees if condition(emp)]
|
||||||
|
elif isinstance(condition, str):
|
||||||
|
return [emp for emp in employees if emp.get('employeeType') == condition]
|
||||||
|
return []
|
||||||
|
|
||||||
|
def getFirstWeekShifts(self, shifts):
|
||||||
|
"""Get shifts for the first week (simplified)"""
|
||||||
|
# For simplicity, return all shifts or implement week filtering logic
|
||||||
|
return shifts
|
||||||
|
|
||||||
|
def createTraineePartners(self, workers, availabilities):
|
||||||
|
"""Create trainee-experienced partnerships based on availability"""
|
||||||
|
# Simplified implementation - return empty dict for now
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def detectAllViolations(self, assignments, employees, availabilities, constraints, shifts):
|
||||||
|
"""Detect all constraint violations"""
|
||||||
|
violations = []
|
||||||
|
employee_map = {emp['id']: emp for emp in employees}
|
||||||
|
|
||||||
|
# Check for understaffed shifts
|
||||||
|
for shift in shifts:
|
||||||
|
shift_id = shift['id']
|
||||||
|
assigned_count = len(assignments.get(shift_id, []))
|
||||||
|
min_required = shift.get('minWorkers', 1)
|
||||||
|
|
||||||
|
if assigned_count < min_required:
|
||||||
|
violations.append(f"UNDERSTAFFED: Shift {shift_id} has {assigned_count} employees but requires {min_required}")
|
||||||
|
|
||||||
|
# Check for trainee supervision
|
||||||
|
for shift in shifts:
|
||||||
|
shift_id = shift['id']
|
||||||
|
assigned_employees = assignments.get(shift_id, [])
|
||||||
|
has_trainee = any(employee_map.get(emp_id, {}).get('employeeType') == 'trainee' for emp_id in assigned_employees)
|
||||||
|
has_experienced = any(employee_map.get(emp_id, {}).get('employeeType') == 'experienced' for emp_id in assigned_employees)
|
||||||
|
|
||||||
|
if has_trainee and not has_experienced:
|
||||||
|
violations.append(f"TRAINEE_UNSUPERVISED: Shift {shift_id} has trainee but no experienced employee")
|
||||||
|
|
||||||
|
# Check for multiple shifts per day
|
||||||
|
shifts_by_day = self.groupShiftsByDay(shifts)
|
||||||
|
for employee in employees:
|
||||||
|
employee_id = employee['id']
|
||||||
|
for date, day_shifts in shifts_by_day.items():
|
||||||
|
shifts_assigned = 0
|
||||||
|
for shift in day_shifts:
|
||||||
|
if employee_id in assignments.get(shift['id'], []):
|
||||||
|
shifts_assigned += 1
|
||||||
|
|
||||||
|
if shifts_assigned > 1:
|
||||||
|
violations.append(f"MULTIPLE_SHIFTS: {employee.get('name', employee_id)} has {shifts_assigned} shifts on {date}")
|
||||||
|
|
||||||
|
# Check contract type constraints
|
||||||
|
for employee in employees:
|
||||||
|
employee_id = employee['id']
|
||||||
|
contract_type = employee.get('contractType', 'large')
|
||||||
|
expected_shifts = 5 if contract_type == 'small' else 10
|
||||||
|
|
||||||
|
total_shifts = 0
|
||||||
|
for shift_assignments in assignments.values():
|
||||||
|
if employee_id in shift_assignments:
|
||||||
|
total_shifts += 1
|
||||||
|
|
||||||
|
if total_shifts != expected_shifts:
|
||||||
|
violations.append(f"CONTRACT_VIOLATION: {employee.get('name', employee_id)} has {total_shifts} shifts but should have exactly {expected_shifts} ({contract_type} contract)")
|
||||||
|
|
||||||
|
return violations
|
||||||
|
|
||||||
|
def fixViolations(self, assignments, employees, availabilities, constraints, shifts, maxIterations=20):
|
||||||
|
"""Fix violations in assignments"""
|
||||||
|
# Simplified implementation - return original assignments
|
||||||
|
# In a real implementation, this would iteratively fix violations
|
||||||
|
return assignments
|
||||||
|
|
||||||
|
def assignManagersToPriority(self, managers, assignments, availabilities, shifts):
|
||||||
|
"""Assign managers to priority shifts"""
|
||||||
|
# Simplified implementation - return original assignments
|
||||||
|
return assignments
|
||||||
|
|
||||||
|
def countCriticalViolations(self, violations):
|
||||||
|
"""Count critical violations"""
|
||||||
|
critical_keywords = ['UNDERSTAFFED', 'TRAINEE_UNSUPERVISED', 'CONTRACT_VIOLATION']
|
||||||
|
return sum(1 for violation in violations if any(keyword in violation for keyword in critical_keywords))
|
||||||
|
|
||||||
|
def errorResult(self, error):
|
||||||
|
"""Return error result"""
|
||||||
|
return {
|
||||||
|
'assignments': {},
|
||||||
|
'violations': [f'Error: {str(error)}'],
|
||||||
|
'success': False,
|
||||||
|
'resolution_report': [f'Critical error: {str(error)}'],
|
||||||
|
'error': str(error)
|
||||||
|
}
|
||||||
|
|
||||||
# Main execution for Python script
|
# Main execution for Python script
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -26,10 +26,6 @@ app.use('/api/shift-plans', shiftPlanRoutes);
|
|||||||
app.use('/api/scheduled-shifts', scheduledShifts);
|
app.use('/api/scheduled-shifts', scheduledShifts);
|
||||||
app.use('/api/scheduling', schedulingRoutes);
|
app.use('/api/scheduling', schedulingRoutes);
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
|
||||||
console.log(`Scheduling server running on port ${PORT}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Error handling middleware should come after routes
|
// Error handling middleware should come after routes
|
||||||
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
console.error('Unhandled error:', err);
|
console.error('Unhandled error:', err);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { parentPort, workerData } from 'worker_threads';
|
|||||||
import { CPModel, CPSolver } from './cp-sat-wrapper.js';
|
import { CPModel, CPSolver } from './cp-sat-wrapper.js';
|
||||||
import { ShiftPlan, Shift } from '../models/ShiftPlan.js';
|
import { ShiftPlan, Shift } from '../models/ShiftPlan.js';
|
||||||
import { Employee, EmployeeAvailability } from '../models/Employee.js';
|
import { Employee, EmployeeAvailability } from '../models/Employee.js';
|
||||||
import { Availability, Constraint } from '../models/scheduling.js';
|
import { Availability, Constraint, Violation, SolverOptions, Solution, Assignment } from '../models/scheduling.js';
|
||||||
|
|
||||||
interface WorkerData {
|
interface WorkerData {
|
||||||
shiftPlan: ShiftPlan;
|
shiftPlan: ShiftPlan;
|
||||||
@@ -13,6 +13,7 @@ interface WorkerData {
|
|||||||
shifts: Shift[];
|
shifts: Shift[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function buildSchedulingModel(model: CPModel, data: WorkerData): void {
|
function buildSchedulingModel(model: CPModel, data: WorkerData): void {
|
||||||
const { employees, shifts, availabilities, constraints } = data;
|
const { employees, shifts, availabilities, constraints } = data;
|
||||||
|
|
||||||
@@ -93,26 +94,36 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 6. Contract Hours Constraints
|
// 6. Employee cant workalone
|
||||||
employees.forEach((employee: any) => {
|
employees.forEach((employee: any) => {
|
||||||
const contractHours = employee.contractType === 'small' ? 20 : 40;
|
if (employee.employeeType === 'experienced' && employee.canWorkAlone) {
|
||||||
const shiftHoursVars: string[] = [];
|
shifts.forEach((shift: any) => {
|
||||||
|
const varName = `assign_${employee.id}_${shift.id}`;
|
||||||
|
// Allow this employee to work alone (no additional constraint needed)
|
||||||
|
// This is more about not preventing single assignments
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. Contract Type Shifts Constraint
|
||||||
|
employees.forEach((employee: any) => {
|
||||||
|
const exactShiftsPerWeek = employee.contractType === 'small' ? 5 : 10; // Example: exactly 5 shifts for small, 10 for large
|
||||||
|
const shiftVars: string[] = [];
|
||||||
|
|
||||||
shifts.forEach((shift: any) => {
|
shifts.forEach((shift: any) => {
|
||||||
const shiftHours = 8; // Assuming 8 hours per shift
|
|
||||||
const varName = `assign_${employee.id}_${shift.id}`;
|
const varName = `assign_${employee.id}_${shift.id}`;
|
||||||
shiftHoursVars.push(`${shiftHours} * ${varName}`);
|
shiftVars.push(varName);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (shiftHoursVars.length > 0) {
|
if (shiftVars.length > 0) {
|
||||||
model.addConstraint(
|
model.addConstraint(
|
||||||
`${shiftHoursVars.join(' + ')} <= ${contractHours}`,
|
`${shiftVars.join(' + ')} == ${exactShiftsPerWeek}`,
|
||||||
`Contract hours limit for ${employee.name}`
|
`Exact shifts per week for ${employee.name} (${employee.contractType} contract)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 7. Ziel: Verfügbarkeits-Score maximieren
|
// 8. Ziel: Verfügbarkeits-Score maximieren
|
||||||
let objectiveExpression = '';
|
let objectiveExpression = '';
|
||||||
employees.forEach((employee: any) => {
|
employees.forEach((employee: any) => {
|
||||||
shifts.forEach((shift: any) => {
|
shifts.forEach((shift: any) => {
|
||||||
@@ -2,10 +2,9 @@
|
|||||||
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 { IntelligentShiftScheduler, AssignmentResult, WeeklyPattern } from './scheduling/useScheduling';
|
import { AssignmentResult, ScheduleRequest } from '../models/scheduling';
|
||||||
import { AssignmentResult } from '../models/scheduling';
|
|
||||||
|
|
||||||
const API_BASE_URL = 'http://localhost:3002/api/scheduled-shifts';
|
const API_BASE_URL = 'http://localhost:3002/api';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -23,7 +22,7 @@ export class ShiftAssignmentService {
|
|||||||
try {
|
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}`, {
|
const response = await fetch(`${API_BASE_URL}/scheduled-shifts/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@@ -72,7 +71,7 @@ export class ShiftAssignmentService {
|
|||||||
|
|
||||||
async getScheduledShift(id: string): Promise<any> {
|
async getScheduledShift(id: string): Promise<any> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/${id}`, {
|
const response = await fetch(`${API_BASE_URL}/scheduled-shifts/${id}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
}
|
}
|
||||||
@@ -103,7 +102,7 @@ export class ShiftAssignmentService {
|
|||||||
// New method to get all scheduled shifts for a plan
|
// New method to get all scheduled shifts for a plan
|
||||||
async getScheduledShiftsForPlan(planId: string): Promise<ScheduledShift[]> {
|
async getScheduledShiftsForPlan(planId: string): Promise<ScheduledShift[]> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/plan/${planId}`, {
|
const response = await fetch(`${API_BASE_URL}/scheduled-shifts/plan/${planId}`, {
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||||
}
|
}
|
||||||
@@ -134,23 +133,43 @@ export class ShiftAssignmentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async callSchedulingAPI(request: ScheduleRequest): Promise<AssignmentResult> {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/scheduling/generate-schedule`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...authService.getAuthHeaders()
|
||||||
|
},
|
||||||
|
body: JSON.stringify(request)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || 'Scheduling failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
async assignShifts(
|
async assignShifts(
|
||||||
shiftPlan: ShiftPlan,
|
shiftPlan: ShiftPlan,
|
||||||
employees: Employee[],
|
employees: Employee[],
|
||||||
availabilities: EmployeeAvailability[],
|
availabilities: EmployeeAvailability[],
|
||||||
constraints: any = {}
|
constraints: any = {}
|
||||||
): Promise<AssignmentResult> {
|
): Promise<AssignmentResult> {
|
||||||
|
console.log('🧠 Starting scheduling optimization...');
|
||||||
|
|
||||||
console.log('🧠 Starting intelligent scheduling for FIRST WEEK ONLY...');
|
const scheduleRequest: ScheduleRequest = {
|
||||||
|
shiftPlan,
|
||||||
|
employees,
|
||||||
|
availabilities: availabilities.map(avail => ({
|
||||||
return {
|
...avail,
|
||||||
assignments: ,
|
preferenceLevel: avail.preferenceLevel as 1 | 2 | 3
|
||||||
violations: ,
|
})),
|
||||||
success: ,
|
constraints: Array.isArray(constraints) ? constraints : []
|
||||||
resolutionReport: ,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return await this.callSchedulingAPI(scheduleRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user