changed role handling, employee name handling and availibilitie handling with shift.id

This commit is contained in:
2025-10-20 01:22:38 +02:00
parent 37f75727cc
commit 9393de5239
12 changed files with 497 additions and 442 deletions

View File

@@ -34,9 +34,8 @@ export interface JWTPayload {
export interface RegisterRequest {
email: string;
password: string;
name: string;
//employee_type?: string;
//is_sufficiently_independent?: string;
firstname: String;
lastname: String;
role?: string;
}
@@ -53,7 +52,7 @@ export const login = async (req: Request, res: Response) => {
// Get user from database
const user = await db.get<EmployeeWithPassword>(
'SELECT id, email, password, name, role, employee_type as employeeType, contract_type as contractType, can_work_alone as canWorkAlone, is_active as isActive FROM employees WHERE email = ? AND is_active = 1',
'SELECT id, email, password, firstname, lastname, role, employee_type as employeeType, contract_type as contractType, can_work_alone as canWorkAlone, is_active as isActive FROM employees WHERE email = ? AND is_active = 1',
[email]
);
@@ -117,7 +116,7 @@ export const getCurrentUser = async (req: Request, res: Response) => {
}
const user = await db.get<Employee>(
'SELECT id, email, name, role, employee_type as employeeType, contract_type as contractType, can_work_alone as canWorkAlone, is_active as isActive FROM employees WHERE id = ? AND is_active = 1',
'SELECT id, email, firstname, lastname, role, employee_type as employeeType, contract_type as contractType, can_work_alone as canWorkAlone, is_active as isActive FROM employees WHERE id = ? AND is_active = 1',
[jwtUser.userId]
);
@@ -163,10 +162,10 @@ export const validateToken = async (req: Request, res: Response) => {
export const register = async (req: Request, res: Response) => {
try {
const { email, password, name, role = 'user' } = req.body as RegisterRequest;
const { email, password, firstname, lastname, role = 'user' } = req.body as RegisterRequest;
// Validate required fields
if (!email || !password || !name) {
if (!email || !password || !firstname || !lastname) {
return res.status(400).json({
error: 'E-Mail, Passwort und Name sind erforderlich'
});
@@ -188,11 +187,11 @@ export const register = async (req: Request, res: Response) => {
const hashedPassword = await bcrypt.hash(password, 10);
// Insert user
const result = await db.run(
`INSERT INTO employees (id, email, password, name, role, employee_type, contract_type, can_work_alone, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[uuidv4(), email, hashedPassword, name, role, 'experienced', 'small', false, 1]
);
const result = await db.run(
`INSERT INTO employees (id, email, password, firstname, lastname, role, employee_type, contract_type, can_work_alone, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[uuidv4(), email, hashedPassword, firstname, lastname, role, 'experienced', 'small', false, 1]
);
if (!result.lastID) {
throw new Error('Benutzer konnte nicht erstellt werden');

View File

@@ -15,7 +15,7 @@ export const getEmployees = async (req: AuthRequest, res: Response): Promise<voi
let query = `
SELECT
id, email, name, role, is_active as isActive,
id, email, firstname, lastname, role, is_active as isActive,
employee_type as employeeType,
contract_type as contractType,
can_work_alone as canWorkAlone,
@@ -46,7 +46,7 @@ export const getEmployee = async (req: AuthRequest, res: Response): Promise<void
const employee = await db.get<any>(`
SELECT
id, email, name, role, is_active as isActive,
id, email, firstname, lastname, role, is_active as isActive,
employee_type as employeeType,
contract_type as contractType,
can_work_alone as canWorkAlone,
@@ -78,15 +78,16 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
const {
email,
password,
name,
firstname,
lastname,
role,
employeeType,
contractType,
canWorkAlone // Statt isSufficientlyIndependent
canWorkAlone
} = req.body as CreateEmployeeRequest;
// Validierung
if (!email || !password || !name || !role || !employeeType || !contractType) {
if (!email || !password || !firstname || !lastname || !role || !employeeType || !contractType) {
console.log('❌ Validation failed: Missing required fields');
res.status(400).json({
error: 'Email, password, name, role, employeeType und contractType sind erforderlich'
@@ -115,18 +116,19 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
await db.run(
`INSERT INTO employees (
id, email, password, name, role, employee_type, contract_type, can_work_alone,
id, email, password, firstname, lastname, role, employee_type, contract_type, can_work_alone,
is_active
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
employeeId,
email,
hashedPassword,
name,
firstname, // Changed from name
lastname, // Added
role,
employeeType,
contractType,
canWorkAlone ? 1 : 0, // Statt isSufficientlyIndependent
canWorkAlone ? 1 : 0,
1
]
);
@@ -154,8 +156,7 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
export const updateEmployee = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const { id } = req.params;
const { name, role, isActive, employeeType, contractType, canWorkAlone } = req.body; // Statt isSufficientlyIndependent
const { firstname, lastname, role, isActive, employeeType, contractType, canWorkAlone } = req.body;
console.log('📝 Update Employee Request:', { id, name, role, isActive, employeeType, contractType, canWorkAlone });
// Check if employee exists
@@ -168,14 +169,15 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
// Update employee
await db.run(
`UPDATE employees
SET name = COALESCE(?, name),
role = COALESCE(?, role),
is_active = COALESCE(?, is_active),
employee_type = COALESCE(?, employee_type),
contract_type = COALESCE(?, contract_type),
can_work_alone = COALESCE(?, can_work_alone)
WHERE id = ?`,
[name, role, isActive, employeeType, contractType, canWorkAlone, id]
SET firstname = COALESCE(?, firstname),
lastname = COALESCE(?, lastname),
role = COALESCE(?, role),
is_active = COALESCE(?, is_active),
employee_type = COALESCE(?, employee_type),
contract_type = COALESCE(?, contract_type),
can_work_alone = COALESCE(?, can_work_alone)
WHERE id = ?`,
[firstname, lastname, role, isActive, employeeType, contractType, canWorkAlone, id]
);
console.log('✅ Employee updated successfully');
@@ -290,9 +292,11 @@ export const getAvailabilities = async (req: AuthRequest, res: Response): Promis
}
const availabilities = await db.all<any>(`
SELECT * FROM employee_availability
WHERE employee_id = ?
ORDER BY day_of_week, time_slot_id
SELECT ea.*, s.day_of_week, s.time_slot_id
FROM employee_availability ea
JOIN shifts s ON ea.shift_id = s.id
WHERE ea.employee_id = ?
ORDER BY s.day_of_week, s.time_slot_id
`, [employeeId]);
//console.log('✅ Successfully got availabilities from employee:', availabilities);
@@ -334,14 +338,13 @@ export const updateAvailabilities = async (req: AuthRequest, res: Response): Pro
for (const availability of availabilities) {
const availabilityId = uuidv4();
await db.run(
`INSERT INTO employee_availability (id, employee_id, plan_id, day_of_week, time_slot_id, preference_level, notes)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
`INSERT INTO employee_availability (id, employee_id, plan_id, shift_id, preference_level, notes)
VALUES (?, ?, ?, ?, ?, ?)`,
[
availabilityId,
employeeId,
planId,
availability.dayOfWeek,
availability.timeSlotId,
availability.shiftId,
availability.preferenceLevel,
availability.notes || null
]

View File

@@ -4,7 +4,6 @@ import bcrypt from 'bcrypt';
import { v4 as uuidv4 } from 'uuid';
import { randomUUID } from 'crypto';
import { db } from '../services/databaseService.js';
//import { initializeDefaultTemplates } from './shiftPlanController.js';
export const checkSetupStatus = async (req: Request, res: Response): Promise<void> => {
try {
@@ -44,14 +43,14 @@ export const setupAdmin = async (req: Request, res: Response): Promise<void> =>
return;
}
const { password, name } = req.body;
const { password, firstname, lastname } = req.body;
const email = 'admin@instandhaltung.de';
console.log('👤 Creating admin with data:', { name, email });
// Validation
if (!password || !name) {
res.status(400).json({ error: 'Passwort und Name sind erforderlich' });
if (!password || !firstname || !lastname) {
res.status(400).json({ error: 'Passwort, Vorname und Nachname sind erforderlich' });
return;
}
@@ -73,9 +72,9 @@ export const setupAdmin = async (req: Request, res: Response): Promise<void> =>
try {
// Create admin user
await db.run(
`INSERT INTO employees (id, email, password, name, role, is_active, employee_type, contract_type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[adminId, email, hashedPassword, name, 'admin', 1, 'manager', 'large']
`INSERT INTO employees (id, email, password, firstname, lastname, role, employee_type, contract_type, can_work_alone, is_active)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[adminId, email, hashedPassword, firstname, lastname, 'admin', 'manager', 'large', true, 1]
);
console.log('✅ Admin user created successfully');

View File

@@ -12,7 +12,7 @@ import { createPlanFromPreset, TEMPLATE_PRESETS } from '../models/defaults/shift
async function getPlanWithDetails(planId: string) {
const plan = await db.get<any>(`
SELECT sp.*, e.name as created_by_name
SELECT sp.*, e.firstname || ' ' || e.lastname as created_by_name
FROM shift_plans sp
LEFT JOIN employees e ON sp.created_by = e.id
WHERE sp.id = ?
@@ -69,7 +69,7 @@ async function getPlanWithDetails(planId: string) {
export const getShiftPlans = async (req: Request, res: Response): Promise<void> => {
try {
const plans = await db.all<any>(`
SELECT sp.*, e.name as created_by_name
SELECT sp.*, e.firstname || ' ' || e.lastname as created_by_name
FROM shift_plans sp
LEFT JOIN employees e ON sp.created_by = e.id
ORDER BY sp.created_at DESC
@@ -94,7 +94,7 @@ export const getShiftPlan = async (req: Request, res: Response): Promise<void> =
const { id } = req.params;
const plan = await db.get<any>(`
SELECT sp.*, e.name as created_by_name
SELECT sp.*, e.firstname || ' ' || e.lastname as created_by_name
FROM shift_plans sp
LEFT JOIN employees e ON sp.created_by = e.id
WHERE sp.id = ?
@@ -555,7 +555,7 @@ export const deleteShiftPlan = async (req: Request, res: Response): Promise<void
// Helper function to get plan by ID
async function getShiftPlanById(planId: string): Promise<any> {
const plan = await db.get<any>(`
SELECT sp.*, e.name as created_by_name
SELECT sp.*, e.firstname || ' ' || e.lastname as created_by_name
FROM shift_plans sp
LEFT JOIN employees e ON sp.created_by = e.id
WHERE sp.id = ?

View File

@@ -3,8 +3,8 @@ CREATE TABLE IF NOT EXISTS employees (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
name TEXT NOT NULL,
role TEXT CHECK(role IN ('admin', 'user', 'maintenance')) NOT NULL,
firstname TEXT NOT NULL,
lastname TEXT NOT NULL,
employee_type TEXT CHECK(employee_type IN ('manager', 'trainee', 'experienced')) NOT NULL,
contract_type TEXT CHECK(contract_type IN ('small', 'large')) NOT NULL,
can_work_alone BOOLEAN DEFAULT FALSE,
@@ -13,6 +13,18 @@ CREATE TABLE IF NOT EXISTS employees (
last_login TEXT DEFAULT NULL
);
-- Roles lookup table
CREATE TABLE IF NOT EXISTS roles (
role TEXT PRIMARY KEY CHECK(role IN ('admin', 'user', 'maintenance'))
);
-- Junction table: many-to-many relationship
CREATE TABLE IF NOT EXISTS employee_roles (
employee_id TEXT NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
role TEXT NOT NULL REFERENCES roles(role),
PRIMARY KEY (employee_id, role)
);
-- Shift plans table
CREATE TABLE IF NOT EXISTS shift_plans (
id TEXT PRIMARY KEY,
@@ -83,14 +95,13 @@ CREATE TABLE IF NOT EXISTS employee_availability (
id TEXT PRIMARY KEY,
employee_id TEXT NOT NULL,
plan_id TEXT NOT NULL,
day_of_week INTEGER NOT NULL CHECK (day_of_week >= 1 AND day_of_week <= 7),
time_slot_id TEXT NOT NULL,
shift_id TEXT NOT NULL,
preference_level INTEGER CHECK(preference_level IN (1, 2, 3)) NOT NULL,
notes TEXT,
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
FOREIGN KEY (plan_id) REFERENCES shift_plans(id) ON DELETE CASCADE,
FOREIGN KEY (time_slot_id) REFERENCES time_slots(id) ON DELETE CASCADE,
UNIQUE(employee_id, plan_id, day_of_week, time_slot_id)
FOREIGN KEY (shift_id) REFERENCES shifts(id) ON DELETE CASCADE,
UNIQUE(employee_id, plan_id, shift_id)
);
-- Performance indexes

View File

@@ -2,7 +2,8 @@
export interface Employee {
id: string;
email: string;
name: string;
firstname: string;
lastname: string;
role: 'admin' | 'maintenance' | 'user';
employeeType: 'manager' | 'trainee' | 'experienced';
contractType: 'small' | 'large';
@@ -15,7 +16,8 @@ export interface Employee {
export interface CreateEmployeeRequest {
email: string;
password: string;
name: string;
firstname: string;
lastname: string;
role: 'admin' | 'maintenance' | 'user';
employeeType: 'manager' | 'trainee' | 'experienced';
contractType: 'small' | 'large';
@@ -23,7 +25,8 @@ export interface CreateEmployeeRequest {
}
export interface UpdateEmployeeRequest {
name?: string;
firstname?: string;
lastname?: string;
role?: 'admin' | 'maintenance' | 'user';
employeeType?: 'manager' | 'trainee' | 'experienced';
contractType?: 'small' | 'large';
@@ -39,8 +42,7 @@ export interface EmployeeAvailability {
id: string;
employeeId: string;
planId: string;
dayOfWeek: number; // 1=Monday, 7=Sunday
timeSlotId: string;
shiftId: string; // Now references shift_id instead of time_slot_id + day_of_week
preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable
notes?: string;
}
@@ -73,3 +75,13 @@ export interface ManagerSelfAssignmentRequest {
export interface EmployeeWithAvailabilities extends Employee {
availabilities: EmployeeAvailability[];
}
// Additional types for the new roles system
export interface Role {
role: 'admin' | 'user' | 'maintenance';
}
export interface EmployeeRole {
employeeId: string;
role: 'admin' | 'user' | 'maintenance';
}

View File

@@ -50,16 +50,6 @@ export interface ShiftAssignment {
assignedBy: string;
}
export interface EmployeeAvailability {
id: string;
employeeId: string;
planId: string;
dayOfWeek: number;
timeSlotId: string;
preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable
notes?: string;
}
// Request/Response DTOs
export interface CreateShiftPlanRequest {
name: string;
@@ -95,10 +85,6 @@ export interface AssignEmployeeRequest {
scheduledShiftId: string;
}
export interface UpdateAvailabilityRequest {
planId: string;
availabilities: Omit<EmployeeAvailability, 'id' | 'employeeId'>[];
}
export interface UpdateRequiredEmployeesRequest {
requiredEmployees: number;

View File

@@ -84,38 +84,42 @@ export class SchedulingService {
}
private generateScheduledShiftsFromTemplate(shiftPlan: ShiftPlan): any[] {
const shifts: any[] = [];
if (!shiftPlan.startDate || !shiftPlan.shifts) {
return shifts;
}
const startDate = new Date(shiftPlan.startDate);
// Generate shifts for one week (Monday to Sunday)
for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
const currentDate = new Date(startDate);
currentDate.setDate(startDate.getDate() + dayOffset);
const dayOfWeek = currentDate.getDay() === 0 ? 7 : currentDate.getDay(); // Convert Sunday from 0 to 7
const dayShifts = shiftPlan.shifts.filter(shift => shift.dayOfWeek === dayOfWeek);
dayShifts.forEach(shift => {
shifts.push({
id: `generated_${currentDate.toISOString().split('T')[0]}_${shift.timeSlotId}`,
date: currentDate.toISOString().split('T')[0],
timeSlotId: shift.timeSlotId,
requiredEmployees: shift.requiredEmployees,
minWorkers: 1,
maxWorkers: 2,
isPriority: false
});
});
}
console.log("Created shifts for one week. Amount: ", shifts.length);
const shifts: any[] = [];
if (!shiftPlan || !shiftPlan.startDate) {
return shifts;
}
const startDate = new Date(shiftPlan.startDate);
// Generate shifts for one week (Monday to Sunday)
for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
const currentDate = new Date(startDate);
currentDate.setDate(startDate.getDate() + dayOffset);
const dayOfWeek = currentDate.getDay() === 0 ? 7 : currentDate.getDay(); // Convert Sunday from 0 to 7
const dayShifts = shiftPlan.shifts.filter(shift => shift.dayOfWeek === dayOfWeek);
dayShifts.forEach(shift => {
// ✅ Use day-of-week pattern instead of date-based pattern
const shiftId = `${shift.id}`;
shifts.push({
id: shiftId, // This matches what frontend expects
date: currentDate.toISOString().split('T')[0],
timeSlotId: shift.timeSlotId,
requiredEmployees: shift.requiredEmployees,
minWorkers: 1,
maxWorkers: 2,
isPriority: false
});
console.log(`✅ Generated shift: ${shiftId} for day ${dayOfWeek}, timeSlot ${shift.timeSlotId}`);
});
}
console.log("Created shifts for one week. Amount: ", shifts.length);
return shifts;
}
private prepareAvailabilities(availabilities: Availability[], shiftPlan: ShiftPlan): any[] {

View File

@@ -18,15 +18,16 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
const { employees, shifts, availabilities, constraints } = data;
// Filter employees to only include active ones
const nonManagerEmployees = employees.filter(emp => emp.isActive && emp.employeeType !== 'manager');
const activeEmployees = employees.filter(emp => emp.isActive);
const trainees = activeEmployees.filter(emp => emp.employeeType === 'trainee');
const experienced = activeEmployees.filter(emp => emp.employeeType === 'experienced');
const trainees = nonManagerEmployees.filter(emp => emp.employeeType === 'trainee');
const experienced = nonManagerEmployees.filter(emp => emp.employeeType === 'experienced');
console.log(`Building model with ${activeEmployees.length} employees, ${shifts.length} shifts`);
console.log(`Building model with ${nonManagerEmployees.length} employees, ${shifts.length} shifts`);
console.log(`Available shifts per week: ${shifts.length}`);
// 1. Create assignment variables for all possible assignments
activeEmployees.forEach((employee: any) => {
nonManagerEmployees.forEach((employee: any) => {
shifts.forEach((shift: any) => {
const varName = `assign_${employee.id}_${shift.id}`;
model.addVariable(varName, 'bool');
@@ -34,7 +35,7 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
});
// 2. Availability constraints
activeEmployees.forEach((employee: any) => {
nonManagerEmployees.forEach((employee: any) => {
shifts.forEach((shift: any) => {
const availability = availabilities.find(
(a: any) => a.employeeId === employee.id && a.shiftId === shift.id
@@ -53,7 +54,7 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
// 3. Max 1 shift per day per employee
const shiftsByDate = groupShiftsByDate(shifts);
activeEmployees.forEach((employee: any) => {
nonManagerEmployees.forEach((employee: any) => {
Object.entries(shiftsByDate).forEach(([date, dayShifts]) => {
const dayAssignmentVars = (dayShifts as any[]).map(
(shift: any) => `assign_${employee.id}_${shift.id}`
@@ -70,7 +71,7 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
// 4. Shift staffing constraints
shifts.forEach((shift: any) => {
const assignmentVars = activeEmployees.map(
const assignmentVars = nonManagerEmployees.map(
(emp: any) => `assign_${emp.id}_${shift.id}`
);
@@ -115,11 +116,42 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
});
});
// 6. Contract type constraints
// 6. Employees who cannot work alone constraint
const employeesWhoCantWorkAlone = nonManagerEmployees.filter(emp => !emp.canWorkAlone);
console.log(`Found ${employeesWhoCantWorkAlone.length} employees who cannot work alone`);
employeesWhoCantWorkAlone.forEach((employee: any) => {
shifts.forEach((shift: any) => {
const employeeVar = `assign_${employee.id}_${shift.id}`;
const otherEmployees = nonManagerEmployees.filter(emp =>
emp.id !== employee.id && emp.isActive
);
if (otherEmployees.length === 0) {
// No other employees available, this employee cannot work this shift
model.addConstraint(
`${employeeVar} == 0`,
`No other employees available for ${employee.name} in shift ${shift.id}`
);
} else {
const otherEmployeeVars = otherEmployees.map(emp =>
`assign_${emp.id}_${shift.id}`
);
// Constraint: if this employee works, at least one other must work
model.addConstraint(
`${employeeVar} <= ${otherEmployeeVars.join(' + ')}`,
`${employee.name} cannot work alone in ${shift.id}`
);
}
});
});
// 7. Contract type constraints
const totalShifts = shifts.length;
console.log(`Total available shifts: ${totalShifts}`);
activeEmployees.forEach((employee: any) => {
nonManagerEmployees.forEach((employee: any) => {
const contractType = employee.contractType || 'large';
// EXACT SHIFTS PER WEEK
@@ -146,11 +178,11 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
}
});
// 7. Objective: Maximize preferred assignments with soft constraints
// 8. Objective: Maximize preferred assignments with soft constraints
let objectiveExpression = '';
let softConstraintPenalty = '';
activeEmployees.forEach((employee: any) => {
nonManagerEmployees.forEach((employee: any) => {
shifts.forEach((shift: any) => {
const varName = `assign_${employee.id}_${shift.id}`;
const availability = availabilities.find(
@@ -192,23 +224,13 @@ function groupShiftsByDate(shifts: any[]): Record<string, any[]> {
function extractAssignmentsFromSolution(solution: any, employees: any[], shifts: any[]): any {
const assignments: any = {};
const employeeAssignments: any = {};
console.log('=== SOLUTION DEBUG INFO ===');
console.log('Solution success:', solution.success);
console.log('Raw assignments from Python:', solution.assignments?.length || 0);
console.log('Variables in solution:', Object.keys(solution.variables || {}).length);
// Initialize assignments object
shifts.forEach((shift: any) => {
assignments[shift.id] = [];
console.log('🔍 DEBUG: Available shifts with new ID pattern:');
shifts.forEach(shift => {
console.log(` - ${shift.id} (Day: ${shift.id.split('-')[0]}, TimeSlot: ${shift.id.split('-')[1]})`);
});
employees.forEach((employee: any) => {
employeeAssignments[employee.id] = 0;
});
// Python-parsed assignments
// Your existing assignment extraction logic...
if (solution.assignments && solution.assignments.length > 0) {
console.log('Using Python-parsed assignments (cleaner)');
@@ -216,32 +238,28 @@ function extractAssignmentsFromSolution(solution: any, employees: any[], shifts:
const shiftId = assignment.shiftId;
const employeeId = assignment.employeeId;
if (shiftId && employeeId && assignments[shiftId]) {
if (shiftId && employeeId) {
if (!assignments[shiftId]) {
assignments[shiftId] = [];
}
// Check if this assignment already exists to avoid duplicates
if (!assignments[shiftId].includes(employeeId)) {
assignments[shiftId].push(employeeId);
employeeAssignments[employeeId]++;
}
}
});
}
// Log results
console.log('=== ASSIGNMENT RESULTS ===');
employees.forEach((employee: any) => {
console.log(` ${employee.name}: ${employeeAssignments[employee.id]} shifts`);
// 🆕 ADD: Enhanced logging with employee names
console.log('🎯 FINAL ASSIGNMENTS WITH EMPLOYEE :');
Object.entries(assignments).forEach(([shiftId, employeeIds]) => {
const employeeNames = (employeeIds as string[]).map(empId => {
const employee = employees.find(emp => emp.id === empId);
return employee ? employee.id : 'Unknown';
});
console.log(` 📅 ${shiftId}: ${employeeNames.join(', ')}`);
});
let totalAssignments = 0;
shifts.forEach((shift: any) => {
const count = assignments[shift.id]?.length || 0;
totalAssignments += count;
console.log(` Shift ${shift.id}: ${count} employees`);
});
console.log(`Total assignments: ${totalAssignments}`);
console.log('==========================');
return assignments;
}
@@ -277,6 +295,20 @@ function detectViolations(assignments: any, employees: any[], shifts: any[]): st
}
});
// Check for employees working alone who shouldn't
shifts.forEach((shift: any) => {
const assignedEmployees = assignments[shift.id] || [];
if (assignedEmployees.length === 1) {
const singleEmployeeId = assignedEmployees[0];
const singleEmployee = employeeMap.get(singleEmployeeId);
if (singleEmployee && !singleEmployee.canWorkAlone) {
violations.push(`EMPLOYEE_ALONE: ${singleEmployee.name} is working alone in shift ${shift.id} but cannot work alone`);
}
}
});
// Check for multiple shifts per day per employee
const shiftsByDate = groupShiftsByDate(shifts);
employees.forEach((employee: any) => {
@@ -297,6 +329,35 @@ function detectViolations(assignments: any, employees: any[], shifts: any[]): st
return violations;
}
function assignManagersToShifts(assignments: any, managers: any[], shifts: any[], availabilities: any[]): any {
const managersToAssign = managers.filter(emp => emp.isActive && emp.employeeType === 'manager');
console.log(`Assigning ${managersToAssign.length} managers to shifts based on availability=1`);
managersToAssign.forEach((manager: any) => {
shifts.forEach((shift: any) => {
const availability = availabilities.find(
(a: any) => a.employeeId === manager.id && a.shiftId === shift.id
);
// Assign manager if they have availability=1 (preferred)
if (availability?.preferenceLevel === 1) {
if (!assignments[shift.id]) {
assignments[shift.id] = [];
}
// Check if manager is already assigned (avoid duplicates)
if (!assignments[shift.id].includes(manager.id)) {
assignments[shift.id].push(manager.id);
console.log(`✅ Assigned manager ${manager.name} to shift ${shift.id} (availability=1)`);
}
}
});
});
return assignments;
}
async function runScheduling() {
const data: WorkerData = workerData;
const startTime = Date.now();
@@ -314,6 +375,8 @@ async function runScheduling() {
console.log(`Optimizing ${data.shifts.length} shifts for ${data.employees.length} employees`);
const nonManagerEmployees = data.employees.filter(emp => emp.isActive && emp.employeeType !== 'manager');
const model = new CPModel();
buildSchedulingModel(model, data);
@@ -340,26 +403,38 @@ async function runScheduling() {
];
if (solution.success) {
// Extract assignments from solution
assignments = extractAssignmentsFromSolution(solution, data.employees, data.shifts);
// Extract assignments from solution (non-managers only)
assignments = extractAssignmentsFromSolution(solution, nonManagerEmployees, data.shifts);
// Only detect violations if we actually have assignments
// 🆕 ADD THIS: Assign managers to shifts where they have availability=1
assignments = assignManagersToShifts(assignments, data.employees, data.shifts, data.availabilities);
// Only detect violations for non-manager assignments
if (Object.keys(assignments).length > 0) {
violations = detectViolations(assignments, data.employees, data.shifts);
violations = detectViolations(assignments, nonManagerEmployees, data.shifts);
} else {
violations.push('NO_ASSIGNMENTS: Solver reported success but produced no assignments');
console.warn('Solver reported success but produced no assignments. Solution:', solution);
}
// Add assignment statistics
// Update resolution report
if (violations.length === 0) {
resolutionReport.push('✅ No constraint violations detected for non-manager employees');
} else {
resolutionReport.push(`⚠️ Found ${violations.length} violations for non-manager employees:`);
violations.forEach(violation => {
resolutionReport.push(` - ${violation}`);
});
}
// Add assignment statistics (including managers)
const totalAssignments = Object.values(assignments).reduce((sum: number, shiftAssignments: any) =>
sum + shiftAssignments.length, 0
);
resolutionReport.push(`📊 Total assignments: ${totalAssignments}`);
resolutionReport.push(`📊 Total assignments: ${totalAssignments} (including managers)`);
} else {
violations.push('SCHEDULING_FAILED: No feasible solution found');
resolutionReport.push('❌ No feasible solution could be found');
violations.push('SCHEDULING_FAILED: No feasible solution found for non-manager employees');
resolutionReport.push('❌ No feasible solution could be found for non-manager employees');
}
parentPort?.postMessage({

View File

@@ -12,9 +12,11 @@ interface AvailabilityManagerProps {
}
// Local interface extensions
interface ExtendedTimeSlot extends TimeSlot {
interface ExtendedShift extends Shift {
timeSlotName?: string;
startTime?: string;
endTime?: string;
displayName?: string;
source?: string;
}
interface Availability extends EmployeeAvailability {
@@ -68,20 +70,42 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
return time.substring(0, 5);
};
// Create a data structure that maps days to their actual time slots
// Create a data structure that maps days to their shifts with time slot info
const getTimetableData = () => {
if (!selectedPlan || !selectedPlan.shifts || !selectedPlan.timeSlots) {
return { days: [], timeSlotsByDay: {} };
return { days: [], shiftsByDay: {} };
}
// Group shifts by day
// Create a map for quick time slot lookups
const timeSlotMap = new Map(selectedPlan.timeSlots.map(ts => [ts.id, ts]));
// Group shifts by day and enhance with time slot info
const shiftsByDay = selectedPlan.shifts.reduce((acc, shift) => {
if (!acc[shift.dayOfWeek]) {
acc[shift.dayOfWeek] = [];
}
acc[shift.dayOfWeek].push(shift);
const timeSlot = timeSlotMap.get(shift.timeSlotId);
const enhancedShift: ExtendedShift = {
...shift,
timeSlotName: timeSlot?.name,
startTime: timeSlot?.startTime,
endTime: timeSlot?.endTime,
displayName: timeSlot ? `${timeSlot.name} (${formatTime(timeSlot.startTime)}-${formatTime(timeSlot.endTime)})` : shift.id
};
acc[shift.dayOfWeek].push(enhancedShift);
return acc;
}, {} as Record<number, typeof selectedPlan.shifts>);
}, {} as Record<number, ExtendedShift[]>);
// Sort shifts within each day by start time
Object.keys(shiftsByDay).forEach(day => {
shiftsByDay[parseInt(day)].sort((a, b) => {
const timeA = a.startTime || '';
const timeB = b.startTime || '';
return timeA.localeCompare(timeB);
});
});
// Get unique days that have shifts
const days = Array.from(new Set(selectedPlan.shifts.map(shift => shift.dayOfWeek)))
@@ -90,24 +114,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
return daysOfWeek.find(day => day.id === dayId) || { id: dayId, name: `Tag ${dayId}` };
});
// For each day, get the time slots that actually have shifts
const timeSlotsByDay: Record<number, ExtendedTimeSlot[]> = {};
days.forEach(day => {
const shiftsForDay = shiftsByDay[day.id] || [];
const timeSlotIdsForDay = new Set(shiftsForDay.map(shift => shift.timeSlotId));
timeSlotsByDay[day.id] = selectedPlan.timeSlots
.filter(timeSlot => timeSlotIdsForDay.has(timeSlot.id))
.map(timeSlot => ({
...timeSlot,
displayName: `${timeSlot.name} (${formatTime(timeSlot.startTime)}-${formatTime(timeSlot.endTime)})`,
source: `Plan: ${selectedPlan.name}`
}))
.sort((a, b) => a.startTime.localeCompare(b.startTime));
});
return { days, timeSlotsByDay };
return { days, shiftsByDay };
};
const loadData = async () => {
@@ -137,7 +144,6 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
// 3. Select first plan with actual shifts if available
if (plans.length > 0) {
// Find a plan that actually has shifts and time slots
const planWithShifts = plans.find(plan =>
plan.shifts && plan.shifts.length > 0 &&
plan.timeSlots && plan.timeSlots.length > 0
@@ -146,7 +152,6 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
setSelectedPlanId(planWithShifts.id);
console.log('✅ SCHICHTPLAN AUSGEWÄHLT:', planWithShifts.name);
// Load the selected plan to get its actual used time slots and days
await loadSelectedPlan();
}
@@ -170,8 +175,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
name: plan.name,
timeSlotsCount: plan.timeSlots?.length || 0,
shiftsCount: plan.shifts?.length || 0,
usedDays: Array.from(new Set(plan.shifts?.map(s => s.dayOfWeek) || [])).sort(),
usedTimeSlots: Array.from(new Set(plan.shifts?.map(s => s.timeSlotId) || [])).length
usedDays: Array.from(new Set(plan.shifts?.map(s => s.dayOfWeek) || [])).sort()
});
} catch (err: any) {
console.error('❌ FEHLER BEIM LADEN DES SCHICHTPLANS:', err);
@@ -179,14 +183,11 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
}
};
const handleAvailabilityLevelChange = (dayId: number, timeSlotId: string, level: AvailabilityLevel) => {
console.log(`🔄 ÄNDERE VERFÜGBARKEIT: Tag ${dayId}, Slot ${timeSlotId}, Level ${level}`);
const handleAvailabilityLevelChange = (shiftId: string, level: AvailabilityLevel) => {
console.log(`🔄 ÄNDERE VERFÜGBARKEIT: Shift ${shiftId}, Level ${level}`);
setAvailabilities(prev => {
const existingIndex = prev.findIndex(avail =>
avail.dayOfWeek === dayId &&
avail.timeSlotId === timeSlotId
);
const existingIndex = prev.findIndex(avail => avail.shiftId === shiftId);
if (existingIndex >= 0) {
// Update existing availability
@@ -198,13 +199,20 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
};
return updated;
} else {
// Create new availability
// Create new availability using shiftId directly
const shift = selectedPlan?.shifts?.find(s => s.id === shiftId);
if (!shift) {
console.error('❌ Shift nicht gefunden:', shiftId);
return prev;
}
const newAvailability: Availability = {
id: `temp-${dayId}-${timeSlotId}-${Date.now()}`,
id: `temp-${shiftId}-${Date.now()}`,
employeeId: employee.id,
planId: selectedPlanId || '',
dayOfWeek: dayId,
timeSlotId: timeSlotId,
planId: selectedPlanId,
shiftId: shiftId, // Use shiftId directly
dayOfWeek: shift.dayOfWeek, // Keep for backward compatibility if needed
timeSlotId: shift.timeSlotId, // Keep for backward compatibility if needed
preferenceLevel: level,
isAvailable: level !== 3
};
@@ -213,21 +221,16 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
});
};
const getAvailabilityForDayAndSlot = (dayId: number, timeSlotId: string): AvailabilityLevel => {
const availability = availabilities.find(avail =>
avail.dayOfWeek === dayId &&
avail.timeSlotId === timeSlotId
);
const result = availability?.preferenceLevel || 3;
return result;
const getAvailabilityForShift = (shiftId: string): AvailabilityLevel => {
const availability = availabilities.find(avail => avail.shiftId === shiftId);
return availability?.preferenceLevel || 3;
};
// Update the timetable rendering to use the new data structure
// Update the timetable rendering to use shifts directly
const renderTimetable = () => {
const { days, timeSlotsByDay } = getTimetableData();
const { days, shiftsByDay } = getTimetableData();
if (days.length === 0 || Object.keys(timeSlotsByDay).length === 0) {
if (days.length === 0 || Object.keys(shiftsByDay).length === 0) {
return (
<div style={{
padding: '40px',
@@ -244,23 +247,25 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
);
}
// Get all unique time slots across all days for row headers
const allTimeSlotIds = new Set<string>();
// Get all unique shifts across all days for row headers
const allShifts: ExtendedShift[] = [];
const shiftIds = new Set<string>();
days.forEach(day => {
timeSlotsByDay[day.id]?.forEach(timeSlot => {
allTimeSlotIds.add(timeSlot.id);
shiftsByDay[day.id]?.forEach(shift => {
if (!shiftIds.has(shift.id)) {
shiftIds.add(shift.id);
allShifts.push(shift);
}
});
});
const allTimeSlots = Array.from(allTimeSlotIds)
.map(id => selectedPlan?.timeSlots?.find(ts => ts.id === id))
.filter(Boolean)
.map(timeSlot => ({
...timeSlot!,
displayName: `${timeSlot!.name} (${formatTime(timeSlot!.startTime)}-${formatTime(timeSlot!.endTime)})`,
source: `Plan: ${selectedPlan!.name}`
}))
.sort((a, b) => a.startTime.localeCompare(b.startTime));
// Sort shifts by time slot start time
allShifts.sort((a, b) => {
const timeA = a.startTime || '';
const timeB = b.startTime || '';
return timeA.localeCompare(timeB);
});
return (
<div style={{
@@ -277,7 +282,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
}}>
Verfügbarkeit definieren
<div style={{ fontSize: '14px', fontWeight: 'normal', marginTop: '5px' }}>
{allTimeSlots.length} Schichttypen {days.length} Tage Nur tatsächlich im Plan verwendete Schichten
{allShifts.length} Schichten {days.length} Tage Direkte Shift-ID Zuordnung
</div>
</div>
@@ -294,7 +299,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
textAlign: 'left',
border: '1px solid #dee2e6',
fontWeight: 'bold',
minWidth: '100px'
minWidth: '150px'
}}>
Schicht (Zeit)
</th>
@@ -312,9 +317,9 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
</tr>
</thead>
<tbody>
{allTimeSlots.map((timeSlot, timeIndex) => (
<tr key={timeSlot.id} style={{
backgroundColor: timeIndex % 2 === 0 ? 'white' : '#f8f9fa'
{allShifts.map((shift, shiftIndex) => (
<tr key={shift.id} style={{
backgroundColor: shiftIndex % 2 === 0 ? 'white' : '#f8f9fa'
}}>
<td style={{
padding: '12px 16px',
@@ -322,16 +327,16 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
fontWeight: '500',
backgroundColor: '#f8f9fa'
}}>
{timeSlot.displayName}
{shift.displayName}
<div style={{ fontSize: '11px', color: '#666', marginTop: '4px' }}>
{timeSlot.source}
Shift-ID: {shift.id.substring(0, 8)}...
</div>
</td>
{days.map(weekday => {
// Check if this time slot exists for this day
const timeSlotForDay = timeSlotsByDay[weekday.id]?.find(ts => ts.id === timeSlot.id);
// Check if this shift exists for this day
const shiftForDay = shiftsByDay[weekday.id]?.find(s => s.id === shift.id);
if (!timeSlotForDay) {
if (!shiftForDay) {
return (
<td key={weekday.id} style={{
padding: '12px 16px',
@@ -346,7 +351,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
);
}
const currentLevel = getAvailabilityForDayAndSlot(weekday.id, timeSlot.id);
const currentLevel = getAvailabilityForShift(shift.id);
const levelConfig = availabilityLevels.find(l => l.level === currentLevel);
return (
@@ -360,7 +365,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
value={currentLevel}
onChange={(e) => {
const newLevel = parseInt(e.target.value) as AvailabilityLevel;
handleAvailabilityLevelChange(weekday.id, timeSlot.id, newLevel);
handleAvailabilityLevelChange(shift.id, newLevel);
}}
style={{
padding: '8px 12px',
@@ -410,12 +415,12 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
return;
}
const { days, timeSlotsByDay } = getTimetableData();
const { days, shiftsByDay } = getTimetableData();
// Filter availabilities to only include those with actual shifts
const validAvailabilities = availabilities.filter(avail => {
const timeSlotsForDay = timeSlotsByDay[avail.dayOfWeek] || [];
return timeSlotsForDay.some(slot => slot.id === avail.timeSlotId);
// Check if this shiftId exists in the current plan
return selectedPlan?.shifts?.some(shift => shift.id === avail.shiftId);
});
if (validAvailabilities.length === 0) {
@@ -423,13 +428,14 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
return;
}
// Convert to the format expected by the API
// Convert to the format expected by the API - using shiftId directly
const requestData = {
planId: selectedPlanId,
availabilities: validAvailabilities.map(avail => ({
planId: selectedPlanId,
dayOfWeek: avail.dayOfWeek,
timeSlotId: avail.timeSlotId,
shiftId: avail.shiftId, // Use shiftId directly
dayOfWeek: avail.dayOfWeek, // Keep for backward compatibility
timeSlotId: avail.timeSlotId, // Keep for backward compatibility
preferenceLevel: avail.preferenceLevel,
notes: avail.notes
}))
@@ -457,14 +463,14 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
);
}
const { days, timeSlotsByDay } = getTimetableData();
const allTimeSlotIds = new Set<string>();
const { days, shiftsByDay } = getTimetableData();
const allShiftIds = new Set<string>();
days.forEach(day => {
timeSlotsByDay[day.id]?.forEach(timeSlot => {
allTimeSlotIds.add(timeSlot.id);
shiftsByDay[day.id]?.forEach(shift => {
allShiftIds.add(shift.id);
});
});
const timeSlotsCount = allTimeSlotIds.size;
const shiftsCount = allShiftIds.size;
return (
<div style={{
@@ -482,40 +488,30 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
borderBottom: '2px solid #f0f0f0',
paddingBottom: '15px'
}}>
📅 Verfügbarkeit verwalten
📅 Verfügbarkeit verwalten (Shift-ID basiert)
</h2>
{/* Debug-Info */}
<div style={{
backgroundColor: timeSlotsCount === 0 ? '#f8d7da' : '#d1ecf1',
border: `1px solid ${timeSlotsCount === 0 ? '#f5c6cb' : '#bee5eb'}`,
backgroundColor: shiftsCount === 0 ? '#f8d7da' : '#d1ecf1',
border: `1px solid ${shiftsCount === 0 ? '#f5c6cb' : '#bee5eb'}`,
borderRadius: '6px',
padding: '15px',
marginBottom: '20px'
}}>
<h4 style={{
margin: '0 0 10px 0',
color: timeSlotsCount === 0 ? '#721c24' : '#0c5460'
color: shiftsCount === 0 ? '#721c24' : '#0c5460'
}}>
{timeSlotsCount === 0 ? '❌ PROBLEM: Keine Zeit-Slots gefunden' : '✅ Plan-Daten geladen'}
{shiftsCount === 0 ? '❌ PROBLEM: Keine Shifts gefunden' : '✅ Plan-Daten geladen'}
</h4>
<div style={{ fontSize: '12px', fontFamily: 'monospace' }}>
<div><strong>Ausgewählter Plan:</strong> {selectedPlan?.name || 'Keiner'}</div>
<div><strong>Verwendete Zeit-Slots:</strong> {timeSlotsCount}</div>
<div><strong>Einzigartige Shifts:</strong> {shiftsCount}</div>
<div><strong>Verwendete Tage:</strong> {days.length} ({days.map(d => d.name).join(', ')})</div>
<div><strong>Gesamte Shifts im Plan:</strong> {selectedPlan?.shifts?.length || 0}</div>
<div><strong>Methode:</strong> Direkte Shift-ID Zuordnung</div>
</div>
{selectedPlan && selectedPlan.shifts && (
<div style={{ marginTop: '10px' }}>
<strong>Shifts im Plan:</strong>
{selectedPlan.shifts.map((shift, index) => (
<div key={index} style={{ fontSize: '11px', marginLeft: '10px' }}>
Tag {shift.dayOfWeek}: {shift.timeSlotId} ({shift.requiredEmployees} Personen)
</div>
))}
</div>
)}
</div>
{/* Employee Info */}
@@ -524,7 +520,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
{employee.name}
</h3>
<p style={{ margin: 0, color: '#7f8c8d' }}>
Legen Sie die Verfügbarkeit für {employee.name} fest.
Legen Sie die Verfügbarkeit für {employee.name} fest (basierend auf Shift-IDs).
</p>
</div>
@@ -608,7 +604,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
<option value="">Bitte auswählen...</option>
{shiftPlans.map(plan => (
<option key={plan.id} value={plan.id}>
{plan.name} {plan.timeSlots && `(${plan.timeSlots.length} Zeit-Slots)`}
{plan.name} {plan.shifts && `(${plan.shifts.length} Shifts)`}
</option>
))}
</select>
@@ -617,7 +613,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
{selectedPlan && (
<div style={{ fontSize: '14px', color: '#666' }}>
<div><strong>Plan:</strong> {selectedPlan.name}</div>
<div><strong>Zeit-Slots:</strong> {selectedPlan.timeSlots?.length || 0}</div>
<div><strong>Shifts:</strong> {selectedPlan.shifts?.length || 0}</div>
<div><strong>Status:</strong> {selectedPlan.status}</div>
</div>
)}
@@ -651,14 +647,14 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
<button
onClick={handleSave}
disabled={saving || timeSlotsCount === 0 || !selectedPlanId}
disabled={saving || shiftsCount === 0 || !selectedPlanId}
style={{
padding: '12px 24px',
backgroundColor: saving ? '#bdc3c7' : (timeSlotsCount === 0 || !selectedPlanId ? '#95a5a6' : '#3498db'),
backgroundColor: saving ? '#bdc3c7' : (shiftsCount === 0 || !selectedPlanId ? '#95a5a6' : '#3498db'),
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: (saving || timeSlotsCount === 0 || !selectedPlanId) ? 'not-allowed' : 'pointer',
cursor: (saving || shiftsCount === 0 || !selectedPlanId) ? 'not-allowed' : 'pointer',
fontWeight: 'bold'
}}
>

View File

@@ -76,66 +76,87 @@ const ShiftPlanView: React.FC = () => {
};
}, []);
useEffect(() => {
if (assignmentResult) {
console.log("🔄 assignmentResult UPDATED:", {
success: assignmentResult.success,
assignmentsCount: Object.keys(assignmentResult.assignments).length,
assignmentKeys: Object.keys(assignmentResult.assignments).slice(0, 5), // First 5 keys
violations: assignmentResult.violations.length
});
// Log all assignments with their keys
Object.entries(assignmentResult.assignments).forEach(([key, empIds]) => {
console.log(` 🗂️ Assignment Key: ${key}`);
console.log(` Employees: ${empIds.join(', ')}`);
// Try to identify what this key represents
const isUuid = key.length === 36; // UUID format
console.log(` Type: ${isUuid ? 'UUID (likely scheduled shift)' : 'Pattern (likely shift pattern)'}`);
});
}
}, [assignmentResult]);
useEffect(() => {
(window as any).debugRenderLogic = debugRenderLogic;
return () => { (window as any).debugRenderLogic = undefined; };
}, [shiftPlan, scheduledShifts]);
const loadShiftPlanData = async () => {
if (!id) return;
if (!id) return;
try {
setLoading(true);
try {
setLoading(true);
// Load plan and employees first
const [plan, employeesData] = await Promise.all([
shiftPlanService.getShiftPlan(id),
employeeService.getEmployees(),
]);
// Load plan and employees first
const [plan, employeesData] = await Promise.all([
shiftPlanService.getShiftPlan(id),
employeeService.getEmployees(),
]);
setShiftPlan(plan);
setEmployees(employeesData.filter(emp => emp.isActive));
setShiftPlan(plan);
setEmployees(employeesData.filter(emp => emp.isActive));
// CRITICAL: Load scheduled shifts and verify they exist
const shiftsData = await shiftAssignmentService.getScheduledShiftsForPlan(id);
console.log('📋 Loaded scheduled shifts:', shiftsData.length);
// CRITICAL: Load scheduled shifts and verify they exist
const shiftsData = await shiftAssignmentService.getScheduledShiftsForPlan(id);
console.log('📋 Loaded scheduled shifts:', shiftsData.length);
if (shiftsData.length === 0) {
console.warn('⚠️ No scheduled shifts found for plan:', id);
if (shiftsData.length === 0) {
console.warn('⚠️ No scheduled shifts found for plan:', id);
showNotification({
type: 'warning',
title: 'Keine Schichten gefunden',
message: 'Der Schichtplan hat keine generierten Schichten. Bitte überprüfen Sie die Plan-Konfiguration.'
});
}
setScheduledShifts(shiftsData);
// Load availabilities
const availabilityPromises = employeesData
.filter(emp => emp.isActive)
.map(emp => employeeService.getAvailabilities(emp.id));
const allAvailabilities = await Promise.all(availabilityPromises);
const flattenedAvailabilities = allAvailabilities.flat();
const planAvailabilities = flattenedAvailabilities.filter(
availability => availability.planId === id
);
setAvailabilities(planAvailabilities);
} catch (error) {
console.error('Error loading shift plan data:', error);
showNotification({
type: 'warning',
title: 'Keine Schichten gefunden',
message: 'Der Schichtplan hat keine generierten Schichten. Bitte überprüfen Sie die Plan-Konfiguration.'
type: 'error',
title: 'Fehler',
message: 'Daten konnten nicht geladen werden.'
});
} finally {
setLoading(false);
}
setScheduledShifts(shiftsData);
// Load availabilities
const availabilityPromises = employeesData
.filter(emp => emp.isActive)
.map(emp => employeeService.getAvailabilities(emp.id));
const allAvailabilities = await Promise.all(availabilityPromises);
const flattenedAvailabilities = allAvailabilities.flat();
const planAvailabilities = flattenedAvailabilities.filter(
availability => availability.planId === id
);
setAvailabilities(planAvailabilities);
} catch (error) {
console.error('Error loading shift plan data:', error);
showNotification({
type: 'error',
title: 'Fehler',
message: 'Daten konnten nicht geladen werden.'
});
} finally {
setLoading(false);
}
};
};
const handleRecreateAssignments = async () => {
if (!shiftPlan) return;
@@ -207,7 +228,7 @@ const ShiftPlanView: React.FC = () => {
console.log('🔍 RENDER LOGIC DEBUG:');
console.log('=====================');
const { days, allTimeSlots, timeSlotsByDay } = getExtendedTimetableData();
const { days, allTimeSlots, timeSlotsByDay } = getTimetableData();
console.log('📊 TABLE STRUCTURE:');
console.log('- Days in table:', days.length);
@@ -280,54 +301,6 @@ const ShiftPlanView: React.FC = () => {
}
};
const getExtendedTimetableData = () => {
if (!shiftPlan || !shiftPlan.timeSlots) {
return { days: [], timeSlotsByDay: {}, allTimeSlots: [] };
}
// Verwende alle Tage die tatsächlich in scheduledShifts vorkommen
const allDaysInScheduledShifts = [...new Set(scheduledShifts.map(s => getDayOfWeek(s.date)))].sort();
const days = allDaysInScheduledShifts.map(dayId => {
return weekdays.find(day => day.id === dayId) || { id: dayId, name: `Tag ${dayId}` };
});
// Verwende alle TimeSlots die tatsächlich in scheduledShifts vorkommen
const allTimeSlotIdsInScheduledShifts = [...new Set(scheduledShifts.map(s => s.timeSlotId))];
const allTimeSlots = allTimeSlotIdsInScheduledShifts
.map(id => shiftPlan.timeSlots?.find(ts => ts.id === id))
.filter(Boolean)
.map(timeSlot => ({
...timeSlot!,
displayName: `${timeSlot!.name} (${formatTime(timeSlot!.startTime)}-${formatTime(timeSlot!.endTime)})`
}))
.sort((a, b) => a.startTime.localeCompare(b.startTime));
// TimeSlots pro Tag
const timeSlotsByDay: Record<number, ExtendedTimeSlot[]> = {};
days.forEach(day => {
const timeSlotIdsForDay = new Set(
scheduledShifts
.filter(shift => getDayOfWeek(shift.date) === day.id)
.map(shift => shift.timeSlotId)
);
timeSlotsByDay[day.id] = allTimeSlots
.filter(timeSlot => timeSlotIdsForDay.has(timeSlot.id))
.sort((a, b) => a.startTime.localeCompare(b.startTime));
});
/*console.log('🔄 Extended timetable data:', {
days: days.length,
timeSlots: allTimeSlots.length,
totalScheduledShifts: scheduledShifts.length
});*/
return { days, timeSlotsByDay, allTimeSlots };
};
// Extract plan-specific shifts using the same logic as AvailabilityManager
const getTimetableData = () => {
if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) {
@@ -409,9 +382,6 @@ const ShiftPlanView: React.FC = () => {
console.log('- Employees:', refreshedEmployees.length);
console.log('- Availabilities:', refreshedAvailabilities.length);
// DEBUG: Verify we have new data
debugSchedulingInput(refreshedEmployees, refreshedAvailabilities);
// ADD THIS: Define constraints object
const constraints = {
enforceNoTraineeAlone: true,
@@ -428,6 +398,35 @@ const ShiftPlanView: React.FC = () => {
constraints
);
// COMPREHENSIVE DEBUGGING
console.log("🎯 RAW ASSIGNMENT RESULT FROM API:", {
success: result.success,
assignmentsCount: Object.keys(result.assignments).length,
assignmentKeys: Object.keys(result.assignments),
violations: result.violations.length,
resolutionReport: result.resolutionReport?.length || 0
});
// Log the actual assignments with more context
Object.entries(result.assignments).forEach(([shiftId, empIds]) => {
console.log(` 📅 Assignment Key: ${shiftId}`);
console.log(` Employees: ${empIds.join(', ')}`);
// Try to identify what type of ID this is
const isUuid = shiftId.length === 36; // UUID format
console.log(` Type: ${isUuid ? 'UUID (likely scheduled shift)' : 'Pattern (likely shift pattern)'}`);
// If it's a UUID, check if it matches any scheduled shift
if (isUuid) {
const matchingScheduledShift = scheduledShifts.find(s => s.id === shiftId);
if (matchingScheduledShift) {
console.log(` ✅ Matches scheduled shift: ${matchingScheduledShift.date} - TimeSlot: ${matchingScheduledShift.timeSlotId}`);
} else {
console.log(` ❌ No matching scheduled shift found for UUID`);
}
}
});
setAssignmentResult(result);
setShowAssignmentPreview(true);
@@ -443,57 +442,6 @@ const ShiftPlanView: React.FC = () => {
}
};
const debugSchedulingInput = (employees: Employee[], availabilities: EmployeeAvailability[]) => {
console.log('🔍 DEBUG SCHEDULING INPUT:');
console.log('==========================');
// Check if we have the latest data
console.log('📊 Employee Count:', employees.length);
console.log('📊 Availability Count:', availabilities.length);
// Log each employee's availability
employees.forEach(emp => {
const empAvailabilities = availabilities.filter(avail => avail.employeeId === emp.id);
console.log(`👤 ${emp.name} (${emp.role}, ${emp.employeeType}): ${empAvailabilities.length} availabilities`);
if (empAvailabilities.length > 0) {
empAvailabilities.forEach(avail => {
console.log(` - Day ${avail.dayOfWeek}, TimeSlot ${avail.timeSlotId}: Level ${avail.preferenceLevel}`);
});
} else {
console.log(` ❌ NO AVAILABILITIES SET!`);
}
});
// REMOVED: The problematic code that tries to access shiftPlan.employees
// We don't have old employee data stored in shiftPlan
console.log('🔄 All employees are considered "changed" since we loaded fresh data');
};
const forceRefreshData = async () => {
if (!id) return;
try {
const [plan, employeesData, shiftsData] = await Promise.all([
shiftPlanService.getShiftPlan(id),
employeeService.getEmployees(),
shiftAssignmentService.getScheduledShiftsForPlan(id)
]);
setShiftPlan(plan);
setEmployees(employeesData.filter(emp => emp.isActive));
setScheduledShifts(shiftsData);
// Force refresh availabilities
await refreshAllAvailabilities();
console.log('✅ All data force-refreshed');
} catch (error) {
console.error('Error force-refreshing data:', error);
}
};
const handlePublish = async () => {
if (!shiftPlan || !assignmentResult) return;
@@ -502,32 +450,42 @@ const ShiftPlanView: React.FC = () => {
console.log('🔄 Starting to publish assignments...');
// ✅ KORREKTUR: Verwende die neu geladenen Shifts
// Get fresh scheduled shifts
const updatedShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
// Debug: Check if scheduled shifts exist
if (!updatedShifts || updatedShifts.length === 0) {
throw new Error('No scheduled shifts found in the plan');
}
console.log(`📊 Found ${updatedShifts.length} scheduled shifts to update`);
// ✅ KORREKTUR: Verwende updatedShifts statt scheduledShifts
const updatePromises = updatedShifts.map(async (scheduledShift) => {
const assignedEmployees = assignmentResult.assignments[scheduledShift.id] || [];
// ✅ FIX: Map scheduled shift to shift pattern to find assignments
const dayOfWeek = getDayOfWeek(scheduledShift.date);
//console.log(`📝 Updating shift ${scheduledShift.id} with`, assignedEmployees, 'employees');
// Find the corresponding shift pattern for this day and time slot
const shiftPattern = shiftPlan.shifts?.find(shift =>
shift.dayOfWeek === dayOfWeek &&
shift.timeSlotId === scheduledShift.timeSlotId
);
let assignedEmployees: string[] = [];
if (shiftPattern) {
// Look for assignments using the shift pattern ID (what scheduler uses)
assignedEmployees = assignmentResult.assignments[shiftPattern.id] || [];
console.log(`📝 Updating scheduled shift ${scheduledShift.id} (Day ${dayOfWeek}, TimeSlot ${scheduledShift.timeSlotId}) with`, assignedEmployees, 'employees');
} else {
console.warn(`⚠️ No shift pattern found for scheduled shift ${scheduledShift.id} (Day ${dayOfWeek}, TimeSlot ${scheduledShift.timeSlotId})`);
}
try {
// Update the shift with assigned employees
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
// Update the scheduled shift with assigned employees
await shiftAssignmentService.updateScheduledShift(scheduledShift.id, {
assignedEmployees
});
if (scheduledShifts.some(s => s.id === scheduledShift.id)) {
console.log(`✅ Successfully updated scheduled shift ${scheduledShift.id}`);
}
console.log(`✅ Successfully updated scheduled shift ${scheduledShift.id}`);
} catch (error) {
console.error(`❌ Failed to update shift ${scheduledShift.id}:`, error);
throw error;
@@ -542,7 +500,7 @@ const ShiftPlanView: React.FC = () => {
status: 'published'
});
// ✅ KORREKTUR: Explizit alle Daten neu laden und State aktualisieren
// Reload all data to reflect changes
const [reloadedPlan, reloadedShifts] = await Promise.all([
shiftPlanService.getShiftPlan(shiftPlan.id),
shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id)
@@ -699,19 +657,31 @@ const ShiftPlanView: React.FC = () => {
}
};
// Füge diese Funktion zu den verfügbaren Aktionen hinzu
const handleReloadData = async () => {
await loadShiftPlanData();
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Daten wurden neu geladen.'
});
const getAssignmentsForScheduledShift = (scheduledShift: ScheduledShift): string[] => {
if (!assignmentResult) return [];
// First try direct match with scheduled shift ID
if (assignmentResult.assignments[scheduledShift.id]) {
return assignmentResult.assignments[scheduledShift.id];
}
// If no direct match, try to find by day and timeSlot pattern
const dayOfWeek = getDayOfWeek(scheduledShift.date);
const shiftPattern = shiftPlan?.shifts?.find(shift =>
shift.dayOfWeek === dayOfWeek &&
shift.timeSlotId === scheduledShift.timeSlotId
);
if (shiftPattern && assignmentResult.assignments[shiftPattern.id]) {
return assignmentResult.assignments[shiftPattern.id];
}
return [];
};
// Render timetable using the same structure as AvailabilityManager
const renderTimetable = () => {
const { days, allTimeSlots, timeSlotsByDay } = getExtendedTimetableData();
const { days, allTimeSlots, timeSlotsByDay } = getTimetableData();
if (!shiftPlan?.id) {
console.warn("Shift plan ID is missing");
return null;
@@ -847,8 +817,8 @@ const ShiftPlanView: React.FC = () => {
scheduled.timeSlotId === timeSlot.id;
});
if (scheduledShift && assignmentResult.assignments[scheduledShift.id]) {
assignedEmployees = assignmentResult.assignments[scheduledShift.id];
if (scheduledShift) {
assignedEmployees = getAssignmentsForScheduledShift(scheduledShift);
displayText = assignedEmployees.map(empId => {
const employee = employees.find(emp => emp.id === empId);
return employee ? employee.name : 'Unbekannt';
@@ -900,7 +870,7 @@ const ShiftPlanView: React.FC = () => {
if (loading) return <div>Lade Schichtplan...</div>;
if (!shiftPlan) return <div>Schichtplan nicht gefunden</div>;
const { days, allTimeSlots } = getExtendedTimetableData();
const { days, allTimeSlots } = getTimetableData();
const availabilityStatus = getAvailabilityStatus();