mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 06:55:45 +01:00
using cp for plans
This commit is contained in:
@@ -15,7 +15,8 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0",
|
||||||
|
"worker_threads": "native"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcryptjs": "^2.4.2",
|
"@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 shiftPlanRoutes from './routes/shiftPlans.js';
|
||||||
import setupRoutes from './routes/setup.js';
|
import setupRoutes from './routes/setup.js';
|
||||||
import scheduledShifts from './routes/scheduledShifts.js';
|
import scheduledShifts from './routes/scheduledShifts.js';
|
||||||
|
import schedulingRoutes from './routes/scheduling.js';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 3002;
|
const PORT = 3002;
|
||||||
@@ -23,6 +24,11 @@ app.use('/api/auth', authRoutes);
|
|||||||
app.use('/api/employees', employeeRoutes);
|
app.use('/api/employees', employeeRoutes);
|
||||||
app.use('/api/shift-plans', shiftPlanRoutes);
|
app.use('/api/shift-plans', shiftPlanRoutes);
|
||||||
app.use('/api/scheduled-shifts', scheduledShifts);
|
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
|
// 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) => {
|
||||||
|
|||||||
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-router-dom": "^7.9.3",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4",
|
||||||
|
"worker_threads": "native"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"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 { shiftPlanService } from '../../services/shiftPlanService';
|
||||||
import { employeeService } from '../../services/employeeService';
|
import { employeeService } from '../../services/employeeService';
|
||||||
import { shiftAssignmentService, ShiftAssignmentService } from '../../services/shiftAssignmentService';
|
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 { ShiftPlan, TimeSlot, ScheduledShift } from '../../models/ShiftPlan';
|
||||||
import { Employee, EmployeeAvailability } from '../../models/Employee';
|
import { Employee, EmployeeAvailability } from '../../models/Employee';
|
||||||
import { useNotification } from '../../contexts/NotificationContext';
|
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 { 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';
|
import { IntelligentShiftScheduler, AssignmentResult, WeeklyPattern } from '../hooks/useScheduling';
|
||||||
import { isScheduledShift } from '../models/helpers';
|
import { isScheduledShift } from '../models/helpers';
|
||||||
|
|
||||||
const API_BASE_URL = 'http://localhost:3002/api/scheduled-shifts';
|
const API_BASE_URL = 'http://localhost:3002/api/scheduled-shifts';
|
||||||
|
|||||||
Reference in New Issue
Block a user