mirror of
https://github.com/donpat1to/Schichtenplaner.git
synced 2025-12-01 06:55:45 +01:00
updated every file for database changes; starting scheduling debugging
This commit is contained in:
@@ -39,7 +39,6 @@ export interface RegisterRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function generateEmail(firstname: string, lastname: string): string {
|
function generateEmail(firstname: string, lastname: string): string {
|
||||||
// Convert German umlauts to their expanded forms
|
|
||||||
const convertUmlauts = (str: string): string => {
|
const convertUmlauts = (str: string): string => {
|
||||||
return str
|
return str
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -49,7 +48,6 @@ function generateEmail(firstname: string, lastname: string): string {
|
|||||||
.replace(/ß/g, 'ss');
|
.replace(/ß/g, 'ss');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove any remaining special characters and convert to lowercase
|
|
||||||
const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, '');
|
const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, '');
|
||||||
const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, '');
|
const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, '');
|
||||||
return `${cleanFirstname}.${cleanLastname}@sp.de`;
|
return `${cleanFirstname}.${cleanLastname}@sp.de`;
|
||||||
@@ -70,8 +68,8 @@ export const login = async (req: Request, res: Response) => {
|
|||||||
const user = await db.get<any>(
|
const user = await db.get<any>(
|
||||||
`SELECT
|
`SELECT
|
||||||
e.id, e.email, e.password, e.firstname, e.lastname,
|
e.id, e.email, e.password, e.firstname, e.lastname,
|
||||||
e.employee_type as employeeType, e.contract_type as contractType,
|
e.employee_type, e.contract_type,
|
||||||
e.can_work_alone as canWorkAlone, e.is_active as isActive,
|
e.can_work_alone, e.is_active, e.is_trainee,
|
||||||
er.role
|
er.role
|
||||||
FROM employees e
|
FROM employees e
|
||||||
LEFT JOIN employee_roles er ON e.id = er.employee_id
|
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' });
|
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
|
// Verify password
|
||||||
const validPassword = await bcrypt.compare(password, user.password);
|
const validPassword = await bcrypt.compare(password, user.password);
|
||||||
console.log('🔑 Password valid:', validPassword);
|
console.log('🔑 Password valid:', validPassword);
|
||||||
@@ -116,7 +123,12 @@ export const login = async (req: Request, res: Response) => {
|
|||||||
const { password: _, ...userWithoutPassword } = user;
|
const { password: _, ...userWithoutPassword } = user;
|
||||||
const userResponse = {
|
const userResponse = {
|
||||||
...userWithoutPassword,
|
...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);
|
console.log('✅ Login successful for:', user.email);
|
||||||
@@ -147,8 +159,8 @@ export const getCurrentUser = async (req: Request, res: Response) => {
|
|||||||
const user = await db.get<any>(
|
const user = await db.get<any>(
|
||||||
`SELECT
|
`SELECT
|
||||||
e.id, e.email, e.firstname, e.lastname,
|
e.id, e.email, e.firstname, e.lastname,
|
||||||
e.employee_type as employeeType, e.contract_type as contractType,
|
e.employee_type, e.contract_type,
|
||||||
e.can_work_alone as canWorkAlone, e.is_active as isActive,
|
e.can_work_alone, e.is_active, e.is_trainee,
|
||||||
er.role
|
er.role
|
||||||
FROM employees e
|
FROM employees e
|
||||||
LEFT JOIN employee_roles er ON e.id = er.employee_id
|
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
|
// Format user response with roles array
|
||||||
const userResponse = {
|
const userResponse = {
|
||||||
...user,
|
...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']
|
roles: user.role ? [user.role] : ['user']
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -237,18 +254,18 @@ export const register = async (req: Request, res: Response) => {
|
|||||||
await db.run('BEGIN TRANSACTION');
|
await db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
try {
|
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(
|
const result = await db.run(
|
||||||
`INSERT INTO employees (id, email, password, firstname, lastname, employee_type, contract_type, can_work_alone, is_active)
|
`INSERT INTO employees (id, email, password, firstname, lastname, employee_type, contract_type, can_work_alone, is_active, is_trainee)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[employeeId, email, hashedPassword, firstname, lastname, 'experienced', 'small', false, 1]
|
[employeeId, email, hashedPassword, firstname, lastname, 'personell', 'small', false, 1, false]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!result.lastID) {
|
if (!result.lastID) {
|
||||||
throw new Error('Benutzer konnte nicht erstellt werden');
|
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) {
|
for (const role of roles) {
|
||||||
await db.run(
|
await db.run(
|
||||||
`INSERT INTO employee_roles (employee_id, role) VALUES (?, ?)`,
|
`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<any>(
|
const newUser = await db.get<any>(
|
||||||
`SELECT
|
`SELECT
|
||||||
e.id, e.email, e.firstname, e.lastname,
|
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
|
er.role
|
||||||
FROM employees e
|
FROM employees e
|
||||||
LEFT JOIN employee_roles er ON e.id = er.employee_id
|
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
|
// Format response with roles array
|
||||||
const userResponse = {
|
const userResponse = {
|
||||||
...newUser,
|
...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']
|
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) => {
|
export const logout = async (req: Request, res: Response) => {
|
||||||
try {
|
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' });
|
res.json({ message: 'Erfolgreich abgemeldet' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Logout error:', error);
|
console.error('Logout error:', error);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { AuthRequest } from '../middleware/auth.js';
|
|||||||
import { CreateEmployeeRequest } from '../models/Employee.js';
|
import { CreateEmployeeRequest } from '../models/Employee.js';
|
||||||
|
|
||||||
function generateEmail(firstname: string, lastname: string): string {
|
function generateEmail(firstname: string, lastname: string): string {
|
||||||
// Convert German umlauts to their expanded forms
|
|
||||||
const convertUmlauts = (str: string): string => {
|
const convertUmlauts = (str: string): string => {
|
||||||
return str
|
return str
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -17,7 +16,6 @@ function generateEmail(firstname: string, lastname: string): string {
|
|||||||
.replace(/ß/g, 'ss');
|
.replace(/ß/g, 'ss');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove any remaining special characters and convert to lowercase
|
|
||||||
const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, '');
|
const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, '');
|
||||||
const cleanLastname = convertUmlauts(lastname).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<voi
|
|||||||
let query = `
|
let query = `
|
||||||
SELECT
|
SELECT
|
||||||
e.id, e.email, e.firstname, e.lastname,
|
e.id, e.email, e.firstname, e.lastname,
|
||||||
e.is_active as isActive,
|
e.is_active,
|
||||||
e.employee_type as employeeType,
|
e.employee_type,
|
||||||
e.contract_type as contractType,
|
e.contract_type,
|
||||||
e.can_work_alone as canWorkAlone,
|
e.can_work_alone,
|
||||||
e.created_at as createdAt,
|
e.is_trainee,
|
||||||
e.last_login as lastLogin,
|
e.created_at,
|
||||||
|
e.last_login,
|
||||||
er.role
|
er.role
|
||||||
FROM employees e
|
FROM employees e
|
||||||
LEFT JOIN employee_roles er ON e.id = er.employee_id
|
LEFT JOIN employee_roles er ON e.id = er.employee_id
|
||||||
@@ -49,14 +48,23 @@ export const getEmployees = async (req: AuthRequest, res: Response): Promise<voi
|
|||||||
query += ' WHERE e.is_active = 1';
|
query += ' WHERE e.is_active = 1';
|
||||||
}
|
}
|
||||||
|
|
||||||
// UPDATED: Order by firstname and lastname
|
|
||||||
query += ' ORDER BY e.firstname, e.lastname';
|
query += ' ORDER BY e.firstname, e.lastname';
|
||||||
|
|
||||||
const employees = await db.all<any>(query);
|
const employees = await db.all<any>(query);
|
||||||
|
|
||||||
// Format employees with roles array for frontend compatibility
|
// Format employees with proper field names and roles array
|
||||||
const employeesWithRoles = employees.map(emp => ({
|
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']
|
roles: emp.role ? [emp.role] : ['user']
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -72,16 +80,16 @@ export const getEmployee = async (req: AuthRequest, res: Response): Promise<void
|
|||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
// UPDATED: Query with role from employee_roles table
|
|
||||||
const employee = await db.get<any>(`
|
const employee = await db.get<any>(`
|
||||||
SELECT
|
SELECT
|
||||||
e.id, e.email, e.firstname, e.lastname,
|
e.id, e.email, e.firstname, e.lastname,
|
||||||
e.is_active as isActive,
|
e.is_active,
|
||||||
e.employee_type as employeeType,
|
e.employee_type,
|
||||||
e.contract_type as contractType,
|
e.contract_type,
|
||||||
e.can_work_alone as canWorkAlone,
|
e.can_work_alone,
|
||||||
e.created_at as createdAt,
|
e.is_trainee,
|
||||||
e.last_login as lastLogin,
|
e.created_at,
|
||||||
|
e.last_login,
|
||||||
er.role
|
er.role
|
||||||
FROM employees e
|
FROM employees e
|
||||||
LEFT JOIN employee_roles er ON e.id = er.employee_id
|
LEFT JOIN employee_roles er ON e.id = er.employee_id
|
||||||
@@ -94,9 +102,19 @@ export const getEmployee = async (req: AuthRequest, res: Response): Promise<void
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format employee with roles array
|
// Format employee with proper field names
|
||||||
const employeeWithRoles = {
|
const employeeWithRoles = {
|
||||||
...employee,
|
id: employee.id,
|
||||||
|
email: employee.email,
|
||||||
|
firstname: employee.firstname,
|
||||||
|
lastname: employee.lastname,
|
||||||
|
isActive: employee.is_active === 1,
|
||||||
|
employeeType: employee.employee_type,
|
||||||
|
contractType: employee.contract_type,
|
||||||
|
canWorkAlone: employee.can_work_alone === 1,
|
||||||
|
isTrainee: employee.is_trainee === 1,
|
||||||
|
createdAt: employee.created_at,
|
||||||
|
lastLogin: employee.last_login,
|
||||||
roles: employee.role ? [employee.role] : ['user']
|
roles: employee.role ? [employee.role] : ['user']
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -118,17 +136,64 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
|
|||||||
password,
|
password,
|
||||||
firstname,
|
firstname,
|
||||||
lastname,
|
lastname,
|
||||||
roles = ['user'], // UPDATED: Now uses roles array
|
roles = ['user'],
|
||||||
employeeType,
|
employeeType,
|
||||||
contractType,
|
contractType,
|
||||||
canWorkAlone
|
canWorkAlone = false,
|
||||||
|
isTrainee = false
|
||||||
} = req.body as CreateEmployeeRequest;
|
} = req.body as CreateEmployeeRequest;
|
||||||
|
|
||||||
// Validation
|
// Validation
|
||||||
if (!password || !firstname || !lastname || !employeeType || !contractType) {
|
if (!password || !firstname || !lastname || !employeeType) {
|
||||||
console.log('❌ Validation failed: Missing required fields');
|
console.log('❌ Validation failed: Missing required fields');
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
error: 'Password, firstname, lastname, employeeType und contractType sind erforderlich'
|
error: 'Password, firstname, lastname und employeeType sind erforderlich'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ENHANCED: Validate employee type exists and get category info
|
||||||
|
const employeeTypeInfo = await db.get<{type: string, category: string, has_contract_type: number}>(
|
||||||
|
'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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -156,12 +221,12 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
|
|||||||
await db.run('BEGIN TRANSACTION');
|
await db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// UPDATED: Insert employee without role (role is now in employee_roles table)
|
// Insert employee with proper contract type handling
|
||||||
await db.run(
|
await db.run(
|
||||||
`INSERT INTO employees (
|
`INSERT INTO employees (
|
||||||
id, email, password, firstname, lastname, employee_type, contract_type, can_work_alone,
|
id, email, password, firstname, lastname, employee_type, contract_type, can_work_alone,
|
||||||
is_active
|
is_active, is_trainee
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
employeeId,
|
employeeId,
|
||||||
email,
|
email,
|
||||||
@@ -169,13 +234,14 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
|
|||||||
firstname,
|
firstname,
|
||||||
lastname,
|
lastname,
|
||||||
employeeType,
|
employeeType,
|
||||||
contractType,
|
contractType, // Will be NULL for external types
|
||||||
canWorkAlone ? 1 : 0,
|
canWorkAlone ? 1 : 0,
|
||||||
1
|
1,
|
||||||
|
isTrainee ? 1 : 0
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// UPDATED: Insert roles into employee_roles table
|
// Insert roles into employee_roles table
|
||||||
for (const role of roles) {
|
for (const role of roles) {
|
||||||
await db.run(
|
await db.run(
|
||||||
`INSERT INTO employee_roles (employee_id, role) VALUES (?, ?)`,
|
`INSERT INTO employee_roles (employee_id, role) VALUES (?, ?)`,
|
||||||
@@ -185,16 +251,17 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
|
|||||||
|
|
||||||
await db.run('COMMIT');
|
await db.run('COMMIT');
|
||||||
|
|
||||||
// Return created employee with role from employee_roles
|
// Return created employee
|
||||||
const newEmployee = await db.get<any>(`
|
const newEmployee = await db.get<any>(`
|
||||||
SELECT
|
SELECT
|
||||||
e.id, e.email, e.firstname, e.lastname,
|
e.id, e.email, e.firstname, e.lastname,
|
||||||
e.is_active as isActive,
|
e.is_active,
|
||||||
e.employee_type as employeeType,
|
e.employee_type,
|
||||||
e.contract_type as contractType,
|
e.contract_type,
|
||||||
e.can_work_alone as canWorkAlone,
|
e.can_work_alone,
|
||||||
e.created_at as createdAt,
|
e.is_trainee,
|
||||||
e.last_login as lastLogin,
|
e.created_at,
|
||||||
|
e.last_login,
|
||||||
er.role
|
er.role
|
||||||
FROM employees e
|
FROM employees e
|
||||||
LEFT JOIN employee_roles er ON e.id = er.employee_id
|
LEFT JOIN employee_roles er ON e.id = er.employee_id
|
||||||
@@ -202,9 +269,19 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
`, [employeeId]);
|
`, [employeeId]);
|
||||||
|
|
||||||
// Format response with roles array
|
// Format response with proper field names
|
||||||
const employeeWithRoles = {
|
const employeeWithRoles = {
|
||||||
...newEmployee,
|
id: newEmployee.id,
|
||||||
|
email: newEmployee.email,
|
||||||
|
firstname: newEmployee.firstname,
|
||||||
|
lastname: newEmployee.lastname,
|
||||||
|
isActive: newEmployee.is_active === 1,
|
||||||
|
employeeType: newEmployee.employee_type,
|
||||||
|
contractType: newEmployee.contract_type,
|
||||||
|
canWorkAlone: newEmployee.can_work_alone === 1,
|
||||||
|
isTrainee: newEmployee.is_trainee === 1,
|
||||||
|
createdAt: newEmployee.created_at,
|
||||||
|
lastLogin: newEmployee.last_login,
|
||||||
roles: newEmployee.role ? [newEmployee.role] : ['user']
|
roles: newEmployee.role ? [newEmployee.role] : ['user']
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -223,9 +300,12 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
|
|||||||
export const updateEmployee = async (req: AuthRequest, res: Response): Promise<void> => {
|
export const updateEmployee = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { 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
|
// Check if employee exists and get current data
|
||||||
const existingEmployee = await db.get<any>('SELECT * FROM employees WHERE id = ?', [id]);
|
const existingEmployee = await db.get<any>('SELECT * FROM employees WHERE id = ?', [id]);
|
||||||
@@ -234,6 +314,21 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate employee type if provided
|
||||||
|
if (employeeType) {
|
||||||
|
const validEmployeeType = await db.get(
|
||||||
|
'SELECT type FROM employee_types WHERE type = ?',
|
||||||
|
[employeeType]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!validEmployeeType) {
|
||||||
|
res.status(400).json({
|
||||||
|
error: `Ungültiger employeeType: ${employeeType}`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Generate new email if firstname or lastname changed
|
// Generate new email if firstname or lastname changed
|
||||||
let email = existingEmployee.email;
|
let email = existingEmployee.email;
|
||||||
if (firstname || lastname) {
|
if (firstname || lastname) {
|
||||||
@@ -259,7 +354,7 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
|
|||||||
await db.run('BEGIN TRANSACTION');
|
await db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// UPDATED: Update employee without role (role is now in employee_roles table)
|
// Update employee with new schema
|
||||||
await db.run(
|
await db.run(
|
||||||
`UPDATE employees
|
`UPDATE employees
|
||||||
SET firstname = COALESCE(?, firstname),
|
SET firstname = COALESCE(?, firstname),
|
||||||
@@ -268,12 +363,13 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
|
|||||||
is_active = COALESCE(?, is_active),
|
is_active = COALESCE(?, is_active),
|
||||||
employee_type = COALESCE(?, employee_type),
|
employee_type = COALESCE(?, employee_type),
|
||||||
contract_type = COALESCE(?, contract_type),
|
contract_type = COALESCE(?, contract_type),
|
||||||
can_work_alone = COALESCE(?, can_work_alone)
|
can_work_alone = COALESCE(?, can_work_alone),
|
||||||
|
is_trainee = COALESCE(?, is_trainee)
|
||||||
WHERE id = ?`,
|
WHERE id = ?`,
|
||||||
[firstname, lastname, email, isActive, employeeType, contractType, canWorkAlone, id]
|
[firstname, lastname, email, isActive, employeeType, contractType, canWorkAlone, isTrainee, id]
|
||||||
);
|
);
|
||||||
|
|
||||||
// UPDATED: Update roles if provided
|
// Update roles if provided
|
||||||
if (roles) {
|
if (roles) {
|
||||||
// Delete existing roles
|
// Delete existing roles
|
||||||
await db.run('DELETE FROM employee_roles WHERE employee_id = ?', [id]);
|
await db.run('DELETE FROM employee_roles WHERE employee_id = ?', [id]);
|
||||||
@@ -291,16 +387,17 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
|
|||||||
|
|
||||||
console.log('✅ Employee updated successfully with email:', email);
|
console.log('✅ Employee updated successfully with email:', email);
|
||||||
|
|
||||||
// Return updated employee with role from employee_roles
|
// Return updated employee
|
||||||
const updatedEmployee = await db.get<any>(`
|
const updatedEmployee = await db.get<any>(`
|
||||||
SELECT
|
SELECT
|
||||||
e.id, e.email, e.firstname, e.lastname,
|
e.id, e.email, e.firstname, e.lastname,
|
||||||
e.is_active as isActive,
|
e.is_active,
|
||||||
e.employee_type as employeeType,
|
e.employee_type,
|
||||||
e.contract_type as contractType,
|
e.contract_type,
|
||||||
e.can_work_alone as canWorkAlone,
|
e.can_work_alone,
|
||||||
e.created_at as createdAt,
|
e.is_trainee,
|
||||||
e.last_login as lastLogin,
|
e.created_at,
|
||||||
|
e.last_login,
|
||||||
er.role
|
er.role
|
||||||
FROM employees e
|
FROM employees e
|
||||||
LEFT JOIN employee_roles er ON e.id = er.employee_id
|
LEFT JOIN employee_roles er ON e.id = er.employee_id
|
||||||
@@ -308,9 +405,19 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
`, [id]);
|
`, [id]);
|
||||||
|
|
||||||
// Format response with roles array
|
// Format response with proper field names
|
||||||
const employeeWithRoles = {
|
const employeeWithRoles = {
|
||||||
...updatedEmployee,
|
id: updatedEmployee.id,
|
||||||
|
email: updatedEmployee.email,
|
||||||
|
firstname: updatedEmployee.firstname,
|
||||||
|
lastname: updatedEmployee.lastname,
|
||||||
|
isActive: updatedEmployee.is_active === 1,
|
||||||
|
employeeType: updatedEmployee.employee_type,
|
||||||
|
contractType: updatedEmployee.contract_type,
|
||||||
|
canWorkAlone: updatedEmployee.can_work_alone === 1,
|
||||||
|
isTrainee: updatedEmployee.is_trainee === 1,
|
||||||
|
createdAt: updatedEmployee.created_at,
|
||||||
|
lastLogin: updatedEmployee.last_login,
|
||||||
roles: updatedEmployee.role ? [updatedEmployee.role] : ['user']
|
roles: updatedEmployee.role ? [updatedEmployee.role] : ['user']
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -434,6 +541,7 @@ export const getAvailabilities = async (req: AuthRequest, res: Response): Promis
|
|||||||
id: avail.id,
|
id: avail.id,
|
||||||
employeeId: avail.employee_id,
|
employeeId: avail.employee_id,
|
||||||
planId: avail.plan_id,
|
planId: avail.plan_id,
|
||||||
|
shiftId: avail.shift_id,
|
||||||
dayOfWeek: avail.day_of_week,
|
dayOfWeek: avail.day_of_week,
|
||||||
timeSlotId: avail.time_slot_id,
|
timeSlotId: avail.time_slot_id,
|
||||||
preferenceLevel: avail.preference_level,
|
preferenceLevel: avail.preference_level,
|
||||||
@@ -562,3 +670,33 @@ export const changePassword = async (req: AuthRequest, res: Response): Promise<v
|
|||||||
res.status(500).json({ error: 'Internal server error' });
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const updateLastLogin = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||||
|
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' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -5,9 +5,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import { db } from '../services/databaseService.js';
|
import { db } from '../services/databaseService.js';
|
||||||
|
|
||||||
// Add the same email generation function
|
|
||||||
function generateEmail(firstname: string, lastname: string): string {
|
function generateEmail(firstname: string, lastname: string): string {
|
||||||
// Convert German umlauts to their expanded forms
|
|
||||||
const convertUmlauts = (str: string): string => {
|
const convertUmlauts = (str: string): string => {
|
||||||
return str
|
return str
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -17,7 +15,6 @@ function generateEmail(firstname: string, lastname: string): string {
|
|||||||
.replace(/ß/g, 'ss');
|
.replace(/ß/g, 'ss');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove any remaining special characters and convert to lowercase
|
|
||||||
const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, '');
|
const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, '');
|
||||||
const cleanLastname = convertUmlauts(lastname).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<void> =>
|
|||||||
await db.run('BEGIN TRANSACTION');
|
await db.run('BEGIN TRANSACTION');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create admin user in employees table
|
// ✅ CORRECTED: Create admin with valid 'manager' type and 'flexible' contract
|
||||||
await db.run(
|
await db.run(
|
||||||
`INSERT INTO employees (id, email, password, firstname, lastname, employee_type, contract_type, can_work_alone, is_active)
|
`INSERT INTO employees (id, email, password, firstname, lastname, employee_type, contract_type, can_work_alone, is_active, is_trainee)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[adminId, email, hashedPassword, firstname, lastname, 'manager', 'large', true, 1]
|
[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(
|
await db.run(
|
||||||
`INSERT INTO employee_roles (employee_id, role) VALUES (?, ?)`,
|
`INSERT INTO employee_roles (employee_id, role) VALUES (?, ?)`,
|
||||||
[adminId, 'admin']
|
[adminId, 'admin']
|
||||||
|
|||||||
@@ -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
|
-- Employees table
|
||||||
CREATE TABLE IF NOT EXISTS employees (
|
CREATE TABLE IF NOT EXISTS employees (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
@@ -5,30 +30,27 @@ CREATE TABLE IF NOT EXISTS employees (
|
|||||||
password TEXT NOT NULL,
|
password TEXT NOT NULL,
|
||||||
firstname TEXT NOT NULL,
|
firstname TEXT NOT NULL,
|
||||||
lastname TEXT NOT NULL,
|
lastname TEXT NOT NULL,
|
||||||
employee_type TEXT CHECK(employee_type IN ('manager', 'trainee', 'experienced')) NOT NULL,
|
employee_type TEXT NOT NULL REFERENCES employee_types(type),
|
||||||
contract_type TEXT CHECK(contract_type IN ('small', 'large')) NOT NULL,
|
contract_type TEXT CHECK(contract_type IN ('small', 'large', 'flexible')),
|
||||||
can_work_alone BOOLEAN DEFAULT FALSE,
|
can_work_alone BOOLEAN DEFAULT FALSE,
|
||||||
|
is_trainee BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
last_login TEXT DEFAULT NULL
|
last_login TEXT DEFAULT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Roles lookup table
|
-- Roles Employee Junction table (NACH employees und roles)
|
||||||
CREATE TABLE IF NOT EXISTS roles (
|
|
||||||
role TEXT PRIMARY KEY CHECK(role IN ('admin', 'user', 'maintenance'))
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Junction table: many-to-many relationship
|
|
||||||
CREATE TABLE IF NOT EXISTS employee_roles (
|
CREATE TABLE IF NOT EXISTS employee_roles (
|
||||||
employee_id TEXT NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
|
employee_id TEXT NOT NULL REFERENCES employees(id) ON DELETE CASCADE,
|
||||||
role TEXT NOT NULL REFERENCES roles(role),
|
role TEXT NOT NULL REFERENCES roles(role),
|
||||||
PRIMARY KEY (employee_id, role)
|
PRIMARY KEY (employee_id, role)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Insert default roles if they don't exist
|
-- Insert default roles (NACH roles Tabelle)
|
||||||
INSERT OR IGNORE INTO roles (role) VALUES ('admin');
|
INSERT OR IGNORE INTO roles (role, authority_level, description) VALUES
|
||||||
INSERT OR IGNORE INTO roles (role) VALUES ('user');
|
('admin', 100, 'Vollzugriff'),
|
||||||
INSERT OR IGNORE INTO roles (role) VALUES ('maintenance');
|
('maintenance', 50, 'Wartungszugriff'),
|
||||||
|
('user', 10, 'Standardbenutzer');
|
||||||
|
|
||||||
-- Shift plans table
|
-- Shift plans table
|
||||||
CREATE TABLE IF NOT EXISTS shift_plans (
|
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)
|
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_email_active ON employees(email, is_active);
|
||||||
CREATE INDEX IF NOT EXISTS idx_employees_type_active ON employees(employee_type, 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_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_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_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_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_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_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_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_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_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_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_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_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_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_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);
|
CREATE INDEX IF NOT EXISTS idx_employee_availability_employee_plan ON employee_availability(employee_id, plan_id);
|
||||||
@@ -44,10 +44,11 @@ export const authMiddleware = (req: AuthRequest, res: Response, next: NextFuncti
|
|||||||
export const requireRole = (roles: string[]) => {
|
export const requireRole = (roles: string[]) => {
|
||||||
return (req: AuthRequest, res: Response, next: NextFunction): void => {
|
return (req: AuthRequest, res: Response, next: NextFunction): void => {
|
||||||
if (!req.user || !roles.includes(req.user.role)) {
|
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.' });
|
res.status(403).json({ error: 'Access denied. Insufficient permissions.' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
console.log(`✅ Role check passed for user: ${req.user.email}, role: ${req.user.role}`);
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -4,10 +4,11 @@ export interface Employee {
|
|||||||
email: string;
|
email: string;
|
||||||
firstname: string;
|
firstname: string;
|
||||||
lastname: string;
|
lastname: string;
|
||||||
employeeType: 'manager' | 'trainee' | 'experienced';
|
employeeType: 'manager' | 'personell' | 'apprentice' | 'guest';
|
||||||
contractType: 'small' | 'large';
|
contractType?: 'small' | 'large' | 'flexible';
|
||||||
canWorkAlone: boolean;
|
canWorkAlone: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
isTrainee: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
lastLogin?: string | null;
|
lastLogin?: string | null;
|
||||||
roles?: string[];
|
roles?: string[];
|
||||||
@@ -18,19 +19,21 @@ export interface CreateEmployeeRequest {
|
|||||||
firstname: string;
|
firstname: string;
|
||||||
lastname: string;
|
lastname: string;
|
||||||
roles?: string[];
|
roles?: string[];
|
||||||
employeeType: 'manager' | 'trainee' | 'experienced';
|
employeeType: 'manager' | 'personell' | 'apprentice' | 'guest';
|
||||||
contractType: 'small' | 'large';
|
contractType?: 'small' | 'large' | 'flexible';
|
||||||
canWorkAlone: boolean;
|
canWorkAlone: boolean;
|
||||||
|
isTrainee?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateEmployeeRequest {
|
export interface UpdateEmployeeRequest {
|
||||||
firstname?: string;
|
firstname?: string;
|
||||||
lastname?: string;
|
lastname?: string;
|
||||||
roles?: string[];
|
roles?: string[];
|
||||||
employeeType?: 'manager' | 'trainee' | 'experienced';
|
employeeType?: 'manager' | 'personell' | 'apprentice' | 'guest';
|
||||||
contractType?: 'small' | 'large';
|
contractType?: 'small' | 'large' | 'flexible';
|
||||||
canWorkAlone?: boolean;
|
canWorkAlone?: boolean;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
isTrainee?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EmployeeWithPassword extends Employee {
|
export interface EmployeeWithPassword extends Employee {
|
||||||
@@ -41,7 +44,7 @@ export interface EmployeeAvailability {
|
|||||||
id: string;
|
id: string;
|
||||||
employeeId: string;
|
employeeId: string;
|
||||||
planId: 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
|
preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable
|
||||||
notes?: string;
|
notes?: string;
|
||||||
}
|
}
|
||||||
@@ -84,3 +87,10 @@ export interface EmployeeRole {
|
|||||||
employeeId: string;
|
employeeId: string;
|
||||||
role: 'admin' | 'user' | 'maintenance';
|
role: 'admin' | 'user' | 'maintenance';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Employee type configuration
|
||||||
|
export interface EmployeeType {
|
||||||
|
type: 'manager' | 'personell' | 'apprentice' | 'guest';
|
||||||
|
category: 'internal' | 'external';
|
||||||
|
has_contract_type: boolean;
|
||||||
|
}
|
||||||
@@ -4,19 +4,41 @@ import { EmployeeAvailability, ManagerAvailability } from '../Employee.js';
|
|||||||
// Default employee data for quick creation
|
// Default employee data for quick creation
|
||||||
export const EMPLOYEE_DEFAULTS = {
|
export const EMPLOYEE_DEFAULTS = {
|
||||||
role: 'user' as const,
|
role: 'user' as const,
|
||||||
employeeType: 'experienced' as const,
|
employeeType: 'personell' as const,
|
||||||
contractType: 'small' as const,
|
contractType: 'small' as const,
|
||||||
canWorkAlone: false,
|
canWorkAlone: false,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
isTrainee: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Manager-specific defaults
|
// Manager-specific defaults
|
||||||
export const MANAGER_DEFAULTS = {
|
export const MANAGER_DEFAULTS = {
|
||||||
role: 'admin' as const,
|
role: 'admin' as const,
|
||||||
employeeType: 'manager' as const,
|
employeeType: 'manager' as const,
|
||||||
contractType: 'large' as const,
|
contractType: 'flexible' as const,
|
||||||
canWorkAlone: true,
|
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 = {
|
export const EMPLOYEE_TYPE_CONFIG = {
|
||||||
@@ -24,22 +46,37 @@ export const EMPLOYEE_TYPE_CONFIG = {
|
|||||||
value: 'manager' as const,
|
value: 'manager' as const,
|
||||||
label: 'Chef/Administrator',
|
label: 'Chef/Administrator',
|
||||||
color: '#e74c3c',
|
color: '#e74c3c',
|
||||||
|
category: 'internal' as const,
|
||||||
|
hasContractType: true,
|
||||||
independent: true,
|
independent: true,
|
||||||
description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung'
|
description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung'
|
||||||
},
|
},
|
||||||
experienced: {
|
personell: {
|
||||||
value: 'experienced' as const,
|
value: 'personell' as const,
|
||||||
label: 'Erfahren',
|
label: 'Personal',
|
||||||
color: '#3498db',
|
color: '#3498db',
|
||||||
|
category: 'internal' as const,
|
||||||
|
hasContractType: true,
|
||||||
independent: true,
|
independent: true,
|
||||||
description: 'Langjährige Erfahrung, kann komplexe Aufgaben übernehmen'
|
description: 'Reguläre Mitarbeiter mit Vertrag'
|
||||||
},
|
},
|
||||||
trainee: {
|
apprentice: {
|
||||||
value: 'trainee' as const,
|
value: 'apprentice' as const,
|
||||||
label: 'Neuling',
|
label: 'Auszubildender',
|
||||||
color: '#27ae60',
|
color: '#9b59b6',
|
||||||
|
category: 'internal' as const,
|
||||||
|
hasContractType: true,
|
||||||
independent: false,
|
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;
|
} as const;
|
||||||
|
|
||||||
@@ -53,9 +90,10 @@ export const ROLE_CONFIG = [
|
|||||||
export const CONTRACT_TYPE_DESCRIPTIONS = {
|
export const CONTRACT_TYPE_DESCRIPTIONS = {
|
||||||
small: '1 Schicht pro Woche',
|
small: '1 Schicht pro Woche',
|
||||||
large: '2 Schichten pro Woche',
|
large: '2 Schichten pro Woche',
|
||||||
manager: 'Kein Vertragslimit - Immer MO und DI verfügbar'
|
flexible: 'Flexible Arbeitszeiten'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
||||||
// Availability preference descriptions
|
// Availability preference descriptions
|
||||||
export const AVAILABILITY_PREFERENCES = {
|
export const AVAILABILITY_PREFERENCES = {
|
||||||
1: { label: 'Bevorzugt', color: '#10b981', description: 'Möchte diese Schicht arbeiten' },
|
1: { label: 'Bevorzugt', color: '#10b981', description: 'Möchte diese Schicht arbeiten' },
|
||||||
@@ -105,3 +143,16 @@ export function createManagerDefaultSchedule(managerId: string, planId: string,
|
|||||||
|
|
||||||
return assignments;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
// backend/src/models/helpers/employeeHelpers.ts
|
// backend/src/models/helpers/employeeHelpers.ts
|
||||||
import { Employee, CreateEmployeeRequest, EmployeeAvailability } from '../Employee.js';
|
import { Employee, CreateEmployeeRequest, EmployeeAvailability } from '../Employee.js';
|
||||||
|
|
||||||
// Email generation function (same as in controllers)
|
// Email generation function
|
||||||
function generateEmail(firstname: string, lastname: string): string {
|
function generateEmail(firstname: string, lastname: string): string {
|
||||||
// Convert German umlauts to their expanded forms
|
|
||||||
const convertUmlauts = (str: string): string => {
|
const convertUmlauts = (str: string): string => {
|
||||||
return str
|
return str
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -13,19 +12,16 @@ function generateEmail(firstname: string, lastname: string): string {
|
|||||||
.replace(/ß/g, 'ss');
|
.replace(/ß/g, 'ss');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove any remaining special characters and convert to lowercase
|
|
||||||
const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, '');
|
const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, '');
|
||||||
const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, '');
|
const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, '');
|
||||||
|
|
||||||
return `${cleanFirstname}.${cleanLastname}@sp.de`;
|
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[] {
|
export function validateEmployeeData(employee: CreateEmployeeRequest): string[] {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
// Email is now auto-generated, so no email validation needed
|
|
||||||
|
|
||||||
if (employee.password?.length < 6) {
|
if (employee.password?.length < 6) {
|
||||||
errors.push('Password must be at least 6 characters long');
|
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');
|
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;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate email for employee (new helper function)
|
// Generate email for employee
|
||||||
export function generateEmployeeEmail(firstname: string, lastname: string): string {
|
export function generateEmployeeEmail(firstname: string, lastname: string): string {
|
||||||
return generateEmail(firstname, lastname);
|
return generateEmail(firstname, lastname);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simplified business logic helpers
|
// UPDATED: Business logic helpers for new employee types
|
||||||
export const isManager = (employee: Employee): boolean =>
|
export const isManager = (employee: Employee): boolean =>
|
||||||
employee.employeeType === 'manager';
|
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 =>
|
export const isTrainee = (employee: Employee): boolean =>
|
||||||
employee.employeeType === 'trainee';
|
employee.employeeType === 'personell' && employee.isTrainee;
|
||||||
|
|
||||||
export const isExperienced = (employee: Employee): boolean =>
|
export const isExperienced = (employee: Employee): boolean =>
|
||||||
employee.employeeType === 'experienced';
|
employee.employeeType === 'personell' && !employee.isTrainee;
|
||||||
|
|
||||||
|
// Role-based helpers
|
||||||
export const isAdmin = (employee: Employee): boolean =>
|
export const isAdmin = (employee: Employee): boolean =>
|
||||||
employee.roles?.includes('admin') || false;
|
employee.roles?.includes('admin') || false;
|
||||||
|
|
||||||
@@ -65,13 +107,11 @@ export const isMaintenance = (employee: Employee): boolean =>
|
|||||||
export const isUser = (employee: Employee): boolean =>
|
export const isUser = (employee: Employee): boolean =>
|
||||||
employee.roles?.includes('user') || false;
|
employee.roles?.includes('user') || false;
|
||||||
|
|
||||||
|
// UPDATED: Work alone permission - managers and experienced personell can work alone
|
||||||
export const canEmployeeWorkAlone = (employee: Employee): boolean =>
|
export const canEmployeeWorkAlone = (employee: Employee): boolean =>
|
||||||
employee.canWorkAlone && isExperienced(employee);
|
employee.canWorkAlone && (isManager(employee) || isExperienced(employee));
|
||||||
|
|
||||||
export const getEmployeeWorkHours = (employee: Employee): number =>
|
// Helper for full name display
|
||||||
isManager(employee) ? 999 : (employee.contractType === 'small' ? 1 : 2);
|
|
||||||
|
|
||||||
// New helper for full name display
|
|
||||||
export const getFullName = (employee: { firstname: string; lastname: string }): string =>
|
export const getFullName = (employee: { firstname: string; lastname: string }): string =>
|
||||||
`${employee.firstname} ${employee.lastname}`;
|
`${employee.firstname} ${employee.lastname}`;
|
||||||
|
|
||||||
@@ -93,3 +133,13 @@ export function validateAvailabilityData(availability: Omit<EmployeeAvailability
|
|||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UPDATED: Helper to get employee type category
|
||||||
|
export const getEmployeeCategory = (employee: Employee): 'internal' | 'external' => {
|
||||||
|
return isInternal(employee) ? 'internal' : 'external';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to check if employee requires contract type
|
||||||
|
export const requiresContractType = (employee: Employee): boolean => {
|
||||||
|
return isInternal(employee);
|
||||||
|
};
|
||||||
@@ -7,10 +7,10 @@ export interface Availability {
|
|||||||
id: string;
|
id: string;
|
||||||
employeeId: string;
|
employeeId: string;
|
||||||
planId: 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
|
preferenceLevel: 1 | 2 | 3;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
// Optional convenience fields (can be joined from shifts and time_slots tables)
|
// Optional convenience fields
|
||||||
dayOfWeek?: number;
|
dayOfWeek?: number;
|
||||||
timeSlotId?: string;
|
timeSlotId?: string;
|
||||||
timeSlotName?: string;
|
timeSlotName?: string;
|
||||||
@@ -30,7 +30,7 @@ export interface Constraint {
|
|||||||
maxHoursPerWeek?: number;
|
maxHoursPerWeek?: number;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
weight?: number; // For soft constraints
|
weight?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScheduleRequest {
|
export interface ScheduleRequest {
|
||||||
@@ -153,8 +153,9 @@ export interface AvailabilityWithDetails extends Availability {
|
|||||||
id: string;
|
id: string;
|
||||||
firstname: string;
|
firstname: string;
|
||||||
lastname: string;
|
lastname: string;
|
||||||
employeeType: 'manager' | 'trainee' | 'experienced';
|
employeeType: 'manager' | 'personell' | 'apprentice' | 'guest';
|
||||||
canWorkAlone: boolean;
|
canWorkAlone: boolean;
|
||||||
|
isTrainee: boolean;
|
||||||
};
|
};
|
||||||
shift?: {
|
shift?: {
|
||||||
dayOfWeek: number;
|
dayOfWeek: number;
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
deleteEmployee,
|
deleteEmployee,
|
||||||
getAvailabilities,
|
getAvailabilities,
|
||||||
updateAvailabilities,
|
updateAvailabilities,
|
||||||
changePassword
|
changePassword,
|
||||||
|
updateLastLogin
|
||||||
} from '../controllers/employeeController.js';
|
} from '../controllers/employeeController.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -18,12 +19,13 @@ const router = express.Router();
|
|||||||
router.use(authMiddleware);
|
router.use(authMiddleware);
|
||||||
|
|
||||||
// Employee CRUD Routes
|
// Employee CRUD Routes
|
||||||
router.get('/', authMiddleware, getEmployees);
|
router.get('/', requireRole(['admin']), getEmployees);
|
||||||
router.get('/:id', requireRole(['admin', 'instandhalter']), getEmployee);
|
router.get('/:id', requireRole(['admin', 'instandhalter']), getEmployee);
|
||||||
router.post('/', requireRole(['admin']), createEmployee);
|
router.post('/', requireRole(['admin']), createEmployee);
|
||||||
router.put('/:id', requireRole(['admin']), updateEmployee);
|
router.put('/:id', requireRole(['admin']), updateEmployee);
|
||||||
router.delete('/:id', requireRole(['admin']), deleteEmployee);
|
router.delete('/:id', requireRole(['admin']), deleteEmployee);
|
||||||
router.put('/:id/password', authMiddleware, changePassword);
|
router.put('/:id/password', authMiddleware, changePassword);
|
||||||
|
router.put('/:id/last-login', authMiddleware, updateLastLogin);
|
||||||
|
|
||||||
// Availability Routes
|
// Availability Routes
|
||||||
router.get('/:employeeId/availabilities', authMiddleware, getAvailabilities);
|
router.get('/:employeeId/availabilities', authMiddleware, getAvailabilities);
|
||||||
|
|||||||
@@ -6,6 +6,25 @@ import { dirname, join } from 'path';
|
|||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
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
|
// Helper function to ensure migrations are tracked
|
||||||
async function ensureMigrationTable() {
|
async function ensureMigrationTable() {
|
||||||
await db.exec(`
|
await db.exec(`
|
||||||
@@ -19,7 +38,7 @@ async function ensureMigrationTable() {
|
|||||||
|
|
||||||
// Helper function to check if a migration has been applied
|
// Helper function to check if a migration has been applied
|
||||||
async function isMigrationApplied(migrationName: string): Promise<boolean> {
|
async function isMigrationApplied(migrationName: string): Promise<boolean> {
|
||||||
const result = await db.get<{ count: number }>(
|
const result = await db.get<CountResult>(
|
||||||
'SELECT COUNT(*) as count FROM applied_migrations WHERE name = ?',
|
'SELECT COUNT(*) as count FROM applied_migrations WHERE name = ?',
|
||||||
[migrationName]
|
[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<TableColumnInfo>(`
|
||||||
|
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() {
|
export async function applyMigration() {
|
||||||
try {
|
try {
|
||||||
console.log('📦 Starting database migration...');
|
console.log('📦 Starting database migration...');
|
||||||
@@ -41,8 +139,13 @@ export async function applyMigration() {
|
|||||||
// Ensure migration tracking table exists
|
// Ensure migration tracking table exists
|
||||||
await ensureMigrationTable();
|
await ensureMigrationTable();
|
||||||
|
|
||||||
|
// Apply schema updates for new employee type system
|
||||||
|
await applySchemaUpdates();
|
||||||
|
|
||||||
// Get all migration files
|
// Get all migration files
|
||||||
const migrationsDir = join(__dirname, '../database/migrations');
|
const migrationsDir = join(__dirname, '../database/migrations');
|
||||||
|
|
||||||
|
try {
|
||||||
const files = await readdir(migrationsDir);
|
const files = await readdir(migrationsDir);
|
||||||
|
|
||||||
// Sort files to ensure consistent order
|
// Sort files to ensure consistent order
|
||||||
@@ -96,6 +199,9 @@ export async function applyMigration() {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('ℹ️ No migration directory found or no migration files, skipping file-based migrations...');
|
||||||
|
}
|
||||||
|
|
||||||
console.log('✅ All migrations completed successfully');
|
console.log('✅ All migrations completed successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export async function initializeDatabase(): Promise<void> {
|
|||||||
|
|
||||||
console.log('Existing tables found:', existingTables.map(t => t.name).join(', ') || 'none');
|
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 = [
|
const tablesToDrop = [
|
||||||
'employee_availability',
|
'employee_availability',
|
||||||
'shift_assignments',
|
'shift_assignments',
|
||||||
@@ -50,9 +50,10 @@ export async function initializeDatabase(): Promise<void> {
|
|||||||
'shifts',
|
'shifts',
|
||||||
'time_slots',
|
'time_slots',
|
||||||
'employee_roles',
|
'employee_roles',
|
||||||
'shift_plans',
|
|
||||||
'roles',
|
'roles',
|
||||||
'employees',
|
'employees',
|
||||||
|
'employee_types',
|
||||||
|
'shift_plans',
|
||||||
'applied_migrations'
|
'applied_migrations'
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -100,15 +101,22 @@ export async function initializeDatabase(): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UPDATED: Insert default roles after creating the tables
|
// UPDATED: Insert default data in correct order
|
||||||
try {
|
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...');
|
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, authority_level, description) VALUES ('admin', 100, 'Vollzugriff')`);
|
||||||
await db.run(`INSERT OR IGNORE INTO roles (role) VALUES ('user')`);
|
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) VALUES ('maintenance')`);
|
await db.run(`INSERT OR IGNORE INTO roles (role, authority_level, description) VALUES ('user', 10, 'Standardbenutzer')`);
|
||||||
console.log('✅ Default roles inserted');
|
console.log('✅ Default roles inserted');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error inserting default roles:', error);
|
console.error('Error inserting default data:', error);
|
||||||
await db.run('ROLLBACK');
|
await db.run('ROLLBACK');
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,16 +97,17 @@ export class SchedulingService {
|
|||||||
const currentDate = new Date(startDate);
|
const currentDate = new Date(startDate);
|
||||||
currentDate.setDate(startDate.getDate() + dayOffset);
|
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);
|
const dayShifts = shiftPlan.shifts.filter(shift => shift.dayOfWeek === dayOfWeek);
|
||||||
|
|
||||||
dayShifts.forEach(shift => {
|
dayShifts.forEach(shift => {
|
||||||
// ✅ Use day-of-week pattern instead of date-based pattern
|
// ✅ CRITICAL FIX: Use consistent shift ID format that matches availability lookup
|
||||||
const shiftId = `${shift.id}`;
|
const shiftId = `shift_${dayOfWeek}_${shift.timeSlotId}`;
|
||||||
|
const dateStr = currentDate.toISOString().split('T')[0];
|
||||||
|
|
||||||
shifts.push({
|
shifts.push({
|
||||||
id: shiftId, // This matches what frontend expects
|
id: shiftId, // This will match what availabilities are looking for
|
||||||
date: currentDate.toISOString().split('T')[0],
|
date: dateStr,
|
||||||
timeSlotId: shift.timeSlotId,
|
timeSlotId: shift.timeSlotId,
|
||||||
requiredEmployees: shift.requiredEmployees,
|
requiredEmployees: shift.requiredEmployees,
|
||||||
minWorkers: 1,
|
minWorkers: 1,
|
||||||
@@ -114,7 +115,7 @@ export class SchedulingService {
|
|||||||
isPriority: false
|
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}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,17 +17,42 @@ interface WorkerData {
|
|||||||
function buildSchedulingModel(model: CPModel, data: WorkerData): void {
|
function buildSchedulingModel(model: CPModel, data: WorkerData): void {
|
||||||
const { employees, shifts, availabilities, constraints } = data;
|
const { employees, shifts, availabilities, constraints } = data;
|
||||||
|
|
||||||
// Filter employees to only include active ones
|
const schedulableEmployees = employees.filter(emp =>
|
||||||
const nonManagerEmployees = employees.filter(emp => emp.isActive && emp.employeeType !== 'manager');
|
emp.isActive &&
|
||||||
const activeEmployees = employees.filter(emp => emp.isActive);
|
emp.employeeType === 'personell'
|
||||||
const trainees = nonManagerEmployees.filter(emp => emp.employeeType === 'trainee');
|
);
|
||||||
const experienced = nonManagerEmployees.filter(emp => emp.employeeType === 'experienced');
|
|
||||||
|
|
||||||
console.log(`Building model with ${nonManagerEmployees.length} employees, ${shifts.length} shifts`);
|
// Debug: Count constraints that will be created
|
||||||
console.log(`Available shifts per week: ${shifts.length}`);
|
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
|
// 1. Create assignment variables for all possible assignments
|
||||||
nonManagerEmployees.forEach((employee: any) => {
|
schedulableEmployees.forEach((employee: any) => {
|
||||||
shifts.forEach((shift: any) => {
|
shifts.forEach((shift: any) => {
|
||||||
const varName = `assign_${employee.id}_${shift.id}`;
|
const varName = `assign_${employee.id}_${shift.id}`;
|
||||||
model.addVariable(varName, 'bool');
|
model.addVariable(varName, 'bool');
|
||||||
@@ -35,18 +60,18 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 2. Availability constraints
|
// 2. Availability constraints
|
||||||
nonManagerEmployees.forEach((employee: any) => {
|
schedulableEmployees.forEach((employee: any) => {
|
||||||
shifts.forEach((shift: any) => {
|
shifts.forEach((shift: any) => {
|
||||||
const availability = availabilities.find(
|
const availability = availabilities.find(
|
||||||
(a: any) => a.employeeId === employee.id && a.shiftId === shift.id
|
(a: any) => a.employeeId === employee.id && a.shiftId === shift.id
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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(
|
model.addConstraint(
|
||||||
`${varName} == 0`,
|
`${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
|
// 3. Max 1 shift per day per employee
|
||||||
const shiftsByDate = groupShiftsByDate(shifts);
|
const shiftsByDate = groupShiftsByDate(shifts);
|
||||||
nonManagerEmployees.forEach((employee: any) => {
|
schedulableEmployees.forEach((employee: any) => {
|
||||||
Object.entries(shiftsByDate).forEach(([date, dayShifts]) => {
|
Object.entries(shiftsByDate).forEach(([date, dayShifts]) => {
|
||||||
const dayAssignmentVars = (dayShifts as any[]).map(
|
const dayAssignmentVars = (dayShifts as any[]).map(
|
||||||
(shift: any) => `assign_${employee.id}_${shift.id}`
|
(shift: any) => `assign_${employee.id}_${shift.id}`
|
||||||
@@ -71,7 +96,7 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
|
|||||||
|
|
||||||
// 4. Shift staffing constraints
|
// 4. Shift staffing constraints
|
||||||
shifts.forEach((shift: any) => {
|
shifts.forEach((shift: any) => {
|
||||||
const assignmentVars = nonManagerEmployees.map(
|
const assignmentVars = schedulableEmployees.map(
|
||||||
(emp: any) => `assign_${emp.id}_${shift.id}`
|
(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
|
// 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`);
|
console.log(`Found ${employeesWhoCantWorkAlone.length} employees who cannot work alone`);
|
||||||
|
|
||||||
employeesWhoCantWorkAlone.forEach((employee: any) => {
|
employeesWhoCantWorkAlone.forEach((employee: any) => {
|
||||||
shifts.forEach((shift: any) => {
|
shifts.forEach((shift: any) => {
|
||||||
const employeeVar = `assign_${employee.id}_${shift.id}`;
|
const employeeVar = `assign_${employee.id}_${shift.id}`;
|
||||||
const otherEmployees = nonManagerEmployees.filter(emp =>
|
const otherEmployees = schedulableEmployees.filter(emp =>
|
||||||
emp.id !== employee.id && emp.isActive
|
emp.id !== employee.id && emp.isActive
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -151,7 +176,7 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
|
|||||||
const totalShifts = shifts.length;
|
const totalShifts = shifts.length;
|
||||||
console.log(`Total available shifts: ${totalShifts}`);
|
console.log(`Total available shifts: ${totalShifts}`);
|
||||||
|
|
||||||
nonManagerEmployees.forEach((employee: any) => {
|
schedulableEmployees.forEach((employee: any) => {
|
||||||
const contractType = employee.contractType || 'large';
|
const contractType = employee.contractType || 'large';
|
||||||
|
|
||||||
// EXACT SHIFTS PER WEEK
|
// EXACT SHIFTS PER WEEK
|
||||||
@@ -182,7 +207,7 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
|
|||||||
let objectiveExpression = '';
|
let objectiveExpression = '';
|
||||||
let softConstraintPenalty = '';
|
let softConstraintPenalty = '';
|
||||||
|
|
||||||
nonManagerEmployees.forEach((employee: any) => {
|
schedulableEmployees.forEach((employee: any) => {
|
||||||
shifts.forEach((shift: any) => {
|
shifts.forEach((shift: any) => {
|
||||||
const varName = `assign_${employee.id}_${shift.id}`;
|
const varName = `assign_${employee.id}_${shift.id}`;
|
||||||
const availability = availabilities.find(
|
const availability = availabilities.find(
|
||||||
@@ -191,25 +216,28 @@ function buildSchedulingModel(model: CPModel, data: WorkerData): void {
|
|||||||
|
|
||||||
let score = 0;
|
let score = 0;
|
||||||
if (availability) {
|
if (availability) {
|
||||||
score = availability.preferenceLevel === 1 ? 10 :
|
// Only give positive scores for shifts employees actually signed up for
|
||||||
availability.preferenceLevel === 2 ? 5 :
|
if (availability.preferenceLevel === 1) {
|
||||||
-10000; // Very heavy penalty for unavailable
|
score = 100; // High reward for preferred shifts
|
||||||
} else {
|
} else if (availability.preferenceLevel === 2) {
|
||||||
// No availability info - slight preference to assign
|
score = 50; // Medium reward for available shifts
|
||||||
score = 1;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (score > 0) {
|
||||||
if (objectiveExpression) {
|
if (objectiveExpression) {
|
||||||
objectiveExpression += ` + ${score} * ${varName}`;
|
objectiveExpression += ` + ${score} * ${varName}`;
|
||||||
} else {
|
} else {
|
||||||
objectiveExpression = `${score} * ${varName}`;
|
objectiveExpression = `${score} * ${varName}`;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (objectiveExpression) {
|
if (objectiveExpression) {
|
||||||
model.maximize(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 assignedEmployees = assignments[shift.id] || [];
|
||||||
const hasTrainee = assignedEmployees.some((empId: string) => {
|
const hasTrainee = assignedEmployees.some((empId: string) => {
|
||||||
const emp = employeeMap.get(empId);
|
const emp = employeeMap.get(empId);
|
||||||
return emp?.employeeType === 'trainee';
|
return emp?.isTrainee;
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasExperienced = assignedEmployees.some((empId: string) => {
|
const hasExperienced = assignedEmployees.some((empId: string) => {
|
||||||
const emp = employeeMap.get(empId);
|
const emp = employeeMap.get(empId);
|
||||||
return emp?.employeeType === 'experienced';
|
return emp && !emp.isTrainee;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hasTrainee && !hasExperienced) {
|
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 {
|
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(`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) => {
|
managersToAssign.forEach((manager: any) => {
|
||||||
shifts.forEach((shift: 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)
|
// Check if manager is already assigned (avoid duplicates)
|
||||||
if (!assignments[shift.id].includes(manager.id)) {
|
if (!assignments[shift.id].includes(manager.id)) {
|
||||||
assignments[shift.id].push(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`);
|
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');
|
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();
|
const model = new CPModel();
|
||||||
buildSchedulingModel(model, data);
|
buildSchedulingModel(model, data);
|
||||||
|
|
||||||
@@ -406,7 +540,6 @@ async function runScheduling() {
|
|||||||
// Extract assignments from solution (non-managers only)
|
// Extract assignments from solution (non-managers only)
|
||||||
assignments = extractAssignmentsFromSolution(solution, nonManagerEmployees, data.shifts);
|
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);
|
assignments = assignManagersToShifts(assignments, data.employees, data.shifts, data.availabilities);
|
||||||
|
|
||||||
// Only detect violations for non-manager assignments
|
// 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');
|
violations.push('NO_ASSIGNMENTS: Solver reported success but produced no assignments');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update resolution report
|
|
||||||
if (violations.length === 0) {
|
if (violations.length === 0) {
|
||||||
resolutionReport.push('✅ No constraint violations detected for non-manager employees');
|
resolutionReport.push('✅ No constraint violations detected for non-manager employees');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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<SchedulerProps> = ({
|
|
||||||
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 (
|
|
||||||
<div style={{ padding: '20px', border: '1px solid #e0e0e0', borderRadius: '8px' }}>
|
|
||||||
<h3>Automatic Schedule Generation</h3>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleGenerateSchedule}
|
|
||||||
disabled={loading}
|
|
||||||
style={{
|
|
||||||
padding: '12px 24px',
|
|
||||||
backgroundColor: loading ? '#95a5a6' : '#3498db',
|
|
||||||
color: 'white',
|
|
||||||
border: 'none',
|
|
||||||
borderRadius: '4px',
|
|
||||||
cursor: loading ? 'not-allowed' : 'pointer',
|
|
||||||
fontSize: '16px',
|
|
||||||
fontWeight: 'bold'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{loading ? '🔄 Generating Schedule...' : '🚀 Generate Optimal Schedule'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{loading && (
|
|
||||||
<div style={{ marginTop: '15px' }}>
|
|
||||||
<div style={{
|
|
||||||
width: '100%',
|
|
||||||
height: '8px',
|
|
||||||
backgroundColor: '#ecf0f1',
|
|
||||||
borderRadius: '4px',
|
|
||||||
overflow: 'hidden'
|
|
||||||
}}>
|
|
||||||
<div style={{
|
|
||||||
width: '70%',
|
|
||||||
height: '100%',
|
|
||||||
backgroundColor: '#3498db',
|
|
||||||
animation: 'pulse 2s infinite',
|
|
||||||
borderRadius: '4px'
|
|
||||||
}} />
|
|
||||||
</div>
|
|
||||||
<p style={{ color: '#7f8c8d', fontSize: '14px', marginTop: '8px' }}>
|
|
||||||
Optimizing schedule... (max 2 minutes)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<div style={{
|
|
||||||
marginTop: '15px',
|
|
||||||
padding: '12px',
|
|
||||||
backgroundColor: '#f8d7da',
|
|
||||||
border: '1px solid #f5c6cb',
|
|
||||||
borderRadius: '4px',
|
|
||||||
color: '#721c24'
|
|
||||||
}}>
|
|
||||||
<strong>Error:</strong> {error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{result && (
|
|
||||||
<div style={{ marginTop: '20px' }}>
|
|
||||||
<ScheduleResultView result={result} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ScheduleResultView: React.FC<{ result: any }> = ({ result }) => {
|
|
||||||
return (
|
|
||||||
<div style={{
|
|
||||||
padding: '15px',
|
|
||||||
backgroundColor: result.success ? '#d4edda' : '#f8d7da',
|
|
||||||
border: `1px solid ${result.success ? '#c3e6cb' : '#f5c6cb'}`,
|
|
||||||
borderRadius: '4px'
|
|
||||||
}}>
|
|
||||||
<h4 style={{
|
|
||||||
color: result.success ? '#155724' : '#721c24',
|
|
||||||
marginTop: 0
|
|
||||||
}}>
|
|
||||||
{result.success ? '✅ Schedule Generated Successfully' : '❌ Schedule Generation Failed'}
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '10px' }}>
|
|
||||||
<strong>Assignments:</strong> {Object.keys(result.assignments || {}).length} shifts assigned
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ marginBottom: '10px' }}>
|
|
||||||
<strong>Violations:</strong> {result.violations?.length || 0}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{result.resolution_report && result.resolution_report.length > 0 && (
|
|
||||||
<details style={{ marginTop: '10px' }}>
|
|
||||||
<summary style={{ cursor: 'pointer', fontWeight: 'bold' }}>
|
|
||||||
Resolution Report
|
|
||||||
</summary>
|
|
||||||
<div style={{
|
|
||||||
marginTop: '10px',
|
|
||||||
maxHeight: '200px',
|
|
||||||
overflow: 'auto',
|
|
||||||
fontSize: '12px',
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
backgroundColor: 'rgba(0,0,0,0.05)',
|
|
||||||
padding: '10px',
|
|
||||||
borderRadius: '4px'
|
|
||||||
}}>
|
|
||||||
{result.resolution_report.map((line: string, index: number) => (
|
|
||||||
<div key={index} style={{ marginBottom: '2px' }}>{line}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</details>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Scheduler;
|
|
||||||
@@ -165,7 +165,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
|||||||
refreshUser,
|
refreshUser,
|
||||||
needsSetup: needsSetup === null ? true : needsSetup,
|
needsSetup: needsSetup === null ? true : needsSetup,
|
||||||
checkSetupStatus,
|
checkSetupStatus,
|
||||||
updateUser, // Add this to the context value
|
updateUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ export interface Employee {
|
|||||||
email: string;
|
email: string;
|
||||||
firstname: string;
|
firstname: string;
|
||||||
lastname: string;
|
lastname: string;
|
||||||
employeeType: 'manager' | 'trainee' | 'experienced';
|
employeeType: 'manager' | 'personell' | 'apprentice' | 'guest';
|
||||||
contractType: 'small' | 'large';
|
contractType?: 'small' | 'large' | 'flexible';
|
||||||
canWorkAlone: boolean;
|
canWorkAlone: boolean;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
isTrainee: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
lastLogin?: string | null;
|
lastLogin?: string | null;
|
||||||
roles?: string[];
|
roles?: string[];
|
||||||
@@ -17,20 +18,22 @@ export interface CreateEmployeeRequest {
|
|||||||
password: string;
|
password: string;
|
||||||
firstname: string;
|
firstname: string;
|
||||||
lastname: string;
|
lastname: string;
|
||||||
roles: string[];
|
roles?: string[];
|
||||||
employeeType: 'manager' | 'trainee' | 'experienced';
|
employeeType: 'manager' | 'personell' | 'apprentice' | 'guest';
|
||||||
contractType: 'small' | 'large';
|
contractType?: 'small' | 'large' | 'flexible';
|
||||||
canWorkAlone: boolean;
|
canWorkAlone: boolean;
|
||||||
|
isTrainee?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateEmployeeRequest {
|
export interface UpdateEmployeeRequest {
|
||||||
firstname?: string;
|
firstname?: string;
|
||||||
lastname?: string;
|
lastname?: string;
|
||||||
roles?: string[];
|
roles?: string[];
|
||||||
employeeType?: 'manager' | 'trainee' | 'experienced';
|
employeeType?: 'manager' | 'personell' | 'apprentice' | 'guest';
|
||||||
contractType?: 'small' | 'large';
|
contractType?: 'small' | 'large' | 'flexible';
|
||||||
canWorkAlone?: boolean;
|
canWorkAlone?: boolean;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
isTrainee?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EmployeeWithPassword extends Employee {
|
export interface EmployeeWithPassword extends Employee {
|
||||||
@@ -41,7 +44,7 @@ export interface EmployeeAvailability {
|
|||||||
id: string;
|
id: string;
|
||||||
employeeId: string;
|
employeeId: string;
|
||||||
planId: 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
|
preferenceLevel: 1 | 2 | 3; // 1:preferred, 2:available, 3:unavailable
|
||||||
notes?: string;
|
notes?: string;
|
||||||
}
|
}
|
||||||
@@ -84,3 +87,10 @@ export interface EmployeeRole {
|
|||||||
employeeId: string;
|
employeeId: string;
|
||||||
role: 'admin' | 'user' | 'maintenance';
|
role: 'admin' | 'user' | 'maintenance';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Employee type configuration
|
||||||
|
export interface EmployeeType {
|
||||||
|
type: 'manager' | 'personell' | 'apprentice' | 'guest';
|
||||||
|
category: 'internal' | 'external';
|
||||||
|
has_contract_type: boolean;
|
||||||
|
}
|
||||||
@@ -4,19 +4,41 @@ import { EmployeeAvailability, ManagerAvailability } from '../Employee.js';
|
|||||||
// Default employee data for quick creation
|
// Default employee data for quick creation
|
||||||
export const EMPLOYEE_DEFAULTS = {
|
export const EMPLOYEE_DEFAULTS = {
|
||||||
role: 'user' as const,
|
role: 'user' as const,
|
||||||
employeeType: 'experienced' as const,
|
employeeType: 'personell' as const,
|
||||||
contractType: 'small' as const,
|
contractType: 'small' as const,
|
||||||
canWorkAlone: false,
|
canWorkAlone: false,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
isTrainee: false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Manager-specific defaults
|
// Manager-specific defaults
|
||||||
export const MANAGER_DEFAULTS = {
|
export const MANAGER_DEFAULTS = {
|
||||||
role: 'admin' as const,
|
role: 'admin' as const,
|
||||||
employeeType: 'manager' as const,
|
employeeType: 'manager' as const,
|
||||||
contractType: 'large' as const,
|
contractType: 'flexible' as const,
|
||||||
canWorkAlone: true,
|
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 = {
|
export const EMPLOYEE_TYPE_CONFIG = {
|
||||||
@@ -24,22 +46,37 @@ export const EMPLOYEE_TYPE_CONFIG = {
|
|||||||
value: 'manager' as const,
|
value: 'manager' as const,
|
||||||
label: 'Chef/Administrator',
|
label: 'Chef/Administrator',
|
||||||
color: '#e74c3c',
|
color: '#e74c3c',
|
||||||
|
category: 'internal' as const,
|
||||||
|
hasContractType: true,
|
||||||
independent: true,
|
independent: true,
|
||||||
description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung'
|
description: 'Vollzugriff auf alle Funktionen und Mitarbeiterverwaltung'
|
||||||
},
|
},
|
||||||
experienced: {
|
personell: {
|
||||||
value: 'experienced' as const,
|
value: 'personell' as const,
|
||||||
label: 'Erfahren',
|
label: 'Personal',
|
||||||
color: '#3498db',
|
color: '#3498db',
|
||||||
|
category: 'internal' as const,
|
||||||
|
hasContractType: true,
|
||||||
independent: true,
|
independent: true,
|
||||||
description: 'Langjährige Erfahrung, kann komplexe Aufgaben übernehmen'
|
description: 'Reguläre Mitarbeiter mit Vertrag'
|
||||||
},
|
},
|
||||||
trainee: {
|
apprentice: {
|
||||||
value: 'trainee' as const,
|
value: 'apprentice' as const,
|
||||||
label: 'Neuling',
|
label: 'Auszubildender',
|
||||||
color: '#27ae60',
|
color: '#9b59b6',
|
||||||
|
category: 'internal' as const,
|
||||||
|
hasContractType: true,
|
||||||
independent: false,
|
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;
|
} as const;
|
||||||
|
|
||||||
@@ -53,9 +90,10 @@ export const ROLE_CONFIG = [
|
|||||||
export const CONTRACT_TYPE_DESCRIPTIONS = {
|
export const CONTRACT_TYPE_DESCRIPTIONS = {
|
||||||
small: '1 Schicht pro Woche',
|
small: '1 Schicht pro Woche',
|
||||||
large: '2 Schichten pro Woche',
|
large: '2 Schichten pro Woche',
|
||||||
manager: 'Kein Vertragslimit - Immer MO und DI verfügbar'
|
flexible: 'Flexible Arbeitszeiten'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
||||||
// Availability preference descriptions
|
// Availability preference descriptions
|
||||||
export const AVAILABILITY_PREFERENCES = {
|
export const AVAILABILITY_PREFERENCES = {
|
||||||
1: { label: 'Bevorzugt', color: '#10b981', description: 'Möchte diese Schicht arbeiten' },
|
1: { label: 'Bevorzugt', color: '#10b981', description: 'Möchte diese Schicht arbeiten' },
|
||||||
@@ -105,3 +143,16 @@ export function createManagerDefaultSchedule(managerId: string, planId: string,
|
|||||||
|
|
||||||
return assignments;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
// backend/src/models/helpers/employeeHelpers.ts
|
// backend/src/models/helpers/employeeHelpers.ts
|
||||||
import { Employee, CreateEmployeeRequest, EmployeeAvailability } from '../Employee.js';
|
import { Employee, CreateEmployeeRequest, EmployeeAvailability } from '../Employee.js';
|
||||||
|
|
||||||
// Email generation function (same as in controllers)
|
// Email generation function
|
||||||
function generateEmail(firstname: string, lastname: string): string {
|
function generateEmail(firstname: string, lastname: string): string {
|
||||||
// Convert German umlauts to their expanded forms
|
|
||||||
const convertUmlauts = (str: string): string => {
|
const convertUmlauts = (str: string): string => {
|
||||||
return str
|
return str
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -13,19 +12,16 @@ function generateEmail(firstname: string, lastname: string): string {
|
|||||||
.replace(/ß/g, 'ss');
|
.replace(/ß/g, 'ss');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove any remaining special characters and convert to lowercase
|
|
||||||
const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, '');
|
const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, '');
|
||||||
const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, '');
|
const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, '');
|
||||||
|
|
||||||
return `${cleanFirstname}.${cleanLastname}@sp.de`;
|
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[] {
|
export function validateEmployeeData(employee: CreateEmployeeRequest): string[] {
|
||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
// Email is now auto-generated, so no email validation needed
|
|
||||||
|
|
||||||
if (employee.password?.length < 6) {
|
if (employee.password?.length < 6) {
|
||||||
errors.push('Password must be at least 6 characters long');
|
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');
|
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;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate email for employee (new helper function)
|
// Generate email for employee
|
||||||
export function generateEmployeeEmail(firstname: string, lastname: string): string {
|
export function generateEmployeeEmail(firstname: string, lastname: string): string {
|
||||||
return generateEmail(firstname, lastname);
|
return generateEmail(firstname, lastname);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simplified business logic helpers
|
// UPDATED: Business logic helpers for new employee types
|
||||||
export const isManager = (employee: Employee): boolean =>
|
export const isManager = (employee: Employee): boolean =>
|
||||||
employee.employeeType === 'manager';
|
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 =>
|
export const isTrainee = (employee: Employee): boolean =>
|
||||||
employee.employeeType === 'trainee';
|
employee.employeeType === 'personell' && employee.isTrainee;
|
||||||
|
|
||||||
export const isExperienced = (employee: Employee): boolean =>
|
export const isExperienced = (employee: Employee): boolean =>
|
||||||
employee.employeeType === 'experienced';
|
employee.employeeType === 'personell' && !employee.isTrainee;
|
||||||
|
|
||||||
|
// Role-based helpers
|
||||||
export const isAdmin = (employee: Employee): boolean =>
|
export const isAdmin = (employee: Employee): boolean =>
|
||||||
employee.roles?.includes('admin') || false;
|
employee.roles?.includes('admin') || false;
|
||||||
|
|
||||||
@@ -64,13 +106,12 @@ export const isMaintenance = (employee: Employee): boolean =>
|
|||||||
|
|
||||||
export const isUser = (employee: Employee): boolean =>
|
export const isUser = (employee: Employee): boolean =>
|
||||||
employee.roles?.includes('user') || false;
|
employee.roles?.includes('user') || false;
|
||||||
|
|
||||||
|
// UPDATED: Work alone permission - managers and experienced personell can work alone
|
||||||
export const canEmployeeWorkAlone = (employee: Employee): boolean =>
|
export const canEmployeeWorkAlone = (employee: Employee): boolean =>
|
||||||
employee.canWorkAlone && isExperienced(employee);
|
employee.canWorkAlone && (isManager(employee) || isExperienced(employee));
|
||||||
|
|
||||||
export const getEmployeeWorkHours = (employee: Employee): number =>
|
// Helper for full name display
|
||||||
isManager(employee) ? 999 : (employee.contractType === 'small' ? 1 : 2);
|
|
||||||
|
|
||||||
// New helper for full name display
|
|
||||||
export const getFullName = (employee: { firstname: string; lastname: string }): string =>
|
export const getFullName = (employee: { firstname: string; lastname: string }): string =>
|
||||||
`${employee.firstname} ${employee.lastname}`;
|
`${employee.firstname} ${employee.lastname}`;
|
||||||
|
|
||||||
@@ -92,3 +133,13 @@ export function validateAvailabilityData(availability: Omit<EmployeeAvailability
|
|||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UPDATED: Helper to get employee type category
|
||||||
|
export const getEmployeeCategory = (employee: Employee): 'internal' | 'external' => {
|
||||||
|
return isInternal(employee) ? 'internal' : 'external';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to check if employee requires contract type
|
||||||
|
export const requiresContractType = (employee: Employee): boolean => {
|
||||||
|
return isInternal(employee);
|
||||||
|
};
|
||||||
@@ -7,10 +7,10 @@ export interface Availability {
|
|||||||
id: string;
|
id: string;
|
||||||
employeeId: string;
|
employeeId: string;
|
||||||
planId: 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
|
preferenceLevel: 1 | 2 | 3;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
// Optional convenience fields (can be joined from shifts and time_slots tables)
|
// Optional convenience fields
|
||||||
dayOfWeek?: number;
|
dayOfWeek?: number;
|
||||||
timeSlotId?: string;
|
timeSlotId?: string;
|
||||||
timeSlotName?: string;
|
timeSlotName?: string;
|
||||||
@@ -30,7 +30,7 @@ export interface Constraint {
|
|||||||
maxHoursPerWeek?: number;
|
maxHoursPerWeek?: number;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
weight?: number; // For soft constraints
|
weight?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScheduleRequest {
|
export interface ScheduleRequest {
|
||||||
@@ -153,8 +153,9 @@ export interface AvailabilityWithDetails extends Availability {
|
|||||||
id: string;
|
id: string;
|
||||||
firstname: string;
|
firstname: string;
|
||||||
lastname: string;
|
lastname: string;
|
||||||
employeeType: 'manager' | 'trainee' | 'experienced';
|
employeeType: 'manager' | 'personell' | 'apprentice' | 'guest';
|
||||||
canWorkAlone: boolean;
|
canWorkAlone: boolean;
|
||||||
|
isTrainee: boolean;
|
||||||
};
|
};
|
||||||
shift?: {
|
shift?: {
|
||||||
dayOfWeek: number;
|
dayOfWeek: number;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import { useNotification } from '../../contexts/NotificationContext';
|
import { useNotification } from '../../contexts/NotificationContext';
|
||||||
|
import { employeeService } from '../../services/employeeService';
|
||||||
|
|
||||||
const Login: React.FC = () => {
|
const Login: React.FC = () => {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
@@ -12,7 +13,6 @@ const Login: React.FC = () => {
|
|||||||
const { showNotification } = useNotification();
|
const { showNotification } = useNotification();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// 🔥 NEU: Redirect wenn bereits eingeloggt
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
console.log('✅ User already logged in, redirecting to dashboard');
|
console.log('✅ User already logged in, redirecting to dashboard');
|
||||||
@@ -28,7 +28,6 @@ const Login: React.FC = () => {
|
|||||||
console.log('🔐 Attempting login for:', email);
|
console.log('🔐 Attempting login for:', email);
|
||||||
await login({ email, password });
|
await login({ email, password });
|
||||||
|
|
||||||
// 🔥 WICHTIG: Erfolgsmeldung und Redirect
|
|
||||||
console.log('✅ Login successful, redirecting to dashboard');
|
console.log('✅ Login successful, redirecting to dashboard');
|
||||||
showNotification({
|
showNotification({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ interface DashboardData {
|
|||||||
}>;
|
}>;
|
||||||
teamStats: {
|
teamStats: {
|
||||||
totalEmployees: number;
|
totalEmployees: number;
|
||||||
|
personell: number;
|
||||||
manager: number;
|
manager: number;
|
||||||
trainee: number;
|
trainee: number;
|
||||||
experienced: number;
|
experienced: number;
|
||||||
@@ -36,6 +37,7 @@ const Dashboard: React.FC = () => {
|
|||||||
upcomingShifts: [],
|
upcomingShifts: [],
|
||||||
teamStats: {
|
teamStats: {
|
||||||
totalEmployees: 0,
|
totalEmployees: 0,
|
||||||
|
personell: 0,
|
||||||
manager: 0,
|
manager: 0,
|
||||||
trainee: 0,
|
trainee: 0,
|
||||||
experienced: 0
|
experienced: 0
|
||||||
@@ -208,11 +210,13 @@ const Dashboard: React.FC = () => {
|
|||||||
|
|
||||||
// Count by type
|
// Count by type
|
||||||
const managerCount = employees.filter(e => e.employeeType === 'manager').length;
|
const managerCount = employees.filter(e => e.employeeType === 'manager').length;
|
||||||
const traineeCount = employees.filter(e => e.employeeType === 'trainee').length;
|
const personellCount = employees.filter(e => e.employeeType === 'personell').length;
|
||||||
const experiencedCount = employees.filter(e => e.employeeType === 'experienced').length;
|
const traineeCount = employees.filter(e => e.isTrainee === true).length;
|
||||||
|
const experiencedCount = employees.filter(e => e.isTrainee === false).length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalEmployees,
|
totalEmployees,
|
||||||
|
personell: personellCount,
|
||||||
manager: managerCount,
|
manager: managerCount,
|
||||||
trainee: traineeCount,
|
trainee: traineeCount,
|
||||||
experienced: experiencedCount,
|
experienced: experiencedCount,
|
||||||
@@ -538,7 +542,7 @@ const Dashboard: React.FC = () => {
|
|||||||
<h3 style={{ margin: '0 0 15px 0', color: '#2c3e50' }}>👥 Team-Übersicht</h3>
|
<h3 style={{ margin: '0 0 15px 0', color: '#2c3e50' }}>👥 Team-Übersicht</h3>
|
||||||
<div style={{ display: 'grid', gap: '12px' }}>
|
<div style={{ display: 'grid', gap: '12px' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<span>Mitarbeiter:</span>
|
<span>Gesamte Belegschaft:</span>
|
||||||
<span style={{ fontWeight: 'bold', fontSize: '18px' }}>
|
<span style={{ fontWeight: 'bold', fontSize: '18px' }}>
|
||||||
{data.teamStats.totalEmployees}
|
{data.teamStats.totalEmployees}
|
||||||
</span>
|
</span>
|
||||||
@@ -550,15 +554,9 @@ const Dashboard: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
<span>Erfahrene:</span>
|
<span>Personal:</span>
|
||||||
<span style={{ fontWeight: 'bold', color: '#f39c12' }}>
|
<span style={{ fontWeight: 'bold', color: '#f39c12' }}>
|
||||||
{data.teamStats.experienced}
|
{data.teamStats.personell}
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
||||||
<span>Neue:</span>
|
|
||||||
<span style={{ fontWeight: 'bold', color: '#e74c3c' }}>
|
|
||||||
{data.teamStats.trainee}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -54,15 +54,122 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
{ level: 3 as AvailabilityLevel, label: 'Nicht möglich', color: '#e74c3c', bgColor: '#fadbd8', description: 'Nicht verfügbar' }
|
{ level: 3 as AvailabilityLevel, label: 'Nicht möglich', color: '#e74c3c', bgColor: '#fadbd8', description: 'Nicht verfügbar' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Lade initial die Schichtpläne
|
||||||
useEffect(() => {
|
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]);
|
}, [employee.id]);
|
||||||
|
|
||||||
|
// Lade Plan-Details und Verfügbarkeiten wenn selectedPlanId sich ändert
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedPlanId) {
|
const loadPlanData = async () => {
|
||||||
loadSelectedPlan();
|
if (!selectedPlanId) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}, [selectedPlanId]);
|
|
||||||
|
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 => {
|
const formatTime = (time: string): string => {
|
||||||
if (!time) return '--:--';
|
if (!time) return '--:--';
|
||||||
@@ -116,73 +223,12 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
return { days, shiftsByDay };
|
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) => {
|
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}`);
|
console.log(`🔄 ÄNDERE VERFÜGBARKEIT: Shift ${shiftId}, Level ${level}`);
|
||||||
|
|
||||||
setAvailabilities(prev => {
|
setAvailabilities(prev => {
|
||||||
@@ -238,24 +284,56 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all unique shifts across all days for row headers
|
// Create a map for quick time slot lookups
|
||||||
const allShifts: ExtendedShift[] = [];
|
const timeSlotMap = new Map(selectedPlan?.timeSlots?.map(ts => [ts.id, ts]) || []);
|
||||||
const shiftIds = new Set<string>();
|
|
||||||
|
|
||||||
|
// Get all unique time slots (rows) by collecting from all shifts
|
||||||
|
const allTimeSlots = new Map();
|
||||||
days.forEach(day => {
|
days.forEach(day => {
|
||||||
shiftsByDay[day.id]?.forEach(shift => {
|
shiftsByDay[day.id]?.forEach(shift => {
|
||||||
if (!shiftIds.has(shift.id)) {
|
const timeSlot = timeSlotMap.get(shift.timeSlotId);
|
||||||
shiftIds.add(shift.id);
|
if (timeSlot && !allTimeSlots.has(timeSlot.id)) {
|
||||||
allShifts.push(shift);
|
allTimeSlots.set(timeSlot.id, {
|
||||||
|
...timeSlot,
|
||||||
|
shiftsByDay: {} // Initialize empty object to store shifts by day
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort shifts by time slot start time
|
// Populate shifts for each time slot by day
|
||||||
allShifts.sort((a, b) => {
|
days.forEach(day => {
|
||||||
const timeA = a.startTime || '';
|
shiftsByDay[day.id]?.forEach(shift => {
|
||||||
const timeB = b.startTime || '';
|
const timeSlot = allTimeSlots.get(shift.timeSlotId);
|
||||||
return timeA.localeCompare(timeB);
|
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 (
|
return (
|
||||||
@@ -273,10 +351,39 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
}}>
|
}}>
|
||||||
Verfügbarkeit definieren
|
Verfügbarkeit definieren
|
||||||
<div style={{ fontSize: '14px', fontWeight: 'normal', marginTop: '5px' }}>
|
<div style={{ fontSize: '14px', fontWeight: 'normal', marginTop: '5px' }}>
|
||||||
{allShifts.length} Schichten • {days.length} Tage • Direkte Shift-ID Zuordnung
|
{sortedTimeSlots.length} Zeitslots • {days.length} Tage • Zeitbasierte Darstellung
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Validation Warnings */}
|
||||||
|
{validationErrors.length > 0 && (
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: '#fff3cd',
|
||||||
|
border: '1px solid #ffeaa7',
|
||||||
|
padding: '15px',
|
||||||
|
margin: '10px'
|
||||||
|
}}>
|
||||||
|
<h4 style={{ margin: '0 0 10px 0', color: '#856404' }}>⚠️ Validierungswarnungen:</h4>
|
||||||
|
<ul style={{ margin: 0, paddingLeft: '20px', fontSize: '12px' }}>
|
||||||
|
{validationErrors.map((error, index) => (
|
||||||
|
<li key={index}>{error}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Timetable Structure Info */}
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: '#d1ecf1',
|
||||||
|
border: '1px solid #bee5eb',
|
||||||
|
padding: '10px 15px',
|
||||||
|
margin: '10px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '12px'
|
||||||
|
}}>
|
||||||
|
<strong>Struktur-Info:</strong> {sortedTimeSlots.length} Zeitslots × {days.length} Tage = {sortedTimeSlots.length * days.length} Zellen
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ overflowX: 'auto' }}>
|
<div style={{ overflowX: 'auto' }}>
|
||||||
<table style={{
|
<table style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -290,9 +397,9 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
border: '1px solid #dee2e6',
|
border: '1px solid #dee2e6',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
minWidth: '150px'
|
minWidth: '200px'
|
||||||
}}>
|
}}>
|
||||||
Schicht (Zeit)
|
Zeitslot
|
||||||
</th>
|
</th>
|
||||||
{days.map(weekday => (
|
{days.map(weekday => (
|
||||||
<th key={weekday.id} style={{
|
<th key={weekday.id} style={{
|
||||||
@@ -300,7 +407,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
border: '1px solid #dee2e6',
|
border: '1px solid #dee2e6',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
minWidth: '90px'
|
minWidth: '120px'
|
||||||
}}>
|
}}>
|
||||||
{weekday.name}
|
{weekday.name}
|
||||||
</th>
|
</th>
|
||||||
@@ -308,26 +415,32 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{allShifts.map((shift, shiftIndex) => (
|
{sortedTimeSlots.map((timeSlot, timeSlotIndex) => (
|
||||||
<tr key={shift.id} style={{
|
<tr key={timeSlot.id} style={{
|
||||||
backgroundColor: shiftIndex % 2 === 0 ? 'white' : '#f8f9fa'
|
backgroundColor: timeSlotIndex % 2 === 0 ? 'white' : '#f8f9fa'
|
||||||
}}>
|
}}>
|
||||||
<td style={{
|
<td style={{
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
border: '1px solid #dee2e6',
|
border: '1px solid #dee2e6',
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
backgroundColor: '#f8f9fa'
|
backgroundColor: '#f8f9fa',
|
||||||
|
position: 'sticky',
|
||||||
|
left: 0
|
||||||
}}>
|
}}>
|
||||||
{shift.displayName}
|
<div style={{ fontWeight: 'bold' }}>
|
||||||
<div style={{ fontSize: '11px', color: '#666', marginTop: '4px' }}>
|
{timeSlot.name}
|
||||||
Shift-ID: {shift.id.substring(0, 8)}...
|
</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#666' }}>
|
||||||
|
{formatTime(timeSlot.startTime)} - {formatTime(timeSlot.endTime)}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '11px', color: '#999', marginTop: '4px' }}>
|
||||||
|
ID: {timeSlot.id.substring(0, 8)}...
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{days.map(weekday => {
|
{days.map(weekday => {
|
||||||
// Check if this shift exists for this day
|
const shift = timeSlot.shiftsByDay[weekday.id];
|
||||||
const shiftForDay = shiftsByDay[weekday.id]?.find(s => s.id === shift.id);
|
|
||||||
|
|
||||||
if (!shiftForDay) {
|
if (!shift) {
|
||||||
return (
|
return (
|
||||||
<td key={weekday.id} style={{
|
<td key={weekday.id} style={{
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
@@ -337,11 +450,14 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
color: '#ccc',
|
color: '#ccc',
|
||||||
fontStyle: 'italic'
|
fontStyle: 'italic'
|
||||||
}}>
|
}}>
|
||||||
-
|
Kein Shift
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 currentLevel = getAvailabilityForShift(shift.id);
|
||||||
const levelConfig = availabilityLevels.find(l => l.level === currentLevel);
|
const levelConfig = availabilityLevels.find(l => l.level === currentLevel);
|
||||||
|
|
||||||
@@ -350,8 +466,31 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
border: '1px solid #dee2e6',
|
border: '1px solid #dee2e6',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
backgroundColor: levelConfig?.bgColor
|
backgroundColor: !isValidShift ? '#fff3cd' : (levelConfig?.bgColor || 'white'),
|
||||||
|
position: 'relative'
|
||||||
}}>
|
}}>
|
||||||
|
{/* Validation indicator */}
|
||||||
|
{!isValidShift && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '2px',
|
||||||
|
right: '2px',
|
||||||
|
backgroundColor: '#f39c12',
|
||||||
|
color: 'white',
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: '16px',
|
||||||
|
height: '16px',
|
||||||
|
fontSize: '10px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
title={`Shift Validierung: timeSlotId=${shift.timeSlotId}, dayOfWeek=${shift.dayOfWeek}`}
|
||||||
|
>
|
||||||
|
⚠️
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<select
|
<select
|
||||||
value={currentLevel}
|
value={currentLevel}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -360,10 +499,10 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
border: `2px solid ${levelConfig?.color || '#ddd'}`,
|
border: `2px solid ${!isValidShift ? '#f39c12' : (levelConfig?.color || '#ddd')}`,
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
backgroundColor: levelConfig?.bgColor || 'white',
|
backgroundColor: !isValidShift ? '#fff3cd' : (levelConfig?.bgColor || 'white'),
|
||||||
color: levelConfig?.color || '#333',
|
color: !isValidShift ? '#856404' : (levelConfig?.color || '#333'),
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
minWidth: '140px',
|
minWidth: '140px',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
@@ -384,6 +523,23 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
{/* Shift debug info */}
|
||||||
|
<div style={{
|
||||||
|
fontSize: '10px',
|
||||||
|
color: '#666',
|
||||||
|
marginTop: '4px',
|
||||||
|
textAlign: 'left',
|
||||||
|
fontFamily: 'monospace'
|
||||||
|
}}>
|
||||||
|
<div>Shift: {shift.id.substring(0, 6)}...</div>
|
||||||
|
<div>Day: {shift.dayOfWeek}</div>
|
||||||
|
{!isValidShift && (
|
||||||
|
<div style={{ color: '#e74c3c', fontWeight: 'bold' }}>
|
||||||
|
VALIDATION ERROR
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -392,6 +548,27 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Summary Statistics */}
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: '#f8f9fa',
|
||||||
|
padding: '15px',
|
||||||
|
borderTop: '1px solid #dee2e6',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#666'
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<div>
|
||||||
|
<strong>Zusammenfassung:</strong> {sortedTimeSlots.length} Zeitslots × {days.length} Tage = {sortedTimeSlots.length * days.length} mögliche Shifts
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Aktive Verfügbarkeiten:</strong> {availabilities.filter(a => a.preferenceLevel !== 3).length}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Validierungsfehler:</strong> {validationErrors.length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -406,14 +583,24 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { days, shiftsByDay } = getTimetableData();
|
// Filter availabilities to only include those with actual shifts AND valid shiftIds
|
||||||
|
|
||||||
// Filter availabilities to only include those with actual shifts
|
|
||||||
const validAvailabilities = availabilities.filter(avail => {
|
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
|
// Check if this shiftId exists in the current plan
|
||||||
return selectedPlan?.shifts?.some(shift => shift.id === avail.shiftId);
|
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) {
|
if (validAvailabilities.length === 0) {
|
||||||
setError('Keine gültigen Verfügbarkeiten zum Speichern gefunden');
|
setError('Keine gültigen Verfügbarkeiten zum Speichern gefunden');
|
||||||
return;
|
return;
|
||||||
@@ -485,25 +672,63 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
|
|
||||||
{/* Debug-Info */}
|
{/* Debug-Info */}
|
||||||
<div style={{
|
<div style={{
|
||||||
backgroundColor: shiftsCount === 0 ? '#f8d7da' : '#d1ecf1',
|
backgroundColor: !selectedPlan ? '#f8d7da' : (shiftsCount === 0 ? '#fff3cd' : '#d1ecf1'),
|
||||||
border: `1px solid ${shiftsCount === 0 ? '#f5c6cb' : '#bee5eb'}`,
|
border: `1px solid ${!selectedPlan ? '#f5c6cb' : (shiftsCount === 0 ? '#ffeaa7' : '#bee5eb')}`,
|
||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
padding: '15px',
|
padding: '15px',
|
||||||
marginBottom: '20px'
|
marginBottom: '20px'
|
||||||
}}>
|
}}>
|
||||||
<h4 style={{
|
<h4 style={{
|
||||||
margin: '0 0 10px 0',
|
margin: '0 0 10px 0',
|
||||||
color: shiftsCount === 0 ? '#721c24' : '#0c5460'
|
color: !selectedPlan ? '#721c24' : (shiftsCount === 0 ? '#856404' : '#0c5460')
|
||||||
}}>
|
}}>
|
||||||
{shiftsCount === 0 ? '❌ PROBLEM: Keine Shifts gefunden' : '✅ Plan-Daten geladen'}
|
{!selectedPlan ? '❌ KEIN PLAN AUSGEWÄHLT' :
|
||||||
|
shiftsCount === 0 ? '⚠️ KEINE SHIFTS GEFUNDEN' : '✅ PLAN-DATEN GELADEN'}
|
||||||
</h4>
|
</h4>
|
||||||
<div style={{ fontSize: '12px', fontFamily: 'monospace' }}>
|
<div style={{ fontSize: '12px', fontFamily: 'monospace' }}>
|
||||||
<div><strong>Ausgewählter Plan:</strong> {selectedPlan?.name || 'Keiner'}</div>
|
<div><strong>Ausgewählter Plan:</strong> {selectedPlan?.name || 'Keiner'}</div>
|
||||||
|
<div><strong>Plan ID:</strong> {selectedPlanId || 'Nicht gesetzt'}</div>
|
||||||
|
<div><strong>Geladene Pläne:</strong> {shiftPlans.length}</div>
|
||||||
<div><strong>Einzigartige Shifts:</strong> {shiftsCount}</div>
|
<div><strong>Einzigartige Shifts:</strong> {shiftsCount}</div>
|
||||||
|
<div><strong>Geladene Verfügbarkeiten:</strong> {availabilities.length}</div>
|
||||||
|
{selectedPlan && (
|
||||||
|
<>
|
||||||
<div><strong>Verwendete Tage:</strong> {days.length} ({days.map(d => d.name).join(', ')})</div>
|
<div><strong>Verwendete Tage:</strong> {days.length} ({days.map(d => d.name).join(', ')})</div>
|
||||||
<div><strong>Gesamte Shifts im Plan:</strong> {selectedPlan?.shifts?.length || 0}</div>
|
<div><strong>Gesamte Shifts im Plan:</strong> {selectedPlan.shifts?.length || 0}</div>
|
||||||
<div><strong>Methode:</strong> Direkte Shift-ID Zuordnung</div>
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Show existing preferences */}
|
||||||
|
{availabilities.length > 0 && (
|
||||||
|
<div style={{ marginTop: '10px', paddingTop: '10px', borderTop: '1px solid #bee5eb' }}>
|
||||||
|
<strong>Vorhandene Präferenzen:</strong>
|
||||||
|
{availabilities.slice(0, 5).map(avail => {
|
||||||
|
// SICHERHEITSCHECK: Stelle sicher, dass shiftId existiert
|
||||||
|
if (!avail.shiftId) {
|
||||||
|
return (
|
||||||
|
<div key={avail.id} style={{ fontSize: '11px', color: 'red' }}>
|
||||||
|
• UNGÜLTIG: Keine Shift-ID
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shift = selectedPlan?.shifts?.find(s => s.id === avail.shiftId);
|
||||||
|
const shiftIdDisplay = avail.shiftId ? avail.shiftId.substring(0, 8) + '...' : 'KEINE ID';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={avail.id} style={{ fontSize: '11px' }}>
|
||||||
|
• Shift {shiftIdDisplay} (Day {shift?.dayOfWeek || '?'}): Level {avail.preferenceLevel}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{availabilities.length > 5 && (
|
||||||
|
<div style={{ fontSize: '11px', fontStyle: 'italic' }}>
|
||||||
|
... und {availabilities.length - 5} weitere
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Employee Info */}
|
{/* Employee Info */}
|
||||||
@@ -588,7 +813,12 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={selectedPlanId}
|
value={selectedPlanId}
|
||||||
onChange={(e) => setSelectedPlanId(e.target.value)}
|
onChange={(e) => {
|
||||||
|
const newPlanId = e.target.value;
|
||||||
|
console.log('🔄 PLAN WECHSELN ZU:', newPlanId);
|
||||||
|
setSelectedPlanId(newPlanId);
|
||||||
|
// Der useEffect wird automatisch ausgelöst
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
padding: '8px 12px',
|
padding: '8px 12px',
|
||||||
border: '1px solid #ddd',
|
border: '1px solid #ddd',
|
||||||
@@ -609,10 +839,25 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
|
|||||||
<div style={{ fontSize: '14px', color: '#666' }}>
|
<div style={{ fontSize: '14px', color: '#666' }}>
|
||||||
<div><strong>Plan:</strong> {selectedPlan.name}</div>
|
<div><strong>Plan:</strong> {selectedPlan.name}</div>
|
||||||
<div><strong>Shifts:</strong> {selectedPlan.shifts?.length || 0}</div>
|
<div><strong>Shifts:</strong> {selectedPlan.shifts?.length || 0}</div>
|
||||||
|
<div><strong>Zeitslots:</strong> {selectedPlan.timeSlots?.length || 0}</div>
|
||||||
<div><strong>Status:</strong> {selectedPlan.status}</div>
|
<div><strong>Status:</strong> {selectedPlan.status}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Debug Info für Plan Loading */}
|
||||||
|
{!selectedPlanId && shiftPlans.length > 0 && (
|
||||||
|
<div style={{
|
||||||
|
marginTop: '10px',
|
||||||
|
padding: '10px',
|
||||||
|
backgroundColor: '#fff3cd',
|
||||||
|
border: '1px solid #ffeaa7',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '12px'
|
||||||
|
}}>
|
||||||
|
⚠️ Bitte wählen Sie einen Schichtplan aus
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Availability Timetable */}
|
{/* Availability Timetable */}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ interface EmployeeFormProps {
|
|||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EmployeeType = 'manager' | 'personell' | 'apprentice' | 'guest';
|
||||||
|
type ContractType = 'small' | 'large' | 'flexible';
|
||||||
|
|
||||||
const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
||||||
mode,
|
mode,
|
||||||
employee,
|
employee,
|
||||||
@@ -20,13 +23,14 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
firstname: '',
|
firstname: '',
|
||||||
lastname: '',
|
lastname: '',
|
||||||
email: '', // Will be auto-generated and display only
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
roles: ['user'] as string[], // Changed from single role to array
|
roles: ['user'] as string[],
|
||||||
employeeType: 'trainee' as 'manager' | 'trainee' | 'experienced',
|
employeeType: 'personell' as EmployeeType,
|
||||||
contractType: 'small' as 'small' | 'large',
|
contractType: 'small' as ContractType | undefined,
|
||||||
canWorkAlone: false,
|
canWorkAlone: false,
|
||||||
isActive: true
|
isActive: true,
|
||||||
|
isTrainee: false
|
||||||
});
|
});
|
||||||
const [passwordForm, setPasswordForm] = useState({
|
const [passwordForm, setPasswordForm] = useState({
|
||||||
newPassword: '',
|
newPassword: '',
|
||||||
@@ -62,12 +66,13 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
firstname: employee.firstname,
|
firstname: employee.firstname,
|
||||||
lastname: employee.lastname,
|
lastname: employee.lastname,
|
||||||
email: employee.email,
|
email: employee.email,
|
||||||
password: '', // Password wird beim Bearbeiten nicht angezeigt
|
password: '',
|
||||||
roles: employee.roles || ['user'], // Use roles array
|
roles: employee.roles || ['user'],
|
||||||
employeeType: employee.employeeType,
|
employeeType: employee.employeeType,
|
||||||
contractType: employee.contractType,
|
contractType: employee.contractType,
|
||||||
canWorkAlone: employee.canWorkAlone,
|
canWorkAlone: employee.canWorkAlone,
|
||||||
isActive: employee.isActive
|
isActive: employee.isActive,
|
||||||
|
isTrainee: employee.isTrainee || false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [mode, employee]);
|
}, [mode, employee]);
|
||||||
@@ -92,13 +97,11 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
const handleRoleChange = (role: string, checked: boolean) => {
|
const handleRoleChange = (role: string, checked: boolean) => {
|
||||||
setFormData(prev => {
|
setFormData(prev => {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
// Add role if checked
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
roles: [...prev.roles, role]
|
roles: [...prev.roles, role]
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Remove role if unchecked
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
roles: prev.roles.filter(r => r !== role)
|
roles: prev.roles.filter(r => r !== role)
|
||||||
@@ -107,18 +110,36 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEmployeeTypeChange = (employeeType: 'manager' | 'trainee' | 'experienced') => {
|
const handleEmployeeTypeChange = (employeeType: EmployeeType) => {
|
||||||
// Manager and experienced can work alone, trainee cannot
|
// Determine if contract type should be shown and set default
|
||||||
const canWorkAlone = employeeType === 'manager' || employeeType === 'experienced';
|
const requiresContract = employeeType !== 'guest';
|
||||||
|
const defaultContractType = requiresContract ? 'small' as ContractType : undefined;
|
||||||
|
|
||||||
|
// Determine if can work alone based on employee type
|
||||||
|
const canWorkAlone = employeeType === 'manager' ||
|
||||||
|
(employeeType === 'personell' && !formData.isTrainee);
|
||||||
|
|
||||||
|
// Reset isTrainee if not personell
|
||||||
|
const isTrainee = employeeType === 'personell' ? formData.isTrainee : false;
|
||||||
|
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
employeeType,
|
employeeType,
|
||||||
canWorkAlone
|
contractType: defaultContractType,
|
||||||
|
canWorkAlone,
|
||||||
|
isTrainee
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleContractTypeChange = (contractType: 'small' | 'large') => {
|
const handleTraineeChange = (isTrainee: boolean) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
isTrainee,
|
||||||
|
canWorkAlone: prev.employeeType === 'personell' ? !isTrainee : prev.canWorkAlone
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContractTypeChange = (contractType: ContractType) => {
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
contractType
|
contractType
|
||||||
@@ -136,25 +157,27 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
firstname: formData.firstname.trim(),
|
firstname: formData.firstname.trim(),
|
||||||
lastname: formData.lastname.trim(),
|
lastname: formData.lastname.trim(),
|
||||||
password: formData.password,
|
password: formData.password,
|
||||||
roles: formData.roles, // Use roles array
|
roles: formData.roles,
|
||||||
employeeType: formData.employeeType,
|
employeeType: formData.employeeType,
|
||||||
contractType: formData.contractType,
|
contractType: formData.employeeType !== 'guest' ? formData.contractType : undefined,
|
||||||
canWorkAlone: formData.canWorkAlone
|
canWorkAlone: formData.canWorkAlone,
|
||||||
|
isTrainee: formData.isTrainee
|
||||||
};
|
};
|
||||||
await employeeService.createEmployee(createData);
|
await employeeService.createEmployee(createData);
|
||||||
} else if (employee) {
|
} else if (employee) {
|
||||||
const updateData: UpdateEmployeeRequest = {
|
const updateData: UpdateEmployeeRequest = {
|
||||||
firstname: formData.firstname.trim(),
|
firstname: formData.firstname.trim(),
|
||||||
lastname: formData.lastname.trim(),
|
lastname: formData.lastname.trim(),
|
||||||
roles: formData.roles, // Use roles array
|
roles: formData.roles,
|
||||||
employeeType: formData.employeeType,
|
employeeType: formData.employeeType,
|
||||||
contractType: formData.contractType,
|
contractType: formData.employeeType !== 'guest' ? formData.contractType : undefined,
|
||||||
canWorkAlone: formData.canWorkAlone,
|
canWorkAlone: formData.canWorkAlone,
|
||||||
isActive: formData.isActive,
|
isActive: formData.isActive,
|
||||||
|
isTrainee: formData.isTrainee
|
||||||
};
|
};
|
||||||
await employeeService.updateEmployee(employee.id, updateData);
|
await employeeService.updateEmployee(employee.id, updateData);
|
||||||
|
|
||||||
// If password change is requested and user is admin
|
// Password change logic remains the same
|
||||||
if (showPasswordSection && passwordForm.newPassword && hasRole(['admin'])) {
|
if (showPasswordSection && passwordForm.newPassword && hasRole(['admin'])) {
|
||||||
if (passwordForm.newPassword.length < 6) {
|
if (passwordForm.newPassword.length < 6) {
|
||||||
throw new Error('Das neue Passwort muss mindestens 6 Zeichen lang sein');
|
throw new Error('Das neue Passwort muss mindestens 6 Zeichen lang sein');
|
||||||
@@ -163,9 +186,8 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
throw new Error('Die Passwörter stimmen nicht überein');
|
throw new Error('Die Passwörter stimmen nicht überein');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the password change endpoint
|
|
||||||
await employeeService.changePassword(employee.id, {
|
await employeeService.changePassword(employee.id, {
|
||||||
currentPassword: '', // Empty for admin reset - backend should handle this
|
currentPassword: '',
|
||||||
newPassword: passwordForm.newPassword
|
newPassword: passwordForm.newPassword
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -189,9 +211,12 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
|
|
||||||
const contractTypeOptions = [
|
const contractTypeOptions = [
|
||||||
{ value: 'small' as const, label: 'Kleiner Vertrag', description: '1 Schicht pro Woche' },
|
{ value: 'small' as const, label: 'Kleiner Vertrag', description: '1 Schicht pro Woche' },
|
||||||
{ value: 'large' as const, label: 'Großer Vertrag', description: '2 Schichten pro Woche' }
|
{ value: 'large' as const, label: 'Großer Vertrag', description: '2 Schichten pro Woche' },
|
||||||
|
{ value: 'flexible' as const, label: 'Flexibler Vertrag', description: 'Flexible Arbeitszeiten' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const showContractType = formData.employeeType !== 'guest';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
maxWidth: '700px',
|
maxWidth: '700px',
|
||||||
@@ -330,8 +355,8 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Vertragstyp (nur für Admins) */}
|
{/* Vertragstyp (nur für Admins und interne Mitarbeiter) */}
|
||||||
{hasRole(['admin']) && (
|
{hasRole(['admin']) && showContractType && (
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '20px',
|
padding: '20px',
|
||||||
backgroundColor: '#e8f4fd',
|
backgroundColor: '#e8f4fd',
|
||||||
@@ -470,6 +495,37 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* FIXED: Trainee checkbox for personell type */}
|
||||||
|
{formData.employeeType === 'personell' && (
|
||||||
|
<div style={{
|
||||||
|
marginTop: '15px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '10px',
|
||||||
|
padding: '15px',
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
borderRadius: '6px',
|
||||||
|
backgroundColor: '#fff'
|
||||||
|
}}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="isTrainee"
|
||||||
|
id="isTrainee"
|
||||||
|
checked={formData.isTrainee}
|
||||||
|
onChange={(e) => handleTraineeChange(e.target.checked)}
|
||||||
|
style={{ width: '18px', height: '18px' }}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<label htmlFor="isTrainee" style={{ fontWeight: 'bold', color: '#2c3e50', display: 'block' }}>
|
||||||
|
Als Neuling markieren
|
||||||
|
</label>
|
||||||
|
<div style={{ fontSize: '12px', color: '#7f8c8d' }}>
|
||||||
|
Neulinge benötigen zusätzliche Betreuung und können nicht eigenständig arbeiten.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Eigenständigkeit */}
|
{/* Eigenständigkeit */}
|
||||||
@@ -496,11 +552,11 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
id="canWorkAlone"
|
id="canWorkAlone"
|
||||||
checked={formData.canWorkAlone}
|
checked={formData.canWorkAlone}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
disabled={formData.employeeType === 'manager'}
|
disabled={formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)}
|
||||||
style={{
|
style={{
|
||||||
width: '20px',
|
width: '20px',
|
||||||
height: '20px',
|
height: '20px',
|
||||||
opacity: formData.employeeType === 'manager' ? 0.5 : 1
|
opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.5 : 1
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
@@ -508,14 +564,16 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: '#2c3e50',
|
color: '#2c3e50',
|
||||||
display: 'block',
|
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
|
Als ausreichend eigenständig markieren
|
||||||
{formData.employeeType === 'manager' && ' (Automatisch für Chefs)'}
|
{(formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) && ' (Automatisch festgelegt)'}
|
||||||
</label>
|
</label>
|
||||||
<div style={{ fontSize: '14px', color: '#7f8c8d' }}>
|
<div style={{ fontSize: '14px', color: '#7f8c8d' }}>
|
||||||
{formData.employeeType === 'manager'
|
{formData.employeeType === 'manager'
|
||||||
? 'Chefs sind automatisch als eigenständig markiert.'
|
? '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.'
|
: 'Dieser Mitarbeiter kann komplexe Aufgaben eigenständig lösen und benötigt keine ständige Betreuung.'
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -527,7 +585,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
borderRadius: '15px',
|
borderRadius: '15px',
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
fontWeight: 'bold',
|
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'}
|
{formData.canWorkAlone ? 'EIGENSTÄNDIG' : 'BETREUUNG'}
|
||||||
</div>
|
</div>
|
||||||
@@ -632,7 +690,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Systemrollen (nur für Admins) */}
|
{/* Systemrollen (nur für Admins) - AKTUALISIERT FÜR MEHRFACHE ROLLEN */}
|
||||||
{hasRole(['admin']) && (
|
{hasRole(['admin']) && (
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: '20px',
|
padding: '20px',
|
||||||
@@ -714,6 +772,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Buttons */}
|
{/* Buttons */}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// EmployeeList.tsx
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults';
|
import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults';
|
||||||
import { Employee } from '../../../models/Employee';
|
import { Employee } from '../../../models/Employee';
|
||||||
@@ -13,6 +14,9 @@ interface EmployeeListProps {
|
|||||||
type SortField = 'name' | 'employeeType' | 'canWorkAlone' | 'role' | 'lastLogin';
|
type SortField = 'name' | 'employeeType' | 'canWorkAlone' | 'role' | 'lastLogin';
|
||||||
type SortDirection = 'asc' | 'desc';
|
type SortDirection = 'asc' | 'desc';
|
||||||
|
|
||||||
|
// FIXED: Use the actual employee types from the Employee interface
|
||||||
|
type EmployeeType = 'manager' | 'personell' | 'apprentice' | 'guest';
|
||||||
|
|
||||||
const EmployeeList: React.FC<EmployeeListProps> = ({
|
const EmployeeList: React.FC<EmployeeListProps> = ({
|
||||||
employees,
|
employees,
|
||||||
onEdit,
|
onEdit,
|
||||||
@@ -122,18 +126,18 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Using shared configuration for consistent styling
|
const getEmployeeTypeBadge = (type: EmployeeType, isTrainee: boolean = false) => {
|
||||||
type EmployeeType = 'manager' | 'trainee' | 'experienced';
|
|
||||||
|
|
||||||
const getEmployeeTypeBadge = (type: EmployeeType) => {
|
|
||||||
const config = EMPLOYEE_TYPE_CONFIG[type];
|
const config = EMPLOYEE_TYPE_CONFIG[type];
|
||||||
|
|
||||||
|
// FIXED: Updated color mapping for actual employee types
|
||||||
const bgColor =
|
const bgColor =
|
||||||
type === 'manager'
|
type === 'manager'
|
||||||
? '#fadbd8'
|
? '#fadbd8' // light red
|
||||||
: type === 'trainee'
|
: type === 'personell'
|
||||||
? '#d5f4e6'
|
? isTrainee ? '#d5f4e6' : '#d6eaf8' // light green for trainee, light blue for experienced
|
||||||
: '#d6eaf8'; // experienced
|
: type === 'apprentice'
|
||||||
|
? '#e8d7f7' // light purple for apprentice
|
||||||
|
: '#f8f9fa'; // light gray for guest
|
||||||
|
|
||||||
return { text: config.label, color: config.color, bgColor };
|
return { text: config.label, color: config.color, bgColor };
|
||||||
};
|
};
|
||||||
@@ -296,7 +300,8 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sortedEmployees.map(employee => {
|
{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 independence = getIndependenceBadge(employee.canWorkAlone);
|
||||||
const roleInfo = getRoleBadge(employee.roles);
|
const roleInfo = getRoleBadge(employee.roles);
|
||||||
const status = getStatusBadge(employee.isActive);
|
const status = getStatusBadge(employee.isActive);
|
||||||
@@ -541,19 +546,30 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
|
|||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
fontSize: '11px',
|
fontSize: '11px',
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
}}>👴 ERFAHREN</span>
|
}}>👨🏭 PERSONAL</span>
|
||||||
<span style={{ fontSize: '12px', color: '#666' }}>Langjährige Erfahrung</span>
|
<span style={{ fontSize: '12px', color: '#666' }}>Reguläre Mitarbeiter</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||||
<span style={{
|
<span style={{
|
||||||
backgroundColor: '#d5f4e6',
|
backgroundColor: '#e8d7f7',
|
||||||
color: '#27ae60',
|
color: '#9b59b6',
|
||||||
padding: '4px 8px',
|
padding: '4px 8px',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
fontSize: '11px',
|
fontSize: '11px',
|
||||||
fontWeight: 'bold'
|
fontWeight: 'bold'
|
||||||
}}>👶 NEULING</span>
|
}}>👨🎓 AUSZUBILDENDER</span>
|
||||||
<span style={{ fontSize: '12px', color: '#666' }}>Benötigt Einarbeitung</span>
|
<span style={{ fontSize: '12px', color: '#666' }}>Auszubildende</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
|
||||||
|
<span style={{
|
||||||
|
backgroundColor: '#f8f9fa',
|
||||||
|
color: '#95a5a6',
|
||||||
|
padding: '4px 8px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
fontSize: '11px',
|
||||||
|
fontWeight: 'bold'
|
||||||
|
}}>👤 GAST</span>
|
||||||
|
<span style={{ fontSize: '12px', color: '#666' }}>Externe Mitarbeiter</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,14 +16,27 @@ interface ExtendedTimeSlot extends TimeSlot {
|
|||||||
displayName?: string;
|
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 = [
|
const weekdays = [
|
||||||
{ id: 1, name: 'Mo' },
|
{ id: 1, name: 'Montag' },
|
||||||
{ id: 2, name: 'Di' },
|
{ id: 2, name: 'Dienstag' },
|
||||||
{ id: 3, name: 'Mi' },
|
{ id: 3, name: 'Mittwoch' },
|
||||||
{ id: 4, name: 'Do' },
|
{ id: 4, name: 'Donnerstag' },
|
||||||
{ id: 5, name: 'Fr' },
|
{ id: 5, name: 'Freitag' },
|
||||||
{ id: 6, name: 'Sa' },
|
{ id: 6, name: 'Samstag' },
|
||||||
{ id: 7, name: 'So' }
|
{ id: 7, name: 'Sonntag' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const ShiftPlanView: React.FC = () => {
|
const ShiftPlanView: React.FC = () => {
|
||||||
@@ -35,14 +48,13 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
const [shiftPlan, setShiftPlan] = useState<ShiftPlan | null>(null);
|
const [shiftPlan, setShiftPlan] = useState<ShiftPlan | null>(null);
|
||||||
const [employees, setEmployees] = useState<Employee[]>([]);
|
const [employees, setEmployees] = useState<Employee[]>([]);
|
||||||
const [availabilities, setAvailabilities] = useState<EmployeeAvailability[]>([]);
|
const [availabilities, setAvailabilities] = useState<EmployeeAvailability[]>([]);
|
||||||
const [assignmentResult, setAssignmentResult] = useState<AssignmentResult | null>(null); // Add this line
|
const [assignmentResult, setAssignmentResult] = useState<AssignmentResult | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [publishing, setPublishing] = useState(false);
|
const [publishing, setPublishing] = useState(false);
|
||||||
const [scheduledShifts, setScheduledShifts] = useState<ScheduledShift[]>([]);
|
const [scheduledShifts, setScheduledShifts] = useState<ScheduledShift[]>([]);
|
||||||
const [showAssignmentPreview, setShowAssignmentPreview] = useState(false);
|
const [showAssignmentPreview, setShowAssignmentPreview] = useState(false);
|
||||||
const [recreating, setRecreating] = useState(false);
|
const [recreating, setRecreating] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadShiftPlanData();
|
loadShiftPlanData();
|
||||||
|
|
||||||
@@ -76,31 +88,133 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Add this useEffect to debug state changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (assignmentResult) {
|
console.log('🔍 STATE DEBUG - showAssignmentPreview:', showAssignmentPreview);
|
||||||
console.log("🔄 assignmentResult UPDATED:", {
|
console.log('🔍 STATE DEBUG - assignmentResult:', assignmentResult ? 'EXISTS' : 'NULL');
|
||||||
success: assignmentResult.success,
|
console.log('🔍 STATE DEBUG - publishing:', publishing);
|
||||||
assignmentsCount: Object.keys(assignmentResult.assignments).length,
|
}, [showAssignmentPreview, assignmentResult, publishing]);
|
||||||
assignmentKeys: Object.keys(assignmentResult.assignments).slice(0, 5), // First 5 keys
|
|
||||||
violations: assignmentResult.violations.length
|
// 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<number, ExtendedShift[]>);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log all assignments with their keys
|
// Get unique days that have shifts - SAME LOGIC AS AVAILABILITYMANAGER
|
||||||
Object.entries(assignmentResult.assignments).forEach(([key, empIds]) => {
|
const days = Array.from(new Set(shiftPlan.shifts.map(shift => shift.dayOfWeek)))
|
||||||
console.log(` 🗂️ Assignment Key: ${key}`);
|
.sort()
|
||||||
console.log(` Employees: ${empIds.join(', ')}`);
|
.map(dayId => {
|
||||||
|
return weekdays.find(day => day.id === dayId) || { id: dayId, name: `Tag ${dayId}` };
|
||||||
|
});
|
||||||
|
|
||||||
// Try to identify what this key represents
|
// Get all unique time slots (rows) by collecting from all shifts - SAME LOGIC AS AVAILABILITYMANAGER
|
||||||
const isUuid = key.length === 36; // UUID format
|
const allTimeSlotsMap = new Map();
|
||||||
console.log(` Type: ${isUuid ? 'UUID (likely scheduled shift)' : 'Pattern (likely shift pattern)'}`);
|
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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [assignmentResult]);
|
});
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
// Populate shifts for each time slot by day - SAME LOGIC AS AVAILABILITYMANAGER
|
||||||
(window as any).debugRenderLogic = debugRenderLogic;
|
days.forEach(day => {
|
||||||
return () => { (window as any).debugRenderLogic = undefined; };
|
shiftsByDay[day.id]?.forEach(shift => {
|
||||||
}, [shiftPlan, scheduledShifts]);
|
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 () => {
|
const loadShiftPlanData = async () => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -132,7 +246,9 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
|
|
||||||
setScheduledShifts(shiftsData);
|
setScheduledShifts(shiftsData);
|
||||||
|
|
||||||
// Load availabilities
|
// Load availabilities - USING THE SAME LOGIC AS AVAILABILITYMANAGER
|
||||||
|
console.log('🔄 LADE VERFÜGBARKEITEN FÜR PLAN:', id);
|
||||||
|
|
||||||
const availabilityPromises = employeesData
|
const availabilityPromises = employeesData
|
||||||
.filter(emp => emp.isActive)
|
.filter(emp => emp.isActive)
|
||||||
.map(emp => employeeService.getAvailabilities(emp.id));
|
.map(emp => employeeService.getAvailabilities(emp.id));
|
||||||
@@ -140,12 +256,21 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
const allAvailabilities = await Promise.all(availabilityPromises);
|
const allAvailabilities = await Promise.all(availabilityPromises);
|
||||||
const flattenedAvailabilities = allAvailabilities.flat();
|
const flattenedAvailabilities = allAvailabilities.flat();
|
||||||
|
|
||||||
|
// Filter to only include availabilities for the current plan - SAME LOGIC AS AVAILABILITYMANAGER
|
||||||
const planAvailabilities = flattenedAvailabilities.filter(
|
const planAvailabilities = flattenedAvailabilities.filter(
|
||||||
availability => availability.planId === id
|
availability => availability.planId === id
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log('✅ VERFÜGBARKEITEN FÜR DIESEN PLAN:', planAvailabilities.length);
|
||||||
|
|
||||||
setAvailabilities(planAvailabilities);
|
setAvailabilities(planAvailabilities);
|
||||||
|
|
||||||
|
// Run validation
|
||||||
|
const validation = validateTimetableStructure();
|
||||||
|
if (!validation.isValid) {
|
||||||
|
console.warn('⚠️ TIMETABLE VALIDATION ERRORS:', validation.errors);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading shift plan data:', error);
|
console.error('Error loading shift plan data:', error);
|
||||||
showNotification({
|
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<number, typeof shiftPlan.shifts>);
|
|
||||||
|
|
||||||
// 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<number, ExtendedTimeSlot[]> = {};
|
|
||||||
|
|
||||||
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<string>();
|
|
||||||
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 getDayOfWeek = (dateString: string): number => {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
return date.getDay() === 0 ? 7 : date.getDay();
|
return date.getDay() === 0 ? 7 : date.getDay();
|
||||||
@@ -369,20 +357,31 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setPublishing(true);
|
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
|
// FORCE COMPLETE REFRESH - don't rely on cached state
|
||||||
const [refreshedEmployees, refreshedAvailabilities] = await Promise.all([
|
const [refreshedEmployees, refreshedAvailabilities] = await Promise.all([
|
||||||
// Reload employees fresh
|
|
||||||
employeeService.getEmployees().then(emps => emps.filter(emp => emp.isActive)),
|
employeeService.getEmployees().then(emps => emps.filter(emp => emp.isActive)),
|
||||||
// Reload availabilities fresh
|
|
||||||
refreshAllAvailabilities()
|
refreshAllAvailabilities()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
console.log('🔄 USING FRESH DATA:');
|
console.log('🔄 USING FRESH DATA:');
|
||||||
console.log('- Employees:', refreshedEmployees.length);
|
console.log('- Employees:', refreshedEmployees.length);
|
||||||
console.log('- Availabilities:', refreshedAvailabilities.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 = {
|
const constraints = {
|
||||||
enforceNoTraineeAlone: true,
|
enforceNoTraineeAlone: true,
|
||||||
enforceExperiencedWithChef: true,
|
enforceExperiencedWithChef: true,
|
||||||
@@ -390,6 +389,8 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
targetEmployeesPerShift: 2
|
targetEmployeesPerShift: 2
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('🧠 Calling shift assignment service...');
|
||||||
|
|
||||||
// Use the freshly loaded data, not the state
|
// Use the freshly loaded data, not the state
|
||||||
const result = await shiftAssignmentService.assignShifts(
|
const result = await shiftAssignmentService.assignShifts(
|
||||||
shiftPlan,
|
shiftPlan,
|
||||||
@@ -398,7 +399,6 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
constraints
|
constraints
|
||||||
);
|
);
|
||||||
|
|
||||||
// COMPREHENSIVE DEBUGGING
|
|
||||||
console.log("🎯 RAW ASSIGNMENT RESULT FROM API:", {
|
console.log("🎯 RAW ASSIGNMENT RESULT FROM API:", {
|
||||||
success: result.success,
|
success: result.success,
|
||||||
assignmentsCount: Object.keys(result.assignments).length,
|
assignmentsCount: Object.keys(result.assignments).length,
|
||||||
@@ -407,31 +407,31 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
resolutionReport: result.resolutionReport?.length || 0
|
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]) => {
|
Object.entries(result.assignments).forEach(([shiftId, empIds]) => {
|
||||||
console.log(` 📅 Assignment Key: ${shiftId}`);
|
const shiftPattern = shiftPlan.shifts?.find(s => s.id === shiftId);
|
||||||
console.log(` Employees: ${empIds.join(', ')}`);
|
|
||||||
|
|
||||||
// Try to identify what type of ID this is
|
if (shiftPattern) {
|
||||||
const isUuid = shiftId.length === 36; // UUID format
|
console.log(` ✅ Shift Pattern: ${shiftId}`);
|
||||||
console.log(` Type: ${isUuid ? 'UUID (likely scheduled shift)' : 'Pattern (likely shift pattern)'}`);
|
console.log(` - Day: ${shiftPattern.dayOfWeek}, TimeSlot: ${shiftPattern.timeSlotId}`);
|
||||||
|
console.log(` - Employees: ${empIds.join(', ')}`);
|
||||||
// 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 {
|
} else {
|
||||||
console.log(` ❌ No matching scheduled shift found for UUID`);
|
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);
|
setAssignmentResult(result);
|
||||||
setShowAssignmentPreview(true);
|
setShowAssignmentPreview(true);
|
||||||
|
|
||||||
|
console.log('✅ Assignment preview ready, modal should be visible');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during assignment:', error);
|
console.error('❌ Error during assignment:', error);
|
||||||
showNotification({
|
showNotification({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Fehler',
|
title: 'Fehler',
|
||||||
@@ -458,13 +458,13 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`📊 Found ${updatedShifts.length} scheduled shifts to update`);
|
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) => {
|
const updatePromises = updatedShifts.map(async (scheduledShift) => {
|
||||||
// ✅ FIX: Map scheduled shift to shift pattern to find assignments
|
|
||||||
const dayOfWeek = getDayOfWeek(scheduledShift.date);
|
const dayOfWeek = getDayOfWeek(scheduledShift.date);
|
||||||
|
|
||||||
// Find the corresponding shift pattern for this day and time slot
|
// 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.dayOfWeek === dayOfWeek &&
|
||||||
shift.timeSlotId === scheduledShift.timeSlotId
|
shift.timeSlotId === scheduledShift.timeSlotId
|
||||||
);
|
);
|
||||||
@@ -472,9 +472,13 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
let assignedEmployees: string[] = [];
|
let assignedEmployees: string[] = [];
|
||||||
|
|
||||||
if (shiftPattern) {
|
if (shiftPattern) {
|
||||||
// Look for assignments using the shift pattern ID (what scheduler uses)
|
|
||||||
assignedEmployees = assignmentResult.assignments[shiftPattern.id] || [];
|
assignedEmployees = assignmentResult.assignments[shiftPattern.id] || [];
|
||||||
console.log(`📝 Updating scheduled shift ${scheduledShift.id} (Day ${dayOfWeek}, TimeSlot ${scheduledShift.timeSlotId}) with`, assignedEmployees, 'employees');
|
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 {
|
} else {
|
||||||
console.warn(`⚠️ No shift pattern found for scheduled shift ${scheduledShift.id} (Day ${dayOfWeek}, TimeSlot ${scheduledShift.timeSlotId})`);
|
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);
|
setShiftPlan(reloadedPlan);
|
||||||
setScheduledShifts(reloadedShifts);
|
setScheduledShifts(reloadedShifts);
|
||||||
|
|
||||||
|
setShowAssignmentPreview(false);
|
||||||
|
setAssignmentResult(null);
|
||||||
|
|
||||||
|
console.log('✅ Publishing completed, modal closed');
|
||||||
|
|
||||||
showNotification({
|
showNotification({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: 'Erfolg',
|
title: 'Erfolg',
|
||||||
message: 'Schichtplan wurde erfolgreich veröffentlicht!'
|
message: 'Schichtplan wurde erfolgreich veröffentlicht!'
|
||||||
});
|
});
|
||||||
|
|
||||||
setShowAssignmentPreview(false);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error publishing shift plan:', error);
|
console.error('❌ Error publishing shift plan:', error);
|
||||||
|
|
||||||
@@ -576,37 +583,37 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateSchedulingData = (): boolean => {
|
const debugShiftMatching = () => {
|
||||||
console.log('🔍 Validating scheduling data...');
|
if (!shiftPlan || !scheduledShifts.length) return;
|
||||||
|
|
||||||
const totalEmployees = employees.length;
|
console.log('🔍 DEBUG: Shift Pattern to Scheduled Shift Matching');
|
||||||
const employeesWithAvailabilities = new Set(
|
console.log('==================================================');
|
||||||
availabilities.map(avail => avail.employeeId)
|
|
||||||
).size;
|
|
||||||
|
|
||||||
const availabilityStatus = {
|
shiftPlan.shifts?.forEach(shiftPattern => {
|
||||||
totalEmployees,
|
const matchingScheduledShifts = scheduledShifts.filter(scheduled => {
|
||||||
employeesWithAvailabilities,
|
const dayOfWeek = getDayOfWeek(scheduled.date);
|
||||||
coverage: Math.round((employeesWithAvailabilities / totalEmployees) * 100)
|
return dayOfWeek === shiftPattern.dayOfWeek &&
|
||||||
};
|
scheduled.timeSlotId === shiftPattern.timeSlotId;
|
||||||
|
});
|
||||||
|
|
||||||
console.log('📊 Availability Coverage:', availabilityStatus);
|
console.log(`📅 Shift Pattern: ${shiftPattern.id}`);
|
||||||
|
console.log(` - Day: ${shiftPattern.dayOfWeek}, TimeSlot: ${shiftPattern.timeSlotId}`);
|
||||||
|
console.log(` - Matching scheduled shifts: ${matchingScheduledShifts.length}`);
|
||||||
|
|
||||||
// Check if we have ALL employee availabilities
|
if (assignmentResult) {
|
||||||
if (employeesWithAvailabilities < totalEmployees) {
|
const assignments = assignmentResult.assignments[shiftPattern.id] || [];
|
||||||
const missingEmployees = employees.filter(emp =>
|
console.log(` - Assignments: ${assignments.length} employees`);
|
||||||
!availabilities.some(avail => avail.employeeId === emp.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.warn('⚠️ Missing availabilities for employees:',
|
|
||||||
missingEmployees.map(emp => emp.email));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Rufe die Debug-Funktion auf, wenn Assignment-Ergebnisse geladen werden
|
||||||
|
useEffect(() => {
|
||||||
|
if (assignmentResult && shiftPlan) {
|
||||||
|
debugShiftMatching();
|
||||||
|
}
|
||||||
|
}, [assignmentResult, shiftPlan]);
|
||||||
|
|
||||||
const canPublish = () => {
|
const canPublish = () => {
|
||||||
if (!shiftPlan || shiftPlan.status === 'published') return false;
|
if (!shiftPlan || shiftPlan.status === 'published') return false;
|
||||||
|
|
||||||
@@ -660,32 +667,33 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
const getAssignmentsForScheduledShift = (scheduledShift: ScheduledShift): string[] => {
|
const getAssignmentsForScheduledShift = (scheduledShift: ScheduledShift): string[] => {
|
||||||
if (!assignmentResult) return [];
|
if (!assignmentResult) return [];
|
||||||
|
|
||||||
// First try direct match with scheduled shift ID
|
|
||||||
if (assignmentResult.assignments[scheduledShift.id]) {
|
|
||||||
return assignmentResult.assignments[scheduledShift.id];
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no direct match, try to find by day and timeSlot pattern
|
|
||||||
const dayOfWeek = getDayOfWeek(scheduledShift.date);
|
const 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.dayOfWeek === dayOfWeek &&
|
||||||
shift.timeSlotId === scheduledShift.timeSlotId
|
shift.timeSlotId === scheduledShift.timeSlotId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shiftPattern && assignmentResult.assignments[shiftPattern.id]) {
|
if (shiftPattern && assignmentResult.assignments[shiftPattern.id]) {
|
||||||
|
console.log(`✅ Found assignments for shift pattern ${shiftPattern.id}:`, assignmentResult.assignments[shiftPattern.id]);
|
||||||
return 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 [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render timetable using the same structure as AvailabilityManager
|
// Render timetable using the same structure as AvailabilityManager
|
||||||
const renderTimetable = () => {
|
const renderTimetable = () => {
|
||||||
const { days, allTimeSlots, timeSlotsByDay } = getTimetableData();
|
const { days, allTimeSlots } = getTimetableData();
|
||||||
if (!shiftPlan?.id) {
|
const validation = validateTimetableStructure();
|
||||||
console.warn("Shift plan ID is missing");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (days.length === 0 || allTimeSlots.length === 0) {
|
if (days.length === 0 || allTimeSlots.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -719,10 +727,39 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
}}>
|
}}>
|
||||||
Schichtplan
|
Schichtplan
|
||||||
<div style={{ fontSize: '14px', fontWeight: 'normal', marginTop: '5px' }}>
|
<div style={{ fontSize: '14px', fontWeight: 'normal', marginTop: '5px' }}>
|
||||||
{allTimeSlots.length} Schichttypen • {days.length} Tage • Nur tatsächlich im Plan verwendete Schichten
|
{allTimeSlots.length} Zeitslots • {days.length} Tage • Zeitbasierte Darstellung
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Validation Warnings - SAME AS AVAILABILITYMANAGER */}
|
||||||
|
{!validation.isValid && (
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: '#fff3cd',
|
||||||
|
border: '1px solid #ffeaa7',
|
||||||
|
padding: '15px',
|
||||||
|
margin: '10px'
|
||||||
|
}}>
|
||||||
|
<h4 style={{ margin: '0 0 10px 0', color: '#856404' }}>⚠️ Validierungswarnungen:</h4>
|
||||||
|
<ul style={{ margin: 0, paddingLeft: '20px', fontSize: '12px' }}>
|
||||||
|
{validation.errors.map((error, index) => (
|
||||||
|
<li key={index}>{error}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Timetable Structure Info - SAME AS AVAILABILITYMANAGER */}
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: '#d1ecf1',
|
||||||
|
border: '1px solid #bee5eb',
|
||||||
|
padding: '10px 15px',
|
||||||
|
margin: '10px',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '12px'
|
||||||
|
}}>
|
||||||
|
<strong>Struktur-Info:</strong> {allTimeSlots.length} Zeitslots × {days.length} Tage = {allTimeSlots.length * days.length} Zellen
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ overflowX: 'auto' }}>
|
<div style={{ overflowX: 'auto' }}>
|
||||||
<table style={{
|
<table style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -754,23 +791,32 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{allTimeSlots.map((timeSlot, timeIndex) => (
|
{allTimeSlots.map((timeSlot, timeSlotIndex) => (
|
||||||
<tr key={timeSlot.id} style={{
|
<tr key={timeSlot.id} style={{
|
||||||
backgroundColor: timeIndex % 2 === 0 ? 'white' : '#f8f9fa'
|
backgroundColor: timeSlotIndex % 2 === 0 ? 'white' : '#f8f9fa'
|
||||||
}}>
|
}}>
|
||||||
<td style={{
|
<td style={{
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
border: '1px solid #dee2e6',
|
border: '1px solid #dee2e6',
|
||||||
fontWeight: '500',
|
fontWeight: '500',
|
||||||
backgroundColor: '#f8f9fa'
|
backgroundColor: '#f8f9fa',
|
||||||
|
position: 'sticky',
|
||||||
|
left: 0
|
||||||
}}>
|
}}>
|
||||||
{timeSlot.displayName}
|
<div style={{ fontWeight: 'bold' }}>
|
||||||
|
{timeSlot.name}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#666' }}>
|
||||||
|
{formatTime(timeSlot.startTime)} - {formatTime(timeSlot.endTime)}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontSize: '11px', color: '#999', marginTop: '4px' }}>
|
||||||
|
ID: {timeSlot.id.substring(0, 8)}...
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
{days.map(weekday => {
|
{days.map(weekday => {
|
||||||
// Check if this time slot exists for this day
|
const shift = timeSlot.shiftsByDay[weekday.id];
|
||||||
const timeSlotForDay = timeSlotsByDay[weekday.id]?.find(ts => ts.id === timeSlot.id);
|
|
||||||
|
|
||||||
if (!timeSlotForDay) {
|
if (!shift) {
|
||||||
return (
|
return (
|
||||||
<td key={weekday.id} style={{
|
<td key={weekday.id} style={{
|
||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
@@ -780,11 +826,14 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
color: '#ccc',
|
color: '#ccc',
|
||||||
fontStyle: 'italic'
|
fontStyle: 'italic'
|
||||||
}}>
|
}}>
|
||||||
-
|
Kein Shift
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 assignedEmployees: string[] = [];
|
||||||
let displayText = '';
|
let displayText = '';
|
||||||
|
|
||||||
@@ -806,7 +855,7 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
|
|
||||||
displayText = assignedEmployees.map(empId => {
|
displayText = assignedEmployees.map(empId => {
|
||||||
const employee = employees.find(emp => emp.id === empId);
|
const employee = employees.find(emp => emp.id === empId);
|
||||||
return employee ? employee.email : 'Unbekannt';
|
return employee ? `${employee.firstname} ${employee.lastname}` : 'Unbekannt';
|
||||||
}).join(', ');
|
}).join(', ');
|
||||||
}
|
}
|
||||||
} else if (assignmentResult) {
|
} else if (assignmentResult) {
|
||||||
@@ -821,20 +870,20 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
assignedEmployees = getAssignmentsForScheduledShift(scheduledShift);
|
assignedEmployees = getAssignmentsForScheduledShift(scheduledShift);
|
||||||
displayText = assignedEmployees.map(empId => {
|
displayText = assignedEmployees.map(empId => {
|
||||||
const employee = employees.find(emp => emp.id === empId);
|
const employee = employees.find(emp => emp.id === empId);
|
||||||
return employee ? employee.email : 'Unbekannt';
|
return employee ? `${employee.firstname} ${employee.lastname}` : 'Unbekannt';
|
||||||
}).join(', ');
|
}).join(', ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no assignments yet, show empty or required count
|
// If no assignments yet, show empty or required count
|
||||||
if (!displayText) {
|
if (!displayText) {
|
||||||
const shiftsForSlot = shiftPlan?.shifts?.filter(shift =>
|
const shiftsForSlot = shiftPlan?.shifts?.filter(s =>
|
||||||
shift.dayOfWeek === weekday.id &&
|
s.dayOfWeek === weekday.id &&
|
||||||
shift.timeSlotId === timeSlot.id
|
s.timeSlotId === timeSlot.id
|
||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
const totalRequired = shiftsForSlot.reduce((sum, shift) =>
|
const totalRequired = shiftsForSlot.reduce((sum, s) =>
|
||||||
sum + shift.requiredEmployees, 0);
|
sum + s.requiredEmployees, 0);
|
||||||
|
|
||||||
// Show "0/2" instead of just "0" to indicate it's empty
|
// Show "0/2" instead of just "0" to indicate it's empty
|
||||||
displayText = `0/${totalRequired}`;
|
displayText = `0/${totalRequired}`;
|
||||||
@@ -850,11 +899,51 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
padding: '12px 16px',
|
padding: '12px 16px',
|
||||||
border: '1px solid #dee2e6',
|
border: '1px solid #dee2e6',
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
backgroundColor: assignedEmployees.length > 0 ? '#e8f5e8' : 'transparent',
|
backgroundColor: !isValidShift ? '#fff3cd' : (assignedEmployees.length > 0 ? '#e8f5e8' : 'transparent'),
|
||||||
color: assignedEmployees.length > 0 ? '#2c3e50' : '#666',
|
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 && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '2px',
|
||||||
|
right: '2px',
|
||||||
|
backgroundColor: '#f39c12',
|
||||||
|
color: 'white',
|
||||||
|
borderRadius: '50%',
|
||||||
|
width: '16px',
|
||||||
|
height: '16px',
|
||||||
|
fontSize: '10px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
title={`Shift Validierung: timeSlotId=${shift.timeSlotId}, dayOfWeek=${shift.dayOfWeek}`}
|
||||||
|
>
|
||||||
|
⚠️
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{displayText}
|
{displayText}
|
||||||
|
|
||||||
|
{/* Shift debug info - SAME AS AVAILABILITYMANAGER */}
|
||||||
|
<div style={{
|
||||||
|
fontSize: '10px',
|
||||||
|
color: '#666',
|
||||||
|
marginTop: '4px',
|
||||||
|
textAlign: 'left',
|
||||||
|
fontFamily: 'monospace'
|
||||||
|
}}>
|
||||||
|
<div>Shift: {shift.id.substring(0, 6)}...</div>
|
||||||
|
<div>Day: {shift.dayOfWeek}</div>
|
||||||
|
{!isValidShift && (
|
||||||
|
<div style={{ color: '#e74c3c', fontWeight: 'bold' }}>
|
||||||
|
VALIDATION ERROR
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -863,6 +952,24 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Summary Statistics - SAME AS AVAILABILITYMANAGER */}
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: '#f8f9fa',
|
||||||
|
padding: '15px',
|
||||||
|
borderTop: '1px solid #dee2e6',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#666'
|
||||||
|
}}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||||
|
<div>
|
||||||
|
<strong>Zusammenfassung:</strong> {allTimeSlots.length} Zeitslots × {days.length} Tage = {allTimeSlots.length * days.length} mögliche Shifts
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Validierungsfehler:</strong> {validation.errors.length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -872,7 +979,7 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
|
|
||||||
const { days, allTimeSlots } = getTimetableData();
|
const { days, allTimeSlots } = getTimetableData();
|
||||||
const availabilityStatus = getAvailabilityStatus();
|
const availabilityStatus = getAvailabilityStatus();
|
||||||
|
const validation = validateTimetableStructure();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '20px' }}>
|
<div style={{ padding: '20px' }}>
|
||||||
@@ -938,6 +1045,50 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Debug Info - Enhanced */}
|
||||||
|
<div style={{
|
||||||
|
backgroundColor: validation.errors.length > 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'
|
||||||
|
}}>
|
||||||
|
<h4 style={{
|
||||||
|
margin: '0 0 10px 0',
|
||||||
|
color: validation.errors.length > 0 ? '#856404' : (allTimeSlots.length === 0 ? '#721c24' : '#0c5460')
|
||||||
|
}}>
|
||||||
|
{validation.errors.length > 0 ? '⚠️ VALIDIERUNGSPROBLEME' :
|
||||||
|
allTimeSlots.length === 0 ? '❌ KEINE SHIFTS GEFUNDEN' : '✅ PLAN-DATEN GELADEN'}
|
||||||
|
</h4>
|
||||||
|
<div style={{ fontSize: '12px', fontFamily: 'monospace' }}>
|
||||||
|
<div><strong>Ausgewählter Plan:</strong> {shiftPlan.name}</div>
|
||||||
|
<div><strong>Plan ID:</strong> {shiftPlan.id}</div>
|
||||||
|
<div><strong>Einzigartige Zeitslots:</strong> {allTimeSlots.length}</div>
|
||||||
|
<div><strong>Verwendete Tage:</strong> {days.length} ({days.map(d => d.name).join(', ')})</div>
|
||||||
|
<div><strong>Shift Patterns:</strong> {shiftPlan.shifts?.length || 0}</div>
|
||||||
|
<div><strong>Scheduled Shifts:</strong> {scheduledShifts.length}</div>
|
||||||
|
<div><strong>Geladene Verfügbarkeiten:</strong> {availabilities.length}</div>
|
||||||
|
<div><strong>Aktive Mitarbeiter:</strong> {employees.length}</div>
|
||||||
|
{assignmentResult && (
|
||||||
|
<div><strong>Assignment Keys:</strong> {Object.keys(assignmentResult.assignments).length}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Show shift pattern vs scheduled shift matching */}
|
||||||
|
{shiftPlan.shifts && scheduledShifts.length > 0 && (
|
||||||
|
<div style={{ marginTop: '10px', paddingTop: '10px', borderTop: '1px solid #bee5eb' }}>
|
||||||
|
<strong>Shift Matching:</strong>
|
||||||
|
<div style={{ fontSize: '11px' }}>
|
||||||
|
• {shiftPlan.shifts.length} Patterns → {scheduledShifts.length} Scheduled Shifts
|
||||||
|
{assignmentResult && (
|
||||||
|
<div>• {Object.keys(assignmentResult.assignments).length} Assignment Keys</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Rest of the component remains the same... */}
|
||||||
{/* Availability Status - only show for drafts */}
|
{/* Availability Status - only show for drafts */}
|
||||||
{shiftPlan.status === 'draft' && (
|
{shiftPlan.status === 'draft' && (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -1017,8 +1168,8 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Assignment Preview Modal */}
|
{/* Assignment Preview Modal - FIXED CONDITION */}
|
||||||
{showAssignmentPreview && assignmentResult && (
|
{(showAssignmentPreview || assignmentResult) && (
|
||||||
<div style={{
|
<div style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
@@ -1037,12 +1188,13 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
padding: '30px',
|
padding: '30px',
|
||||||
maxWidth: '800px',
|
maxWidth: '800px',
|
||||||
maxHeight: '80vh',
|
maxHeight: '80vh',
|
||||||
overflow: 'auto'
|
overflow: 'auto',
|
||||||
|
width: '90%'
|
||||||
}}>
|
}}>
|
||||||
<h2>Wochenmuster-Zuordnung</h2>
|
<h2>Wochenmuster-Zuordnung</h2>
|
||||||
|
|
||||||
{/* Detaillierter Reparatur-Bericht anzeigen */}
|
{/* Detaillierter Reparatur-Bericht anzeigen */}
|
||||||
{assignmentResult.resolutionReport && (
|
{assignmentResult?.resolutionReport && (
|
||||||
<div style={{
|
<div style={{
|
||||||
backgroundColor: '#f8f9fa',
|
backgroundColor: '#f8f9fa',
|
||||||
border: '1px solid #e9ecef',
|
border: '1px solid #e9ecef',
|
||||||
@@ -1170,7 +1322,10 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
|
|
||||||
<div style={{ display: 'flex', gap: '10px', justifyContent: 'flex-end' }}>
|
<div style={{ display: 'flex', gap: '10px', justifyContent: 'flex-end' }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowAssignmentPreview(false)}
|
onClick={() => {
|
||||||
|
setShowAssignmentPreview(false);
|
||||||
|
setAssignmentResult(null);
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
padding: '8px 16px',
|
padding: '8px 16px',
|
||||||
backgroundColor: '#95a5a6',
|
backgroundColor: '#95a5a6',
|
||||||
@@ -1183,32 +1338,35 @@ const ShiftPlanView: React.FC = () => {
|
|||||||
Abbrechen
|
Abbrechen
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* KORRIGIERTER BUTTON MIT TYPESCRIPT-FIX */}
|
||||||
<button
|
<button
|
||||||
onClick={handlePublish}
|
onClick={handlePublish}
|
||||||
disabled={publishing || assignmentResult.violations.filter(v =>
|
disabled={publishing || (assignmentResult ? assignmentResult.violations.filter(v =>
|
||||||
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
||||||
).length > 0}
|
).length > 0 : true)}
|
||||||
style={{
|
style={{
|
||||||
padding: '10px 20px',
|
padding: '10px 20px',
|
||||||
backgroundColor: assignmentResult.violations.filter(v =>
|
backgroundColor: assignmentResult ? (assignmentResult.violations.filter(v =>
|
||||||
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
||||||
).length === 0 ? '#2ecc71' : '#95a5a6',
|
).length === 0 ? '#2ecc71' : '#95a5a6') : '#95a5a6',
|
||||||
color: 'white',
|
color: 'white',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
cursor: assignmentResult.violations.filter(v =>
|
cursor: assignmentResult ? (assignmentResult.violations.filter(v =>
|
||||||
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
||||||
).length === 0 ? 'pointer' : 'not-allowed',
|
).length === 0 ? 'pointer' : 'not-allowed') : 'not-allowed',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
fontSize: '16px'
|
fontSize: '16px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{publishing ? 'Veröffentliche...' : (
|
{publishing ? 'Veröffentliche...' : (
|
||||||
|
assignmentResult ? (
|
||||||
assignmentResult.violations.filter(v =>
|
assignmentResult.violations.filter(v =>
|
||||||
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
v.includes('ERROR:') || v.includes('❌ KRITISCH:')
|
||||||
).length === 0
|
).length === 0
|
||||||
? 'Schichtplan veröffentlichen'
|
? 'Schichtplan veröffentlichen'
|
||||||
: 'Kritische Probleme müssen behoben werden'
|
: 'Kritische Probleme müssen behoben werden'
|
||||||
|
) : 'Lade Zuordnungen...'
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -130,6 +130,22 @@ export class EmployeeService {
|
|||||||
throw new Error(error.error || 'Failed to change password');
|
throw new Error(error.error || 'Failed to change password');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateLastLogin(employeeId: string): Promise<void> {
|
||||||
|
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();
|
export const employeeService = new EmployeeService();
|
||||||
Reference in New Issue
Block a user