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;
}
@@ -72,4 +74,14 @@ 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

@@ -39,7 +39,7 @@ router.post('/generate-schedule', async (req, res) => {
assignments: Object.keys(result.assignments).length,
violations: result.violations.length
});
res.json(result);
} catch (error) {
console.error('Scheduling failed:', error);

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] = [];
});
employees.forEach((employee: any) => {
employeeAssignments[employee.id] = 0;
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]})`);
});
// 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();
@@ -313,6 +374,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({