mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-11-30 22:45:46 +01:00
changed role handling, employee name handling and availibilitie handling with shift.id
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 = ?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user