updated frorntend to backend

This commit is contained in:
2025-10-19 16:20:13 +02:00
parent a8d371dbe9
commit 2976d091cf
6 changed files with 240 additions and 36 deletions

View File

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

View File

@@ -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__":

View File

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

View File

@@ -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) => {

View File

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