mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 15:05:45 +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();
|
||||
@@ -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",
|
||||
|
||||
43
frontend/src/components/Scheduler.tsx
Normal file
43
frontend/src/components/Scheduler.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
37
frontend/src/hooks/useScheduling.ts
Normal file
37
frontend/src/hooks/useScheduling.ts
Normal 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 };
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user