From 3127692d297c0dd8f4a6d8673627db41046b86ff Mon Sep 17 00:00:00 2001 From: donpat1to Date: Tue, 21 Oct 2025 00:51:23 +0200 Subject: [PATCH] updated every file for database changes; starting scheduling debugging --- backend/src/controllers/authController.ts | 49 +- backend/src/controllers/employeeController.ts | 246 +++++-- backend/src/controllers/setupController.ts | 13 +- backend/src/database/schema.sql | 56 +- backend/src/middleware/auth.ts | 3 +- backend/src/models/Employee.ts | 24 +- .../src/models/defaults/employeeDefaults.ts | 79 +- backend/src/models/helpers/employeeHelpers.ts | 82 ++- backend/src/models/scheduling.ts | 11 +- backend/src/routes/employees.ts | 6 +- backend/src/scripts/applyMigration.ts | 198 +++-- backend/src/scripts/initializeDatabase.ts | 22 +- backend/src/services/SchedulingService.ts | 13 +- backend/src/workers/scheduler-worker.ts | 210 +++++- frontend/src/components/Scheduler.tsx | 141 ---- frontend/src/contexts/AuthContext.tsx | 2 +- frontend/src/models/Employee.ts | 26 +- .../src/models/defaults/employeeDefaults.ts | 79 +- .../src/models/helpers/employeeHelpers.ts | 83 ++- frontend/src/models/scheduling.ts | 11 +- frontend/src/pages/Auth/Login.tsx | 5 +- frontend/src/pages/Dashboard/Dashboard.tsx | 20 +- .../components/AvailabilityManager.tsx | 471 +++++++++--- .../Employees/components/EmployeeForm.tsx | 123 +++- .../Employees/components/EmployeeList.tsx | 46 +- .../src/pages/ShiftPlans/ShiftPlanView.tsx | 692 +++++++++++------- frontend/src/services/employeeService.ts | 16 + 27 files changed, 1861 insertions(+), 866 deletions(-) delete mode 100644 frontend/src/components/Scheduler.tsx diff --git a/backend/src/controllers/authController.ts b/backend/src/controllers/authController.ts index ebe8458..273cd90 100644 --- a/backend/src/controllers/authController.ts +++ b/backend/src/controllers/authController.ts @@ -39,7 +39,6 @@ export interface RegisterRequest { } function generateEmail(firstname: string, lastname: string): string { - // Convert German umlauts to their expanded forms const convertUmlauts = (str: string): string => { return str .toLowerCase() @@ -49,7 +48,6 @@ function generateEmail(firstname: string, lastname: string): string { .replace(/ß/g, 'ss'); }; - // Remove any remaining special characters and convert to lowercase const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, ''); const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, ''); return `${cleanFirstname}.${cleanLastname}@sp.de`; @@ -70,8 +68,8 @@ export const login = async (req: Request, res: Response) => { const user = await db.get( `SELECT e.id, e.email, e.password, e.firstname, e.lastname, - e.employee_type as employeeType, e.contract_type as contractType, - e.can_work_alone as canWorkAlone, e.is_active as isActive, + e.employee_type, e.contract_type, + e.can_work_alone, e.is_active, e.is_trainee, er.role FROM employees e LEFT JOIN employee_roles er ON e.id = er.employee_id @@ -87,6 +85,15 @@ export const login = async (req: Request, res: Response) => { return res.status(401).json({ error: 'Ungültige Anmeldedaten' }); } + // Update last_login with current timestamp + const currentTimestamp = new Date().toISOString(); + await db.run( + 'UPDATE employees SET last_login = ? WHERE id = ?', + [currentTimestamp, user.id] + ); + + console.log('✅ Timestamp set:', currentTimestamp); + // Verify password const validPassword = await bcrypt.compare(password, user.password); console.log('🔑 Password valid:', validPassword); @@ -116,7 +123,12 @@ export const login = async (req: Request, res: Response) => { const { password: _, ...userWithoutPassword } = user; const userResponse = { ...userWithoutPassword, - roles: user.role ? [user.role] : ['user'] // Convert single role to array for frontend compatibility + employeeType: user.employee_type, + contractType: user.contract_type, + canWorkAlone: user.can_work_alone === 1, + isActive: user.is_active === 1, + isTrainee: user.is_trainee === 1, + roles: user.role ? [user.role] : ['user'] }; console.log('✅ Login successful for:', user.email); @@ -147,8 +159,8 @@ export const getCurrentUser = async (req: Request, res: Response) => { const user = await db.get( `SELECT e.id, e.email, e.firstname, e.lastname, - e.employee_type as employeeType, e.contract_type as contractType, - e.can_work_alone as canWorkAlone, e.is_active as isActive, + e.employee_type, e.contract_type, + e.can_work_alone, e.is_active, e.is_trainee, er.role FROM employees e LEFT JOIN employee_roles er ON e.id = er.employee_id @@ -167,6 +179,11 @@ export const getCurrentUser = async (req: Request, res: Response) => { // Format user response with roles array const userResponse = { ...user, + employeeType: user.employee_type, + contractType: user.contract_type, + canWorkAlone: user.can_work_alone === 1, + isActive: user.is_active === 1, + isTrainee: user.is_trainee === 1, roles: user.role ? [user.role] : ['user'] }; @@ -237,18 +254,18 @@ export const register = async (req: Request, res: Response) => { await db.run('BEGIN TRANSACTION'); try { - // Insert user without role (role is now in employee_roles table) + // ✅ CORRECTED: Use valid 'personell' type with proper contract type const result = await db.run( - `INSERT INTO employees (id, email, password, firstname, lastname, employee_type, contract_type, can_work_alone, is_active) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [employeeId, email, hashedPassword, firstname, lastname, 'experienced', 'small', false, 1] + `INSERT INTO employees (id, email, password, firstname, lastname, employee_type, contract_type, can_work_alone, is_active, is_trainee) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [employeeId, email, hashedPassword, firstname, lastname, 'personell', 'small', false, 1, false] ); if (!result.lastID) { throw new Error('Benutzer konnte nicht erstellt werden'); } - // UPDATED: Insert roles into employee_roles table + // Insert roles into employee_roles table for (const role of roles) { await db.run( `INSERT INTO employee_roles (employee_id, role) VALUES (?, ?)`, @@ -262,6 +279,7 @@ export const register = async (req: Request, res: Response) => { const newUser = await db.get( `SELECT e.id, e.email, e.firstname, e.lastname, + e.employee_type, e.contract_type, e.can_work_alone, e.is_active, e.is_trainee, er.role FROM employees e LEFT JOIN employee_roles er ON e.id = er.employee_id @@ -273,6 +291,11 @@ export const register = async (req: Request, res: Response) => { // Format response with roles array const userResponse = { ...newUser, + employeeType: newUser.employee_type, + contractType: newUser.contract_type, + canWorkAlone: newUser.can_work_alone === 1, + isActive: newUser.is_active === 1, + isTrainee: newUser.is_trainee === 1, roles: newUser.role ? [newUser.role] : ['user'] }; @@ -293,8 +316,6 @@ export const register = async (req: Request, res: Response) => { export const logout = async (req: Request, res: Response) => { try { - // Note: Since we're using JWTs, we don't need to do anything server-side - // The client should remove the token from storage res.json({ message: 'Erfolgreich abgemeldet' }); } catch (error) { console.error('Logout error:', error); diff --git a/backend/src/controllers/employeeController.ts b/backend/src/controllers/employeeController.ts index 3cedbcd..aa3e056 100644 --- a/backend/src/controllers/employeeController.ts +++ b/backend/src/controllers/employeeController.ts @@ -7,7 +7,6 @@ import { AuthRequest } from '../middleware/auth.js'; import { CreateEmployeeRequest } from '../models/Employee.js'; function generateEmail(firstname: string, lastname: string): string { - // Convert German umlauts to their expanded forms const convertUmlauts = (str: string): string => { return str .toLowerCase() @@ -17,7 +16,6 @@ function generateEmail(firstname: string, lastname: string): string { .replace(/ß/g, 'ss'); }; - // Remove any remaining special characters and convert to lowercase const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, ''); const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, ''); @@ -34,12 +32,13 @@ export const getEmployees = async (req: AuthRequest, res: Response): Promise(query); - // Format employees with roles array for frontend compatibility + // Format employees with proper field names and roles array const employeesWithRoles = employees.map(emp => ({ - ...emp, + id: emp.id, + email: emp.email, + firstname: emp.firstname, + lastname: emp.lastname, + isActive: emp.is_active === 1, + employeeType: emp.employee_type, + contractType: emp.contract_type, + canWorkAlone: emp.can_work_alone === 1, + isTrainee: emp.is_trainee === 1, + createdAt: emp.created_at, + lastLogin: emp.last_login, roles: emp.role ? [emp.role] : ['user'] })); @@ -72,16 +80,16 @@ export const getEmployee = async (req: AuthRequest, res: Response): Promise(` SELECT e.id, e.email, e.firstname, e.lastname, - e.is_active as isActive, - e.employee_type as employeeType, - e.contract_type as contractType, - e.can_work_alone as canWorkAlone, - e.created_at as createdAt, - e.last_login as lastLogin, + e.is_active, + e.employee_type, + e.contract_type, + e.can_work_alone, + e.is_trainee, + e.created_at, + e.last_login, er.role FROM employees e LEFT JOIN employee_roles er ON e.id = er.employee_id @@ -94,9 +102,19 @@ export const getEmployee = async (req: AuthRequest, res: Response): Promise( + 'SELECT type, category, has_contract_type FROM employee_types WHERE type = ?', + [employeeType] + ); + + if (!employeeTypeInfo) { + res.status(400).json({ + error: `Ungültiger employeeType: ${employeeType}. Gültige Typen: manager, personell, apprentice, guest` + }); + return; + } + + // ✅ ENHANCED: Contract type validation based on category + if (employeeTypeInfo.has_contract_type === 1) { + // Internal types require contract type + if (!contractType) { + res.status(400).json({ + error: `contractType ist erforderlich für employeeType: ${employeeType}` + }); + return; + } + if (!['small', 'large', 'flexible'].includes(contractType)) { + res.status(400).json({ + error: `Ungültiger contractType: ${contractType}. Gültige Werte: small, large, flexible` + }); + return; + } + } else { + // External types (guest) should not have contract type + if (contractType) { + res.status(400).json({ + error: `contractType ist nicht erlaubt für employeeType: ${employeeType}` + }); + return; + } + } + + // ✅ ENHANCED: isTrainee validation - only applicable for personell type + if (isTrainee && employeeType !== 'personell') { + res.status(400).json({ + error: `isTrainee ist nur für employeeType 'personell' erlaubt` }); return; } @@ -156,12 +221,12 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise(` SELECT e.id, e.email, e.firstname, e.lastname, - e.is_active as isActive, - e.employee_type as employeeType, - e.contract_type as contractType, - e.can_work_alone as canWorkAlone, - e.created_at as createdAt, - e.last_login as lastLogin, + e.is_active, + e.employee_type, + e.contract_type, + e.can_work_alone, + e.is_trainee, + e.created_at, + e.last_login, er.role FROM employees e LEFT JOIN employee_roles er ON e.id = er.employee_id @@ -202,9 +269,19 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise => { try { const { id } = req.params; - const { firstname, lastname, roles, isActive, employeeType, contractType, canWorkAlone } = req.body; + const { firstname, lastname, roles, isActive, employeeType, contractType, canWorkAlone, isTrainee } = req.body; - console.log('📝 Update Employee Request:', { id, firstname, lastname, roles, isActive, employeeType, contractType, canWorkAlone }); + console.log('📝 Update Employee Request:', { + id, firstname, lastname, roles, isActive, + employeeType, contractType, canWorkAlone, isTrainee + }); // Check if employee exists and get current data const existingEmployee = await db.get('SELECT * FROM employees WHERE id = ?', [id]); @@ -234,6 +314,21 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise(` SELECT e.id, e.email, e.firstname, e.lastname, - e.is_active as isActive, - e.employee_type as employeeType, - e.contract_type as contractType, - e.can_work_alone as canWorkAlone, - e.created_at as createdAt, - e.last_login as lastLogin, + e.is_active, + e.employee_type, + e.contract_type, + e.can_work_alone, + e.is_trainee, + e.created_at, + e.last_login, er.role FROM employees e LEFT JOIN employee_roles er ON e.id = er.employee_id @@ -308,9 +405,19 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise => { + try { + const { id } = req.params; + + // Check if employee exists + const employee = await db.get('SELECT id FROM employees WHERE id = ?', [id]); + if (!employee) { + res.status(404).json({ error: 'Employee not found' }); + return; + } + + // Update last_login with current timestamp + const currentTimestamp = new Date().toISOString(); + await db.run( + 'UPDATE employees SET last_login = ? WHERE id = ?', + [currentTimestamp, id] + ); + + console.log(`✅ Last login updated for employee ${id}: ${currentTimestamp}`); + + res.json({ + message: 'Last login updated successfully', + lastLogin: currentTimestamp + }); + } catch (error) { + console.error('Error updating last login:', error); + res.status(500).json({ error: 'Internal server error' }); + } }; \ No newline at end of file diff --git a/backend/src/controllers/setupController.ts b/backend/src/controllers/setupController.ts index a1c773e..b2a9672 100644 --- a/backend/src/controllers/setupController.ts +++ b/backend/src/controllers/setupController.ts @@ -5,9 +5,7 @@ import { v4 as uuidv4 } from 'uuid'; import { randomUUID } from 'crypto'; import { db } from '../services/databaseService.js'; -// Add the same email generation function function generateEmail(firstname: string, lastname: string): string { - // Convert German umlauts to their expanded forms const convertUmlauts = (str: string): string => { return str .toLowerCase() @@ -17,7 +15,6 @@ function generateEmail(firstname: string, lastname: string): string { .replace(/ß/g, 'ss'); }; - // Remove any remaining special characters and convert to lowercase const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, ''); const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, ''); @@ -98,14 +95,14 @@ export const setupAdmin = async (req: Request, res: Response): Promise => await db.run('BEGIN TRANSACTION'); try { - // Create admin user in employees table + // ✅ CORRECTED: Create admin with valid 'manager' type and 'flexible' contract await db.run( - `INSERT INTO employees (id, email, password, firstname, lastname, employee_type, contract_type, can_work_alone, is_active) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [adminId, email, hashedPassword, firstname, lastname, 'manager', 'large', true, 1] + `INSERT INTO employees (id, email, password, firstname, lastname, employee_type, contract_type, can_work_alone, is_active, is_trainee) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [adminId, email, hashedPassword, firstname, lastname, 'manager', 'flexible', true, 1, false] ); - // UPDATED: Assign admin role in employee_roles table + // Assign admin role in employee_roles table await db.run( `INSERT INTO employee_roles (employee_id, role) VALUES (?, ?)`, [adminId, 'admin'] diff --git a/backend/src/database/schema.sql b/backend/src/database/schema.sql index 39f0849..452036e 100644 --- a/backend/src/database/schema.sql +++ b/backend/src/database/schema.sql @@ -1,3 +1,28 @@ +-- Employee Types +CREATE TABLE IF NOT EXISTS employee_types ( + type TEXT PRIMARY KEY, + category TEXT CHECK(category IN ('internal', 'external')) NOT NULL, + has_contract_type BOOLEAN NOT NULL DEFAULT FALSE +); + +-- Default Employee Types +-- 'manager' and 'apprentice' contract_type_default = flexible +-- 'personell' contract_type_default = small +-- employee_types category 'external' contract_type = NONE +-- moved experienced and trainee into personell -> is_trainee boolean added in employees +INSERT OR IGNORE INTO employee_types (type, category, has_contract_type) VALUES + ('manager', 'internal', 1), + ('personell', 'internal', 1), + ('apprentice', 'internal', 1), + ('guest', 'external', 0); + +-- Roles lookup table +CREATE TABLE IF NOT EXISTS roles ( + role TEXT PRIMARY KEY CHECK(role IN ('admin', 'user', 'maintenance')), + authority_level INTEGER NOT NULL UNIQUE CHECK(authority_level BETWEEN 1 AND 100), + description TEXT +); + -- Employees table CREATE TABLE IF NOT EXISTS employees ( id TEXT PRIMARY KEY, @@ -5,30 +30,27 @@ CREATE TABLE IF NOT EXISTS employees ( password TEXT 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, + employee_type TEXT NOT NULL REFERENCES employee_types(type), + contract_type TEXT CHECK(contract_type IN ('small', 'large', 'flexible')), can_work_alone BOOLEAN DEFAULT FALSE, + is_trainee BOOLEAN DEFAULT FALSE NOT NULL, is_active BOOLEAN DEFAULT TRUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, 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 +-- Roles Employee Junction table (NACH employees und roles) 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) ); --- Insert default roles if they don't exist -INSERT OR IGNORE INTO roles (role) VALUES ('admin'); -INSERT OR IGNORE INTO roles (role) VALUES ('user'); -INSERT OR IGNORE INTO roles (role) VALUES ('maintenance'); +-- Insert default roles (NACH roles Tabelle) +INSERT OR IGNORE INTO roles (role, authority_level, description) VALUES + ('admin', 100, 'Vollzugriff'), + ('maintenance', 50, 'Wartungszugriff'), + ('user', 10, 'Standardbenutzer'); -- Shift plans table CREATE TABLE IF NOT EXISTS shift_plans ( @@ -109,29 +131,21 @@ CREATE TABLE IF NOT EXISTS employee_availability ( UNIQUE(employee_id, plan_id, shift_id) ); --- Performance indexes (UPDATED - removed role index from employees) +-- Performance indexes CREATE INDEX IF NOT EXISTS idx_employees_email_active ON employees(email, is_active); CREATE INDEX IF NOT EXISTS idx_employees_type_active ON employees(employee_type, is_active); - --- Index for employee_roles table (NEW) CREATE INDEX IF NOT EXISTS idx_employee_roles_employee ON employee_roles(employee_id); CREATE INDEX IF NOT EXISTS idx_employee_roles_role ON employee_roles(role); - CREATE INDEX IF NOT EXISTS idx_shift_plans_status_date ON shift_plans(status, start_date, end_date); CREATE INDEX IF NOT EXISTS idx_shift_plans_created_by ON shift_plans(created_by); CREATE INDEX IF NOT EXISTS idx_shift_plans_template ON shift_plans(is_template, status); - CREATE INDEX IF NOT EXISTS idx_time_slots_plan ON time_slots(plan_id); - CREATE INDEX IF NOT EXISTS idx_shifts_plan_day ON shifts(plan_id, day_of_week); CREATE INDEX IF NOT EXISTS idx_shifts_required_employees ON shifts(required_employees); CREATE INDEX IF NOT EXISTS idx_shifts_plan_time ON shifts(plan_id, time_slot_id, day_of_week); - CREATE INDEX IF NOT EXISTS idx_scheduled_shifts_plan_date ON scheduled_shifts(plan_id, date); CREATE INDEX IF NOT EXISTS idx_scheduled_shifts_date_time ON scheduled_shifts(date, time_slot_id); CREATE INDEX IF NOT EXISTS idx_scheduled_shifts_required_employees ON scheduled_shifts(required_employees); - CREATE INDEX IF NOT EXISTS idx_shift_assignments_employee ON shift_assignments(employee_id); CREATE INDEX IF NOT EXISTS idx_shift_assignments_shift ON shift_assignments(scheduled_shift_id); - CREATE INDEX IF NOT EXISTS idx_employee_availability_employee_plan ON employee_availability(employee_id, plan_id); \ No newline at end of file diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index 9b81a89..36320f5 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -44,10 +44,11 @@ export const authMiddleware = (req: AuthRequest, res: Response, next: NextFuncti export const requireRole = (roles: string[]) => { return (req: AuthRequest, res: Response, next: NextFunction): void => { if (!req.user || !roles.includes(req.user.role)) { - console.log('❌ Insufficient permissions for user:', req.user?.email); + console.log(`❌ Insufficient permissions for user: ${req.user?.email}, role: ${req.user?.role}, required: ${roles.join(', ')}`); res.status(403).json({ error: 'Access denied. Insufficient permissions.' }); return; } + console.log(`✅ Role check passed for user: ${req.user.email}, role: ${req.user.role}`); next(); }; }; \ No newline at end of file diff --git a/backend/src/models/Employee.ts b/backend/src/models/Employee.ts index 0114021..6cfcb29 100644 --- a/backend/src/models/Employee.ts +++ b/backend/src/models/Employee.ts @@ -4,10 +4,11 @@ export interface Employee { email: string; firstname: string; lastname: string; - employeeType: 'manager' | 'trainee' | 'experienced'; - contractType: 'small' | 'large'; + employeeType: 'manager' | 'personell' | 'apprentice' | 'guest'; + contractType?: 'small' | 'large' | 'flexible'; canWorkAlone: boolean; isActive: boolean; + isTrainee: boolean; createdAt: string; lastLogin?: string | null; roles?: string[]; @@ -18,19 +19,21 @@ export interface CreateEmployeeRequest { firstname: string; lastname: string; roles?: string[]; - employeeType: 'manager' | 'trainee' | 'experienced'; - contractType: 'small' | 'large'; + employeeType: 'manager' | 'personell' | 'apprentice' | 'guest'; + contractType?: 'small' | 'large' | 'flexible'; canWorkAlone: boolean; + isTrainee?: boolean; } export interface UpdateEmployeeRequest { firstname?: string; lastname?: string; roles?: string[]; - employeeType?: 'manager' | 'trainee' | 'experienced'; - contractType?: 'small' | 'large'; + employeeType?: 'manager' | 'personell' | 'apprentice' | 'guest'; + contractType?: 'small' | 'large' | 'flexible'; canWorkAlone?: boolean; isActive?: boolean; + isTrainee?: boolean; } export interface EmployeeWithPassword extends Employee { @@ -41,7 +44,7 @@ export interface EmployeeAvailability { id: string; employeeId: string; planId: string; - shiftId: string; // Now references shift_id instead of time_slot_id + day_of_week + shiftId: string; preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable notes?: string; } @@ -83,4 +86,11 @@ export interface Role { export interface EmployeeRole { employeeId: string; role: 'admin' | 'user' | 'maintenance'; +} + +// Employee type configuration +export interface EmployeeType { + type: 'manager' | 'personell' | 'apprentice' | 'guest'; + category: 'internal' | 'external'; + has_contract_type: boolean; } \ No newline at end of file diff --git a/backend/src/models/defaults/employeeDefaults.ts b/backend/src/models/defaults/employeeDefaults.ts index 7a61cae..e80e189 100644 --- a/backend/src/models/defaults/employeeDefaults.ts +++ b/backend/src/models/defaults/employeeDefaults.ts @@ -4,19 +4,41 @@ import { EmployeeAvailability, ManagerAvailability } from '../Employee.js'; // Default employee data for quick creation export const EMPLOYEE_DEFAULTS = { role: 'user' as const, - employeeType: 'experienced' as const, + employeeType: 'personell' as const, contractType: 'small' as const, canWorkAlone: false, - isActive: true + isActive: true, + isTrainee: false }; // Manager-specific defaults export const MANAGER_DEFAULTS = { role: 'admin' as const, employeeType: 'manager' as const, - contractType: 'large' as const, + contractType: 'flexible' as const, canWorkAlone: true, - isActive: true + isActive: true, + isTrainee: false +}; + +// Apprentice defaults +export const APPRENTICE_DEFAULTS = { + role: 'user' as const, + employeeType: 'apprentice' as const, + contractType: 'flexible' as const, + canWorkAlone: false, + isActive: true, + isTrainee: false +}; + +// Guest defaults +export const GUEST_DEFAULTS = { + role: 'user' as const, + employeeType: 'guest' as const, + contractType: undefined, + canWorkAlone: false, + isActive: true, + isTrainee: false }; export const EMPLOYEE_TYPE_CONFIG = { @@ -24,22 +46,37 @@ export const EMPLOYEE_TYPE_CONFIG = { value: 'manager' as const, label: 'Chef/Administrator', color: '#e74c3c', + category: 'internal' as const, + hasContractType: true, independent: true, description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung' }, - experienced: { - value: 'experienced' as const, - label: 'Erfahren', + personell: { + value: 'personell' as const, + label: 'Personal', color: '#3498db', + category: 'internal' as const, + hasContractType: true, independent: true, - description: 'Langjährige Erfahrung, kann komplexe Aufgaben übernehmen' + description: 'Reguläre Mitarbeiter mit Vertrag' }, - trainee: { - value: 'trainee' as const, - label: 'Neuling', - color: '#27ae60', + apprentice: { + value: 'apprentice' as const, + label: 'Auszubildender', + color: '#9b59b6', + category: 'internal' as const, + hasContractType: true, independent: false, - description: 'Benötigt Einarbeitung und Unterstützung' + description: 'Auszubildende mit flexiblem Vertrag' + }, + guest: { + value: 'guest' as const, + label: 'Gast', + color: '#95a5a6', + category: 'external' as const, + hasContractType: false, + independent: false, + description: 'Externe Mitarbeiter ohne Vertrag' } } as const; @@ -53,9 +90,10 @@ export const ROLE_CONFIG = [ export const CONTRACT_TYPE_DESCRIPTIONS = { small: '1 Schicht pro Woche', large: '2 Schichten pro Woche', - manager: 'Kein Vertragslimit - Immer MO und DI verfügbar' + flexible: 'Flexible Arbeitszeiten' } as const; + // Availability preference descriptions export const AVAILABILITY_PREFERENCES = { 1: { label: 'Bevorzugt', color: '#10b981', description: 'Möchte diese Schicht arbeiten' }, @@ -104,4 +142,17 @@ export function createManagerDefaultSchedule(managerId: string, planId: string, } return assignments; +} + +export function getDefaultsByEmployeeType(employeeType: string) { + switch (employeeType) { + case 'manager': + return MANAGER_DEFAULTS; + case 'apprentice': + return APPRENTICE_DEFAULTS; + case 'guest': + return GUEST_DEFAULTS; + default: + return EMPLOYEE_DEFAULTS; + } } \ No newline at end of file diff --git a/backend/src/models/helpers/employeeHelpers.ts b/backend/src/models/helpers/employeeHelpers.ts index 9c75ecf..63b4f45 100644 --- a/backend/src/models/helpers/employeeHelpers.ts +++ b/backend/src/models/helpers/employeeHelpers.ts @@ -1,9 +1,8 @@ // backend/src/models/helpers/employeeHelpers.ts import { Employee, CreateEmployeeRequest, EmployeeAvailability } from '../Employee.js'; -// Email generation function (same as in controllers) +// Email generation function function generateEmail(firstname: string, lastname: string): string { - // Convert German umlauts to their expanded forms const convertUmlauts = (str: string): string => { return str .toLowerCase() @@ -13,19 +12,16 @@ function generateEmail(firstname: string, lastname: string): string { .replace(/ß/g, 'ss'); }; - // Remove any remaining special characters and convert to lowercase const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, ''); const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, ''); return `${cleanFirstname}.${cleanLastname}@sp.de`; } -// UPDATED: Validation for new employee model +// UPDATED: Validation for new employee model with employee types export function validateEmployeeData(employee: CreateEmployeeRequest): string[] { const errors: string[] = []; - // Email is now auto-generated, so no email validation needed - if (employee.password?.length < 6) { errors.push('Password must be at least 6 characters long'); } @@ -38,24 +34,70 @@ export function validateEmployeeData(employee: CreateEmployeeRequest): string[] errors.push('Last name is required and must be at least 2 characters long'); } + // Validate employee type + const validEmployeeTypes = ['manager', 'personell', 'apprentice', 'guest']; + if (!employee.employeeType || !validEmployeeTypes.includes(employee.employeeType)) { + errors.push(`Employee type must be one of: ${validEmployeeTypes.join(', ')}`); + } + + // Validate contract type based on employee type + if (employee.employeeType !== 'guest') { + // Internal types require contract type + if (!employee.contractType) { + errors.push(`Contract type is required for employee type: ${employee.employeeType}`); + } else { + const validContractTypes = ['small', 'large', 'flexible']; + if (!validContractTypes.includes(employee.contractType)) { + errors.push(`Contract type must be one of: ${validContractTypes.join(', ')}`); + } + } + } else { + // External types (guest) should not have contract type + if (employee.contractType) { + errors.push('Contract type is not allowed for guest employees'); + } + } + + // Validate isTrainee - only applicable for personell type + if (employee.isTrainee && employee.employeeType !== 'personell') { + errors.push('isTrainee is only allowed for personell employee type'); + } + return errors; } -// Generate email for employee (new helper function) +// Generate email for employee export function generateEmployeeEmail(firstname: string, lastname: string): string { return generateEmail(firstname, lastname); } -// Simplified business logic helpers +// UPDATED: Business logic helpers for new employee types export const isManager = (employee: Employee): boolean => employee.employeeType === 'manager'; +export const isPersonell = (employee: Employee): boolean => + employee.employeeType === 'personell'; + +export const isApprentice = (employee: Employee): boolean => + employee.employeeType === 'apprentice'; + +export const isGuest = (employee: Employee): boolean => + employee.employeeType === 'guest'; + +export const isInternal = (employee: Employee): boolean => + ['manager', 'personell', 'apprentice'].includes(employee.employeeType); + +export const isExternal = (employee: Employee): boolean => + employee.employeeType === 'guest'; + +// UPDATED: Trainee logic - now based on isTrainee field for personell type export const isTrainee = (employee: Employee): boolean => - employee.employeeType === 'trainee'; + employee.employeeType === 'personell' && employee.isTrainee; export const isExperienced = (employee: Employee): boolean => - employee.employeeType === 'experienced'; + employee.employeeType === 'personell' && !employee.isTrainee; +// Role-based helpers export const isAdmin = (employee: Employee): boolean => employee.roles?.includes('admin') || false; @@ -65,13 +107,11 @@ export const isMaintenance = (employee: Employee): boolean => export const isUser = (employee: Employee): boolean => employee.roles?.includes('user') || false; +// UPDATED: Work alone permission - managers and experienced personell can work alone export const canEmployeeWorkAlone = (employee: Employee): boolean => - employee.canWorkAlone && isExperienced(employee); + employee.canWorkAlone && (isManager(employee) || isExperienced(employee)); -export const getEmployeeWorkHours = (employee: Employee): number => - isManager(employee) ? 999 : (employee.contractType === 'small' ? 1 : 2); - -// New helper for full name display +// Helper for full name display export const getFullName = (employee: { firstname: string; lastname: string }): string => `${employee.firstname} ${employee.lastname}`; @@ -92,4 +132,14 @@ export function validateAvailabilityData(availability: Omit { + return isInternal(employee) ? 'internal' : 'external'; +}; + +// Helper to check if employee requires contract type +export const requiresContractType = (employee: Employee): boolean => { + return isInternal(employee); +}; \ No newline at end of file diff --git a/backend/src/models/scheduling.ts b/backend/src/models/scheduling.ts index 5943cc1..b8a9c14 100644 --- a/backend/src/models/scheduling.ts +++ b/backend/src/models/scheduling.ts @@ -7,10 +7,10 @@ export interface Availability { id: string; employeeId: string; planId: 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 + shiftId: string; + preferenceLevel: 1 | 2 | 3; notes?: string; - // Optional convenience fields (can be joined from shifts and time_slots tables) + // Optional convenience fields dayOfWeek?: number; timeSlotId?: string; timeSlotName?: string; @@ -30,7 +30,7 @@ export interface Constraint { maxHoursPerWeek?: number; [key: string]: any; }; - weight?: number; // For soft constraints + weight?: number; } export interface ScheduleRequest { @@ -153,8 +153,9 @@ export interface AvailabilityWithDetails extends Availability { id: string; firstname: string; lastname: string; - employeeType: 'manager' | 'trainee' | 'experienced'; + employeeType: 'manager' | 'personell' | 'apprentice' | 'guest'; canWorkAlone: boolean; + isTrainee: boolean; }; shift?: { dayOfWeek: number; diff --git a/backend/src/routes/employees.ts b/backend/src/routes/employees.ts index d458c47..c970b94 100644 --- a/backend/src/routes/employees.ts +++ b/backend/src/routes/employees.ts @@ -9,7 +9,8 @@ import { deleteEmployee, getAvailabilities, updateAvailabilities, - changePassword + changePassword, + updateLastLogin } from '../controllers/employeeController.js'; const router = express.Router(); @@ -18,12 +19,13 @@ const router = express.Router(); router.use(authMiddleware); // Employee CRUD Routes -router.get('/', authMiddleware, getEmployees); +router.get('/', requireRole(['admin']), getEmployees); router.get('/:id', requireRole(['admin', 'instandhalter']), getEmployee); router.post('/', requireRole(['admin']), createEmployee); router.put('/:id', requireRole(['admin']), updateEmployee); router.delete('/:id', requireRole(['admin']), deleteEmployee); router.put('/:id/password', authMiddleware, changePassword); +router.put('/:id/last-login', authMiddleware, updateLastLogin); // Availability Routes router.get('/:employeeId/availabilities', authMiddleware, getAvailabilities); diff --git a/backend/src/scripts/applyMigration.ts b/backend/src/scripts/applyMigration.ts index 1efeacc..ef6832e 100644 --- a/backend/src/scripts/applyMigration.ts +++ b/backend/src/scripts/applyMigration.ts @@ -6,6 +6,25 @@ import { dirname, join } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +// Define interfaces for type safety +interface TableColumnInfo { + cid: number; + name: string; + type: string; + notnull: number; + dflt_value: string | null; + pk: number; +} + +interface CountResult { + count: number; +} + +interface EmployeeRecord { + id: string; + // Note: roles column doesn't exist in new schema +} + // Helper function to ensure migrations are tracked async function ensureMigrationTable() { await db.exec(` @@ -19,7 +38,7 @@ async function ensureMigrationTable() { // Helper function to check if a migration has been applied async function isMigrationApplied(migrationName: string): Promise { - const result = await db.get<{ count: number }>( + const result = await db.get( 'SELECT COUNT(*) as count FROM applied_migrations WHERE name = ?', [migrationName] ); @@ -34,6 +53,85 @@ async function markMigrationAsApplied(migrationName: string) { ); } +// UPDATED: Function to handle schema changes for the new employee type system +async function applySchemaUpdates() { + console.log('🔄 Applying schema updates for new employee type system...'); + + await db.run('BEGIN TRANSACTION'); + + try { + // 1. Create employee_types table if it doesn't exist + await db.exec(` + CREATE TABLE IF NOT EXISTS employee_types ( + type TEXT PRIMARY KEY, + category TEXT CHECK(category IN ('internal', 'external')) NOT NULL, + has_contract_type BOOLEAN NOT NULL DEFAULT FALSE + ) + `); + + // 2. Insert default employee types + await db.run(`INSERT OR IGNORE INTO employee_types (type, category, has_contract_type) VALUES ('manager', 'internal', 1)`); + await db.run(`INSERT OR IGNORE INTO employee_types (type, category, has_contract_type) VALUES ('personell', 'internal', 1)`); + await db.run(`INSERT OR IGNORE INTO employee_types (type, category, has_contract_type) VALUES ('apprentice', 'internal', 1)`); + await db.run(`INSERT OR IGNORE INTO employee_types (type, category, has_contract_type) VALUES ('guest', 'external', 0)`); + + // 3. Check if employees table needs to be altered + const employeesTableInfo = await db.all(` + PRAGMA table_info(employees) + `); + + // FIXED: Check for employee_type column (not roles column) + const hasEmployeeType = employeesTableInfo.some((col: TableColumnInfo) => col.name === 'employee_type'); + const hasIsTrainee = employeesTableInfo.some((col: TableColumnInfo) => col.name === 'is_trainee'); + + if (!hasEmployeeType) { + console.log('🔄 Adding employee_type column to employees table...'); + await db.run('ALTER TABLE employees ADD COLUMN employee_type TEXT NOT NULL DEFAULT "personell"'); + } + + if (!hasIsTrainee) { + console.log('🔄 Adding is_trainee column to employees table...'); + await db.run('ALTER TABLE employees ADD COLUMN is_trainee BOOLEAN DEFAULT FALSE NOT NULL'); + } + + // 4. Create roles table if it doesn't exist + await db.exec(` + CREATE TABLE IF NOT EXISTS roles ( + role TEXT PRIMARY KEY CHECK(role IN ('admin', 'user', 'maintenance')), + authority_level INTEGER NOT NULL UNIQUE CHECK(authority_level BETWEEN 1 AND 100), + description TEXT + ) + `); + + // 5. Insert default roles + await db.run(`INSERT OR IGNORE INTO roles (role, authority_level, description) VALUES ('admin', 100, 'Vollzugriff')`); + await db.run(`INSERT OR IGNORE INTO roles (role, authority_level, description) VALUES ('maintenance', 50, 'Wartungszugriff')`); + await db.run(`INSERT OR IGNORE INTO roles (role, authority_level, description) VALUES ('user', 10, 'Standardbenutzer')`); + + // 6. Create employee_roles junction table if it doesn't exist + await db.exec(` + 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) + ) + `); + + // 7. REMOVED: Role migration logic since roles column doesn't exist in new schema + // In a fresh installation, we don't need to migrate existing roles + + console.log('ℹ️ Skipping role migration - fresh installation with new schema'); + + await db.run('COMMIT'); + console.log('✅ Schema updates applied successfully'); + + } catch (error) { + await db.run('ROLLBACK'); + console.error('❌ Schema updates failed:', error); + throw error; + } +} + export async function applyMigration() { try { console.log('📦 Starting database migration...'); @@ -41,60 +139,68 @@ export async function applyMigration() { // Ensure migration tracking table exists await ensureMigrationTable(); + // Apply schema updates for new employee type system + await applySchemaUpdates(); + // Get all migration files const migrationsDir = join(__dirname, '../database/migrations'); - const files = await readdir(migrationsDir); - // Sort files to ensure consistent order - const migrationFiles = files - .filter(f => f.endsWith('.sql')) - .sort(); - - // Process each migration file - for (const migrationFile of migrationFiles) { - if (await isMigrationApplied(migrationFile)) { - console.log(`ℹ️ Migration ${migrationFile} already applied, skipping...`); - continue; - } + try { + const files = await readdir(migrationsDir); - console.log(`📄 Applying migration: ${migrationFile}`); - const migrationPath = join(migrationsDir, migrationFile); - const migrationSQL = await readFile(migrationPath, 'utf-8'); + // Sort files to ensure consistent order + const migrationFiles = files + .filter(f => f.endsWith('.sql')) + .sort(); - // Split into individual statements - const statements = migrationSQL - .split(';') - .map(s => s.trim()) - .filter(s => s.length > 0); - - // Start transaction for this migration - await db.run('BEGIN TRANSACTION'); - - try { - // Execute each statement - for (const statement of statements) { - try { - await db.exec(statement); - console.log('✅ Executed:', statement.slice(0, 50) + '...'); - } catch (error) { - const err = error as { code: string; message: string }; - if (err.code === 'SQLITE_ERROR' && err.message.includes('duplicate column name')) { - console.log('ℹ️ Column already exists, skipping...'); - continue; - } - throw error; - } + // Process each migration file + for (const migrationFile of migrationFiles) { + if (await isMigrationApplied(migrationFile)) { + console.log(`ℹ️ Migration ${migrationFile} already applied, skipping...`); + continue; } - // Mark migration as applied - await markMigrationAsApplied(migrationFile); - await db.run('COMMIT'); - console.log(`✅ Migration ${migrationFile} applied successfully`); + console.log(`📄 Applying migration: ${migrationFile}`); + const migrationPath = join(migrationsDir, migrationFile); + const migrationSQL = await readFile(migrationPath, 'utf-8'); - } catch (error) { - await db.run('ROLLBACK'); - throw error; + // Split into individual statements + const statements = migrationSQL + .split(';') + .map(s => s.trim()) + .filter(s => s.length > 0); + + // Start transaction for this migration + await db.run('BEGIN TRANSACTION'); + + try { + // Execute each statement + for (const statement of statements) { + try { + await db.exec(statement); + console.log('✅ Executed:', statement.slice(0, 50) + '...'); + } catch (error) { + const err = error as { code: string; message: string }; + if (err.code === 'SQLITE_ERROR' && err.message.includes('duplicate column name')) { + console.log('ℹ️ Column already exists, skipping...'); + continue; + } + throw error; + } + } + + // Mark migration as applied + await markMigrationAsApplied(migrationFile); + await db.run('COMMIT'); + console.log(`✅ Migration ${migrationFile} applied successfully`); + + } catch (error) { + await db.run('ROLLBACK'); + throw error; + } } + } catch (error) { + console.log('ℹ️ No migration directory found or no migration files, skipping file-based migrations...'); } console.log('✅ All migrations completed successfully'); diff --git a/backend/src/scripts/initializeDatabase.ts b/backend/src/scripts/initializeDatabase.ts index 7901873..4ef1eb1 100644 --- a/backend/src/scripts/initializeDatabase.ts +++ b/backend/src/scripts/initializeDatabase.ts @@ -42,7 +42,7 @@ export async function initializeDatabase(): Promise { console.log('Existing tables found:', existingTables.map(t => t.name).join(', ') || 'none'); - // UPDATED: Drop tables in correct dependency order + // UPDATED: Drop tables in correct dependency order for new schema const tablesToDrop = [ 'employee_availability', 'shift_assignments', @@ -50,9 +50,10 @@ export async function initializeDatabase(): Promise { 'shifts', 'time_slots', 'employee_roles', - 'shift_plans', 'roles', 'employees', + 'employee_types', + 'shift_plans', 'applied_migrations' ]; @@ -100,15 +101,22 @@ export async function initializeDatabase(): Promise { } } - // UPDATED: Insert default roles after creating the tables + // UPDATED: Insert default data in correct order try { + console.log('Inserting default employee types...'); + await db.run(`INSERT OR IGNORE INTO employee_types (type, category, has_contract_type) VALUES ('manager', 'internal', 1)`); + await db.run(`INSERT OR IGNORE INTO employee_types (type, category, has_contract_type) VALUES ('personell', 'internal', 1)`); + await db.run(`INSERT OR IGNORE INTO employee_types (type, category, has_contract_type) VALUES ('apprentice', 'internal', 1)`); + await db.run(`INSERT OR IGNORE INTO employee_types (type, category, has_contract_type) VALUES ('guest', 'external', 0)`); + console.log('✅ Default employee types inserted'); + console.log('Inserting default roles...'); - await db.run(`INSERT OR IGNORE INTO roles (role) VALUES ('admin')`); - await db.run(`INSERT OR IGNORE INTO roles (role) VALUES ('user')`); - await db.run(`INSERT OR IGNORE INTO roles (role) VALUES ('maintenance')`); + await db.run(`INSERT OR IGNORE INTO roles (role, authority_level, description) VALUES ('admin', 100, 'Vollzugriff')`); + await db.run(`INSERT OR IGNORE INTO roles (role, authority_level, description) VALUES ('maintenance', 50, 'Wartungszugriff')`); + await db.run(`INSERT OR IGNORE INTO roles (role, authority_level, description) VALUES ('user', 10, 'Standardbenutzer')`); console.log('✅ Default roles inserted'); } catch (error) { - console.error('Error inserting default roles:', error); + console.error('Error inserting default data:', error); await db.run('ROLLBACK'); throw error; } diff --git a/backend/src/services/SchedulingService.ts b/backend/src/services/SchedulingService.ts index 15c8d6a..e058eb1 100644 --- a/backend/src/services/SchedulingService.ts +++ b/backend/src/services/SchedulingService.ts @@ -97,16 +97,17 @@ export class SchedulingService { 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 dayOfWeek = currentDate.getDay() === 0 ? 7 : currentDate.getDay(); 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}`; + // ✅ CRITICAL FIX: Use consistent shift ID format that matches availability lookup + const shiftId = `shift_${dayOfWeek}_${shift.timeSlotId}`; + const dateStr = currentDate.toISOString().split('T')[0]; shifts.push({ - id: shiftId, // This matches what frontend expects - date: currentDate.toISOString().split('T')[0], + id: shiftId, // This will match what availabilities are looking for + date: dateStr, timeSlotId: shift.timeSlotId, requiredEmployees: shift.requiredEmployees, minWorkers: 1, @@ -114,7 +115,7 @@ export class SchedulingService { isPriority: false }); - console.log(`✅ Generated shift: ${shiftId} for day ${dayOfWeek}, timeSlot ${shift.timeSlotId}`); + console.log(`✅ Generated shift: ${shiftId} for date ${dateStr}, day ${dayOfWeek}, timeSlot ${shift.timeSlotId}`); }); } diff --git a/backend/src/workers/scheduler-worker.ts b/backend/src/workers/scheduler-worker.ts index f46bebb..60a1112 100644 --- a/backend/src/workers/scheduler-worker.ts +++ b/backend/src/workers/scheduler-worker.ts @@ -17,17 +17,42 @@ interface WorkerData { 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 = nonManagerEmployees.filter(emp => emp.employeeType === 'trainee'); - const experienced = nonManagerEmployees.filter(emp => emp.employeeType === 'experienced'); + const schedulableEmployees = employees.filter(emp => + emp.isActive && + emp.employeeType === 'personell' + ); - console.log(`Building model with ${nonManagerEmployees.length} employees, ${shifts.length} shifts`); - console.log(`Available shifts per week: ${shifts.length}`); + // Debug: Count constraints that will be created + console.log('\n🔧 CONSTRAINT ANALYSIS:'); + let hardConstraints = 0; + let availabilityConstraints = 0; + + // Count availability constraints + schedulableEmployees.forEach((employee: any) => { + shifts.forEach((shift: any) => { + const availability = availabilities.find( + (a: any) => a.employeeId === employee.id && a.shiftId === shift.id + ); + + if (!availability || availability.preferenceLevel === 3) { + availabilityConstraints++; + } + }); + }); + + console.log(`Availability constraints to create: ${availabilityConstraints}`); + console.log(`Total possible assignments: ${schedulableEmployees.length * shifts.length}`); + + const trainees = schedulableEmployees.filter(emp => emp.isTrainee); + const experienced = schedulableEmployees.filter(emp => !emp.isTrainee); + + console.log(`Building model with ${schedulableEmployees.length} schedulable employees, ${shifts.length} shifts`); + console.log(`- Trainees: ${trainees.length}`); + console.log(`- Experienced: ${experienced.length}`); + console.log(`- Excluded: ${employees.filter(emp => !emp.isActive || emp.employeeType !== 'personell').length} employees (managers, apprentices, guests, inactive)`); // 1. Create assignment variables for all possible assignments - nonManagerEmployees.forEach((employee: any) => { + schedulableEmployees.forEach((employee: any) => { shifts.forEach((shift: any) => { const varName = `assign_${employee.id}_${shift.id}`; model.addVariable(varName, 'bool'); @@ -35,18 +60,18 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void { }); // 2. Availability constraints - nonManagerEmployees.forEach((employee: any) => { + schedulableEmployees.forEach((employee: any) => { shifts.forEach((shift: any) => { const availability = availabilities.find( (a: any) => a.employeeId === employee.id && a.shiftId === shift.id ); - // Hard constraint: never assign when preference level is 3 (unavailable) - if (availability?.preferenceLevel === 3) { - const varName = `assign_${employee.id}_${shift.id}`; + const varName = `assign_${employee.id}_${shift.id}`; + + if (!availability || availability.preferenceLevel === 3) { model.addConstraint( `${varName} == 0`, - `Hard availability constraint for ${employee.name} in shift ${shift.id}` + `Employee ${employee.firstname} ${employee.lastname} has not signed up for shift ${shift.id}` ); } }); @@ -54,7 +79,7 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void { // 3. Max 1 shift per day per employee const shiftsByDate = groupShiftsByDate(shifts); - nonManagerEmployees.forEach((employee: any) => { + schedulableEmployees.forEach((employee: any) => { Object.entries(shiftsByDate).forEach(([date, dayShifts]) => { const dayAssignmentVars = (dayShifts as any[]).map( (shift: any) => `assign_${employee.id}_${shift.id}` @@ -71,7 +96,7 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void { // 4. Shift staffing constraints shifts.forEach((shift: any) => { - const assignmentVars = nonManagerEmployees.map( + const assignmentVars = schedulableEmployees.map( (emp: any) => `assign_${emp.id}_${shift.id}` ); @@ -117,13 +142,13 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void { }); // 6. Employees who cannot work alone constraint - const employeesWhoCantWorkAlone = nonManagerEmployees.filter(emp => !emp.canWorkAlone); + const employeesWhoCantWorkAlone = schedulableEmployees.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 => + const otherEmployees = schedulableEmployees.filter(emp => emp.id !== employee.id && emp.isActive ); @@ -151,7 +176,7 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void { const totalShifts = shifts.length; console.log(`Total available shifts: ${totalShifts}`); - nonManagerEmployees.forEach((employee: any) => { + schedulableEmployees.forEach((employee: any) => { const contractType = employee.contractType || 'large'; // EXACT SHIFTS PER WEEK @@ -181,8 +206,8 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void { // 8. Objective: Maximize preferred assignments with soft constraints let objectiveExpression = ''; let softConstraintPenalty = ''; - - nonManagerEmployees.forEach((employee: any) => { + + schedulableEmployees.forEach((employee: any) => { shifts.forEach((shift: any) => { const varName = `assign_${employee.id}_${shift.id}`; const availability = availabilities.find( @@ -191,25 +216,28 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void { let score = 0; if (availability) { - score = availability.preferenceLevel === 1 ? 10 : - availability.preferenceLevel === 2 ? 5 : - -10000; // Very heavy penalty for unavailable - } else { - // No availability info - slight preference to assign - score = 1; + // Only give positive scores for shifts employees actually signed up for + if (availability.preferenceLevel === 1) { + score = 100; // High reward for preferred shifts + } else if (availability.preferenceLevel === 2) { + score = 50; // Medium reward for available shifts + } } - - if (objectiveExpression) { - objectiveExpression += ` + ${score} * ${varName}`; - } else { - objectiveExpression = `${score} * ${varName}`; + if (score > 0) { + if (objectiveExpression) { + objectiveExpression += ` + ${score} * ${varName}`; + } else { + objectiveExpression = `${score} * ${varName}`; + } } }); }); - + if (objectiveExpression) { model.maximize(objectiveExpression); - console.log('Objective function set with preference optimization'); + console.log('Objective function set with strict availability enforcement'); + } else { + console.warn('No valid objective expression could be created'); } } @@ -282,12 +310,12 @@ function detectViolations(assignments: any, employees: any[], shifts: any[]): st const assignedEmployees = assignments[shift.id] || []; const hasTrainee = assignedEmployees.some((empId: string) => { const emp = employeeMap.get(empId); - return emp?.employeeType === 'trainee'; + return emp?.isTrainee; }); const hasExperienced = assignedEmployees.some((empId: string) => { const emp = employeeMap.get(empId); - return emp?.employeeType === 'experienced'; + return emp && !emp.isTrainee; }); if (hasTrainee && !hasExperienced) { @@ -330,9 +358,13 @@ function detectViolations(assignments: any, employees: any[], shifts: any[]): st } function assignManagersToShifts(assignments: any, managers: any[], shifts: any[], availabilities: any[]): any { - const managersToAssign = managers.filter(emp => emp.isActive && emp.employeeType === 'manager'); + const managersToAssign = managers.filter(emp => + emp.isActive && + emp.employeeType === 'manager' + ); console.log(`Assigning ${managersToAssign.length} managers to shifts based on availability=1`); + console.log(`- Excluded ${managers.filter(emp => !emp.isActive || emp.employeeType !== 'manager').length} non-manager employees from auto-assignment`); managersToAssign.forEach((manager: any) => { shifts.forEach((shift: any) => { @@ -349,7 +381,7 @@ function assignManagersToShifts(assignments: any, managers: any[], shifts: any[] // 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)`); + console.log(`✅ Assigned manager ${manager.firstname} ${manager.lastname} to shift ${shift.id} (availability=1)`); } } }); @@ -375,8 +407,110 @@ async function runScheduling() { console.log(`Optimizing ${data.shifts.length} shifts for ${data.employees.length} employees`); + // 🆕 COMPREHENSIVE AVAILABILITY DEBUGGING + console.log('\n🔍 ===== AVAILABILITY ANALYSIS ====='); + console.log(`Total shifts: ${data.shifts.length}`); + console.log(`Total availability records: ${data.availabilities.length}`); + + // Count by preference level + const pref1Count = data.availabilities.filter(a => a.preferenceLevel === 1).length; + const pref2Count = data.availabilities.filter(a => a.preferenceLevel === 2).length; + const pref3Count = data.availabilities.filter(a => a.preferenceLevel === 3).length; + + console.log(`Preference Level 1 (Preferred): ${pref1Count}`); + console.log(`Preference Level 2 (Available): ${pref2Count}`); + console.log(`Preference Level 3 (Unavailable): ${pref3Count}`); + + // Analyze each shift + console.log('\n📊 AVAILABILITIES PER SHIFT:'); + data.shifts.forEach((shift, index) => { + const shiftAvailabilities = data.availabilities.filter(avail => avail.shiftId === shift.id); + const pref1 = shiftAvailabilities.filter(a => a.preferenceLevel === 1).length; + const pref2 = shiftAvailabilities.filter(a => a.preferenceLevel === 2).length; + const pref3 = shiftAvailabilities.filter(a => a.preferenceLevel === 3).length; + + console.log(`Shift ${index + 1}: ${shift.id}`); + console.log(` 📅 Date: ${shift.dayOfWeek}, TimeSlot: ${shift.timeSlotId}`); + console.log(` 👥 Required: ${shift.requiredEmployees}`); + console.log(` ✅ Preferred (1): ${pref1}`); + console.log(` 🔶 Available (2): ${pref2}`); + console.log(` ❌ Unavailable (3): ${pref3}`); + console.log(` 📋 Total signed up: ${pref1 + pref2}`); + + // Show employee names for each preference level + if (pref1 > 0) { + const pref1Employees = shiftAvailabilities + .filter(a => a.preferenceLevel === 1) + .map(a => { + const emp = data.employees.find(e => e.id === a.employeeId); + return emp ? `${emp.firstname} ${emp.lastname}` : 'Unknown'; + }); + console.log(` Preferred employees: ${pref1Employees.join(', ')}`); + } + + if (pref2 > 0) { + const pref2Employees = shiftAvailabilities + .filter(a => a.preferenceLevel === 2) + .map(a => { + const emp = data.employees.find(e => e.id === a.employeeId); + return emp ? `${emp.firstname} ${emp.lastname}` : 'Unknown'; + }); + console.log(` Available employees: ${pref2Employees.join(', ')}`); + } + }); + + // Employee-level analysis + console.log('\n👤 EMPLOYEE SIGNUP SUMMARY:'); + const schedulableEmployees = data.employees.filter(emp => + emp.isActive && emp.employeeType === 'personell' + ); + + schedulableEmployees.forEach(employee => { + const employeeAvailabilities = data.availabilities.filter(avail => avail.employeeId === employee.id); + const pref1 = employeeAvailabilities.filter(a => a.preferenceLevel === 1).length; + const pref2 = employeeAvailabilities.filter(a => a.preferenceLevel === 2).length; + const pref3 = employeeAvailabilities.filter(a => a.preferenceLevel === 3).length; + + console.log(`${employee.firstname} ${employee.lastname} (${employee.contractType}):`); + console.log(` ✅ Preferred: ${pref1} shifts, 🔶 Available: ${pref2} shifts, ❌ Unavailable: ${pref3} shifts`); + }); + + // Check shift ID matching + console.log('\n🔍 SHIFT ID MATCHING ANALYSIS:'); + const shiftsWithNoSignups = data.shifts.filter(shift => { + const shiftAvailabilities = data.availabilities.filter(avail => avail.shiftId === shift.id); + const signedUp = shiftAvailabilities.filter(a => a.preferenceLevel === 1 || a.preferenceLevel === 2); + return signedUp.length === 0; + }); + + console.log(`Shifts with NO signups: ${shiftsWithNoSignups.length}/${data.shifts.length}`); + shiftsWithNoSignups.forEach(shift => { + console.log(` ❌ No signups: ${shift.id} (${shift.dayOfWeek}, ${shift.timeSlotId})`); + }); + + console.log('===== END AVAILABILITY ANALYSIS =====\n'); + const nonManagerEmployees = data.employees.filter(emp => emp.isActive && emp.employeeType !== 'manager'); + // Check if we have any employees who signed up for shifts + const employeesWithSignups = new Set( + data.availabilities + .filter(avail => avail.preferenceLevel === 1 || avail.preferenceLevel === 2) + .map(avail => avail.employeeId) + ); + + if (employeesWithSignups.size === 0) { + console.log('❌ CRITICAL: No employees have signed up for any shifts!'); + parentPort?.postMessage({ + assignments: {}, + violations: ['NO_SIGNUPS: No employees have signed up for any shifts with preference level 1 or 2'], + success: false, + resolutionReport: ['❌ Scheduling failed: No employees have signed up for any shifts'], + processingTime: Date.now() - startTime + }); + return; + } + const model = new CPModel(); buildSchedulingModel(model, data); @@ -406,7 +540,6 @@ async function runScheduling() { // Extract assignments from solution (non-managers only) assignments = extractAssignmentsFromSolution(solution, nonManagerEmployees, data.shifts); - // 🆕 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 @@ -416,7 +549,6 @@ async function runScheduling() { violations.push('NO_ASSIGNMENTS: Solver reported success but produced no assignments'); } - // Update resolution report if (violations.length === 0) { resolutionReport.push('✅ No constraint violations detected for non-manager employees'); } else { diff --git a/frontend/src/components/Scheduler.tsx b/frontend/src/components/Scheduler.tsx deleted file mode 100644 index 8013794..0000000 --- a/frontend/src/components/Scheduler.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import React from 'react'; -import { useScheduling } from '../services/scheduling/useScheduling'; -import { ScheduleRequest } from '../models/scheduling'; - -interface SchedulerProps { - scheduleRequest: ScheduleRequest; - onScheduleGenerated?: (result: any) => void; -} - -export const Scheduler: React.FC = ({ - scheduleRequest, - onScheduleGenerated -}) => { - const { generateSchedule, loading, error, result } = useScheduling(); - - const handleGenerateSchedule = async () => { - try { - const scheduleResult = await generateSchedule(scheduleRequest); - if (onScheduleGenerated) { - onScheduleGenerated(scheduleResult); - } - } catch (err) { - console.error('Scheduling failed:', err); - } - }; - - return ( -
-

Automatic Schedule Generation

- - - - {loading && ( -
-
-
-
-

- Optimizing schedule... (max 2 minutes) -

-
- )} - - {error && ( -
- Error: {error} -
- )} - - {result && ( -
- -
- )} -
- ); -}; - -const ScheduleResultView: React.FC<{ result: any }> = ({ result }) => { - return ( -
-

- {result.success ? '✅ Schedule Generated Successfully' : '❌ Schedule Generation Failed'} -

- -
- Assignments: {Object.keys(result.assignments || {}).length} shifts assigned -
- -
- Violations: {result.violations?.length || 0} -
- - {result.resolution_report && result.resolution_report.length > 0 && ( -
- - Resolution Report - -
- {result.resolution_report.map((line: string, index: number) => ( -
{line}
- ))} -
-
- )} -
- ); -}; - -export default Scheduler; \ No newline at end of file diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index 9b5c59b..61eac0d 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -165,7 +165,7 @@ export const AuthProvider: React.FC = ({ children }) => { refreshUser, needsSetup: needsSetup === null ? true : needsSetup, checkSetupStatus, - updateUser, // Add this to the context value + updateUser, }; return ( diff --git a/frontend/src/models/Employee.ts b/frontend/src/models/Employee.ts index 7833780..6cfcb29 100644 --- a/frontend/src/models/Employee.ts +++ b/frontend/src/models/Employee.ts @@ -4,10 +4,11 @@ export interface Employee { email: string; firstname: string; lastname: string; - employeeType: 'manager' | 'trainee' | 'experienced'; - contractType: 'small' | 'large'; + employeeType: 'manager' | 'personell' | 'apprentice' | 'guest'; + contractType?: 'small' | 'large' | 'flexible'; canWorkAlone: boolean; isActive: boolean; + isTrainee: boolean; createdAt: string; lastLogin?: string | null; roles?: string[]; @@ -17,20 +18,22 @@ export interface CreateEmployeeRequest { password: string; firstname: string; lastname: string; - roles: string[]; - employeeType: 'manager' | 'trainee' | 'experienced'; - contractType: 'small' | 'large'; + roles?: string[]; + employeeType: 'manager' | 'personell' | 'apprentice' | 'guest'; + contractType?: 'small' | 'large' | 'flexible'; canWorkAlone: boolean; + isTrainee?: boolean; } export interface UpdateEmployeeRequest { firstname?: string; lastname?: string; roles?: string[]; - employeeType?: 'manager' | 'trainee' | 'experienced'; - contractType?: 'small' | 'large'; + employeeType?: 'manager' | 'personell' | 'apprentice' | 'guest'; + contractType?: 'small' | 'large' | 'flexible'; canWorkAlone?: boolean; isActive?: boolean; + isTrainee?: boolean; } export interface EmployeeWithPassword extends Employee { @@ -41,7 +44,7 @@ export interface EmployeeAvailability { id: string; employeeId: string; planId: string; - shiftId: string; // Now references shift_id instead of time_slot_id + day_of_week + shiftId: string; preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable notes?: string; } @@ -83,4 +86,11 @@ export interface Role { export interface EmployeeRole { employeeId: string; role: 'admin' | 'user' | 'maintenance'; +} + +// Employee type configuration +export interface EmployeeType { + type: 'manager' | 'personell' | 'apprentice' | 'guest'; + category: 'internal' | 'external'; + has_contract_type: boolean; } \ No newline at end of file diff --git a/frontend/src/models/defaults/employeeDefaults.ts b/frontend/src/models/defaults/employeeDefaults.ts index 7a61cae..e80e189 100644 --- a/frontend/src/models/defaults/employeeDefaults.ts +++ b/frontend/src/models/defaults/employeeDefaults.ts @@ -4,19 +4,41 @@ import { EmployeeAvailability, ManagerAvailability } from '../Employee.js'; // Default employee data for quick creation export const EMPLOYEE_DEFAULTS = { role: 'user' as const, - employeeType: 'experienced' as const, + employeeType: 'personell' as const, contractType: 'small' as const, canWorkAlone: false, - isActive: true + isActive: true, + isTrainee: false }; // Manager-specific defaults export const MANAGER_DEFAULTS = { role: 'admin' as const, employeeType: 'manager' as const, - contractType: 'large' as const, + contractType: 'flexible' as const, canWorkAlone: true, - isActive: true + isActive: true, + isTrainee: false +}; + +// Apprentice defaults +export const APPRENTICE_DEFAULTS = { + role: 'user' as const, + employeeType: 'apprentice' as const, + contractType: 'flexible' as const, + canWorkAlone: false, + isActive: true, + isTrainee: false +}; + +// Guest defaults +export const GUEST_DEFAULTS = { + role: 'user' as const, + employeeType: 'guest' as const, + contractType: undefined, + canWorkAlone: false, + isActive: true, + isTrainee: false }; export const EMPLOYEE_TYPE_CONFIG = { @@ -24,22 +46,37 @@ export const EMPLOYEE_TYPE_CONFIG = { value: 'manager' as const, label: 'Chef/Administrator', color: '#e74c3c', + category: 'internal' as const, + hasContractType: true, independent: true, description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung' }, - experienced: { - value: 'experienced' as const, - label: 'Erfahren', + personell: { + value: 'personell' as const, + label: 'Personal', color: '#3498db', + category: 'internal' as const, + hasContractType: true, independent: true, - description: 'Langjährige Erfahrung, kann komplexe Aufgaben übernehmen' + description: 'Reguläre Mitarbeiter mit Vertrag' }, - trainee: { - value: 'trainee' as const, - label: 'Neuling', - color: '#27ae60', + apprentice: { + value: 'apprentice' as const, + label: 'Auszubildender', + color: '#9b59b6', + category: 'internal' as const, + hasContractType: true, independent: false, - description: 'Benötigt Einarbeitung und Unterstützung' + description: 'Auszubildende mit flexiblem Vertrag' + }, + guest: { + value: 'guest' as const, + label: 'Gast', + color: '#95a5a6', + category: 'external' as const, + hasContractType: false, + independent: false, + description: 'Externe Mitarbeiter ohne Vertrag' } } as const; @@ -53,9 +90,10 @@ export const ROLE_CONFIG = [ export const CONTRACT_TYPE_DESCRIPTIONS = { small: '1 Schicht pro Woche', large: '2 Schichten pro Woche', - manager: 'Kein Vertragslimit - Immer MO und DI verfügbar' + flexible: 'Flexible Arbeitszeiten' } as const; + // Availability preference descriptions export const AVAILABILITY_PREFERENCES = { 1: { label: 'Bevorzugt', color: '#10b981', description: 'Möchte diese Schicht arbeiten' }, @@ -104,4 +142,17 @@ export function createManagerDefaultSchedule(managerId: string, planId: string, } return assignments; +} + +export function getDefaultsByEmployeeType(employeeType: string) { + switch (employeeType) { + case 'manager': + return MANAGER_DEFAULTS; + case 'apprentice': + return APPRENTICE_DEFAULTS; + case 'guest': + return GUEST_DEFAULTS; + default: + return EMPLOYEE_DEFAULTS; + } } \ No newline at end of file diff --git a/frontend/src/models/helpers/employeeHelpers.ts b/frontend/src/models/helpers/employeeHelpers.ts index 5b55f12..63b4f45 100644 --- a/frontend/src/models/helpers/employeeHelpers.ts +++ b/frontend/src/models/helpers/employeeHelpers.ts @@ -1,9 +1,8 @@ // backend/src/models/helpers/employeeHelpers.ts import { Employee, CreateEmployeeRequest, EmployeeAvailability } from '../Employee.js'; -// Email generation function (same as in controllers) +// Email generation function function generateEmail(firstname: string, lastname: string): string { - // Convert German umlauts to their expanded forms const convertUmlauts = (str: string): string => { return str .toLowerCase() @@ -13,19 +12,16 @@ function generateEmail(firstname: string, lastname: string): string { .replace(/ß/g, 'ss'); }; - // Remove any remaining special characters and convert to lowercase const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, ''); const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, ''); return `${cleanFirstname}.${cleanLastname}@sp.de`; } -// UPDATED: Validation for new employee model +// UPDATED: Validation for new employee model with employee types export function validateEmployeeData(employee: CreateEmployeeRequest): string[] { const errors: string[] = []; - // Email is now auto-generated, so no email validation needed - if (employee.password?.length < 6) { errors.push('Password must be at least 6 characters long'); } @@ -38,24 +34,70 @@ export function validateEmployeeData(employee: CreateEmployeeRequest): string[] errors.push('Last name is required and must be at least 2 characters long'); } + // Validate employee type + const validEmployeeTypes = ['manager', 'personell', 'apprentice', 'guest']; + if (!employee.employeeType || !validEmployeeTypes.includes(employee.employeeType)) { + errors.push(`Employee type must be one of: ${validEmployeeTypes.join(', ')}`); + } + + // Validate contract type based on employee type + if (employee.employeeType !== 'guest') { + // Internal types require contract type + if (!employee.contractType) { + errors.push(`Contract type is required for employee type: ${employee.employeeType}`); + } else { + const validContractTypes = ['small', 'large', 'flexible']; + if (!validContractTypes.includes(employee.contractType)) { + errors.push(`Contract type must be one of: ${validContractTypes.join(', ')}`); + } + } + } else { + // External types (guest) should not have contract type + if (employee.contractType) { + errors.push('Contract type is not allowed for guest employees'); + } + } + + // Validate isTrainee - only applicable for personell type + if (employee.isTrainee && employee.employeeType !== 'personell') { + errors.push('isTrainee is only allowed for personell employee type'); + } + return errors; } -// Generate email for employee (new helper function) +// Generate email for employee export function generateEmployeeEmail(firstname: string, lastname: string): string { return generateEmail(firstname, lastname); } -// Simplified business logic helpers +// UPDATED: Business logic helpers for new employee types export const isManager = (employee: Employee): boolean => employee.employeeType === 'manager'; +export const isPersonell = (employee: Employee): boolean => + employee.employeeType === 'personell'; + +export const isApprentice = (employee: Employee): boolean => + employee.employeeType === 'apprentice'; + +export const isGuest = (employee: Employee): boolean => + employee.employeeType === 'guest'; + +export const isInternal = (employee: Employee): boolean => + ['manager', 'personell', 'apprentice'].includes(employee.employeeType); + +export const isExternal = (employee: Employee): boolean => + employee.employeeType === 'guest'; + +// UPDATED: Trainee logic - now based on isTrainee field for personell type export const isTrainee = (employee: Employee): boolean => - employee.employeeType === 'trainee'; + employee.employeeType === 'personell' && employee.isTrainee; export const isExperienced = (employee: Employee): boolean => - employee.employeeType === 'experienced'; + employee.employeeType === 'personell' && !employee.isTrainee; +// Role-based helpers export const isAdmin = (employee: Employee): boolean => employee.roles?.includes('admin') || false; @@ -64,13 +106,12 @@ export const isMaintenance = (employee: Employee): boolean => export const isUser = (employee: Employee): boolean => employee.roles?.includes('user') || false; + +// UPDATED: Work alone permission - managers and experienced personell can work alone export const canEmployeeWorkAlone = (employee: Employee): boolean => - employee.canWorkAlone && isExperienced(employee); + employee.canWorkAlone && (isManager(employee) || isExperienced(employee)); -export const getEmployeeWorkHours = (employee: Employee): number => - isManager(employee) ? 999 : (employee.contractType === 'small' ? 1 : 2); - -// New helper for full name display +// Helper for full name display export const getFullName = (employee: { firstname: string; lastname: string }): string => `${employee.firstname} ${employee.lastname}`; @@ -91,4 +132,14 @@ export function validateAvailabilityData(availability: Omit { + return isInternal(employee) ? 'internal' : 'external'; +}; + +// Helper to check if employee requires contract type +export const requiresContractType = (employee: Employee): boolean => { + return isInternal(employee); +}; \ No newline at end of file diff --git a/frontend/src/models/scheduling.ts b/frontend/src/models/scheduling.ts index 5943cc1..b8a9c14 100644 --- a/frontend/src/models/scheduling.ts +++ b/frontend/src/models/scheduling.ts @@ -7,10 +7,10 @@ export interface Availability { id: string; employeeId: string; planId: 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 + shiftId: string; + preferenceLevel: 1 | 2 | 3; notes?: string; - // Optional convenience fields (can be joined from shifts and time_slots tables) + // Optional convenience fields dayOfWeek?: number; timeSlotId?: string; timeSlotName?: string; @@ -30,7 +30,7 @@ export interface Constraint { maxHoursPerWeek?: number; [key: string]: any; }; - weight?: number; // For soft constraints + weight?: number; } export interface ScheduleRequest { @@ -153,8 +153,9 @@ export interface AvailabilityWithDetails extends Availability { id: string; firstname: string; lastname: string; - employeeType: 'manager' | 'trainee' | 'experienced'; + employeeType: 'manager' | 'personell' | 'apprentice' | 'guest'; canWorkAlone: boolean; + isTrainee: boolean; }; shift?: { dayOfWeek: number; diff --git a/frontend/src/pages/Auth/Login.tsx b/frontend/src/pages/Auth/Login.tsx index ccc0513..cad7cfa 100644 --- a/frontend/src/pages/Auth/Login.tsx +++ b/frontend/src/pages/Auth/Login.tsx @@ -3,6 +3,7 @@ import React, { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../../contexts/AuthContext'; import { useNotification } from '../../contexts/NotificationContext'; +import { employeeService } from '../../services/employeeService'; const Login: React.FC = () => { const [email, setEmail] = useState(''); @@ -12,7 +13,6 @@ const Login: React.FC = () => { const { showNotification } = useNotification(); const navigate = useNavigate(); - // 🔥 NEU: Redirect wenn bereits eingeloggt useEffect(() => { if (user) { console.log('✅ User already logged in, redirecting to dashboard'); @@ -27,8 +27,7 @@ const Login: React.FC = () => { try { console.log('🔐 Attempting login for:', email); await login({ email, password }); - - // 🔥 WICHTIG: Erfolgsmeldung und Redirect + console.log('✅ Login successful, redirecting to dashboard'); showNotification({ type: 'success', diff --git a/frontend/src/pages/Dashboard/Dashboard.tsx b/frontend/src/pages/Dashboard/Dashboard.tsx index 74b8399..a246eb6 100644 --- a/frontend/src/pages/Dashboard/Dashboard.tsx +++ b/frontend/src/pages/Dashboard/Dashboard.tsx @@ -20,6 +20,7 @@ interface DashboardData { }>; teamStats: { totalEmployees: number; + personell: number; manager: number; trainee: number; experienced: number; @@ -36,6 +37,7 @@ const Dashboard: React.FC = () => { upcomingShifts: [], teamStats: { totalEmployees: 0, + personell: 0, manager: 0, trainee: 0, experienced: 0 @@ -208,11 +210,13 @@ const Dashboard: React.FC = () => { // Count by type const managerCount = employees.filter(e => e.employeeType === 'manager').length; - const traineeCount = employees.filter(e => e.employeeType === 'trainee').length; - const experiencedCount = employees.filter(e => e.employeeType === 'experienced').length; + const personellCount = employees.filter(e => e.employeeType === 'personell').length; + const traineeCount = employees.filter(e => e.isTrainee === true).length; + const experiencedCount = employees.filter(e => e.isTrainee === false).length; return { totalEmployees, + personell: personellCount, manager: managerCount, trainee: traineeCount, experienced: experiencedCount, @@ -538,7 +542,7 @@ const Dashboard: React.FC = () => {

👥 Team-Übersicht

- Mitarbeiter: + Gesamte Belegschaft: {data.teamStats.totalEmployees} @@ -550,15 +554,9 @@ const Dashboard: React.FC = () => {
- Erfahrene: + Personal: - {data.teamStats.experienced} - -
-
- Neue: - - {data.teamStats.trainee} + {data.teamStats.personell}
diff --git a/frontend/src/pages/Employees/components/AvailabilityManager.tsx b/frontend/src/pages/Employees/components/AvailabilityManager.tsx index 5090407..1331ea6 100644 --- a/frontend/src/pages/Employees/components/AvailabilityManager.tsx +++ b/frontend/src/pages/Employees/components/AvailabilityManager.tsx @@ -54,15 +54,122 @@ const AvailabilityManager: React.FC = ({ { level: 3 as AvailabilityLevel, label: 'Nicht möglich', color: '#e74c3c', bgColor: '#fadbd8', description: 'Nicht verfügbar' } ]; + // Lade initial die Schichtpläne useEffect(() => { - loadData(); + const loadInitialData = async () => { + try { + setLoading(true); + console.log('🔄 LADE INITIALDATEN FÜR MITARBEITER:', employee.id); + + // 1. Lade alle Schichtpläne + const plans = await shiftPlanService.getShiftPlans(); + console.log('✅ SCHICHTPLÄNE GELADEN:', plans.length); + setShiftPlans(plans); + + // 2. Wähle ersten verfügbaren Plan aus + if (plans.length > 0) { + const planWithShifts = plans.find(plan => + plan.shifts && plan.shifts.length > 0 && + plan.timeSlots && plan.timeSlots.length > 0 + ) || plans[0]; + + console.log('✅ ERSTER PLAN AUSGEWÄHLT:', planWithShifts.name); + setSelectedPlanId(planWithShifts.id); + } else { + setLoading(false); + } + + } catch (err: any) { + console.error('❌ FEHLER BEIM LADEN DER INITIALDATEN:', err); + setError('Daten konnten nicht geladen werden: ' + (err.message || 'Unbekannter Fehler')); + setLoading(false); + } + }; + + loadInitialData(); }, [employee.id]); + // Lade Plan-Details und Verfügbarkeiten wenn selectedPlanId sich ändert useEffect(() => { - if (selectedPlanId) { - loadSelectedPlan(); - } - }, [selectedPlanId]); + const loadPlanData = async () => { + if (!selectedPlanId) { + setLoading(false); + return; + } + + try { + setLoading(true); + console.log('🔄 LADE PLAN-DATEN FÜR:', selectedPlanId); + + // 1. Lade Schichtplan Details + const plan = await shiftPlanService.getShiftPlan(selectedPlanId); + setSelectedPlan(plan); + console.log('✅ SCHICHTPLAN DETAILS GELADEN:', { + name: plan.name, + timeSlotsCount: plan.timeSlots?.length || 0, + shiftsCount: plan.shifts?.length || 0, + usedDays: Array.from(new Set(plan.shifts?.map(s => s.dayOfWeek) || [])).sort() + }); + + // 2. Lade Verfügbarkeiten für DIESEN Mitarbeiter und DIESEN Plan + console.log('🔄 LADE VERFÜGBARKEITEN FÜR:', { + employeeId: employee.id, + planId: selectedPlanId + }); + + try { + const allAvailabilities = await employeeService.getAvailabilities(employee.id); + console.log('📋 ALLE VERFÜGBARKEITEN DES MITARBEITERS:', allAvailabilities.length); + + // Filtere nach dem aktuellen Plan UND stelle sicher, dass shiftId vorhanden ist + const planAvailabilities = allAvailabilities.filter( + avail => avail.planId === selectedPlanId && avail.shiftId + ); + + console.log('✅ VERFÜGBARKEITEN FÜR DIESEN PLAN (MIT SHIFT-ID):', planAvailabilities.length); + + // Debug: Zeige auch ungültige Einträge + const invalidAvailabilities = allAvailabilities.filter( + avail => avail.planId === selectedPlanId && !avail.shiftId + ); + if (invalidAvailabilities.length > 0) { + console.warn('⚠️ UNGÜLTIGE VERFÜGBARKEITEN (OHNE SHIFT-ID):', invalidAvailabilities.length); + invalidAvailabilities.forEach(invalid => { + console.warn(' - Ungültiger Eintrag:', invalid); + }); + } + + // Transformiere die Daten + const transformedAvailabilities: Availability[] = planAvailabilities.map(avail => ({ + ...avail, + isAvailable: avail.preferenceLevel !== 3 + })); + + setAvailabilities(transformedAvailabilities); + + // Debug: Zeige vorhandene Präferenzen + if (planAvailabilities.length > 0) { + console.log('🎯 VORHANDENE PRÄFERENZEN:'); + planAvailabilities.forEach(avail => { + const shift = plan.shifts?.find(s => s.id === avail.shiftId); + console.log(` - Shift: ${avail.shiftId} (Day: ${shift?.dayOfWeek}), Level: ${avail.preferenceLevel}`); + }); + } + } catch (availError) { + console.error('❌ FEHLER BEIM LADEN DER VERFÜGBARKEITEN:', availError); + setAvailabilities([]); + } + + } catch (err: any) { + console.error('❌ FEHLER BEIM LADEN DES SCHICHTPLANS:', err); + setError('Schichtplan konnte nicht geladen werden: ' + (err.message || 'Unbekannter Fehler')); + } finally { + setLoading(false); + } + }; + + loadPlanData(); + }, [selectedPlanId, employee.id]); const formatTime = (time: string): string => { if (!time) return '--:--'; @@ -116,73 +223,12 @@ const AvailabilityManager: React.FC = ({ return { days, shiftsByDay }; }; - const loadData = async () => { - try { - setLoading(true); - console.log('🔄 LADE DATEN FÜR MITARBEITER:', employee.id); - - // 1. Load availabilities - let existingAvailabilities: Availability[] = []; - try { - const availabilitiesData = await employeeService.getAvailabilities(employee.id); - existingAvailabilities = availabilitiesData.map(avail => ({ - ...avail, - isAvailable: avail.preferenceLevel !== 3 - })); - console.log('✅ VERFÜGBARKEITEN GELADEN:', existingAvailabilities.length); - } catch (err) { - console.log('⚠️ KEINE VERFÜGBARKEITEN GEFUNDEN ODER FEHLER:', err); - } - - // 2. Load shift plans - console.log('🔄 LADE SCHICHTPLÄNE...'); - const plans = await shiftPlanService.getShiftPlans(); - console.log('✅ SCHICHTPLÄNE GELADEN:', plans.length); - - setShiftPlans(plans); - - // 3. Select first plan with actual shifts if available - if (plans.length > 0) { - const planWithShifts = plans.find(plan => - plan.shifts && plan.shifts.length > 0 && - plan.timeSlots && plan.timeSlots.length > 0 - ) || plans[0]; - - setSelectedPlanId(planWithShifts.id); - console.log('✅ SCHICHTPLAN AUSGEWÄHLT:', planWithShifts.name); - - await loadSelectedPlan(); - } - - // 4. Set existing availabilities - setAvailabilities(existingAvailabilities); - - } catch (err: any) { - console.error('❌ FEHLER BEIM LADEN DER DATEN:', err); - setError('Daten konnten nicht geladen werden: ' + (err.message || 'Unbekannter Fehler')); - } finally { - setLoading(false); - } - }; - - const loadSelectedPlan = async () => { - try { - console.log('🔄 LADE AUSGEWÄHLTEN SCHICHTPLAN:', selectedPlanId); - const plan = await shiftPlanService.getShiftPlan(selectedPlanId); - setSelectedPlan(plan); - console.log('✅ SCHICHTPLAN GELADEN:', { - name: plan.name, - timeSlotsCount: plan.timeSlots?.length || 0, - shiftsCount: plan.shifts?.length || 0, - usedDays: Array.from(new Set(plan.shifts?.map(s => s.dayOfWeek) || [])).sort() - }); - } catch (err: any) { - console.error('❌ FEHLER BEIM LADEN DES SCHICHTPLANS:', err); - setError('Schichtplan konnte nicht geladen werden: ' + (err.message || 'Unbekannter Fehler')); - } - }; - const handleAvailabilityLevelChange = (shiftId: string, level: AvailabilityLevel) => { + if (!shiftId) { + console.error('❌ Versuch, Verfügbarkeit ohne Shift-ID zu ändern'); + return; + } + console.log(`🔄 ÄNDERE VERFÜGBARKEIT: Shift ${shiftId}, Level ${level}`); setAvailabilities(prev => { @@ -238,24 +284,56 @@ const AvailabilityManager: React.FC = ({ ); } - // Get all unique shifts across all days for row headers - const allShifts: ExtendedShift[] = []; - const shiftIds = new Set(); - + // Create a map for quick time slot lookups + const timeSlotMap = new Map(selectedPlan?.timeSlots?.map(ts => [ts.id, ts]) || []); + + // Get all unique time slots (rows) by collecting from all shifts + const allTimeSlots = new Map(); days.forEach(day => { shiftsByDay[day.id]?.forEach(shift => { - if (!shiftIds.has(shift.id)) { - shiftIds.add(shift.id); - allShifts.push(shift); + const timeSlot = timeSlotMap.get(shift.timeSlotId); + if (timeSlot && !allTimeSlots.has(timeSlot.id)) { + allTimeSlots.set(timeSlot.id, { + ...timeSlot, + shiftsByDay: {} // Initialize empty object to store shifts by day + }); } }); }); - // Sort shifts by time slot start time - allShifts.sort((a, b) => { - const timeA = a.startTime || ''; - const timeB = b.startTime || ''; - return timeA.localeCompare(timeB); + // Populate shifts for each time slot by day + days.forEach(day => { + shiftsByDay[day.id]?.forEach(shift => { + const timeSlot = allTimeSlots.get(shift.timeSlotId); + if (timeSlot) { + timeSlot.shiftsByDay[day.id] = shift; + } + }); + }); + + // Convert to array and sort by start time + const sortedTimeSlots = Array.from(allTimeSlots.values()).sort((a, b) => { + return (a.startTime || '').localeCompare(b.startTime || ''); + }); + + // Validation: Check if shifts are correctly placed + const validationErrors: string[] = []; + + // Check for missing time slots + const usedTimeSlotIds = new Set(selectedPlan?.shifts?.map(s => s.timeSlotId) || []); + const availableTimeSlotIds = new Set(selectedPlan?.timeSlots?.map(ts => ts.id) || []); + + usedTimeSlotIds.forEach(timeSlotId => { + if (!availableTimeSlotIds.has(timeSlotId)) { + validationErrors.push(`Zeitslot ${timeSlotId} wird verwendet, existiert aber nicht in timeSlots`); + } + }); + + // Check for shifts with invalid day numbers + selectedPlan?.shifts?.forEach(shift => { + if (shift.dayOfWeek < 1 || shift.dayOfWeek > 7) { + validationErrors.push(`Shift ${shift.id} hat ungültigen Wochentag: ${shift.dayOfWeek}`); + } }); return ( @@ -273,10 +351,39 @@ const AvailabilityManager: React.FC = ({ }}> Verfügbarkeit definieren
- {allShifts.length} Schichten • {days.length} Tage • Direkte Shift-ID Zuordnung + {sortedTimeSlots.length} Zeitslots • {days.length} Tage • Zeitbasierte Darstellung
+ {/* Validation Warnings */} + {validationErrors.length > 0 && ( +
+

⚠️ Validierungswarnungen:

+
    + {validationErrors.map((error, index) => ( +
  • {error}
  • + ))} +
+
+ )} + + {/* Timetable Structure Info */} +
+ Struktur-Info: {sortedTimeSlots.length} Zeitslots × {days.length} Tage = {sortedTimeSlots.length * days.length} Zellen +
+
= ({ textAlign: 'left', border: '1px solid #dee2e6', fontWeight: 'bold', - minWidth: '150px' + minWidth: '200px' }}> - Schicht (Zeit) + Zeitslot {days.map(weekday => ( @@ -308,26 +415,32 @@ const AvailabilityManager: React.FC = ({ - {allShifts.map((shift, shiftIndex) => ( - ( + {days.map(weekday => { - // Check if this shift exists for this day - const shiftForDay = shiftsByDay[weekday.id]?.find(s => s.id === shift.id); + const shift = timeSlot.shiftsByDay[weekday.id]; - if (!shiftForDay) { + if (!shift) { return ( ); } + // Validation: Check if shift has correct timeSlotId and dayOfWeek + const isValidShift = shift.timeSlotId === timeSlot.id && shift.dayOfWeek === weekday.id; + const currentLevel = getAvailabilityForShift(shift.id); const levelConfig = availabilityLevels.find(l => l.level === currentLevel); @@ -350,8 +466,31 @@ const AvailabilityManager: React.FC = ({ padding: '12px 16px', border: '1px solid #dee2e6', textAlign: 'center', - backgroundColor: levelConfig?.bgColor + backgroundColor: !isValidShift ? '#fff3cd' : (levelConfig?.bgColor || 'white'), + position: 'relative' }}> + {/* Validation indicator */} + {!isValidShift && ( +
+ ⚠️ +
+ )} + + + {/* Shift debug info */} +
+
Shift: {shift.id.substring(0, 6)}...
+
Day: {shift.dayOfWeek}
+ {!isValidShift && ( +
+ VALIDATION ERROR +
+ )} +
); })} @@ -392,6 +548,27 @@ const AvailabilityManager: React.FC = ({
= ({ textAlign: 'center', border: '1px solid #dee2e6', fontWeight: 'bold', - minWidth: '90px' + minWidth: '120px' }}> {weekday.name}
- {shift.displayName} -
- Shift-ID: {shift.id.substring(0, 8)}... +
+ {timeSlot.name} +
+
+ {formatTime(timeSlot.startTime)} - {formatTime(timeSlot.endTime)} +
+
+ ID: {timeSlot.id.substring(0, 8)}...
= ({ color: '#ccc', fontStyle: 'italic' }}> - - + Kein Shift
+ + {/* Summary Statistics */} +
+
+
+ Zusammenfassung: {sortedTimeSlots.length} Zeitslots × {days.length} Tage = {sortedTimeSlots.length * days.length} mögliche Shifts +
+
+ Aktive Verfügbarkeiten: {availabilities.filter(a => a.preferenceLevel !== 3).length} +
+
+ Validierungsfehler: {validationErrors.length} +
+
+
); }; @@ -406,14 +583,24 @@ const AvailabilityManager: React.FC = ({ return; } - const { days, shiftsByDay } = getTimetableData(); - - // Filter availabilities to only include those with actual shifts + // Filter availabilities to only include those with actual shifts AND valid shiftIds const validAvailabilities = availabilities.filter(avail => { + // Check if this shiftId exists and is valid + if (!avail.shiftId) { + console.warn('⚠️ Überspringe ungültige Verfügbarkeit ohne Shift-ID:', avail); + return false; + } + // Check if this shiftId exists in the current plan return selectedPlan?.shifts?.some(shift => shift.id === avail.shiftId); }); + console.log('💾 SPEICHERE VERFÜGBARKEITEN:', { + total: availabilities.length, + valid: validAvailabilities.length, + invalid: availabilities.length - validAvailabilities.length + }); + if (validAvailabilities.length === 0) { setError('Keine gültigen Verfügbarkeiten zum Speichern gefunden'); return; @@ -485,25 +672,63 @@ const AvailabilityManager: React.FC = ({ {/* Debug-Info */}

- {shiftsCount === 0 ? '❌ PROBLEM: Keine Shifts gefunden' : '✅ Plan-Daten geladen'} + {!selectedPlan ? '❌ KEIN PLAN AUSGEWÄHLT' : + shiftsCount === 0 ? '⚠️ KEINE SHIFTS GEFUNDEN' : '✅ PLAN-DATEN GELADEN'}

Ausgewählter Plan: {selectedPlan?.name || 'Keiner'}
+
Plan ID: {selectedPlanId || 'Nicht gesetzt'}
+
Geladene Pläne: {shiftPlans.length}
Einzigartige Shifts: {shiftsCount}
-
Verwendete Tage: {days.length} ({days.map(d => d.name).join(', ')})
-
Gesamte Shifts im Plan: {selectedPlan?.shifts?.length || 0}
-
Methode: Direkte Shift-ID Zuordnung
+
Geladene Verfügbarkeiten: {availabilities.length}
+ {selectedPlan && ( + <> +
Verwendete Tage: {days.length} ({days.map(d => d.name).join(', ')})
+
Gesamte Shifts im Plan: {selectedPlan.shifts?.length || 0}
+ + )}
+ + {/* Show existing preferences */} + {availabilities.length > 0 && ( +
+ Vorhandene Präferenzen: + {availabilities.slice(0, 5).map(avail => { + // SICHERHEITSCHECK: Stelle sicher, dass shiftId existiert + if (!avail.shiftId) { + return ( +
+ • UNGÜLTIG: Keine Shift-ID +
+ ); + } + + const shift = selectedPlan?.shifts?.find(s => s.id === avail.shiftId); + const shiftIdDisplay = avail.shiftId ? avail.shiftId.substring(0, 8) + '...' : 'KEINE ID'; + + return ( +
+ • Shift {shiftIdDisplay} (Day {shift?.dayOfWeek || '?'}): Level {avail.preferenceLevel} +
+ ); + })} + {availabilities.length > 5 && ( +
+ ... und {availabilities.length - 5} weitere +
+ )} +
+ )}
{/* Employee Info */} @@ -588,7 +813,12 @@ const AvailabilityManager: React.FC = ({ handleTraineeChange(e.target.checked)} + style={{ width: '18px', height: '18px' }} + /> +
+ +
+ Neulinge benötigen zusätzliche Betreuung und können nicht eigenständig arbeiten. +
+
+ + )} {/* Eigenständigkeit */} @@ -496,11 +552,11 @@ const EmployeeForm: React.FC = ({ id="canWorkAlone" checked={formData.canWorkAlone} onChange={handleChange} - disabled={formData.employeeType === 'manager'} + disabled={formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)} style={{ width: '20px', height: '20px', - opacity: formData.employeeType === 'manager' ? 0.5 : 1 + opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.5 : 1 }} />
@@ -508,14 +564,16 @@ const EmployeeForm: React.FC = ({ fontWeight: 'bold', color: '#2c3e50', display: 'block', - opacity: formData.employeeType === 'manager' ? 0.5 : 1 + opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.5 : 1 }}> Als ausreichend eigenständig markieren - {formData.employeeType === 'manager' && ' (Automatisch für Chefs)'} + {(formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) && ' (Automatisch festgelegt)'}
{formData.employeeType === 'manager' ? 'Chefs sind automatisch als eigenständig markiert.' + : formData.employeeType === 'personell' && formData.isTrainee + ? 'Auszubildende können nicht als eigenständig markiert werden.' : 'Dieser Mitarbeiter kann komplexe Aufgaben eigenständig lösen und benötigt keine ständige Betreuung.' }
@@ -527,7 +585,7 @@ const EmployeeForm: React.FC = ({ borderRadius: '15px', fontSize: '12px', fontWeight: 'bold', - opacity: formData.employeeType === 'manager' ? 0.7 : 1 + opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.7 : 1 }}> {formData.canWorkAlone ? 'EIGENSTÄNDIG' : 'BETREUUNG'}
@@ -632,7 +690,7 @@ const EmployeeForm: React.FC = ({ )} - {/* Systemrollen (nur für Admins) */} + {/* Systemrollen (nur für Admins) - AKTUALISIERT FÜR MEHRFACHE ROLLEN */} {hasRole(['admin']) && (
= ({
)} + {/* Buttons */} diff --git a/frontend/src/pages/Employees/components/EmployeeList.tsx b/frontend/src/pages/Employees/components/EmployeeList.tsx index 9555b3a..931ef40 100644 --- a/frontend/src/pages/Employees/components/EmployeeList.tsx +++ b/frontend/src/pages/Employees/components/EmployeeList.tsx @@ -1,3 +1,4 @@ +// EmployeeList.tsx import React, { useState } from 'react'; import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults'; import { Employee } from '../../../models/Employee'; @@ -13,6 +14,9 @@ interface EmployeeListProps { type SortField = 'name' | 'employeeType' | 'canWorkAlone' | 'role' | 'lastLogin'; type SortDirection = 'asc' | 'desc'; +// FIXED: Use the actual employee types from the Employee interface +type EmployeeType = 'manager' | 'personell' | 'apprentice' | 'guest'; + const EmployeeList: React.FC = ({ employees, onEdit, @@ -122,18 +126,18 @@ const EmployeeList: React.FC = ({ return false; }; - // Using shared configuration for consistent styling - type EmployeeType = 'manager' | 'trainee' | 'experienced'; - - const getEmployeeTypeBadge = (type: EmployeeType) => { + const getEmployeeTypeBadge = (type: EmployeeType, isTrainee: boolean = false) => { const config = EMPLOYEE_TYPE_CONFIG[type]; + // FIXED: Updated color mapping for actual employee types const bgColor = type === 'manager' - ? '#fadbd8' - : type === 'trainee' - ? '#d5f4e6' - : '#d6eaf8'; // experienced + ? '#fadbd8' // light red + : type === 'personell' + ? isTrainee ? '#d5f4e6' : '#d6eaf8' // light green for trainee, light blue for experienced + : type === 'apprentice' + ? '#e8d7f7' // light purple for apprentice + : '#f8f9fa'; // light gray for guest return { text: config.label, color: config.color, bgColor }; }; @@ -296,7 +300,8 @@ const EmployeeList: React.FC = ({ {sortedEmployees.map(employee => { - const employeeType = getEmployeeTypeBadge(employee.employeeType); + // FIXED: Type assertion to ensure type safety + const employeeType = getEmployeeTypeBadge(employee.employeeType as EmployeeType, employee.isTrainee); const independence = getIndependenceBadge(employee.canWorkAlone); const roleInfo = getRoleBadge(employee.roles); const status = getStatusBadge(employee.isActive); @@ -541,19 +546,30 @@ const EmployeeList: React.FC = ({ borderRadius: '12px', fontSize: '11px', fontWeight: 'bold' - }}>👴 ERFAHREN - Langjährige Erfahrung + }}>👨‍🏭 PERSONAL + Reguläre Mitarbeiter
👶 NEULING - Benötigt Einarbeitung + }}>👨‍🎓 AUSZUBILDENDER + Auszubildende +
+
+ 👤 GAST + Externe Mitarbeiter
diff --git a/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx b/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx index 30cb950..59211d1 100644 --- a/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx +++ b/frontend/src/pages/ShiftPlans/ShiftPlanView.tsx @@ -16,14 +16,27 @@ interface ExtendedTimeSlot extends TimeSlot { displayName?: string; } +interface ExtendedShift { + id: string; + planId: string; + timeSlotId: string; + dayOfWeek: number; + requiredEmployees: number; + color?: string; + timeSlotName?: string; + startTime?: string; + endTime?: string; + displayName?: string; +} + const weekdays = [ - { id: 1, name: 'Mo' }, - { id: 2, name: 'Di' }, - { id: 3, name: 'Mi' }, - { id: 4, name: 'Do' }, - { id: 5, name: 'Fr' }, - { id: 6, name: 'Sa' }, - { id: 7, name: 'So' } + { id: 1, name: 'Montag' }, + { id: 2, name: 'Dienstag' }, + { id: 3, name: 'Mittwoch' }, + { id: 4, name: 'Donnerstag' }, + { id: 5, name: 'Freitag' }, + { id: 6, name: 'Samstag' }, + { id: 7, name: 'Sonntag' } ]; const ShiftPlanView: React.FC = () => { @@ -35,14 +48,13 @@ const ShiftPlanView: React.FC = () => { const [shiftPlan, setShiftPlan] = useState(null); const [employees, setEmployees] = useState([]); const [availabilities, setAvailabilities] = useState([]); - const [assignmentResult, setAssignmentResult] = useState(null); // Add this line + const [assignmentResult, setAssignmentResult] = useState(null); const [loading, setLoading] = useState(true); const [publishing, setPublishing] = useState(false); const [scheduledShifts, setScheduledShifts] = useState([]); const [showAssignmentPreview, setShowAssignmentPreview] = useState(false); const [recreating, setRecreating] = useState(false); - useEffect(() => { loadShiftPlanData(); @@ -76,31 +88,133 @@ const ShiftPlanView: React.FC = () => { }; }, []); + // Add this useEffect to debug state changes 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]); + console.log('🔍 STATE DEBUG - showAssignmentPreview:', showAssignmentPreview); + console.log('🔍 STATE DEBUG - assignmentResult:', assignmentResult ? 'EXISTS' : 'NULL'); + console.log('🔍 STATE DEBUG - publishing:', publishing); + }, [showAssignmentPreview, assignmentResult, publishing]); - useEffect(() => { - (window as any).debugRenderLogic = debugRenderLogic; - return () => { (window as any).debugRenderLogic = undefined; }; - }, [shiftPlan, scheduledShifts]); + // Create a data structure that maps days to their shifts with time slot info - SAME AS AVAILABILITYMANAGER + const getTimetableData = () => { + if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) { + return { days: [], shiftsByDay: {}, allTimeSlots: [] }; + } + + // Create a map for quick time slot lookups + const timeSlotMap = new Map(shiftPlan.timeSlots.map(ts => [ts.id, ts])); + + // Group shifts by day and enhance with time slot info - SAME LOGIC AS AVAILABILITYMANAGER + const shiftsByDay = shiftPlan.shifts.reduce((acc, shift) => { + if (!acc[shift.dayOfWeek]) { + acc[shift.dayOfWeek] = []; + } + + const timeSlot = timeSlotMap.get(shift.timeSlotId); + const enhancedShift: ExtendedShift = { + ...shift, + timeSlotName: timeSlot?.name, + startTime: timeSlot?.startTime, + endTime: timeSlot?.endTime, + displayName: timeSlot ? `${timeSlot.name} (${formatTime(timeSlot.startTime)}-${formatTime(timeSlot.endTime)})` : shift.id + }; + + acc[shift.dayOfWeek].push(enhancedShift); + return acc; + }, {} as Record); + + // Sort shifts within each day by start time - SAME LOGIC AS AVAILABILITYMANAGER + 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 - SAME LOGIC AS AVAILABILITYMANAGER + const days = Array.from(new Set(shiftPlan.shifts.map(shift => shift.dayOfWeek))) + .sort() + .map(dayId => { + return weekdays.find(day => day.id === dayId) || { id: dayId, name: `Tag ${dayId}` }; + }); + + // Get all unique time slots (rows) by collecting from all shifts - SAME LOGIC AS AVAILABILITYMANAGER + const allTimeSlotsMap = new Map(); + days.forEach(day => { + shiftsByDay[day.id]?.forEach(shift => { + const timeSlot = timeSlotMap.get(shift.timeSlotId); + if (timeSlot && !allTimeSlotsMap.has(timeSlot.id)) { + allTimeSlotsMap.set(timeSlot.id, { + ...timeSlot, + shiftsByDay: {} // Initialize empty object to store shifts by day + }); + } + }); + }); + + // Populate shifts for each time slot by day - SAME LOGIC AS AVAILABILITYMANAGER + days.forEach(day => { + shiftsByDay[day.id]?.forEach(shift => { + const timeSlot = allTimeSlotsMap.get(shift.timeSlotId); + if (timeSlot) { + timeSlot.shiftsByDay[day.id] = shift; + } + }); + }); + + // Convert to array and sort by start time - SAME LOGIC AS AVAILABILITYMANAGER + const allTimeSlots = Array.from(allTimeSlotsMap.values()).sort((a, b) => { + return (a.startTime || '').localeCompare(b.startTime || ''); + }); + + return { days, shiftsByDay, allTimeSlots }; + }; + + // VALIDATION FUNCTION - Check if shifts are correctly placed (like in AvailabilityManager) + const validateTimetableStructure = () => { + if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) { + return { isValid: false, errors: ['No shift plan data available'] }; + } + + const validationErrors: string[] = []; + + // Check for missing time slots - SAME VALIDATION AS AVAILABILITYMANAGER + const usedTimeSlotIds = new Set(shiftPlan.shifts.map(s => s.timeSlotId)); + const availableTimeSlotIds = new Set(shiftPlan.timeSlots.map(ts => ts.id)); + + usedTimeSlotIds.forEach(timeSlotId => { + if (!availableTimeSlotIds.has(timeSlotId)) { + validationErrors.push(`Zeitslot ${timeSlotId} wird verwendet, existiert aber nicht in timeSlots`); + } + }); + + // Check for shifts with invalid day numbers - SAME VALIDATION AS AVAILABILITYMANAGER + shiftPlan.shifts.forEach(shift => { + if (shift.dayOfWeek < 1 || shift.dayOfWeek > 7) { + validationErrors.push(`Shift ${shift.id} hat ungültigen Wochentag: ${shift.dayOfWeek}`); + } + + // Check if shift timeSlotId exists in timeSlots + const timeSlotExists = shiftPlan.timeSlots.some(ts => ts.id === shift.timeSlotId); + if (!timeSlotExists) { + validationErrors.push(`Shift ${shift.id} verweist auf nicht existierenden Zeitslot: ${shift.timeSlotId}`); + } + }); + + // Check for scheduled shifts consistency + scheduledShifts.forEach(scheduledShift => { + const timeSlotExists = shiftPlan.timeSlots.some(ts => ts.id === scheduledShift.timeSlotId); + if (!timeSlotExists) { + validationErrors.push(`Scheduled Shift ${scheduledShift.id} verweist auf nicht existierenden Zeitslot: ${scheduledShift.timeSlotId}`); + } + }); + + return { + isValid: validationErrors.length === 0, + errors: validationErrors + }; + }; const loadShiftPlanData = async () => { if (!id) return; @@ -132,7 +246,9 @@ const ShiftPlanView: React.FC = () => { setScheduledShifts(shiftsData); - // Load availabilities + // Load availabilities - USING THE SAME LOGIC AS AVAILABILITYMANAGER + console.log('🔄 LADE VERFÜGBARKEITEN FÜR PLAN:', id); + const availabilityPromises = employeesData .filter(emp => emp.isActive) .map(emp => employeeService.getAvailabilities(emp.id)); @@ -140,12 +256,21 @@ const ShiftPlanView: React.FC = () => { const allAvailabilities = await Promise.all(availabilityPromises); const flattenedAvailabilities = allAvailabilities.flat(); + // Filter to only include availabilities for the current plan - SAME LOGIC AS AVAILABILITYMANAGER const planAvailabilities = flattenedAvailabilities.filter( availability => availability.planId === id ); + console.log('✅ VERFÜGBARKEITEN FÜR DIESEN PLAN:', planAvailabilities.length); + setAvailabilities(planAvailabilities); + // Run validation + const validation = validateTimetableStructure(); + if (!validation.isValid) { + console.warn('⚠️ TIMETABLE VALIDATION ERRORS:', validation.errors); + } + } catch (error) { console.error('Error loading shift plan data:', error); showNotification({ @@ -222,143 +347,6 @@ const ShiftPlanView: React.FC = () => { } }; - const debugRenderLogic = () => { - if (!shiftPlan) return; - - console.log('🔍 RENDER LOGIC DEBUG:'); - console.log('====================='); - - const { days, allTimeSlots, timeSlotsByDay } = getTimetableData(); - - console.log('📊 TABLE STRUCTURE:'); - console.log('- Days in table:', days.length); - console.log('- TimeSlots in table:', allTimeSlots.length); - console.log('- Days with data:', Object.keys(timeSlotsByDay).length); - - // Zeige die tatsächliche Struktur der Tabelle - console.log('\n📅 ACTUAL TABLE DAYS:'); - days.forEach(day => { - const slotsForDay = timeSlotsByDay[day.id] || []; - console.log(`- ${day.name}: ${slotsForDay.length} time slots`); - }); - - console.log('\n⏰ ACTUAL TIME SLOTS:'); - allTimeSlots.forEach(slot => { - console.log(`- ${slot.name} (${slot.startTime}-${slot.endTime})`); - }); - - // Prüfe wie viele Scheduled Shifts tatsächlich gerendert werden - console.log('\n🔍 SCHEDULED SHIFTS RENDER ANALYSIS:'); - - let totalRenderedShifts = 0; - let shiftsWithAssignments = 0; - - days.forEach(day => { - const slotsForDay = timeSlotsByDay[day.id] || []; - slotsForDay.forEach(timeSlot => { - totalRenderedShifts++; - - // Finde den entsprechenden Scheduled Shift - const scheduledShift = scheduledShifts.find(scheduled => { - const scheduledDayOfWeek = getDayOfWeek(scheduled.date); - return scheduledDayOfWeek === day.id && - scheduled.timeSlotId === timeSlot.id; - }); - - if (scheduledShift && scheduledShift.assignedEmployees && scheduledShift.assignedEmployees.length > 0) { - shiftsWithAssignments++; - } - }); - }); - - console.log(`- Total shifts in table: ${totalRenderedShifts}`); - console.log(`- Shifts with assignments: ${shiftsWithAssignments}`); - console.log(`- Total scheduled shifts: ${scheduledShifts.length}`); - console.log(`- Coverage: ${Math.round((totalRenderedShifts / scheduledShifts.length) * 100)}%`); - - // Problem-Analyse - if (totalRenderedShifts < scheduledShifts.length) { - console.log('\n🚨 PROBLEM: Table is not showing all scheduled shifts!'); - console.log('💡 The table structure (days × timeSlots) is smaller than actual scheduled shifts'); - - // Zeige die fehlenden Shifts - const missingShifts = scheduledShifts.filter(scheduled => { - const dayOfWeek = getDayOfWeek(scheduled.date); - const timeSlotExists = allTimeSlots.some(ts => ts.id === scheduled.timeSlotId); - const dayExists = days.some(day => day.id === dayOfWeek); - - return !(timeSlotExists && dayExists); - }); - - if (missingShifts.length > 0) { - console.log(`❌ ${missingShifts.length} shifts cannot be rendered in table:`); - missingShifts.slice(0, 5).forEach(shift => { - const dayOfWeek = getDayOfWeek(shift.date); - const timeSlot = shiftPlan.timeSlots?.find(ts => ts.id === shift.timeSlotId); - console.log(` - ${shift.date} (Day ${dayOfWeek}): ${timeSlot?.name || 'Unknown'} - ${shift.assignedEmployees?.length || 0} assignments`); - }); - } - } - }; - - // Extract plan-specific shifts using the same logic as AvailabilityManager - const getTimetableData = () => { - if (!shiftPlan || !shiftPlan.shifts || !shiftPlan.timeSlots) { - return { days: [], timeSlotsByDay: {}, allTimeSlots: [] }; - } - - // Group shifts by day - const shiftsByDay = shiftPlan.shifts.reduce((acc, shift) => { - if (!acc[shift.dayOfWeek]) { - acc[shift.dayOfWeek] = []; - } - acc[shift.dayOfWeek].push(shift); - return acc; - }, {} as Record); - - // Get unique days that have shifts - const days = Array.from(new Set(shiftPlan.shifts.map(shift => shift.dayOfWeek))) - .sort() - .map(dayId => { - return weekdays.find(day => day.id === dayId) || { id: dayId, name: `Tag ${dayId}` }; - }); - - // For each day, get the time slots that actually have shifts - const timeSlotsByDay: Record = {}; - - days.forEach(day => { - const shiftsForDay = shiftsByDay[day.id] || []; - const timeSlotIdsForDay = new Set(shiftsForDay.map(shift => shift.timeSlotId)); - - timeSlotsByDay[day.id] = shiftPlan.timeSlots - .filter(timeSlot => timeSlotIdsForDay.has(timeSlot.id)) - .map(timeSlot => ({ - ...timeSlot, - displayName: `${timeSlot.name} (${formatTime(timeSlot.startTime)}-${formatTime(timeSlot.endTime)})` - })) - .sort((a, b) => a.startTime.localeCompare(b.startTime)); - }); - - // Get all unique time slots across all days for row headers - const allTimeSlotIds = new Set(); - days.forEach(day => { - timeSlotsByDay[day.id]?.forEach(timeSlot => { - allTimeSlotIds.add(timeSlot.id); - }); - }); - - const allTimeSlots = Array.from(allTimeSlotIds) - .map(timeSlotId => shiftPlan.timeSlots.find(ts => ts.id === timeSlotId)) - .filter(Boolean) - .map(timeSlot => ({ - ...timeSlot!, - displayName: `${timeSlot!.name} (${formatTime(timeSlot!.startTime)}-${formatTime(timeSlot!.endTime)})` - })) - .sort((a, b) => a.startTime.localeCompare(b.startTime)); - - return { days, timeSlotsByDay, allTimeSlots }; - }; - const getDayOfWeek = (dateString: string): number => { const date = new Date(dateString); return date.getDay() === 0 ? 7 : date.getDay(); @@ -369,20 +357,31 @@ const ShiftPlanView: React.FC = () => { try { setPublishing(true); + setAssignmentResult(null); // Reset previous results + setShowAssignmentPreview(false); // Reset preview + + console.log('🔄 STARTING ASSIGNMENT PREVIEW...'); // FORCE COMPLETE REFRESH - don't rely on cached state const [refreshedEmployees, refreshedAvailabilities] = await Promise.all([ - // Reload employees fresh employeeService.getEmployees().then(emps => emps.filter(emp => emp.isActive)), - // Reload availabilities fresh refreshAllAvailabilities() ]); console.log('🔄 USING FRESH DATA:'); console.log('- Employees:', refreshedEmployees.length); console.log('- Availabilities:', refreshedAvailabilities.length); + console.log('- Shift Patterns:', shiftPlan.shifts?.length || 0); + console.log('- Scheduled Shifts:', scheduledShifts.length); + + // DEBUG: Show shift pattern IDs + if (shiftPlan.shifts) { + console.log('📋 SHIFT PATTERN IDs:'); + shiftPlan.shifts.forEach((shift, index) => { + console.log(` ${index + 1}. ${shift.id} (Day ${shift.dayOfWeek}, TimeSlot ${shift.timeSlotId})`); + }); + } - // ADD THIS: Define constraints object const constraints = { enforceNoTraineeAlone: true, enforceExperiencedWithChef: true, @@ -390,6 +389,8 @@ const ShiftPlanView: React.FC = () => { targetEmployeesPerShift: 2 }; + console.log('🧠 Calling shift assignment service...'); + // Use the freshly loaded data, not the state const result = await shiftAssignmentService.assignShifts( shiftPlan, @@ -398,7 +399,6 @@ const ShiftPlanView: React.FC = () => { constraints ); - // COMPREHENSIVE DEBUGGING console.log("🎯 RAW ASSIGNMENT RESULT FROM API:", { success: result.success, assignmentsCount: Object.keys(result.assignments).length, @@ -407,31 +407,31 @@ const ShiftPlanView: React.FC = () => { resolutionReport: result.resolutionReport?.length || 0 }); - // Log the actual assignments with more context + // Log assignments with shift pattern context + console.log('🔍 ASSIGNMENTS BY SHIFT PATTERN:'); Object.entries(result.assignments).forEach(([shiftId, empIds]) => { - console.log(` 📅 Assignment Key: ${shiftId}`); - console.log(` Employees: ${empIds.join(', ')}`); + const shiftPattern = shiftPlan.shifts?.find(s => s.id === shiftId); - // 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`); - } + if (shiftPattern) { + console.log(` ✅ Shift Pattern: ${shiftId}`); + console.log(` - Day: ${shiftPattern.dayOfWeek}, TimeSlot: ${shiftPattern.timeSlotId}`); + console.log(` - Employees: ${empIds.join(', ')}`); + } else { + console.log(` ❌ UNKNOWN ID: ${shiftId}`); + console.log(` - Employees: ${empIds.join(', ')}`); + console.log(` - This ID does not match any shift pattern!`); } }); + // CRITICAL: Update state and show preview + console.log('🔄 Setting assignment result and showing preview...'); setAssignmentResult(result); setShowAssignmentPreview(true); + + console.log('✅ Assignment preview ready, modal should be visible'); } catch (error) { - console.error('Error during assignment:', error); + console.error('❌ Error during assignment:', error); showNotification({ type: 'error', title: 'Fehler', @@ -458,13 +458,13 @@ const ShiftPlanView: React.FC = () => { } console.log(`📊 Found ${updatedShifts.length} scheduled shifts to update`); + console.log('🎯 Assignment keys from algorithm:', Object.keys(assignmentResult.assignments)); const updatePromises = updatedShifts.map(async (scheduledShift) => { - // ✅ FIX: Map scheduled shift to shift pattern to find assignments const dayOfWeek = getDayOfWeek(scheduledShift.date); // Find the corresponding shift pattern for this day and time slot - const shiftPattern = shiftPlan.shifts?.find(shift => + const shiftPattern = shiftPlan?.shifts?.find(shift => shift.dayOfWeek === dayOfWeek && shift.timeSlotId === scheduledShift.timeSlotId ); @@ -472,9 +472,13 @@ const ShiftPlanView: React.FC = () => { 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'); + + if (assignedEmployees.length === 0) { + console.warn(`⚠️ No assignments found for shift pattern ${shiftPattern.id}`); + console.log('🔍 Available assignment keys:', Object.keys(assignmentResult.assignments)); + } } else { console.warn(`⚠️ No shift pattern found for scheduled shift ${scheduledShift.id} (Day ${dayOfWeek}, TimeSlot ${scheduledShift.timeSlotId})`); } @@ -509,14 +513,17 @@ const ShiftPlanView: React.FC = () => { setShiftPlan(reloadedPlan); setScheduledShifts(reloadedShifts); + setShowAssignmentPreview(false); + setAssignmentResult(null); + + console.log('✅ Publishing completed, modal closed'); + showNotification({ type: 'success', title: 'Erfolg', message: 'Schichtplan wurde erfolgreich veröffentlicht!' }); - setShowAssignmentPreview(false); - } catch (error) { console.error('❌ Error publishing shift plan:', error); @@ -576,37 +583,37 @@ const ShiftPlanView: React.FC = () => { } }; - const validateSchedulingData = (): boolean => { - console.log('🔍 Validating scheduling data...'); + const debugShiftMatching = () => { + if (!shiftPlan || !scheduledShifts.length) return; - const totalEmployees = employees.length; - const employeesWithAvailabilities = new Set( - availabilities.map(avail => avail.employeeId) - ).size; + console.log('🔍 DEBUG: Shift Pattern to Scheduled Shift Matching'); + console.log('=================================================='); - const availabilityStatus = { - totalEmployees, - employeesWithAvailabilities, - coverage: Math.round((employeesWithAvailabilities / totalEmployees) * 100) - }; - - console.log('📊 Availability Coverage:', availabilityStatus); - - // Check if we have ALL employee availabilities - if (employeesWithAvailabilities < totalEmployees) { - const missingEmployees = employees.filter(emp => - !availabilities.some(avail => avail.employeeId === emp.id) - ); + shiftPlan.shifts?.forEach(shiftPattern => { + const matchingScheduledShifts = scheduledShifts.filter(scheduled => { + const dayOfWeek = getDayOfWeek(scheduled.date); + return dayOfWeek === shiftPattern.dayOfWeek && + scheduled.timeSlotId === shiftPattern.timeSlotId; + }); - console.warn('⚠️ Missing availabilities for employees:', - missingEmployees.map(emp => emp.email)); + console.log(`📅 Shift Pattern: ${shiftPattern.id}`); + console.log(` - Day: ${shiftPattern.dayOfWeek}, TimeSlot: ${shiftPattern.timeSlotId}`); + console.log(` - Matching scheduled shifts: ${matchingScheduledShifts.length}`); - return false; - } - - return true; + if (assignmentResult) { + const assignments = assignmentResult.assignments[shiftPattern.id] || []; + console.log(` - Assignments: ${assignments.length} employees`); + } + }); }; + // Rufe die Debug-Funktion auf, wenn Assignment-Ergebnisse geladen werden + useEffect(() => { + if (assignmentResult && shiftPlan) { + debugShiftMatching(); + } + }, [assignmentResult, shiftPlan]); + const canPublish = () => { if (!shiftPlan || shiftPlan.status === 'published') return false; @@ -660,32 +667,33 @@ const ShiftPlanView: React.FC = () => { const getAssignmentsForScheduledShift = (scheduledShift: ScheduledShift): string[] => { if (!assignmentResult) return []; - // First try direct match with scheduled shift ID - if (assignmentResult.assignments[scheduledShift.id]) { - return assignmentResult.assignments[scheduledShift.id]; - } - - // If no direct match, try to find by day and timeSlot pattern const dayOfWeek = getDayOfWeek(scheduledShift.date); + + // Find the corresponding shift pattern for this day and time slot const shiftPattern = shiftPlan?.shifts?.find(shift => shift.dayOfWeek === dayOfWeek && shift.timeSlotId === scheduledShift.timeSlotId ); if (shiftPattern && assignmentResult.assignments[shiftPattern.id]) { + console.log(`✅ Found assignments for shift pattern ${shiftPattern.id}:`, assignmentResult.assignments[shiftPattern.id]); return assignmentResult.assignments[shiftPattern.id]; } + // Fallback: Check if there's a direct match with scheduled shift ID (unlikely) + if (assignmentResult.assignments[scheduledShift.id]) { + console.log(`⚠️ Using direct scheduled shift assignment for ${scheduledShift.id}`); + return assignmentResult.assignments[scheduledShift.id]; + } + + console.warn(`❌ No assignments found for scheduled shift ${scheduledShift.id} (Day ${dayOfWeek}, TimeSlot ${scheduledShift.timeSlotId})`); return []; }; // Render timetable using the same structure as AvailabilityManager const renderTimetable = () => { - const { days, allTimeSlots, timeSlotsByDay } = getTimetableData(); - if (!shiftPlan?.id) { - console.warn("Shift plan ID is missing"); - return null; - } + const { days, allTimeSlots } = getTimetableData(); + const validation = validateTimetableStructure(); if (days.length === 0 || allTimeSlots.length === 0) { return ( @@ -719,10 +727,39 @@ const ShiftPlanView: React.FC = () => { }}> Schichtplan
- {allTimeSlots.length} Schichttypen • {days.length} Tage • Nur tatsächlich im Plan verwendete Schichten + {allTimeSlots.length} Zeitslots • {days.length} Tage • Zeitbasierte Darstellung
+ {/* Validation Warnings - SAME AS AVAILABILITYMANAGER */} + {!validation.isValid && ( +
+

⚠️ Validierungswarnungen:

+
    + {validation.errors.map((error, index) => ( +
  • {error}
  • + ))} +
+
+ )} + + {/* Timetable Structure Info - SAME AS AVAILABILITYMANAGER */} +
+ Struktur-Info: {allTimeSlots.length} Zeitslots × {days.length} Tage = {allTimeSlots.length * days.length} Zellen +
+
{ - {allTimeSlots.map((timeSlot, timeIndex) => ( + {allTimeSlots.map((timeSlot, timeSlotIndex) => ( {days.map(weekday => { - // Check if this time slot exists for this day - const timeSlotForDay = timeSlotsByDay[weekday.id]?.find(ts => ts.id === timeSlot.id); + const shift = timeSlot.shiftsByDay[weekday.id]; - if (!timeSlotForDay) { + if (!shift) { return ( ); } + // Validation: Check if shift has correct timeSlotId and dayOfWeek - SAME AS AVAILABILITYMANAGER + const isValidShift = shift.timeSlotId === timeSlot.id && shift.dayOfWeek === weekday.id; + let assignedEmployees: string[] = []; let displayText = ''; @@ -806,7 +855,7 @@ const ShiftPlanView: React.FC = () => { displayText = assignedEmployees.map(empId => { const employee = employees.find(emp => emp.id === empId); - return employee ? employee.email : 'Unbekannt'; + return employee ? `${employee.firstname} ${employee.lastname}` : 'Unbekannt'; }).join(', '); } } else if (assignmentResult) { @@ -821,20 +870,20 @@ const ShiftPlanView: React.FC = () => { assignedEmployees = getAssignmentsForScheduledShift(scheduledShift); displayText = assignedEmployees.map(empId => { const employee = employees.find(emp => emp.id === empId); - return employee ? employee.email : 'Unbekannt'; + return employee ? `${employee.firstname} ${employee.lastname}` : 'Unbekannt'; }).join(', '); } } // If no assignments yet, show empty or required count if (!displayText) { - const shiftsForSlot = shiftPlan?.shifts?.filter(shift => - shift.dayOfWeek === weekday.id && - shift.timeSlotId === timeSlot.id + const shiftsForSlot = shiftPlan?.shifts?.filter(s => + s.dayOfWeek === weekday.id && + s.timeSlotId === timeSlot.id ) || []; - const totalRequired = shiftsForSlot.reduce((sum, shift) => - sum + shift.requiredEmployees, 0); + const totalRequired = shiftsForSlot.reduce((sum, s) => + sum + s.requiredEmployees, 0); // Show "0/2" instead of just "0" to indicate it's empty displayText = `0/${totalRequired}`; @@ -850,11 +899,51 @@ const ShiftPlanView: React.FC = () => { padding: '12px 16px', border: '1px solid #dee2e6', textAlign: 'center', - backgroundColor: assignedEmployees.length > 0 ? '#e8f5e8' : 'transparent', + backgroundColor: !isValidShift ? '#fff3cd' : (assignedEmployees.length > 0 ? '#e8f5e8' : 'transparent'), color: assignedEmployees.length > 0 ? '#2c3e50' : '#666', - fontSize: assignedEmployees.length > 0 ? '14px' : 'inherit' + fontSize: assignedEmployees.length > 0 ? '14px' : 'inherit', + position: 'relative' }}> + {/* Validation indicator - SAME AS AVAILABILITYMANAGER */} + {!isValidShift && ( +
+ ⚠️ +
+ )} + {displayText} + + {/* Shift debug info - SAME AS AVAILABILITYMANAGER */} +
+
Shift: {shift.id.substring(0, 6)}...
+
Day: {shift.dayOfWeek}
+ {!isValidShift && ( +
+ VALIDATION ERROR +
+ )} +
); })} @@ -863,6 +952,24 @@ const ShiftPlanView: React.FC = () => {
- {timeSlot.displayName} +
+ {timeSlot.name} +
+
+ {formatTime(timeSlot.startTime)} - {formatTime(timeSlot.endTime)} +
+
+ ID: {timeSlot.id.substring(0, 8)}... +
{ color: '#ccc', fontStyle: 'italic' }}> - - + Kein Shift
+ + {/* Summary Statistics - SAME AS AVAILABILITYMANAGER */} +
+
+
+ Zusammenfassung: {allTimeSlots.length} Zeitslots × {days.length} Tage = {allTimeSlots.length * days.length} mögliche Shifts +
+
+ Validierungsfehler: {validation.errors.length} +
+
+
); }; @@ -872,7 +979,7 @@ const ShiftPlanView: React.FC = () => { const { days, allTimeSlots } = getTimetableData(); const availabilityStatus = getAvailabilityStatus(); - + const validation = validateTimetableStructure(); return (
@@ -938,6 +1045,50 @@ const ShiftPlanView: React.FC = () => {
+ {/* Debug Info - Enhanced */} +
0 ? '#fff3cd' : (allTimeSlots.length === 0 ? '#f8d7da' : '#d1ecf1'), + border: `1px solid ${validation.errors.length > 0 ? '#ffeaa7' : (allTimeSlots.length === 0 ? '#f5c6cb' : '#bee5eb')}`, + borderRadius: '6px', + padding: '15px', + marginBottom: '20px' + }}> +

0 ? '#856404' : (allTimeSlots.length === 0 ? '#721c24' : '#0c5460') + }}> + {validation.errors.length > 0 ? '⚠️ VALIDIERUNGSPROBLEME' : + allTimeSlots.length === 0 ? '❌ KEINE SHIFTS GEFUNDEN' : '✅ PLAN-DATEN GELADEN'} +

+
+
Ausgewählter Plan: {shiftPlan.name}
+
Plan ID: {shiftPlan.id}
+
Einzigartige Zeitslots: {allTimeSlots.length}
+
Verwendete Tage: {days.length} ({days.map(d => d.name).join(', ')})
+
Shift Patterns: {shiftPlan.shifts?.length || 0}
+
Scheduled Shifts: {scheduledShifts.length}
+
Geladene Verfügbarkeiten: {availabilities.length}
+
Aktive Mitarbeiter: {employees.length}
+ {assignmentResult && ( +
Assignment Keys: {Object.keys(assignmentResult.assignments).length}
+ )} +
+ + {/* Show shift pattern vs scheduled shift matching */} + {shiftPlan.shifts && scheduledShifts.length > 0 && ( +
+ Shift Matching: +
+ • {shiftPlan.shifts.length} Patterns → {scheduledShifts.length} Scheduled Shifts + {assignmentResult && ( +
• {Object.keys(assignmentResult.assignments).length} Assignment Keys
+ )} +
+
+ )} +
+ + {/* Rest of the component remains the same... */} {/* Availability Status - only show for drafts */} {shiftPlan.status === 'draft' && (
{
)} - {/* Assignment Preview Modal */} - {showAssignmentPreview && assignmentResult && ( + {/* Assignment Preview Modal - FIXED CONDITION */} + {(showAssignmentPreview || assignmentResult) && (
{ padding: '30px', maxWidth: '800px', maxHeight: '80vh', - overflow: 'auto' + overflow: 'auto', + width: '90%' }}>

Wochenmuster-Zuordnung

{/* Detaillierter Reparatur-Bericht anzeigen */} - {assignmentResult.resolutionReport && ( + {assignmentResult?.resolutionReport && (
{
+ {/* KORRIGIERTER BUTTON MIT TYPESCRIPT-FIX */}
- )} + )} {/* Timetable */}
{ + try { + const response = await fetch(`${API_BASE_URL}/employees/${employeeId}/last-login`, { + method: 'PATCH', + headers: getAuthHeaders(), + }); + + if (!response.ok) { + throw new Error('Failed to update last login'); + } + } catch (error) { + console.error('Error updating last login:', error); + throw error; + } + } } export const employeeService = new EmployeeService(); \ No newline at end of file