using cp for plans

This commit is contained in:
2025-10-18 23:35:46 +02:00
parent 07c495a6dc
commit 372b9b2f20
13 changed files with 485 additions and 9 deletions

View File

@@ -15,7 +15,8 @@
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
"sqlite3": "^5.1.6",
"uuid": "^9.0.0"
"uuid": "^9.0.0",
"worker_threads": "native"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.2",

View File

@@ -0,0 +1,51 @@
// backend/src/types/scheduling.ts
import { Employee } from './Employee.js';
import { ShiftPlan } from './ShiftPlan.js';
export interface ScheduleRequest {
shiftPlan: ShiftPlan;
employees: Employee[];
availabilities: Availability[];
constraints: Constraint[];
}
export interface ScheduleResult {
assignments: Assignment[];
violations: Violation[];
success: boolean;
resolutionReport: string[];
processingTime: number;
}
export interface Assignment {
shiftId: string;
employeeId: string;
assignedAt: Date;
score: number; // Qualität der Zuweisung (1-100)
}
export interface Violation {
type: string;
severity: 'critical' | 'warning';
message: string;
involvedEmployees?: string[];
shiftId?: string;
}
export interface SolverOptions {
maxTimeInSeconds: number;
numSearchWorkers: number;
logSearchProgress: boolean;
}
export interface Solution {
assignments: Assignment[];
violations: Violation[];
success: boolean;
metadata: {
solveTime: number;
constraintsAdded: number;
variablesCreated: number;
optimal: boolean;
};
}

View File

@@ -0,0 +1,25 @@
// routes/scheduling.ts
import express from 'express';
import { SchedulingService } from '../services/SchedulingService.js';
const router = express.Router();
router.post('/generate-schedule', async (req, res) => {
try {
const { shiftPlan, employees, availabilities, constraints } = req.body;
const scheduler = new SchedulingService();
const result = await scheduler.generateOptimalSchedule({
shiftPlan,
employees,
availabilities,
constraints
});
res.json(result);
} catch (error) {
res.status(500).json({ error: 'Scheduling failed', details: error });
}
});
export default router;

View File

@@ -9,6 +9,7 @@ import employeeRoutes from './routes/employees.js';
import shiftPlanRoutes from './routes/shiftPlans.js';
import setupRoutes from './routes/setup.js';
import scheduledShifts from './routes/scheduledShifts.js';
import schedulingRoutes from './routes/scheduling.js';
const app = express();
const PORT = 3002;
@@ -23,6 +24,11 @@ app.use('/api/auth', authRoutes);
app.use('/api/employees', employeeRoutes);
app.use('/api/shift-plans', shiftPlanRoutes);
app.use('/api/scheduled-shifts', scheduledShifts);
app.use('/api/scheduling', schedulingRoutes);
app.listen(PORT, () => {
console.log(`Scheduling server running on port ${PORT}`);
});
// Error handling middleware should come after routes
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {

View File

@@ -0,0 +1,47 @@
// src/services/schedulingService.ts
import { Worker } from 'worker_threads';
import path from 'path';
import { Employee, ShiftPlan } from '../models/Employee.js';
export interface ScheduleRequest {
shiftPlan: ShiftPlan;
employees: Employee[];
availabilities: Availability[];
constraints: Constraint[];
}
export interface ScheduleResult {
assignments: Assignment[];
violations: Violation[];
success: boolean;
resolutionReport: string[];
processingTime: number;
}
export class SchedulingService {
async generateOptimalSchedule(request: ScheduleRequest): Promise<ScheduleResult> {
return new Promise((resolve, reject) => {
const worker = new Worker(path.resolve(__dirname, '../workers/scheduler-worker.js'), {
workerData: request
});
// Timeout nach 110 Sekunden
const timeout = setTimeout(() => {
worker.terminate();
reject(new Error('Scheduling timeout after 110 seconds'));
}, 110000);
worker.on('message', (result: ScheduleResult) => {
clearTimeout(timeout);
resolve(result);
});
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`));
}
});
});
}
}

View File

@@ -0,0 +1,105 @@
// backend/src/workers/cp-sat-wrapper.ts
import { execSync } from 'child_process';
import { randomBytes } from 'crypto';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { SolverOptions, Solution, Assignment, Violation } from '../models/scheduling.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export class CPModel {
private modelData: any;
constructor() {
this.modelData = {
variables: {},
constraints: [],
objective: null
};
}
addVariable(name: string, type: 'bool' | 'int', min?: number, max?: number): void {
this.modelData.variables[name] = { type, min, max };
}
addConstraint(expression: string, description?: string): void {
this.modelData.constraints.push({
expression,
description
});
}
maximize(expression: string): void {
this.modelData.objective = {
type: 'maximize',
expression
};
}
minimize(expression: string): void {
this.modelData.objective = {
type: 'minimize',
expression
};
}
export(): any {
return this.modelData;
}
}
export class CPSolver {
constructor(private options: SolverOptions) {}
async solve(model: CPModel): Promise<Solution> {
try {
return await this.solveViaPythonBridge(model);
} catch (error) {
console.error('CP-SAT bridge failed, falling back to TypeScript solver:', error);
return await this.solveWithTypeScript(model);
}
}
private async solveViaPythonBridge(model: CPModel): Promise<Solution> {
const pythonScriptPath = path.resolve(__dirname, '../../python-scripts/scheduling_solver.py');
const modelData = model.export();
const result = execSync(`python3 "${pythonScriptPath}"`, {
input: JSON.stringify(modelData),
timeout: this.options.maxTimeInSeconds * 1000,
encoding: 'utf-8',
maxBuffer: 50 * 1024 * 1024 // 50MB buffer für große Probleme
});
return JSON.parse(result);
}
private async solveWithTypeScript(model: CPModel): Promise<Solution> {
// Einfacher TypeScript CSP Solver als Fallback
return this.basicBacktrackingSolver(model);
}
private async basicBacktrackingSolver(model: CPModel): Promise<Solution> {
// Einfache Backtracking-Implementierung
// Für kleine Probleme geeignet
const startTime = Date.now();
// Hier einfache CSP-Logik implementieren
const assignments: Assignment[] = [];
const violations: Violation[] = [];
return {
assignments,
violations,
success: violations.length === 0,
metadata: {
solveTime: Date.now() - startTime,
constraintsAdded: model.export().constraints.length,
variablesCreated: Object.keys(model.export().variables).length,
optimal: true
}
};
}
}

View File

@@ -0,0 +1,165 @@
// backend/src/workers/scheduler-worker.ts
import { parentPort, workerData } from 'worker_threads';
import { CPModel, CPSolver } from './cp-sat-wrapper.js';
import { ShiftPlan} from '../models/ShiftPlan.js';
import { Employee, } from '../models/Employee.js';
interface WorkerData {
shiftPlan: ShiftPlan;
employees: Employee[];
availabilities: any[];
constraints: any[];
}
function buildSchedulingModel(model: CPModel, data: WorkerData): void {
const { employees, shifts, availabilities, constraints } = data;
// 1. Entscheidungsvariablen erstellen
employees.forEach(employee => {
shifts.forEach(shift => {
const varName = `assign_${employee.id}_${shift.id}`;
model.addVariable(varName, 'bool');
});
});
// 2. Verfügbarkeits-Constraints
employees.forEach(employee => {
shifts.forEach(shift => {
const availability = availabilities.find(
a => a.employeeId === employee.id && a.shiftId === shift.id
);
if (availability?.availability === 3) {
const varName = `assign_${employee.id}_${shift.id}`;
model.addConstraint(`${varName} == 0`, `Availability constraint for ${employee.name}`);
}
});
});
// 3. Schicht-Besetzungs-Constraints
shifts.forEach(shift => {
const assignmentVars = employees.map(
emp => `assign_${emp.id}_${shift.id}`
);
model.addConstraint(
`${assignmentVars.join(' + ')} >= ${shift.minWorkers}`,
`Min workers for shift ${shift.id}`
);
model.addConstraint(
`${assignmentVars.join(' + ')} <= ${shift.maxWorkers}`,
`Max workers for shift ${shift.id}`
);
});
// 4. Keine zwei Schichten pro Tag pro Employee
employees.forEach(employee => {
const shiftsByDate = groupShiftsByDate(shifts);
Object.entries(shiftsByDate).forEach(([date, dayShifts]) => {
const dayAssignmentVars = dayShifts.map(
(shift: any) => `assign_${employee.id}_${shift.id}`
);
model.addConstraint(
`${dayAssignmentVars.join(' + ')} <= 1`,
`Max one shift per day for ${employee.name} on ${date}`
);
});
});
// 5. Trainee-Überwachungs-Constraints
const trainees = employees.filter(emp => emp.employeeType === 'trainee');
const experienced = employees.filter(emp => emp.employeeType === 'experienced');
trainees.forEach(trainee => {
shifts.forEach(shift => {
const traineeVar = `assign_${trainee.id}_${shift.id}`;
const experiencedVars = experienced.map(exp => `assign_${exp.id}_${shift.id}`);
model.addConstraint(
`${traineeVar} <= ${experiencedVars.join(' + ')}`,
`Trainee ${trainee.name} requires supervision in shift ${shift.id}`
);
});
});
// 6. Ziel: Verfügbarkeits-Score maximieren
let objectiveExpression = '';
employees.forEach(employee => {
shifts.forEach(shift => {
const availability = availabilities.find(
a => a.employeeId === employee.id && a.shiftId === shift.id
);
if (availability) {
const score = availability.availability === 1 ? 3 :
availability.availability === 2 ? 1 : 0;
const varName = `assign_${employee.id}_${shift.id}`;
objectiveExpression += objectiveExpression ? ` + ${score} * ${varName}` : `${score} * ${varName}`;
}
});
});
model.maximize(objectiveExpression);
}
function groupShiftsByDate(shifts: any[]): Record<string, any[]> {
return shifts.reduce((groups, shift) => {
const date = shift.date.split('T')[0];
if (!groups[date]) groups[date] = [];
groups[date].push(shift);
return groups;
}, {});
}
async function runScheduling() {
const data: WorkerData = workerData;
try {
console.log('Starting scheduling optimization...');
const startTime = Date.now();
const model = new CPModel();
buildSchedulingModel(model, data);
const solver = new CPSolver({
maxTimeInSeconds: 105,
numSearchWorkers: 8,
logSearchProgress: true
});
const solution = await solver.solve(model);
solution.processingTime = Date.now() - startTime;
console.log(`Scheduling completed in ${solution.processingTime}ms`);
parentPort?.postMessage({
assignments: solution.assignments,
violations: solution.violations,
success: solution.success,
resolutionReport: [
`Solved in ${solution.processingTime}ms`,
`Variables: ${solution.metadata.variablesCreated}`,
`Constraints: ${solution.metadata.constraintsAdded}`,
`Optimal: ${solution.metadata.optimal}`
],
processingTime: solution.processingTime
});
} catch (error) {
console.error('Scheduling worker error:', error);
parentPort?.postMessage({
error: error.message,
success: false,
assignments: [],
violations: [],
resolutionReport: [`Error: ${error.message}`],
processingTime: 0
});
}
}
runScheduling();

View File

@@ -17,7 +17,8 @@
"react-router-dom": "^7.9.3",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
"web-vitals": "^2.1.4",
"worker_threads": "native"
},
"scripts": {
"start": "react-scripts start",

View File

@@ -0,0 +1,43 @@
// src/components/Scheduler.tsx
import React from 'react';
import { useScheduling } from '../hooks/useScheduling';
interface Props {
scheduleRequest: ScheduleRequest;
}
export const Scheduler: React.FC<Props> = ({ scheduleRequest }) => {
const { generateSchedule, loading, error, result } = useScheduling();
const handleGenerateSchedule = async () => {
try {
await generateSchedule(scheduleRequest);
} catch (err) {
// Error handling
}
};
return (
<div>
<button
onClick={handleGenerateSchedule}
disabled={loading}
>
{loading ? 'Generating Schedule...' : 'Generate Optimal Schedule'}
</button>
{loading && (
<div>
<progress max="100" value="70" />
<p>Optimizing schedule... (max 2 minutes)</p>
</div>
)}
{error && <div className="error">{error}</div>}
{result && (
<ScheduleResultView result={result} />
)}
</div>
);
};

View File

@@ -0,0 +1,37 @@
// frontend/src/services/scheduling/scheduling.ts
import { useState, useCallback } from 'react';
import { ScheduleRequest, ScheduleResult } from '../types/scheduling';
export const useScheduling = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [result, setResult] = useState<ScheduleResult | null>(null);
const generateSchedule = useCallback(async (request: ScheduleRequest) => {
setLoading(true);
setError(null);
try {
const response = await fetch('/api/scheduling/generate-schedule', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request)
});
if (!response.ok) throw new Error('Scheduling request failed');
const data: ScheduleResult = await response.json();
setResult(data);
return data;
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
setError(errorMessage);
throw err;
} finally {
setLoading(false);
}
}, []);
return { generateSchedule, loading, error, result };
};

View File

@@ -5,7 +5,7 @@ import { useAuth } from '../../contexts/AuthContext';
import { shiftPlanService } from '../../services/shiftPlanService';
import { employeeService } from '../../services/employeeService';
import { shiftAssignmentService, ShiftAssignmentService } from '../../services/shiftAssignmentService';
import { IntelligentShiftScheduler, SchedulingResult, AssignmentResult } from '../../services/scheduling';
import { IntelligentShiftScheduler, SchedulingResult, AssignmentResult } from '../../hooks/useScheduling';
import { ShiftPlan, TimeSlot, ScheduledShift } from '../../models/ShiftPlan';
import { Employee, EmployeeAvailability } from '../../models/Employee';
import { useNotification } from '../../contexts/NotificationContext';

View File

@@ -1,5 +0,0 @@
// frontend/src/services/scheduling.ts/scheduling.ts
import { Employee, EmployeeAvailability } from '../models/Employee';
import { ScheduledShift, ShiftPlan } from '../models/ShiftPlan';
import { shiftAssignmentService } from './shiftAssignmentService';

View File

@@ -2,7 +2,7 @@
import { ShiftPlan, ScheduledShift } from '../models/ShiftPlan';
import { Employee, EmployeeAvailability } from '../models/Employee';
import { authService } from './authService';
import { IntelligentShiftScheduler, AssignmentResult, WeeklyPattern } from './scheduling';
import { IntelligentShiftScheduler, AssignmentResult, WeeklyPattern } from '../hooks/useScheduling';
import { isScheduledShift } from '../models/helpers';
const API_BASE_URL = 'http://localhost:3002/api/scheduled-shifts';