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 {
|
export interface RegisterRequest {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
name: string;
|
firstname: String;
|
||||||
//employee_type?: string;
|
lastname: String;
|
||||||
//is_sufficiently_independent?: string;
|
|
||||||
role?: string;
|
role?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +52,7 @@ export const login = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
// Get user from database
|
// Get user from database
|
||||||
const user = await db.get<EmployeeWithPassword>(
|
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]
|
[email]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -117,7 +116,7 @@ export const getCurrentUser = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const user = await db.get<Employee>(
|
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]
|
[jwtUser.userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -163,10 +162,10 @@ export const validateToken = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
export const register = async (req: Request, res: Response) => {
|
export const register = async (req: Request, res: Response) => {
|
||||||
try {
|
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
|
// Validate required fields
|
||||||
if (!email || !password || !name) {
|
if (!email || !password || !firstname || !lastname) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
error: 'E-Mail, Passwort und Name sind erforderlich'
|
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);
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
// Insert user
|
// Insert user
|
||||||
const result = await db.run(
|
const result = await db.run(
|
||||||
`INSERT INTO employees (id, email, password, name, role, employee_type, contract_type, can_work_alone, is_active)
|
`INSERT INTO employees (id, email, password, firstname, lastname, role, employee_type, contract_type, can_work_alone, is_active)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[uuidv4(), email, hashedPassword, name, role, 'experienced', 'small', false, 1]
|
[uuidv4(), email, hashedPassword, firstname, lastname, role, 'experienced', 'small', false, 1]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!result.lastID) {
|
if (!result.lastID) {
|
||||||
throw new Error('Benutzer konnte nicht erstellt werden');
|
throw new Error('Benutzer konnte nicht erstellt werden');
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const getEmployees = async (req: AuthRequest, res: Response): Promise<voi
|
|||||||
|
|
||||||
let query = `
|
let query = `
|
||||||
SELECT
|
SELECT
|
||||||
id, email, name, role, is_active as isActive,
|
id, email, firstname, lastname, role, is_active as isActive,
|
||||||
employee_type as employeeType,
|
employee_type as employeeType,
|
||||||
contract_type as contractType,
|
contract_type as contractType,
|
||||||
can_work_alone as canWorkAlone,
|
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>(`
|
const employee = await db.get<any>(`
|
||||||
SELECT
|
SELECT
|
||||||
id, email, name, role, is_active as isActive,
|
id, email, firstname, lastname, role, is_active as isActive,
|
||||||
employee_type as employeeType,
|
employee_type as employeeType,
|
||||||
contract_type as contractType,
|
contract_type as contractType,
|
||||||
can_work_alone as canWorkAlone,
|
can_work_alone as canWorkAlone,
|
||||||
@@ -78,15 +78,16 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
|
|||||||
const {
|
const {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
name,
|
firstname,
|
||||||
|
lastname,
|
||||||
role,
|
role,
|
||||||
employeeType,
|
employeeType,
|
||||||
contractType,
|
contractType,
|
||||||
canWorkAlone // Statt isSufficientlyIndependent
|
canWorkAlone
|
||||||
} = req.body as CreateEmployeeRequest;
|
} = req.body as CreateEmployeeRequest;
|
||||||
|
|
||||||
// Validierung
|
// Validierung
|
||||||
if (!email || !password || !name || !role || !employeeType || !contractType) {
|
if (!email || !password || !firstname || !lastname || !role || !employeeType || !contractType) {
|
||||||
console.log('❌ Validation failed: Missing required fields');
|
console.log('❌ Validation failed: Missing required fields');
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
error: 'Email, password, name, role, employeeType und contractType sind erforderlich'
|
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(
|
await db.run(
|
||||||
`INSERT INTO employees (
|
`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
|
is_active
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
employeeId,
|
employeeId,
|
||||||
email,
|
email,
|
||||||
hashedPassword,
|
hashedPassword,
|
||||||
name,
|
firstname, // Changed from name
|
||||||
|
lastname, // Added
|
||||||
role,
|
role,
|
||||||
employeeType,
|
employeeType,
|
||||||
contractType,
|
contractType,
|
||||||
canWorkAlone ? 1 : 0, // Statt isSufficientlyIndependent
|
canWorkAlone ? 1 : 0,
|
||||||
1
|
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> => {
|
export const updateEmployee = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
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 });
|
console.log('📝 Update Employee Request:', { id, name, role, isActive, employeeType, contractType, canWorkAlone });
|
||||||
|
|
||||||
// Check if employee exists
|
// Check if employee exists
|
||||||
@@ -168,14 +169,15 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
|
|||||||
// Update employee
|
// Update employee
|
||||||
await db.run(
|
await db.run(
|
||||||
`UPDATE employees
|
`UPDATE employees
|
||||||
SET name = COALESCE(?, name),
|
SET firstname = COALESCE(?, firstname),
|
||||||
role = COALESCE(?, role),
|
lastname = COALESCE(?, lastname),
|
||||||
is_active = COALESCE(?, is_active),
|
role = COALESCE(?, role),
|
||||||
employee_type = COALESCE(?, employee_type),
|
is_active = COALESCE(?, is_active),
|
||||||
contract_type = COALESCE(?, contract_type),
|
employee_type = COALESCE(?, employee_type),
|
||||||
can_work_alone = COALESCE(?, can_work_alone)
|
contract_type = COALESCE(?, contract_type),
|
||||||
WHERE id = ?`,
|
can_work_alone = COALESCE(?, can_work_alone)
|
||||||
[name, role, isActive, employeeType, contractType, canWorkAlone, id]
|
WHERE id = ?`,
|
||||||
|
[firstname, lastname, role, isActive, employeeType, contractType, canWorkAlone, id]
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('✅ Employee updated successfully');
|
console.log('✅ Employee updated successfully');
|
||||||
@@ -290,9 +292,11 @@ export const getAvailabilities = async (req: AuthRequest, res: Response): Promis
|
|||||||
}
|
}
|
||||||
|
|
||||||
const availabilities = await db.all<any>(`
|
const availabilities = await db.all<any>(`
|
||||||
SELECT * FROM employee_availability
|
SELECT ea.*, s.day_of_week, s.time_slot_id
|
||||||
WHERE employee_id = ?
|
FROM employee_availability ea
|
||||||
ORDER BY day_of_week, time_slot_id
|
JOIN shifts s ON ea.shift_id = s.id
|
||||||
|
WHERE ea.employee_id = ?
|
||||||
|
ORDER BY s.day_of_week, s.time_slot_id
|
||||||
`, [employeeId]);
|
`, [employeeId]);
|
||||||
|
|
||||||
//console.log('✅ Successfully got availabilities from employee:', availabilities);
|
//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) {
|
for (const availability of availabilities) {
|
||||||
const availabilityId = uuidv4();
|
const availabilityId = uuidv4();
|
||||||
await db.run(
|
await db.run(
|
||||||
`INSERT INTO employee_availability (id, employee_id, plan_id, day_of_week, time_slot_id, preference_level, notes)
|
`INSERT INTO employee_availability (id, employee_id, plan_id, shift_id, preference_level, notes)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
availabilityId,
|
availabilityId,
|
||||||
employeeId,
|
employeeId,
|
||||||
planId,
|
planId,
|
||||||
availability.dayOfWeek,
|
availability.shiftId,
|
||||||
availability.timeSlotId,
|
|
||||||
availability.preferenceLevel,
|
availability.preferenceLevel,
|
||||||
availability.notes || null
|
availability.notes || null
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import bcrypt from 'bcrypt';
|
|||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import { db } from '../services/databaseService.js';
|
import { db } from '../services/databaseService.js';
|
||||||
//import { initializeDefaultTemplates } from './shiftPlanController.js';
|
|
||||||
|
|
||||||
export const checkSetupStatus = async (req: Request, res: Response): Promise<void> => {
|
export const checkSetupStatus = async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@@ -44,14 +43,14 @@ export const setupAdmin = async (req: Request, res: Response): Promise<void> =>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { password, name } = req.body;
|
const { password, firstname, lastname } = req.body;
|
||||||
const email = 'admin@instandhaltung.de';
|
const email = 'admin@instandhaltung.de';
|
||||||
|
|
||||||
console.log('👤 Creating admin with data:', { name, email });
|
console.log('👤 Creating admin with data:', { name, email });
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
if (!password || !name) {
|
if (!password || !firstname || !lastname) {
|
||||||
res.status(400).json({ error: 'Passwort und Name sind erforderlich' });
|
res.status(400).json({ error: 'Passwort, Vorname und Nachname sind erforderlich' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,9 +72,9 @@ export const setupAdmin = async (req: Request, res: Response): Promise<void> =>
|
|||||||
try {
|
try {
|
||||||
// Create admin user
|
// Create admin user
|
||||||
await db.run(
|
await db.run(
|
||||||
`INSERT INTO employees (id, email, password, name, role, is_active, employee_type, contract_type)
|
`INSERT INTO employees (id, email, password, firstname, lastname, role, employee_type, contract_type, can_work_alone, is_active)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[adminId, email, hashedPassword, name, 'admin', 1, 'manager', 'large']
|
[adminId, email, hashedPassword, firstname, lastname, 'admin', 'manager', 'large', true, 1]
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('✅ Admin user created successfully');
|
console.log('✅ Admin user created successfully');
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { createPlanFromPreset, TEMPLATE_PRESETS } from '../models/defaults/shift
|
|||||||
|
|
||||||
async function getPlanWithDetails(planId: string) {
|
async function getPlanWithDetails(planId: string) {
|
||||||
const plan = await db.get<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
|
FROM shift_plans sp
|
||||||
LEFT JOIN employees e ON sp.created_by = e.id
|
LEFT JOIN employees e ON sp.created_by = e.id
|
||||||
WHERE sp.id = ?
|
WHERE sp.id = ?
|
||||||
@@ -69,7 +69,7 @@ async function getPlanWithDetails(planId: string) {
|
|||||||
export const getShiftPlans = async (req: Request, res: Response): Promise<void> => {
|
export const getShiftPlans = async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const plans = await db.all<any>(`
|
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
|
FROM shift_plans sp
|
||||||
LEFT JOIN employees e ON sp.created_by = e.id
|
LEFT JOIN employees e ON sp.created_by = e.id
|
||||||
ORDER BY sp.created_at DESC
|
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 { id } = req.params;
|
||||||
|
|
||||||
const plan = await db.get<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
|
FROM shift_plans sp
|
||||||
LEFT JOIN employees e ON sp.created_by = e.id
|
LEFT JOIN employees e ON sp.created_by = e.id
|
||||||
WHERE sp.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
|
// Helper function to get plan by ID
|
||||||
async function getShiftPlanById(planId: string): Promise<any> {
|
async function getShiftPlanById(planId: string): Promise<any> {
|
||||||
const plan = await db.get<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
|
FROM shift_plans sp
|
||||||
LEFT JOIN employees e ON sp.created_by = e.id
|
LEFT JOIN employees e ON sp.created_by = e.id
|
||||||
WHERE sp.id = ?
|
WHERE sp.id = ?
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ CREATE TABLE IF NOT EXISTS employees (
|
|||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
email TEXT UNIQUE NOT NULL,
|
email TEXT UNIQUE NOT NULL,
|
||||||
password TEXT NOT NULL,
|
password TEXT NOT NULL,
|
||||||
name TEXT NOT NULL,
|
firstname TEXT NOT NULL,
|
||||||
role TEXT CHECK(role IN ('admin', 'user', 'maintenance')) NOT NULL,
|
lastname TEXT NOT NULL,
|
||||||
employee_type TEXT CHECK(employee_type IN ('manager', 'trainee', 'experienced')) 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,
|
contract_type TEXT CHECK(contract_type IN ('small', 'large')) NOT NULL,
|
||||||
can_work_alone BOOLEAN DEFAULT FALSE,
|
can_work_alone BOOLEAN DEFAULT FALSE,
|
||||||
@@ -13,6 +13,18 @@ CREATE TABLE IF NOT EXISTS employees (
|
|||||||
last_login TEXT DEFAULT NULL
|
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
|
-- Shift plans table
|
||||||
CREATE TABLE IF NOT EXISTS shift_plans (
|
CREATE TABLE IF NOT EXISTS shift_plans (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
@@ -83,14 +95,13 @@ CREATE TABLE IF NOT EXISTS employee_availability (
|
|||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
employee_id TEXT NOT NULL,
|
employee_id TEXT NOT NULL,
|
||||||
plan_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),
|
shift_id TEXT NOT NULL,
|
||||||
time_slot_id TEXT NOT NULL,
|
|
||||||
preference_level INTEGER CHECK(preference_level IN (1, 2, 3)) NOT NULL,
|
preference_level INTEGER CHECK(preference_level IN (1, 2, 3)) NOT NULL,
|
||||||
notes TEXT,
|
notes TEXT,
|
||||||
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
|
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (plan_id) REFERENCES shift_plans(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,
|
FOREIGN KEY (shift_id) REFERENCES shifts(id) ON DELETE CASCADE,
|
||||||
UNIQUE(employee_id, plan_id, day_of_week, time_slot_id)
|
UNIQUE(employee_id, plan_id, shift_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Performance indexes
|
-- Performance indexes
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
export interface Employee {
|
export interface Employee {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
role: 'admin' | 'maintenance' | 'user';
|
role: 'admin' | 'maintenance' | 'user';
|
||||||
employeeType: 'manager' | 'trainee' | 'experienced';
|
employeeType: 'manager' | 'trainee' | 'experienced';
|
||||||
contractType: 'small' | 'large';
|
contractType: 'small' | 'large';
|
||||||
@@ -15,7 +16,8 @@ export interface Employee {
|
|||||||
export interface CreateEmployeeRequest {
|
export interface CreateEmployeeRequest {
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
name: string;
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
role: 'admin' | 'maintenance' | 'user';
|
role: 'admin' | 'maintenance' | 'user';
|
||||||
employeeType: 'manager' | 'trainee' | 'experienced';
|
employeeType: 'manager' | 'trainee' | 'experienced';
|
||||||
contractType: 'small' | 'large';
|
contractType: 'small' | 'large';
|
||||||
@@ -23,7 +25,8 @@ export interface CreateEmployeeRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateEmployeeRequest {
|
export interface UpdateEmployeeRequest {
|
||||||
name?: string;
|
firstname?: string;
|
||||||
|
lastname?: string;
|
||||||
role?: 'admin' | 'maintenance' | 'user';
|
role?: 'admin' | 'maintenance' | 'user';
|
||||||
employeeType?: 'manager' | 'trainee' | 'experienced';
|
employeeType?: 'manager' | 'trainee' | 'experienced';
|
||||||
contractType?: 'small' | 'large';
|
contractType?: 'small' | 'large';
|
||||||
@@ -39,8 +42,7 @@ export interface EmployeeAvailability {
|
|||||||
id: string;
|
id: string;
|
||||||
employeeId: string;
|
employeeId: string;
|
||||||
planId: string;
|
planId: string;
|
||||||
dayOfWeek: number; // 1=Monday, 7=Sunday
|
shiftId: string; // Now references shift_id instead of time_slot_id + day_of_week
|
||||||
timeSlotId: string;
|
|
||||||
preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable
|
preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable
|
||||||
notes?: string;
|
notes?: string;
|
||||||
}
|
}
|
||||||
@@ -73,3 +75,13 @@ export interface ManagerSelfAssignmentRequest {
|
|||||||
export interface EmployeeWithAvailabilities extends Employee {
|
export interface EmployeeWithAvailabilities extends Employee {
|
||||||
availabilities: EmployeeAvailability[];
|
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;
|
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
|
// Request/Response DTOs
|
||||||
export interface CreateShiftPlanRequest {
|
export interface CreateShiftPlanRequest {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -95,10 +85,6 @@ export interface AssignEmployeeRequest {
|
|||||||
scheduledShiftId: string;
|
scheduledShiftId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateAvailabilityRequest {
|
|
||||||
planId: string;
|
|
||||||
availabilities: Omit<EmployeeAvailability, 'id' | 'employeeId'>[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UpdateRequiredEmployeesRequest {
|
export interface UpdateRequiredEmployeesRequest {
|
||||||
requiredEmployees: number;
|
requiredEmployees: number;
|
||||||
|
|||||||
@@ -84,38 +84,42 @@ export class SchedulingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private generateScheduledShiftsFromTemplate(shiftPlan: ShiftPlan): any[] {
|
private generateScheduledShiftsFromTemplate(shiftPlan: ShiftPlan): any[] {
|
||||||
const shifts: 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);
|
|
||||||
|
|
||||||
|
if (!shiftPlan || !shiftPlan.startDate) {
|
||||||
return 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 => {
|
||||||
|
// ✅ 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[] {
|
private prepareAvailabilities(availabilities: Availability[], shiftPlan: ShiftPlan): any[] {
|
||||||
|
|||||||
@@ -18,15 +18,16 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
|
|||||||
const { employees, shifts, availabilities, constraints } = data;
|
const { employees, shifts, availabilities, constraints } = data;
|
||||||
|
|
||||||
// Filter employees to only include active ones
|
// 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 activeEmployees = employees.filter(emp => emp.isActive);
|
||||||
const trainees = activeEmployees.filter(emp => emp.employeeType === 'trainee');
|
const trainees = nonManagerEmployees.filter(emp => emp.employeeType === 'trainee');
|
||||||
const experienced = activeEmployees.filter(emp => emp.employeeType === 'experienced');
|
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}`);
|
console.log(`Available shifts per week: ${shifts.length}`);
|
||||||
|
|
||||||
// 1. Create assignment variables for all possible assignments
|
// 1. Create assignment variables for all possible assignments
|
||||||
activeEmployees.forEach((employee: any) => {
|
nonManagerEmployees.forEach((employee: any) => {
|
||||||
shifts.forEach((shift: any) => {
|
shifts.forEach((shift: any) => {
|
||||||
const varName = `assign_${employee.id}_${shift.id}`;
|
const varName = `assign_${employee.id}_${shift.id}`;
|
||||||
model.addVariable(varName, 'bool');
|
model.addVariable(varName, 'bool');
|
||||||
@@ -34,7 +35,7 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 2. Availability constraints
|
// 2. Availability constraints
|
||||||
activeEmployees.forEach((employee: any) => {
|
nonManagerEmployees.forEach((employee: any) => {
|
||||||
shifts.forEach((shift: any) => {
|
shifts.forEach((shift: any) => {
|
||||||
const availability = availabilities.find(
|
const availability = availabilities.find(
|
||||||
(a: any) => a.employeeId === employee.id && a.shiftId === shift.id
|
(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
|
// 3. Max 1 shift per day per employee
|
||||||
const shiftsByDate = groupShiftsByDate(shifts);
|
const shiftsByDate = groupShiftsByDate(shifts);
|
||||||
activeEmployees.forEach((employee: any) => {
|
nonManagerEmployees.forEach((employee: any) => {
|
||||||
Object.entries(shiftsByDate).forEach(([date, dayShifts]) => {
|
Object.entries(shiftsByDate).forEach(([date, dayShifts]) => {
|
||||||
const dayAssignmentVars = (dayShifts as any[]).map(
|
const dayAssignmentVars = (dayShifts as any[]).map(
|
||||||
(shift: any) => `assign_${employee.id}_${shift.id}`
|
(shift: any) => `assign_${employee.id}_${shift.id}`
|
||||||
@@ -70,7 +71,7 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
|
|||||||
|
|
||||||
// 4. Shift staffing constraints
|
// 4. Shift staffing constraints
|
||||||
shifts.forEach((shift: any) => {
|
shifts.forEach((shift: any) => {
|
||||||
const assignmentVars = activeEmployees.map(
|
const assignmentVars = nonManagerEmployees.map(
|
||||||
(emp: any) => `assign_${emp.id}_${shift.id}`
|
(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;
|
const totalShifts = shifts.length;
|
||||||
console.log(`Total available shifts: ${totalShifts}`);
|
console.log(`Total available shifts: ${totalShifts}`);
|
||||||
|
|
||||||
activeEmployees.forEach((employee: any) => {
|
nonManagerEmployees.forEach((employee: any) => {
|
||||||
const contractType = employee.contractType || 'large';
|
const contractType = employee.contractType || 'large';
|
||||||
|
|
||||||
// EXACT SHIFTS PER WEEK
|
// 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 objectiveExpression = '';
|
||||||
let softConstraintPenalty = '';
|
let softConstraintPenalty = '';
|
||||||
|
|
||||||
activeEmployees.forEach((employee: any) => {
|
nonManagerEmployees.forEach((employee: any) => {
|
||||||
shifts.forEach((shift: any) => {
|
shifts.forEach((shift: any) => {
|
||||||
const varName = `assign_${employee.id}_${shift.id}`;
|
const varName = `assign_${employee.id}_${shift.id}`;
|
||||||
const availability = availabilities.find(
|
const availability = availabilities.find(
|
||||||
@@ -192,23 +224,13 @@ function groupShiftsByDate(shifts: any[]): Record<string, any[]> {
|
|||||||
|
|
||||||
function extractAssignmentsFromSolution(solution: any, employees: any[], shifts: any[]): any {
|
function extractAssignmentsFromSolution(solution: any, employees: any[], shifts: any[]): any {
|
||||||
const assignments: any = {};
|
const assignments: any = {};
|
||||||
const employeeAssignments: any = {};
|
|
||||||
|
|
||||||
console.log('=== SOLUTION DEBUG INFO ===');
|
console.log('🔍 DEBUG: Available shifts with new ID pattern:');
|
||||||
console.log('Solution success:', solution.success);
|
shifts.forEach(shift => {
|
||||||
console.log('Raw assignments from Python:', solution.assignments?.length || 0);
|
console.log(` - ${shift.id} (Day: ${shift.id.split('-')[0]}, TimeSlot: ${shift.id.split('-')[1]})`);
|
||||||
console.log('Variables in solution:', Object.keys(solution.variables || {}).length);
|
|
||||||
|
|
||||||
// Initialize assignments object
|
|
||||||
shifts.forEach((shift: any) => {
|
|
||||||
assignments[shift.id] = [];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
employees.forEach((employee: any) => {
|
// Your existing assignment extraction logic...
|
||||||
employeeAssignments[employee.id] = 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Python-parsed assignments
|
|
||||||
if (solution.assignments && solution.assignments.length > 0) {
|
if (solution.assignments && solution.assignments.length > 0) {
|
||||||
console.log('Using Python-parsed assignments (cleaner)');
|
console.log('Using Python-parsed assignments (cleaner)');
|
||||||
|
|
||||||
@@ -216,32 +238,28 @@ function extractAssignmentsFromSolution(solution: any, employees: any[], shifts:
|
|||||||
const shiftId = assignment.shiftId;
|
const shiftId = assignment.shiftId;
|
||||||
const employeeId = assignment.employeeId;
|
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
|
// Check if this assignment already exists to avoid duplicates
|
||||||
if (!assignments[shiftId].includes(employeeId)) {
|
if (!assignments[shiftId].includes(employeeId)) {
|
||||||
assignments[shiftId].push(employeeId);
|
assignments[shiftId].push(employeeId);
|
||||||
employeeAssignments[employeeId]++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log results
|
// 🆕 ADD: Enhanced logging with employee names
|
||||||
console.log('=== ASSIGNMENT RESULTS ===');
|
console.log('🎯 FINAL ASSIGNMENTS WITH EMPLOYEE :');
|
||||||
employees.forEach((employee: any) => {
|
Object.entries(assignments).forEach(([shiftId, employeeIds]) => {
|
||||||
console.log(` ${employee.name}: ${employeeAssignments[employee.id]} shifts`);
|
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;
|
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
|
// Check for multiple shifts per day per employee
|
||||||
const shiftsByDate = groupShiftsByDate(shifts);
|
const shiftsByDate = groupShiftsByDate(shifts);
|
||||||
employees.forEach((employee: any) => {
|
employees.forEach((employee: any) => {
|
||||||
@@ -297,6 +329,35 @@ function detectViolations(assignments: any, employees: any[], shifts: any[]): st
|
|||||||
return violations;
|
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() {
|
async function runScheduling() {
|
||||||
const data: WorkerData = workerData;
|
const data: WorkerData = workerData;
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
@@ -314,6 +375,8 @@ async function runScheduling() {
|
|||||||
|
|
||||||
console.log(`Optimizing ${data.shifts.length} shifts for ${data.employees.length} employees`);
|
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();
|
const model = new CPModel();
|
||||||
buildSchedulingModel(model, data);
|
buildSchedulingModel(model, data);
|
||||||
|
|
||||||
@@ -340,26 +403,38 @@ async function runScheduling() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (solution.success) {
|
if (solution.success) {
|
||||||
// Extract assignments from solution
|
// Extract assignments from solution (non-managers only)
|
||||||
assignments = extractAssignmentsFromSolution(solution, data.employees, data.shifts);
|
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) {
|
if (Object.keys(assignments).length > 0) {
|
||||||
violations = detectViolations(assignments, data.employees, data.shifts);
|
violations = detectViolations(assignments, nonManagerEmployees, data.shifts);
|
||||||
} else {
|
} else {
|
||||||
violations.push('NO_ASSIGNMENTS: Solver reported success but produced no assignments');
|
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) =>
|
const totalAssignments = Object.values(assignments).reduce((sum: number, shiftAssignments: any) =>
|
||||||
sum + shiftAssignments.length, 0
|
sum + shiftAssignments.length, 0
|
||||||
);
|
);
|
||||||
resolutionReport.push(`📊 Total assignments: ${totalAssignments}`);
|
resolutionReport.push(`📊 Total assignments: ${totalAssignments} (including managers)`);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
violations.push('SCHEDULING_FAILED: No feasible solution found');
|
violations.push('SCHEDULING_FAILED: No feasible solution found for non-manager employees');
|
||||||
resolutionReport.push('❌ No feasible solution could be found');
|
resolutionReport.push('❌ No feasible solution could be found for non-manager employees');
|
||||||
}
|
}
|
||||||
|
|
||||||
parentPort?.postMessage({
|
parentPort?.postMessage({
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ interface AvailabilityManagerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Local interface extensions
|
// Local interface extensions
|
||||||
interface ExtendedTimeSlot extends TimeSlot {
|
interface ExtendedShift extends Shift {
|
||||||
|
timeSlotName?: string;
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
source?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Availability extends EmployeeAvailability {
|
interface Availability extends EmployeeAvailability {
|
||||||
@@ -68,20 +70,42 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
return time.substring(0, 5);
|
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 = () => {
|
const getTimetableData = () => {
|
||||||
if (!selectedPlan || !selectedPlan.shifts || !selectedPlan.timeSlots) {
|
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) => {
|
const shiftsByDay = selectedPlan.shifts.reduce((acc, shift) => {
|
||||||
if (!acc[shift.dayOfWeek]) {
|
if (!acc[shift.dayOfWeek]) {
|
||||||
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;
|
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
|
// Get unique days that have shifts
|
||||||
const days = Array.from(new Set(selectedPlan.shifts.map(shift => shift.dayOfWeek)))
|
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}` };
|
return daysOfWeek.find(day => day.id === dayId) || { id: dayId, name: `Tag ${dayId}` };
|
||||||
});
|
});
|
||||||
|
|
||||||
// For each day, get the time slots that actually have shifts
|
return { days, shiftsByDay };
|
||||||
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 };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
@@ -137,7 +144,6 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
|
|
||||||
// 3. Select first plan with actual shifts if available
|
// 3. Select first plan with actual shifts if available
|
||||||
if (plans.length > 0) {
|
if (plans.length > 0) {
|
||||||
// Find a plan that actually has shifts and time slots
|
|
||||||
const planWithShifts = plans.find(plan =>
|
const planWithShifts = plans.find(plan =>
|
||||||
plan.shifts && plan.shifts.length > 0 &&
|
plan.shifts && plan.shifts.length > 0 &&
|
||||||
plan.timeSlots && plan.timeSlots.length > 0
|
plan.timeSlots && plan.timeSlots.length > 0
|
||||||
@@ -146,7 +152,6 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
setSelectedPlanId(planWithShifts.id);
|
setSelectedPlanId(planWithShifts.id);
|
||||||
console.log('✅ SCHICHTPLAN AUSGEWÄHLT:', planWithShifts.name);
|
console.log('✅ SCHICHTPLAN AUSGEWÄHLT:', planWithShifts.name);
|
||||||
|
|
||||||
// Load the selected plan to get its actual used time slots and days
|
|
||||||
await loadSelectedPlan();
|
await loadSelectedPlan();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,8 +175,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
name: plan.name,
|
name: plan.name,
|
||||||
timeSlotsCount: plan.timeSlots?.length || 0,
|
timeSlotsCount: plan.timeSlots?.length || 0,
|
||||||
shiftsCount: plan.shifts?.length || 0,
|
shiftsCount: plan.shifts?.length || 0,
|
||||||
usedDays: Array.from(new Set(plan.shifts?.map(s => s.dayOfWeek) || [])).sort(),
|
usedDays: Array.from(new Set(plan.shifts?.map(s => s.dayOfWeek) || [])).sort()
|
||||||
usedTimeSlots: Array.from(new Set(plan.shifts?.map(s => s.timeSlotId) || [])).length
|
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('❌ FEHLER BEIM LADEN DES SCHICHTPLANS:', err);
|
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) => {
|
const handleAvailabilityLevelChange = (shiftId: string, level: AvailabilityLevel) => {
|
||||||
console.log(`🔄 ÄNDERE VERFÜGBARKEIT: Tag ${dayId}, Slot ${timeSlotId}, Level ${level}`);
|
console.log(`🔄 ÄNDERE VERFÜGBARKEIT: Shift ${shiftId}, Level ${level}`);
|
||||||
|
|
||||||
setAvailabilities(prev => {
|
setAvailabilities(prev => {
|
||||||
const existingIndex = prev.findIndex(avail =>
|
const existingIndex = prev.findIndex(avail => avail.shiftId === shiftId);
|
||||||
avail.dayOfWeek === dayId &&
|
|
||||||
avail.timeSlotId === timeSlotId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
// Update existing availability
|
// Update existing availability
|
||||||
@@ -198,13 +199,20 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
};
|
};
|
||||||
return updated;
|
return updated;
|
||||||
} else {
|
} 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 = {
|
const newAvailability: Availability = {
|
||||||
id: `temp-${dayId}-${timeSlotId}-${Date.now()}`,
|
id: `temp-${shiftId}-${Date.now()}`,
|
||||||
employeeId: employee.id,
|
employeeId: employee.id,
|
||||||
planId: selectedPlanId || '',
|
planId: selectedPlanId,
|
||||||
dayOfWeek: dayId,
|
shiftId: shiftId, // Use shiftId directly
|
||||||
timeSlotId: timeSlotId,
|
dayOfWeek: shift.dayOfWeek, // Keep for backward compatibility if needed
|
||||||
|
timeSlotId: shift.timeSlotId, // Keep for backward compatibility if needed
|
||||||
preferenceLevel: level,
|
preferenceLevel: level,
|
||||||
isAvailable: level !== 3
|
isAvailable: level !== 3
|
||||||
};
|
};
|
||||||
@@ -213,21 +221,16 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAvailabilityForDayAndSlot = (dayId: number, timeSlotId: string): AvailabilityLevel => {
|
const getAvailabilityForShift = (shiftId: string): AvailabilityLevel => {
|
||||||
const availability = availabilities.find(avail =>
|
const availability = availabilities.find(avail => avail.shiftId === shiftId);
|
||||||
avail.dayOfWeek === dayId &&
|
return availability?.preferenceLevel || 3;
|
||||||
avail.timeSlotId === timeSlotId
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = availability?.preferenceLevel || 3;
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update the timetable rendering to use the new data structure
|
// Update the timetable rendering to use shifts directly
|
||||||
const renderTimetable = () => {
|
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 (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '40px',
|
padding: '40px',
|
||||||
@@ -244,23 +247,25 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all unique time slots across all days for row headers
|
// Get all unique shifts across all days for row headers
|
||||||
const allTimeSlotIds = new Set<string>();
|
const allShifts: ExtendedShift[] = [];
|
||||||
|
const shiftIds = new Set<string>();
|
||||||
|
|
||||||
days.forEach(day => {
|
days.forEach(day => {
|
||||||
timeSlotsByDay[day.id]?.forEach(timeSlot => {
|
shiftsByDay[day.id]?.forEach(shift => {
|
||||||
allTimeSlotIds.add(timeSlot.id);
|
if (!shiftIds.has(shift.id)) {
|
||||||
|
shiftIds.add(shift.id);
|
||||||
|
allShifts.push(shift);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const allTimeSlots = Array.from(allTimeSlotIds)
|
// Sort shifts by time slot start time
|
||||||
.map(id => selectedPlan?.timeSlots?.find(ts => ts.id === id))
|
allShifts.sort((a, b) => {
|
||||||
.filter(Boolean)
|
const timeA = a.startTime || '';
|
||||||
.map(timeSlot => ({
|
const timeB = b.startTime || '';
|
||||||
...timeSlot!,
|
return timeA.localeCompare(timeB);
|
||||||
displayName: `${timeSlot!.name} (${formatTime(timeSlot!.startTime)}-${formatTime(timeSlot!.endTime)})`,
|
});
|
||||||
source: `Plan: ${selectedPlan!.name}`
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a.startTime.localeCompare(b.startTime));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -277,7 +282,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
}}>
|
}}>
|
||||||
Verfügbarkeit definieren
|
Verfügbarkeit definieren
|
||||||
<div style={{ fontSize: '14px', fontWeight: 'normal', marginTop: '5px' }}>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -294,7 +299,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
border: '1px solid #dee2e6',
|
border: '1px solid #dee2e6',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
minWidth: '100px'
|
minWidth: '150px'
|
||||||
}}>
|
}}>
|
||||||
Schicht (Zeit)
|
Schicht (Zeit)
|
||||||
</th>
|
</th>
|
||||||
@@ -312,9 +317,9 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{allTimeSlots.map((timeSlot, timeIndex) => (
|
{allShifts.map((shift, shiftIndex) => (
|
||||||
<tr key={timeSlot.id} style={{
|
<tr key={shift.id} style={{
|
||||||
backgroundColor: timeIndex % 2 === 0 ? 'white' : '#f8f9fa'
|
backgroundColor: shiftIndex % 2 === 0 ? 'white' : '#f8f9fa'
|
||||||
}}>
|
}}>
|
||||||
<td style={{
|
<td style={{
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
@@ -322,16 +327,16 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
backgroundColor: '#f8f9fa'
|
backgroundColor: '#f8f9fa'
|
||||||
}}>
|
}}>
|
||||||
{timeSlot.displayName}
|
{shift.displayName}
|
||||||
<div style={{ fontSize: '11px', color: '#666', marginTop: '4px' }}>
|
<div style={{ fontSize: '11px', color: '#666', marginTop: '4px' }}>
|
||||||
{timeSlot.source}
|
Shift-ID: {shift.id.substring(0, 8)}...
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{days.map(weekday => {
|
{days.map(weekday => {
|
||||||
// Check if this time slot exists for this day
|
// Check if this shift exists for this day
|
||||||
const timeSlotForDay = timeSlotsByDay[weekday.id]?.find(ts => ts.id === timeSlot.id);
|
const shiftForDay = shiftsByDay[weekday.id]?.find(s => s.id === shift.id);
|
||||||
|
|
||||||
if (!timeSlotForDay) {
|
if (!shiftForDay) {
|
||||||
return (
|
return (
|
||||||
<td key={weekday.id} style={{
|
<td key={weekday.id} style={{
|
||||||
padding: '12px 16px',
|
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);
|
const levelConfig = availabilityLevels.find(l => l.level === currentLevel);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -360,7 +365,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
value={currentLevel}
|
value={currentLevel}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newLevel = parseInt(e.target.value) as AvailabilityLevel;
|
const newLevel = parseInt(e.target.value) as AvailabilityLevel;
|
||||||
handleAvailabilityLevelChange(weekday.id, timeSlot.id, newLevel);
|
handleAvailabilityLevelChange(shift.id, newLevel);
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
@@ -410,12 +415,12 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { days, timeSlotsByDay } = getTimetableData();
|
const { days, shiftsByDay } = getTimetableData();
|
||||||
|
|
||||||
// Filter availabilities to only include those with actual shifts
|
// Filter availabilities to only include those with actual shifts
|
||||||
const validAvailabilities = availabilities.filter(avail => {
|
const validAvailabilities = availabilities.filter(avail => {
|
||||||
const timeSlotsForDay = timeSlotsByDay[avail.dayOfWeek] || [];
|
// Check if this shiftId exists in the current plan
|
||||||
return timeSlotsForDay.some(slot => slot.id === avail.timeSlotId);
|
return selectedPlan?.shifts?.some(shift => shift.id === avail.shiftId);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (validAvailabilities.length === 0) {
|
if (validAvailabilities.length === 0) {
|
||||||
@@ -423,13 +428,14 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to the format expected by the API
|
// Convert to the format expected by the API - using shiftId directly
|
||||||
const requestData = {
|
const requestData = {
|
||||||
planId: selectedPlanId,
|
planId: selectedPlanId,
|
||||||
availabilities: validAvailabilities.map(avail => ({
|
availabilities: validAvailabilities.map(avail => ({
|
||||||
planId: selectedPlanId,
|
planId: selectedPlanId,
|
||||||
dayOfWeek: avail.dayOfWeek,
|
shiftId: avail.shiftId, // Use shiftId directly
|
||||||
timeSlotId: avail.timeSlotId,
|
dayOfWeek: avail.dayOfWeek, // Keep for backward compatibility
|
||||||
|
timeSlotId: avail.timeSlotId, // Keep for backward compatibility
|
||||||
preferenceLevel: avail.preferenceLevel,
|
preferenceLevel: avail.preferenceLevel,
|
||||||
notes: avail.notes
|
notes: avail.notes
|
||||||
}))
|
}))
|
||||||
@@ -457,14 +463,14 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { days, timeSlotsByDay } = getTimetableData();
|
const { days, shiftsByDay } = getTimetableData();
|
||||||
const allTimeSlotIds = new Set<string>();
|
const allShiftIds = new Set<string>();
|
||||||
days.forEach(day => {
|
days.forEach(day => {
|
||||||
timeSlotsByDay[day.id]?.forEach(timeSlot => {
|
shiftsByDay[day.id]?.forEach(shift => {
|
||||||
allTimeSlotIds.add(timeSlot.id);
|
allShiftIds.add(shift.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
const timeSlotsCount = allTimeSlotIds.size;
|
const shiftsCount = allShiftIds.size;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -482,40 +488,30 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
borderBottom: '2px solid #f0f0f0',
|
borderBottom: '2px solid #f0f0f0',
|
||||||
paddingBottom: '15px'
|
paddingBottom: '15px'
|
||||||
}}>
|
}}>
|
||||||
📅 Verfügbarkeit verwalten
|
📅 Verfügbarkeit verwalten (Shift-ID basiert)
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Debug-Info */}
|
{/* Debug-Info */}
|
||||||
<div style={{
|
<div style={{
|
||||||
backgroundColor: timeSlotsCount === 0 ? '#f8d7da' : '#d1ecf1',
|
backgroundColor: shiftsCount === 0 ? '#f8d7da' : '#d1ecf1',
|
||||||
border: `1px solid ${timeSlotsCount === 0 ? '#f5c6cb' : '#bee5eb'}`,
|
border: `1px solid ${shiftsCount === 0 ? '#f5c6cb' : '#bee5eb'}`,
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
padding: '15px',
|
padding: '15px',
|
||||||
marginBottom: '20px'
|
marginBottom: '20px'
|
||||||
}}>
|
}}>
|
||||||
<h4 style={{
|
<h4 style={{
|
||||||
margin: '0 0 10px 0',
|
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>
|
</h4>
|
||||||
<div style={{ fontSize: '12px', fontFamily: 'monospace' }}>
|
<div style={{ fontSize: '12px', fontFamily: 'monospace' }}>
|
||||||
<div><strong>Ausgewählter Plan:</strong> {selectedPlan?.name || 'Keiner'}</div>
|
<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>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>Gesamte Shifts im Plan:</strong> {selectedPlan?.shifts?.length || 0}</div>
|
||||||
|
<div><strong>Methode:</strong> Direkte Shift-ID Zuordnung</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
{/* Employee Info */}
|
{/* Employee Info */}
|
||||||
@@ -524,7 +520,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
{employee.name}
|
{employee.name}
|
||||||
</h3>
|
</h3>
|
||||||
<p style={{ margin: 0, color: '#7f8c8d' }}>
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -608,7 +604,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
<option value="">Bitte auswählen...</option>
|
<option value="">Bitte auswählen...</option>
|
||||||
{shiftPlans.map(plan => (
|
{shiftPlans.map(plan => (
|
||||||
<option key={plan.id} value={plan.id}>
|
<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>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
@@ -617,7 +613,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
{selectedPlan && (
|
{selectedPlan && (
|
||||||
<div style={{ fontSize: '14px', color: '#666' }}>
|
<div style={{ fontSize: '14px', color: '#666' }}>
|
||||||
<div><strong>Plan:</strong> {selectedPlan.name}</div>
|
<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><strong>Status:</strong> {selectedPlan.status}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -651,14 +647,14 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
disabled={saving || timeSlotsCount === 0 || !selectedPlanId}
|
disabled={saving || shiftsCount === 0 || !selectedPlanId}
|
||||||
style={{
|
style={{
|
||||||
padding: '12px 24px',
|
padding: '12px 24px',
|
||||||
backgroundColor: saving ? '#bdc3c7' : (timeSlotsCount === 0 || !selectedPlanId ? '#95a5a6' : '#3498db'),
|
backgroundColor: saving ? '#bdc3c7' : (shiftsCount === 0 || !selectedPlanId ? '#95a5a6' : '#3498db'),
|
||||||
color: 'white',
|
color: 'white',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
cursor: (saving || timeSlotsCount === 0 || !selectedPlanId) ? 'not-allowed' : 'pointer',
|
cursor: (saving || shiftsCount === 0 || !selectedPlanId) ? 'not-allowed' : 'pointer',
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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(() => {
|
useEffect(() => {
|
||||||
(window as any).debugRenderLogic = debugRenderLogic;
|
(window as any).debugRenderLogic = debugRenderLogic;
|
||||||
return () => { (window as any).debugRenderLogic = undefined; };
|
return () => { (window as any).debugRenderLogic = undefined; };
|
||||||
}, [shiftPlan, scheduledShifts]);
|
}, [shiftPlan, scheduledShifts]);
|
||||||
|
|
||||||
const loadShiftPlanData = async () => {
|
const loadShiftPlanData = async () => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// Load plan and employees first
|
// Load plan and employees first
|
||||||
const [plan, employeesData] = await Promise.all([
|
const [plan, employeesData] = await Promise.all([
|
||||||
shiftPlanService.getShiftPlan(id),
|
shiftPlanService.getShiftPlan(id),
|
||||||
employeeService.getEmployees(),
|
employeeService.getEmployees(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
setShiftPlan(plan);
|
setShiftPlan(plan);
|
||||||
setEmployees(employeesData.filter(emp => emp.isActive));
|
setEmployees(employeesData.filter(emp => emp.isActive));
|
||||||
|
|
||||||
// CRITICAL: Load scheduled shifts and verify they exist
|
// CRITICAL: Load scheduled shifts and verify they exist
|
||||||
const shiftsData = await shiftAssignmentService.getScheduledShiftsForPlan(id);
|
const shiftsData = await shiftAssignmentService.getScheduledShiftsForPlan(id);
|
||||||
console.log('📋 Loaded scheduled shifts:', shiftsData.length);
|
console.log('📋 Loaded scheduled shifts:', shiftsData.length);
|
||||||
|
|
||||||
if (shiftsData.length === 0) {
|
if (shiftsData.length === 0) {
|
||||||
console.warn('⚠️ No scheduled shifts found for plan:', id);
|
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({
|
showNotification({
|
||||||
type: 'warning',
|
type: 'error',
|
||||||
title: 'Keine Schichten gefunden',
|
title: 'Fehler',
|
||||||
message: 'Der Schichtplan hat keine generierten Schichten. Bitte überprüfen Sie die Plan-Konfiguration.'
|
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 () => {
|
const handleRecreateAssignments = async () => {
|
||||||
if (!shiftPlan) return;
|
if (!shiftPlan) return;
|
||||||
@@ -207,7 +228,7 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
console.log('🔍 RENDER LOGIC DEBUG:');
|
console.log('🔍 RENDER LOGIC DEBUG:');
|
||||||
console.log('=====================');
|
console.log('=====================');
|
||||||
|
|
||||||
const { days, allTimeSlots, timeSlotsByDay } = getExtendedTimetableData();
|
const { days, allTimeSlots, timeSlotsByDay } = getTimetableData();
|
||||||
|
|
||||||
console.log('📊 TABLE STRUCTURE:');
|
console.log('📊 TABLE STRUCTURE:');
|
||||||
console.log('- Days in table:', days.length);
|
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
|
// Extract plan-specific shifts using the same logic as AvailabilityManager
|
||||||
const getTimetableData = () => {
|
const getTimetableData = () => {
|
||||||
if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) {
|
if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) {
|
||||||
@@ -409,9 +382,6 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
console.log('- Employees:', refreshedEmployees.length);
|
console.log('- Employees:', refreshedEmployees.length);
|
||||||
console.log('- Availabilities:', refreshedAvailabilities.length);
|
console.log('- Availabilities:', refreshedAvailabilities.length);
|
||||||
|
|
||||||
// DEBUG: Verify we have new data
|
|
||||||
debugSchedulingInput(refreshedEmployees, refreshedAvailabilities);
|
|
||||||
|
|
||||||
// ADD THIS: Define constraints object
|
// ADD THIS: Define constraints object
|
||||||
const constraints = {
|
const constraints = {
|
||||||
enforceNoTraineeAlone: true,
|
enforceNoTraineeAlone: true,
|
||||||
@@ -428,6 +398,35 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
constraints
|
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);
|
setAssignmentResult(result);
|
||||||
setShowAssignmentPreview(true);
|
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 () => {
|
const handlePublish = async () => {
|
||||||
if (!shiftPlan || !assignmentResult) return;
|
if (!shiftPlan || !assignmentResult) return;
|
||||||
|
|
||||||
@@ -502,32 +450,42 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
|
|
||||||
console.log('🔄 Starting to publish assignments...');
|
console.log('🔄 Starting to publish assignments...');
|
||||||
|
|
||||||
// ✅ KORREKTUR: Verwende die neu geladenen Shifts
|
// Get fresh scheduled shifts
|
||||||
const updatedShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
const updatedShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
||||||
|
|
||||||
// Debug: Check if scheduled shifts exist
|
|
||||||
if (!updatedShifts || updatedShifts.length === 0) {
|
if (!updatedShifts || updatedShifts.length === 0) {
|
||||||
throw new Error('No scheduled shifts found in the plan');
|
throw new Error('No scheduled shifts found in the plan');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`📊 Found ${updatedShifts.length} scheduled shifts to update`);
|
console.log(`📊 Found ${updatedShifts.length} scheduled shifts to update`);
|
||||||
|
|
||||||
// ✅ KORREKTUR: Verwende updatedShifts statt scheduledShifts
|
|
||||||
const updatePromises = updatedShifts.map(async (scheduledShift) => {
|
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 {
|
try {
|
||||||
// Update the shift with assigned employees
|
// Update the scheduled shift with assigned employees
|
||||||
const scheduledShifts = await shiftAssignmentService.getScheduledShiftsForPlan(shiftPlan.id);
|
|
||||||
await shiftAssignmentService.updateScheduledShift(scheduledShift.id, {
|
await shiftAssignmentService.updateScheduledShift(scheduledShift.id, {
|
||||||
assignedEmployees
|
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) {
|
} catch (error) {
|
||||||
console.error(`❌ Failed to update shift ${scheduledShift.id}:`, error);
|
console.error(`❌ Failed to update shift ${scheduledShift.id}:`, error);
|
||||||
throw error;
|
throw error;
|
||||||
@@ -542,7 +500,7 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
status: 'published'
|
status: 'published'
|
||||||
});
|
});
|
||||||
|
|
||||||
// ✅ KORREKTUR: Explizit alle Daten neu laden und State aktualisieren
|
// Reload all data to reflect changes
|
||||||
const [reloadedPlan, reloadedShifts] = await Promise.all([
|
const [reloadedPlan, reloadedShifts] = await Promise.all([
|
||||||
shiftPlanService.getShiftPlan(shiftPlan.id),
|
shiftPlanService.getShiftPlan(shiftPlan.id),
|
||||||
shiftAssignmentService.getScheduledShiftsForPlan(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 getAssignmentsForScheduledShift = (scheduledShift: ScheduledShift): string[] => {
|
||||||
const handleReloadData = async () => {
|
if (!assignmentResult) return [];
|
||||||
await loadShiftPlanData();
|
|
||||||
showNotification({
|
// First try direct match with scheduled shift ID
|
||||||
type: 'success',
|
if (assignmentResult.assignments[scheduledShift.id]) {
|
||||||
title: 'Erfolg',
|
return assignmentResult.assignments[scheduledShift.id];
|
||||||
message: 'Daten wurden neu geladen.'
|
}
|
||||||
});
|
|
||||||
|
// 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
|
// Render timetable using the same structure as AvailabilityManager
|
||||||
const renderTimetable = () => {
|
const renderTimetable = () => {
|
||||||
const { days, allTimeSlots, timeSlotsByDay } = getExtendedTimetableData();
|
const { days, allTimeSlots, timeSlotsByDay } = getTimetableData();
|
||||||
if (!shiftPlan?.id) {
|
if (!shiftPlan?.id) {
|
||||||
console.warn("Shift plan ID is missing");
|
console.warn("Shift plan ID is missing");
|
||||||
return null;
|
return null;
|
||||||
@@ -847,8 +817,8 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
scheduled.timeSlotId === timeSlot.id;
|
scheduled.timeSlotId === timeSlot.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (scheduledShift && assignmentResult.assignments[scheduledShift.id]) {
|
if (scheduledShift) {
|
||||||
assignedEmployees = assignmentResult.assignments[scheduledShift.id];
|
assignedEmployees = getAssignmentsForScheduledShift(scheduledShift);
|
||||||
displayText = assignedEmployees.map(empId => {
|
displayText = assignedEmployees.map(empId => {
|
||||||
const employee = employees.find(emp => emp.id === empId);
|
const employee = employees.find(emp => emp.id === empId);
|
||||||
return employee ? employee.name : 'Unbekannt';
|
return employee ? employee.name : 'Unbekannt';
|
||||||
@@ -900,7 +870,7 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
if (loading) return <div>Lade Schichtplan...</div>;
|
if (loading) return <div>Lade Schichtplan...</div>;
|
||||||
if (!shiftPlan) return <div>Schichtplan nicht gefunden</div>;
|
if (!shiftPlan) return <div>Schichtplan nicht gefunden</div>;
|
||||||
|
|
||||||
const { days, allTimeSlots } = getExtendedTimetableData();
|
const { days, allTimeSlots } = getTimetableData();
|
||||||
const availabilityStatus = getAvailabilityStatus();
|
const availabilityStatus = getAvailabilityStatus();
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user