mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-11-30 22:45:46 +01:00
using cp for plans
This commit is contained in:
@@ -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",
|
||||
|
||||
51
backend/src/models/scheduling.ts
Normal file
51
backend/src/models/scheduling.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
25
backend/src/routes/scheduling.ts
Normal file
25
backend/src/routes/scheduling.ts
Normal 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;
|
||||
@@ -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) => {
|
||||
|
||||
47
backend/src/services/SchedulingService.ts
Normal file
47
backend/src/services/SchedulingService.ts
Normal 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}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
105
backend/src/workers/cp-sat-wrapper.ts
Normal file
105
backend/src/workers/cp-sat-wrapper.ts
Normal 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
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
165
backend/src/workers/schedular-worker.ts
Normal file
165
backend/src/workers/schedular-worker.ts
Normal 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();
|
||||
Reference in New Issue
Block a user