fixed role handling for employees

This commit is contained in:
2025-10-20 11:27:06 +02:00
parent ec28c061a0
commit 3c4fbc0798
18 changed files with 640 additions and 318 deletions

View File

@@ -24,7 +24,7 @@ export interface LoginRequest {
} }
export interface JWTPayload { export interface JWTPayload {
id: string; // ← VON number ZU string ÄNDERN id: string;
email: string; email: string;
role: string; role: string;
iat?: number; iat?: number;
@@ -32,16 +32,26 @@ export interface JWTPayload {
} }
export interface RegisterRequest { export interface RegisterRequest {
// REMOVED: email - will be auto-generated
password: string; password: string;
firstname: string; firstname: string;
lastname: string; lastname: string;
role?: string; roles?: string[];
} }
function generateEmail(firstname: string, lastname: string): string { function generateEmail(firstname: string, lastname: string): string {
const cleanFirstname = firstname.toLowerCase().replace(/[^a-z0-9]/g, ''); // Convert German umlauts to their expanded forms
const cleanLastname = lastname.toLowerCase().replace(/[^a-z0-9]/g, ''); const convertUmlauts = (str: string): string => {
return str
.toLowerCase()
.replace(/ü/g, 'ue')
.replace(/ö/g, 'oe')
.replace(/ä/g, 'ae')
.replace(/ß/g, 'ss');
};
// Remove any remaining special characters and convert to lowercase
const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, '');
const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, '');
return `${cleanFirstname}.${cleanLastname}@sp.de`; return `${cleanFirstname}.${cleanLastname}@sp.de`;
} }
@@ -56,9 +66,17 @@ export const login = async (req: Request, res: Response) => {
return res.status(400).json({ error: 'E-Mail und Passwort sind erforderlich' }); return res.status(400).json({ error: 'E-Mail und Passwort sind erforderlich' });
} }
// Get user from database // UPDATED: Get user from database with role from employee_roles table
const user = await db.get<EmployeeWithPassword>( const user = await db.get<any>(
'SELECT id, email, password, firstname, lastname, role, employee_type as employeeType, contract_type as contractType, can_work_alone as canWorkAlone, is_active as isActive FROM employees WHERE email = ? AND is_active = 1', `SELECT
e.id, e.email, e.password, e.firstname, e.lastname,
e.employee_type as employeeType, e.contract_type as contractType,
e.can_work_alone as canWorkAlone, e.is_active as isActive,
er.role
FROM employees e
LEFT JOIN employee_roles er ON e.id = er.employee_id
WHERE e.email = ? AND e.is_active = 1
LIMIT 1`,
[email] [email]
); );
@@ -78,11 +96,11 @@ 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' });
} }
// Create token payload - KORREKT: id field verwenden // Create token payload
const tokenPayload = { const tokenPayload = {
id: user.id.toString(), // ← WICHTIG: Dies wird als 'id' im JWT gespeichert id: user.id.toString(),
email: user.email, email: user.email,
role: user.role role: user.role || 'user' // Fallback to 'user' if no role found
}; };
console.log('🎫 Creating JWT with payload:', tokenPayload); console.log('🎫 Creating JWT with payload:', tokenPayload);
@@ -94,13 +112,17 @@ export const login = async (req: Request, res: Response) => {
{ expiresIn: '24h' } { expiresIn: '24h' }
); );
// Remove password from user object // Remove password from user object and format response
const { password: _, ...userWithoutPassword } = user; const { password: _, ...userWithoutPassword } = user;
const userResponse = {
...userWithoutPassword,
roles: user.role ? [user.role] : ['user'] // Convert single role to array for frontend compatibility
};
console.log('✅ Login successful for:', user.email); console.log('✅ Login successful for:', user.email);
res.json({ res.json({
user: userWithoutPassword, user: userResponse,
token token
}); });
} catch (error) { } catch (error) {
@@ -121,8 +143,17 @@ export const getCurrentUser = async (req: Request, res: Response) => {
return res.status(401).json({ error: 'Nicht authentifiziert' }); return res.status(401).json({ error: 'Nicht authentifiziert' });
} }
const user = await db.get<Employee>( // UPDATED: Get user with role from employee_roles table
'SELECT id, email, firstname, lastname, role, employee_type as employeeType, contract_type as contractType, can_work_alone as canWorkAlone, is_active as isActive FROM employees WHERE id = ? AND is_active = 1', const user = await db.get<any>(
`SELECT
e.id, e.email, e.firstname, e.lastname,
e.employee_type as employeeType, e.contract_type as contractType,
e.can_work_alone as canWorkAlone, e.is_active as isActive,
er.role
FROM employees e
LEFT JOIN employee_roles er ON e.id = er.employee_id
WHERE e.id = ? AND e.is_active = 1
LIMIT 1`,
[jwtUser.userId] [jwtUser.userId]
); );
@@ -133,8 +164,14 @@ export const getCurrentUser = async (req: Request, res: Response) => {
return res.status(404).json({ error: 'Benutzer nicht gefunden' }); return res.status(404).json({ error: 'Benutzer nicht gefunden' });
} }
// Format user response with roles array
const userResponse = {
...user,
roles: user.role ? [user.role] : ['user']
};
console.log('✅ Returning user:', user.email); console.log('✅ Returning user:', user.email);
res.json({ user }); res.json({ user: userResponse });
} catch (error) { } catch (error) {
console.error('Get current user error:', error); console.error('Get current user error:', error);
res.status(500).json({ error: 'Ein Fehler ist beim Abrufen des Benutzers aufgetreten' }); res.status(500).json({ error: 'Ein Fehler ist beim Abrufen des Benutzers aufgetreten' });
@@ -168,9 +205,9 @@ export const validateToken = async (req: Request, res: Response) => {
export const register = async (req: Request, res: Response) => { export const register = async (req: Request, res: Response) => {
try { try {
const { password, firstname, lastname, role = 'user' } = req.body as RegisterRequest; const { password, firstname, lastname, roles = ['user'] } = req.body as RegisterRequest;
// Validate required fields - REMOVED email // Validate required fields
if (!password || !firstname || !lastname) { if (!password || !firstname || !lastname) {
return res.status(400).json({ return res.status(400).json({
error: 'Password, firstname und lastname sind erforderlich' error: 'Password, firstname und lastname sind erforderlich'
@@ -192,27 +229,60 @@ export const register = async (req: Request, res: Response) => {
}); });
} }
// Hash password and create user // Hash password
const hashedPassword = await bcrypt.hash(password, 10); const hashedPassword = await bcrypt.hash(password, 10);
const employeeId = uuidv4();
// Insert user with generated email // Start transaction for registration
await db.run('BEGIN TRANSACTION');
try {
// Insert user without role (role is now in employee_roles table)
const result = await db.run( const result = await db.run(
`INSERT INTO employees (id, email, password, firstname, lastname, role, 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)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[uuidv4(), email, hashedPassword, firstname, lastname, role, 'experienced', 'small', false, 1] [employeeId, email, hashedPassword, firstname, lastname, 'experienced', 'small', false, 1]
); );
if (!result.lastID) { if (!result.lastID) {
throw new Error('Benutzer konnte nicht erstellt werden'); throw new Error('Benutzer konnte nicht erstellt werden');
} }
// Get created user // UPDATED: Insert roles into employee_roles table
const newUser = await db.get<Employee>( for (const role of roles) {
'SELECT id, email, firstname, lastname, role FROM employees WHERE id = ?', await db.run(
[result.lastID] `INSERT INTO employee_roles (employee_id, role) VALUES (?, ?)`,
[employeeId, role]
);
}
await db.run('COMMIT');
// Get created user with role
const newUser = await db.get<any>(
`SELECT
e.id, e.email, e.firstname, e.lastname,
er.role
FROM employees e
LEFT JOIN employee_roles er ON e.id = er.employee_id
WHERE e.id = ?
LIMIT 1`,
[employeeId]
); );
res.status(201).json({ user: newUser }); // Format response with roles array
const userResponse = {
...newUser,
roles: newUser.role ? [newUser.role] : ['user']
};
res.status(201).json({ user: userResponse });
} catch (error) {
await db.run('ROLLBACK');
throw error;
}
} catch (error) { } catch (error) {
console.error('Registration error:', error); console.error('Registration error:', error);
res.status(500).json({ res.status(500).json({

View File

@@ -33,25 +33,35 @@ export const getEmployees = async (req: AuthRequest, res: Response): Promise<voi
let query = ` let query = `
SELECT SELECT
id, email, firstname, lastname, role, is_active as isActive, e.id, e.email, e.firstname, e.lastname,
employee_type as employeeType, e.is_active as isActive,
contract_type as contractType, e.employee_type as employeeType,
can_work_alone as canWorkAlone, e.contract_type as contractType,
created_at as createdAt, e.can_work_alone as canWorkAlone,
last_login as lastLogin e.created_at as createdAt,
FROM employees e.last_login as lastLogin,
er.role
FROM employees e
LEFT JOIN employee_roles er ON e.id = er.employee_id
`; `;
if (!includeInactiveFlag) { if (!includeInactiveFlag) {
query += ' WHERE is_active = 1'; query += ' WHERE e.is_active = 1';
} }
query += ' ORDER BY name'; // UPDATED: Order by firstname and lastname
query += ' ORDER BY e.firstname, e.lastname';
const employees = await db.all<any>(query); const employees = await db.all<any>(query);
console.log('✅ Employees found:', employees.length); // Format employees with roles array for frontend compatibility
res.json(employees); const employeesWithRoles = employees.map(emp => ({
...emp,
roles: emp.role ? [emp.role] : ['user']
}));
console.log('✅ Employees found:', employeesWithRoles.length);
res.json(employeesWithRoles);
} catch (error) { } catch (error) {
console.error('❌ Error fetching employees:', error); console.error('❌ Error fetching employees:', error);
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
@@ -62,16 +72,21 @@ 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
id, email, firstname, lastname, role, is_active as isActive, e.id, e.email, e.firstname, e.lastname,
employee_type as employeeType, e.is_active as isActive,
contract_type as contractType, e.employee_type as employeeType,
can_work_alone as canWorkAlone, e.contract_type as contractType,
created_at as createdAt, e.can_work_alone as canWorkAlone,
last_login as lastLogin e.created_at as createdAt,
FROM employees e.last_login as lastLogin,
WHERE id = ? er.role
FROM employees e
LEFT JOIN employee_roles er ON e.id = er.employee_id
WHERE e.id = ?
LIMIT 1
`, [id]); `, [id]);
if (!employee) { if (!employee) {
@@ -79,7 +94,13 @@ export const getEmployee = async (req: AuthRequest, res: Response): Promise<void
return; return;
} }
res.json(employee); // Format employee with roles array
const employeeWithRoles = {
...employee,
roles: employee.role ? [employee.role] : ['user']
};
res.json(employeeWithRoles);
} catch (error) { } catch (error) {
console.error('Error fetching employee:', error); console.error('Error fetching employee:', error);
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
@@ -97,17 +118,17 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
password, password,
firstname, firstname,
lastname, lastname,
role, roles = ['user'], // UPDATED: Now uses roles array
employeeType, employeeType,
contractType, contractType,
canWorkAlone canWorkAlone
} = req.body as CreateEmployeeRequest; } = req.body as CreateEmployeeRequest;
// Validation - REMOVED email check // Validation
if (!password || !firstname || !lastname || !role || !employeeType || !contractType) { if (!password || !firstname || !lastname || !employeeType || !contractType) {
console.log('❌ Validation failed: Missing required fields'); console.log('❌ Validation failed: Missing required fields');
res.status(400).json({ res.status(400).json({
error: 'Password, firstname, lastname, role, employeeType und contractType sind erforderlich' error: 'Password, firstname, lastname, employeeType und contractType sind erforderlich'
}); });
return; return;
} }
@@ -131,18 +152,22 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
const hashedPassword = await bcrypt.hash(password, 10); const hashedPassword = await bcrypt.hash(password, 10);
const employeeId = uuidv4(); const employeeId = uuidv4();
// Start transaction for employee creation and role assignment
await db.run('BEGIN TRANSACTION');
try {
// UPDATED: Insert employee without role (role is now in employee_roles table)
await db.run( await db.run(
`INSERT INTO employees ( `INSERT INTO employees (
id, email, password, firstname, lastname, role, employee_type, contract_type, can_work_alone, id, email, password, firstname, lastname, employee_type, contract_type, can_work_alone,
is_active is_active
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[ [
employeeId, employeeId,
email, email,
hashedPassword, hashedPassword,
firstname, firstname,
lastname, lastname,
role,
employeeType, employeeType,
contractType, contractType,
canWorkAlone ? 1 : 0, canWorkAlone ? 1 : 0,
@@ -150,20 +175,45 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
] ]
); );
// Return created employee with generated email // UPDATED: Insert roles into employee_roles table
for (const role of roles) {
await db.run(
`INSERT INTO employee_roles (employee_id, role) VALUES (?, ?)`,
[employeeId, role]
);
}
await db.run('COMMIT');
// Return created employee with role from employee_roles
const newEmployee = await db.get<any>(` const newEmployee = await db.get<any>(`
SELECT SELECT
id, email, firstname, lastname, role, is_active as isActive, e.id, e.email, e.firstname, e.lastname,
employee_type as employeeType, e.is_active as isActive,
contract_type as contractType, e.employee_type as employeeType,
can_work_alone as canWorkAlone, e.contract_type as contractType,
created_at as createdAt, e.can_work_alone as canWorkAlone,
last_login as lastLogin e.created_at as createdAt,
FROM employees e.last_login as lastLogin,
WHERE id = ? er.role
FROM employees e
LEFT JOIN employee_roles er ON e.id = er.employee_id
WHERE e.id = ?
LIMIT 1
`, [employeeId]); `, [employeeId]);
res.status(201).json(newEmployee); // Format response with roles array
const employeeWithRoles = {
...newEmployee,
roles: newEmployee.role ? [newEmployee.role] : ['user']
};
res.status(201).json(employeeWithRoles);
} catch (error) {
await db.run('ROLLBACK');
throw error;
}
} catch (error) { } catch (error) {
console.error('Error creating employee:', error); console.error('Error creating employee:', error);
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
@@ -173,9 +223,9 @@ 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, role, isActive, employeeType, contractType, canWorkAlone } = req.body; const { firstname, lastname, roles, isActive, employeeType, contractType, canWorkAlone } = req.body;
console.log('📝 Update Employee Request:', { id, firstname, lastname, role, isActive, employeeType, contractType, canWorkAlone }); console.log('📝 Update Employee Request:', { id, firstname, lastname, roles, isActive, employeeType, contractType, canWorkAlone });
// 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]);
@@ -205,37 +255,71 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
} }
} }
// Update employee with potentially new email // Start transaction for employee update and role management
await db.run('BEGIN TRANSACTION');
try {
// UPDATED: Update employee without role (role is now in employee_roles table)
await db.run( await db.run(
`UPDATE employees `UPDATE employees
SET firstname = COALESCE(?, firstname), SET firstname = COALESCE(?, firstname),
lastname = COALESCE(?, lastname), lastname = COALESCE(?, lastname),
email = ?, email = ?,
role = COALESCE(?, role),
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)
WHERE id = ?`, WHERE id = ?`,
[firstname, lastname, email, role, isActive, employeeType, contractType, canWorkAlone, id] [firstname, lastname, email, isActive, employeeType, contractType, canWorkAlone, id]
); );
// UPDATED: Update roles if provided
if (roles) {
// Delete existing roles
await db.run('DELETE FROM employee_roles WHERE employee_id = ?', [id]);
// Insert new roles
for (const role of roles) {
await db.run(
`INSERT INTO employee_roles (employee_id, role) VALUES (?, ?)`,
[id, role]
);
}
}
await db.run('COMMIT');
console.log('✅ Employee updated successfully with email:', email); console.log('✅ Employee updated successfully with email:', email);
// Return updated employee // Return updated employee with role from employee_roles
const updatedEmployee = await db.get<any>(` const updatedEmployee = await db.get<any>(`
SELECT SELECT
id, email, firstname, lastname, role, is_active as isActive, e.id, e.email, e.firstname, e.lastname,
employee_type as employeeType, e.is_active as isActive,
contract_type as contractType, e.employee_type as employeeType,
can_work_alone as canWorkAlone, e.contract_type as contractType,
created_at as createdAt, e.can_work_alone as canWorkAlone,
last_login as lastLogin e.created_at as createdAt,
FROM employees e.last_login as lastLogin,
WHERE id = ? er.role
FROM employees e
LEFT JOIN employee_roles er ON e.id = er.employee_id
WHERE e.id = ?
LIMIT 1
`, [id]); `, [id]);
res.json(updatedEmployee); // Format response with roles array
const employeeWithRoles = {
...updatedEmployee,
roles: updatedEmployee.role ? [updatedEmployee.role] : ['user']
};
res.json(employeeWithRoles);
} catch (error) {
await db.run('ROLLBACK');
throw error;
}
} catch (error) { } catch (error) {
console.error('Error updating employee:', error); console.error('Error updating employee:', error);
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
@@ -247,11 +331,15 @@ export const deleteEmployee = async (req: AuthRequest, res: Response): Promise<v
const { id } = req.params; const { id } = req.params;
console.log('🗑️ Starting deletion process for employee ID:', id); console.log('🗑️ Starting deletion process for employee ID:', id);
// Check if employee exists // UPDATED: Check if employee exists with role from employee_roles
const existingEmployee = await db.get<any>(` const existingEmployee = await db.get<any>(`
SELECT id, email, name, is_active, role SELECT
FROM employees e.id, e.email, e.firstname, e.lastname, e.is_active,
WHERE id = ? er.role
FROM employees e
LEFT JOIN employee_roles er ON e.id = er.employee_id
WHERE e.id = ?
LIMIT 1
`, [id]); `, [id]);
if (!existingEmployee) { if (!existingEmployee) {
@@ -297,10 +385,13 @@ export const deleteEmployee = async (req: AuthRequest, res: Response): Promise<v
} }
} }
// 3. Nullify created_by references // 3. Remove roles from employee_roles
await db.run('DELETE FROM employee_roles WHERE employee_id = ?', [id]);
// 4. Nullify created_by references
await db.run('UPDATE shift_plans SET created_by = NULL WHERE created_by = ?', [id]); await db.run('UPDATE shift_plans SET created_by = NULL WHERE created_by = ?', [id]);
// 4. Finally delete the employee // 5. Finally delete the employee
await db.run('DELETE FROM employees WHERE id = ?', [id]); await db.run('DELETE FROM employees WHERE id = ?', [id]);
await db.run('COMMIT'); await db.run('COMMIT');
@@ -339,8 +430,6 @@ export const getAvailabilities = async (req: AuthRequest, res: Response): Promis
ORDER BY s.day_of_week, s.time_slot_id ORDER BY s.day_of_week, s.time_slot_id
`, [employeeId]); `, [employeeId]);
//console.log('✅ Successfully got availabilities from employee:', availabilities);
res.json(availabilities.map(avail => ({ res.json(availabilities.map(avail => ({
id: avail.id, id: avail.id,
employeeId: avail.employee_id, employeeId: avail.employee_id,
@@ -393,14 +482,13 @@ export const updateAvailabilities = async (req: AuthRequest, res: Response): Pro
await db.run('COMMIT'); await db.run('COMMIT');
console.log('✅ Successfully updated availablities employee:', );
// Return updated availabilities // Return updated availabilities
const updatedAvailabilities = await db.all<any>(` const updatedAvailabilities = await db.all<any>(`
SELECT * FROM employee_availability SELECT ea.*, s.day_of_week, s.time_slot_id
WHERE employee_id = ? AND plan_id = ? FROM employee_availability ea
ORDER BY day_of_week, time_slot_id JOIN shifts s ON ea.shift_id = s.id
WHERE ea.employee_id = ? AND ea.plan_id = ?
ORDER BY s.day_of_week, s.time_slot_id
`, [employeeId, planId]); `, [employeeId, planId]);
res.json(updatedAvailabilities.map(avail => ({ res.json(updatedAvailabilities.map(avail => ({
@@ -413,7 +501,7 @@ export const updateAvailabilities = async (req: AuthRequest, res: Response): Pro
notes: avail.notes notes: avail.notes
}))); })));
console.log('✅ Successfully updated employee:', updateAvailabilities); console.log('✅ Successfully updated employee availabilities');
} catch (error) { } catch (error) {
await db.run('ROLLBACK'); await db.run('ROLLBACK');

View File

@@ -27,7 +27,10 @@ function generateEmail(firstname: string, lastname: string): string {
export const checkSetupStatus = async (req: Request, res: Response): Promise<void> => { export const checkSetupStatus = async (req: Request, res: Response): Promise<void> => {
try { try {
const adminExists = await db.get<{ 'COUNT(*)': number }>( const adminExists = await db.get<{ 'COUNT(*)': number }>(
'SELECT COUNT(*) FROM employees WHERE role = ? AND is_active = 1', `SELECT COUNT(*)
FROM employees e
JOIN employee_roles er ON e.id = er.employee_id
WHERE er.role = ? AND e.is_active = 1`,
['admin'] ['admin']
); );
@@ -50,7 +53,10 @@ export const setupAdmin = async (req: Request, res: Response): Promise<void> =>
try { try {
// Check if admin already exists // Check if admin already exists
const adminExists = await db.get<{ 'COUNT(*)': number }>( const adminExists = await db.get<{ 'COUNT(*)': number }>(
'SELECT COUNT(*) FROM employees WHERE role = ? AND is_active = 1', `SELECT COUNT(*)
FROM employees e
JOIN employee_roles er ON e.id = er.employee_id
WHERE er.role = ? AND e.is_active = 1`,
['admin'] ['admin']
); );
@@ -62,11 +68,11 @@ export const setupAdmin = async (req: Request, res: Response): Promise<void> =>
return; return;
} }
const { password, firstname, lastname } = req.body; // Changed from name to firstname/lastname const { password, firstname, lastname } = req.body;
console.log('👤 Creating admin with data:', { firstname, lastname }); console.log('👤 Creating admin with data:', { firstname, lastname });
// Validation - updated for firstname/lastname // Validation
if (!password || !firstname || !lastname) { if (!password || !firstname || !lastname) {
res.status(400).json({ error: 'Passwort, Vorname und Nachname sind erforderlich' }); res.status(400).json({ error: 'Passwort, Vorname und Nachname sind erforderlich' });
return; return;
@@ -78,7 +84,7 @@ export const setupAdmin = async (req: Request, res: Response): Promise<void> =>
return; return;
} }
// Generate email automatically using the same pattern // Generate email automatically
const email = generateEmail(firstname, lastname); const email = generateEmail(firstname, lastname);
console.log('📧 Generated admin email:', email); console.log('📧 Generated admin email:', email);
@@ -92,11 +98,17 @@ 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 with generated email // Create admin user in employees table
await db.run( await db.run(
`INSERT INTO employees (id, email, password, firstname, lastname, role, 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)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[adminId, email, hashedPassword, firstname, lastname, 'admin', 'manager', 'large', true, 1] [adminId, email, hashedPassword, firstname, lastname, 'manager', 'large', true, 1]
);
// UPDATED: Assign admin role in employee_roles table
await db.run(
`INSERT INTO employee_roles (employee_id, role) VALUES (?, ?)`,
[adminId, 'admin']
); );
console.log('✅ Admin user created successfully with email:', email); console.log('✅ Admin user created successfully with email:', email);

View File

@@ -25,6 +25,11 @@ CREATE TABLE IF NOT EXISTS employee_roles (
PRIMARY KEY (employee_id, role) PRIMARY KEY (employee_id, role)
); );
-- Insert default roles if they don't exist
INSERT OR IGNORE INTO roles (role) VALUES ('admin');
INSERT OR IGNORE INTO roles (role) VALUES ('user');
INSERT OR IGNORE INTO roles (role) VALUES ('maintenance');
-- Shift plans table -- Shift plans table
CREATE TABLE IF NOT EXISTS shift_plans ( CREATE TABLE IF NOT EXISTS shift_plans (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,

View File

@@ -4,20 +4,20 @@ export interface Employee {
email: string; email: string;
firstname: string; firstname: string;
lastname: string; lastname: string;
role: 'admin' | 'maintenance' | 'user';
employeeType: 'manager' | 'trainee' | 'experienced'; employeeType: 'manager' | 'trainee' | 'experienced';
contractType: 'small' | 'large'; contractType: 'small' | 'large';
canWorkAlone: boolean; canWorkAlone: boolean;
isActive: boolean; isActive: boolean;
createdAt: string; createdAt: string;
lastLogin?: string | null; lastLogin?: string | null;
roles?: string[];
} }
export interface CreateEmployeeRequest { export interface CreateEmployeeRequest {
password: string; password: string;
firstname: string; firstname: string;
lastname: string; lastname: string;
role: 'admin' | 'maintenance' | 'user'; roles?: string[];
employeeType: 'manager' | 'trainee' | 'experienced'; employeeType: 'manager' | 'trainee' | 'experienced';
contractType: 'small' | 'large'; contractType: 'small' | 'large';
canWorkAlone: boolean; canWorkAlone: boolean;
@@ -26,7 +26,7 @@ export interface CreateEmployeeRequest {
export interface UpdateEmployeeRequest { export interface UpdateEmployeeRequest {
firstname?: string; firstname?: string;
lastname?: string; lastname?: string;
role?: 'admin' | 'maintenance' | 'user'; roles?: string[];
employeeType?: 'manager' | 'trainee' | 'experienced'; employeeType?: 'manager' | 'trainee' | 'experienced';
contractType?: 'small' | 'large'; contractType?: 'small' | 'large';
canWorkAlone?: boolean; canWorkAlone?: boolean;

View File

@@ -57,7 +57,13 @@ export const isExperienced = (employee: Employee): boolean =>
employee.employeeType === 'experienced'; employee.employeeType === 'experienced';
export const isAdmin = (employee: Employee): boolean => export const isAdmin = (employee: Employee): boolean =>
employee.role === 'admin'; employee.roles?.includes('admin') || false;
export const isMaintenance = (employee: Employee): boolean =>
employee.roles?.includes('maintenance') || false;
export const isUser = (employee: Employee): boolean =>
employee.roles?.includes('user') || false;
export const canEmployeeWorkAlone = (employee: Employee): boolean => export const canEmployeeWorkAlone = (employee: Employee): boolean =>
employee.canWorkAlone && isExperienced(employee); employee.canWorkAlone && isExperienced(employee);

View File

@@ -14,10 +14,12 @@ export async function initializeDatabase(): Promise<void> {
try { try {
console.log('Starting database initialization...'); console.log('Starting database initialization...');
// Check if users table exists and has data
try { try {
const existingAdmin = await db.get<{ count: number }>( const existingAdmin = await db.get<{ count: number }>(
"SELECT COUNT(*) as count FROM employees WHERE role = 'admin'" `SELECT COUNT(*) as count
FROM employees e
JOIN employee_roles er ON e.id = er.employee_id
WHERE er.role = 'admin' AND e.is_active = 1`
); );
if (existingAdmin && existingAdmin.count > 0) { if (existingAdmin && existingAdmin.count > 0) {
@@ -40,22 +42,28 @@ 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');
// Drop existing tables in reverse order of dependencies if they exist // UPDATED: Drop tables in correct dependency order
const tablesToDrop = [ const tablesToDrop = [
'employees',
'time_slots',
'shifts',
'scheduled_shifts',
'shift_assignments',
'employee_availability', 'employee_availability',
'applied_migrations', 'shift_assignments',
'shift_plans' 'scheduled_shifts',
'shifts',
'time_slots',
'employee_roles',
'shift_plans',
'roles',
'employees',
'applied_migrations'
]; ];
for (const table of tablesToDrop) { for (const table of tablesToDrop) {
if (existingTables.some(t => t.name === table)) { if (existingTables.some(t => t.name === table)) {
console.log(`Dropping table: ${table}`); console.log(`Dropping table: ${table}`);
try {
await db.run(`DROP TABLE IF EXISTS ${table}`); await db.run(`DROP TABLE IF EXISTS ${table}`);
} catch (error) {
console.warn(`Could not drop table ${table}:`, error);
}
} }
} }
} catch (error) { } catch (error) {
@@ -92,6 +100,19 @@ export async function initializeDatabase(): Promise<void> {
} }
} }
// UPDATED: Insert default roles after creating the tables
try {
console.log('Inserting default roles...');
await db.run(`INSERT OR IGNORE INTO roles (role) VALUES ('admin')`);
await db.run(`INSERT OR IGNORE INTO roles (role) VALUES ('user')`);
await db.run(`INSERT OR IGNORE INTO roles (role) VALUES ('maintenance')`);
console.log('✅ Default roles inserted');
} catch (error) {
console.error('Error inserting default roles:', error);
await db.run('ROLLBACK');
throw error;
}
await db.run('COMMIT'); await db.run('COMMIT');
console.log('✅ Database schema successfully initialized'); console.log('✅ Database schema successfully initialized');

View File

@@ -202,7 +202,7 @@ const Navigation: React.FC = () => {
{/* User Menu - Rechts */} {/* User Menu - Rechts */}
<div style={styles.userMenu}> <div style={styles.userMenu}>
<span style={styles.userInfo}> <span style={styles.userInfo}>
{user?.name} <span style={{color: '#999'}}>({user?.role})</span> {user?.firstname} {user?.lastname} <span style={{color: '#999'}}>({user?.roles})</span>
</span> </span>
<button <button
onClick={handleLogout} onClick={handleLogout}
@@ -266,8 +266,8 @@ const Navigation: React.FC = () => {
))} ))}
<div style={styles.mobileUserInfo}> <div style={styles.mobileUserInfo}>
<div style={{marginBottom: '0.5rem'}}> <div style={{marginBottom: '0.5rem'}}>
<span style={{fontWeight: 500}}>{user?.name}</span> <span style={{fontWeight: 500}}>{user?.firstname} {user?.lastname}</span>
<span style={{color: '#999', marginLeft: '0.5rem'}}>({user?.role})</span> <span style={{color: '#999', marginLeft: '0.5rem'}}>({user?.roles})</span>
</div> </div>
<button <button
onClick={handleLogout} onClick={handleLogout}

View File

@@ -136,7 +136,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const hasRole = (roles: string[]): boolean => { const hasRole = (roles: string[]): boolean => {
if (!user) return false; if (!user) return false;
return roles.includes(user.role); return roles.length != 0;
}; };
useEffect(() => { useEffect(() => {

View File

@@ -4,20 +4,20 @@ export interface Employee {
email: string; email: string;
firstname: string; firstname: string;
lastname: string; lastname: string;
role: 'admin' | 'maintenance' | 'user';
employeeType: 'manager' | 'trainee' | 'experienced'; employeeType: 'manager' | 'trainee' | 'experienced';
contractType: 'small' | 'large'; contractType: 'small' | 'large';
canWorkAlone: boolean; canWorkAlone: boolean;
isActive: boolean; isActive: boolean;
createdAt: string; createdAt: string;
lastLogin?: string | null; lastLogin?: string | null;
roles?: string[];
} }
export interface CreateEmployeeRequest { export interface CreateEmployeeRequest {
password: string; password: string;
firstname: string; firstname: string;
lastname: string; lastname: string;
role: 'admin' | 'maintenance' | 'user'; roles: string[];
employeeType: 'manager' | 'trainee' | 'experienced'; employeeType: 'manager' | 'trainee' | 'experienced';
contractType: 'small' | 'large'; contractType: 'small' | 'large';
canWorkAlone: boolean; canWorkAlone: boolean;
@@ -26,7 +26,7 @@ export interface CreateEmployeeRequest {
export interface UpdateEmployeeRequest { export interface UpdateEmployeeRequest {
firstname?: string; firstname?: string;
lastname?: string; lastname?: string;
role?: 'admin' | 'maintenance' | 'user'; roles?: string[];
employeeType?: 'manager' | 'trainee' | 'experienced'; employeeType?: 'manager' | 'trainee' | 'experienced';
contractType?: 'small' | 'large'; contractType?: 'small' | 'large';
canWorkAlone?: boolean; canWorkAlone?: boolean;

View File

@@ -57,8 +57,13 @@ export const isExperienced = (employee: Employee): boolean =>
employee.employeeType === 'experienced'; employee.employeeType === 'experienced';
export const isAdmin = (employee: Employee): boolean => export const isAdmin = (employee: Employee): boolean =>
employee.role === 'admin'; employee.roles?.includes('admin') || false;
export const isMaintenance = (employee: Employee): boolean =>
employee.roles?.includes('maintenance') || false;
export const isUser = (employee: Employee): boolean =>
employee.roles?.includes('user') || false;
export const canEmployeeWorkAlone = (employee: Employee): boolean => export const canEmployeeWorkAlone = (employee: Employee): boolean =>
employee.canWorkAlone && isExperienced(employee); employee.canWorkAlone && isExperienced(employee);

View File

@@ -352,7 +352,7 @@ const Dashboard: React.FC = () => {
}}> }}>
<div> <div>
<h1 style={{ margin: '0 0 10px 0', color: '#2c3e50' }}> <h1 style={{ margin: '0 0 10px 0', color: '#2c3e50' }}>
Willkommen zurück, {user?.name}! 👋 Willkommen zurück, {user?.firstname} {user?.lastname} ! 👋
</h1> </h1>
<p style={{ margin: 0, color: '#546e7a', fontSize: '16px' }}> <p style={{ margin: 0, color: '#546e7a', fontSize: '16px' }}>
{new Date().toLocaleDateString('de-DE', { {new Date().toLocaleDateString('de-DE', {

View File

@@ -84,16 +84,26 @@ const EmployeeManagement: React.FC = () => {
}); });
}; };
// Helper function to get full name
const getFullName = (employee: Employee): string => {
return `${employee.firstname} ${employee.lastname}`;
};
// Verbesserte Lösch-Funktion mit Bestätigungs-Dialog // Verbesserte Lösch-Funktion mit Bestätigungs-Dialog
const handleDeleteEmployee = async (employee: Employee) => { const handleDeleteEmployee = async (employee: Employee) => {
try { try {
const fullName = getFullName(employee);
// Bestätigungs-Dialog basierend auf Rolle // Bestätigungs-Dialog basierend auf Rolle
let confirmMessage = `Möchten Sie den Mitarbeiter "${employee.name}" wirklich PERMANENT LÖSCHEN?\n\nDie Daten des Mitarbeiters werden unwiderruflich gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.`; let confirmMessage = `Möchten Sie den Mitarbeiter "${fullName}" (${employee.email}) wirklich PERMANENT LÖSCHEN?\n\nDie Daten des Mitarbeiters werden unwiderruflich gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.`;
let confirmTitle = 'Mitarbeiter löschen'; let confirmTitle = 'Mitarbeiter löschen';
if (employee.role === 'admin') { // Check if employee has admin role (now in roles array)
const isAdmin = employee.roles?.includes('admin') || false;
if (isAdmin) {
const adminCount = employees.filter(emp => const adminCount = employees.filter(emp =>
emp.role === 'admin' && emp.isActive (emp.roles?.includes('admin') || false) && emp.isActive
).length; ).length;
if (adminCount <= 1) { if (adminCount <= 1) {
@@ -106,7 +116,7 @@ const EmployeeManagement: React.FC = () => {
} }
confirmTitle = 'Administrator löschen'; confirmTitle = 'Administrator löschen';
confirmMessage = `Möchten Sie den Administrator "${employee.name}" wirklich PERMANENT LÖSCHEN?\n\nAchtung: Diese Aktion ist permanent und kann nicht rückgängig gemacht werden.`; confirmMessage = `Möchten Sie den Administrator "${fullName}" (${employee.email}) wirklich PERMANENT LÖSCHEN?\n\nAchtung: Diese Aktion ist permanent und kann nicht rückgängig gemacht werden.`;
} }
const confirmed = await confirmDialog({ const confirmed = await confirmDialog({
@@ -119,7 +129,7 @@ const EmployeeManagement: React.FC = () => {
if (!confirmed) return; if (!confirmed) return;
console.log('Starting deletion process for employee:', employee.name); console.log('Starting deletion process for employee:', fullName);
await employeeService.deleteEmployee(employee.id); await employeeService.deleteEmployee(employee.id);
console.log('Employee deleted, reloading list'); console.log('Employee deleted, reloading list');
@@ -130,7 +140,7 @@ const EmployeeManagement: React.FC = () => {
showNotification({ showNotification({
type: 'success', type: 'success',
title: 'Erfolg', title: 'Erfolg',
message: `Mitarbeiter "${employee.name}" wurde erfolgreich gelöscht` message: `Mitarbeiter "${fullName}" wurde erfolgreich gelöscht`
}); });
} catch (err: any) { } catch (err: any) {
@@ -218,7 +228,7 @@ const EmployeeManagement: React.FC = () => {
<EmployeeList <EmployeeList
employees={employees} employees={employees}
onEdit={handleEditEmployee} onEdit={handleEditEmployee}
onDelete={handleDeleteEmployee} // Jetzt mit Employee-Objekt onDelete={handleDeleteEmployee}
onManageAvailability={handleManageAvailability} onManageAvailability={handleManageAvailability}
/> />
)} )}

View File

@@ -1,4 +1,3 @@
// frontend/src/pages/Employees/components/AvailabilityManager.tsx
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { employeeService } from '../../../services/employeeService'; import { employeeService } from '../../../services/employeeService';
import { shiftPlanService } from '../../../services/shiftPlanService'; import { shiftPlanService } from '../../../services/shiftPlanService';
@@ -200,19 +199,11 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
return updated; return updated;
} else { } else {
// Create new availability using shiftId directly // Create new availability using shiftId directly
const shift = selectedPlan?.shifts?.find(s => s.id === shiftId);
if (!shift) {
console.error('❌ Shift nicht gefunden:', shiftId);
return prev;
}
const newAvailability: Availability = { const newAvailability: Availability = {
id: `temp-${shiftId}-${Date.now()}`, id: `temp-${shiftId}-${Date.now()}`,
employeeId: employee.id, employeeId: employee.id,
planId: selectedPlanId, planId: selectedPlanId,
shiftId: shiftId, // Use shiftId directly shiftId: shiftId,
dayOfWeek: shift.dayOfWeek, // Keep for backward compatibility if needed
timeSlotId: shift.timeSlotId, // Keep for backward compatibility if needed
preferenceLevel: level, preferenceLevel: level,
isAvailable: level !== 3 isAvailable: level !== 3
}; };
@@ -433,9 +424,7 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
planId: selectedPlanId, planId: selectedPlanId,
availabilities: validAvailabilities.map(avail => ({ availabilities: validAvailabilities.map(avail => ({
planId: selectedPlanId, planId: selectedPlanId,
shiftId: avail.shiftId, // Use shiftId directly shiftId: avail.shiftId,
dayOfWeek: avail.dayOfWeek, // Keep for backward compatibility
timeSlotId: avail.timeSlotId, // Keep for backward compatibility
preferenceLevel: avail.preferenceLevel, preferenceLevel: avail.preferenceLevel,
notes: avail.notes notes: avail.notes
})) }))
@@ -472,6 +461,9 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
}); });
const shiftsCount = allShiftIds.size; const shiftsCount = allShiftIds.size;
// Get full name for display
const employeeFullName = `${employee.firstname} ${employee.lastname}`;
return ( return (
<div style={{ <div style={{
maxWidth: '1900px', maxWidth: '1900px',
@@ -517,10 +509,13 @@ const AvailabilityManager: React.FC<AvailabilityManagerProps> = ({
{/* Employee Info */} {/* Employee Info */}
<div style={{ marginBottom: '20px' }}> <div style={{ marginBottom: '20px' }}>
<h3 style={{ margin: '0 0 10px 0', color: '#34495e' }}> <h3 style={{ margin: '0 0 10px 0', color: '#34495e' }}>
{employee.name} {employeeFullName}
</h3> </h3>
<p style={{ margin: 0, color: '#7f8c8d' }}> <p style={{ margin: 0, color: '#7f8c8d' }}>
Legen Sie die Verfügbarkeit für {employee.name} fest (basierend auf Shift-IDs). <strong>Email:</strong> {employee.email}
</p>
<p style={{ margin: '5px 0 0 0', color: '#7f8c8d' }}>
Legen Sie die Verfügbarkeit für {employeeFullName} fest (basierend auf Shift-IDs).
</p> </p>
</div> </div>

View File

@@ -1,4 +1,3 @@
// frontend/src/pages/Employees/components/EmployeeForm.tsx
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest } from '../../../models/Employee'; import { Employee, CreateEmployeeRequest, UpdateEmployeeRequest } from '../../../models/Employee';
import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults'; import { ROLE_CONFIG, EMPLOYEE_TYPE_CONFIG } from '../../../models/defaults/employeeDefaults';
@@ -19,10 +18,11 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
onCancel onCancel
}) => { }) => {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', firstname: '',
email: '', lastname: '',
email: '', // Will be auto-generated and display only
password: '', password: '',
role: 'user' as 'admin' | 'maintenance' | 'user', roles: ['user'] as string[], // Changed from single role to array
employeeType: 'trainee' as 'manager' | 'trainee' | 'experienced', employeeType: 'trainee' as 'manager' | 'trainee' | 'experienced',
contractType: 'small' as 'small' | 'large', contractType: 'small' as 'small' | 'large',
canWorkAlone: false, canWorkAlone: false,
@@ -37,13 +37,33 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
const [error, setError] = useState(''); const [error, setError] = useState('');
const { hasRole } = useAuth(); const { hasRole } = useAuth();
// Generate email preview
const generateEmailPreview = (firstname: string, lastname: string): string => {
const convertUmlauts = (str: string): string => {
return str
.toLowerCase()
.replace(/ü/g, 'ue')
.replace(/ö/g, 'oe')
.replace(/ä/g, 'ae')
.replace(/ß/g, 'ss');
};
const cleanFirstname = convertUmlauts(firstname).replace(/[^a-z0-9]/g, '');
const cleanLastname = convertUmlauts(lastname).replace(/[^a-z0-9]/g, '');
return `${cleanFirstname}.${cleanLastname}@sp.de`;
};
const emailPreview = generateEmailPreview(formData.firstname, formData.lastname);
useEffect(() => { useEffect(() => {
if (mode === 'edit' && employee) { if (mode === 'edit' && employee) {
setFormData({ setFormData({
name: employee.name, firstname: employee.firstname,
lastname: employee.lastname,
email: employee.email, email: employee.email,
password: '', // Passwort wird beim Bearbeiten nicht angezeigt password: '', // Password wird beim Bearbeiten nicht angezeigt
role: employee.role, roles: employee.roles || ['user'], // Use roles array
employeeType: employee.employeeType, employeeType: employee.employeeType,
contractType: employee.contractType, contractType: employee.contractType,
canWorkAlone: employee.canWorkAlone, canWorkAlone: employee.canWorkAlone,
@@ -69,6 +89,24 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
})); }));
}; };
const handleRoleChange = (role: string, checked: boolean) => {
setFormData(prev => {
if (checked) {
// Add role if checked
return {
...prev,
roles: [...prev.roles, role]
};
} else {
// Remove role if unchecked
return {
...prev,
roles: prev.roles.filter(r => r !== role)
};
}
});
};
const handleEmployeeTypeChange = (employeeType: 'manager' | 'trainee' | 'experienced') => { const handleEmployeeTypeChange = (employeeType: 'manager' | 'trainee' | 'experienced') => {
// Manager and experienced can work alone, trainee cannot // Manager and experienced can work alone, trainee cannot
const canWorkAlone = employeeType === 'manager' || employeeType === 'experienced'; const canWorkAlone = employeeType === 'manager' || employeeType === 'experienced';
@@ -95,10 +133,10 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
try { try {
if (mode === 'create') { if (mode === 'create') {
const createData: CreateEmployeeRequest = { const createData: CreateEmployeeRequest = {
name: formData.name.trim(), firstname: formData.firstname.trim(),
email: formData.email.trim(), lastname: formData.lastname.trim(),
password: formData.password, password: formData.password,
role: formData.role, roles: formData.roles, // Use roles array
employeeType: formData.employeeType, employeeType: formData.employeeType,
contractType: formData.contractType, contractType: formData.contractType,
canWorkAlone: formData.canWorkAlone canWorkAlone: formData.canWorkAlone
@@ -106,8 +144,9 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
await employeeService.createEmployee(createData); await employeeService.createEmployee(createData);
} else if (employee) { } else if (employee) {
const updateData: UpdateEmployeeRequest = { const updateData: UpdateEmployeeRequest = {
name: formData.name.trim(), firstname: formData.firstname.trim(),
role: formData.role, lastname: formData.lastname.trim(),
roles: formData.roles, // Use roles array
employeeType: formData.employeeType, employeeType: formData.employeeType,
contractType: formData.contractType, contractType: formData.contractType,
canWorkAlone: formData.canWorkAlone, canWorkAlone: formData.canWorkAlone,
@@ -141,8 +180,8 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
}; };
const isFormValid = mode === 'create' const isFormValid = mode === 'create'
? formData.name.trim() && formData.email.trim() && formData.password.length >= 6 ? formData.firstname.trim() && formData.lastname.trim() && formData.password.length >= 6
: formData.name.trim() && formData.email.trim(); : formData.firstname.trim() && formData.lastname.trim();
const availableRoles = hasRole(['admin']) const availableRoles = hasRole(['admin'])
? ROLE_CONFIG ? ROLE_CONFIG
@@ -200,12 +239,12 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '15px' }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '15px' }}>
<div> <div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}> <label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
Vollständiger Name * Vorname *
</label> </label>
<input <input
type="text" type="text"
name="name" name="firstname"
value={formData.name} value={formData.firstname}
onChange={handleChange} onChange={handleChange}
required required
style={{ style={{
@@ -215,34 +254,54 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
borderRadius: '4px', borderRadius: '4px',
fontSize: '16px' fontSize: '16px'
}} }}
placeholder="Max Mustermann" placeholder="Max"
/> />
</div> </div>
<div> <div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}> <label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
E-Mail Adresse * Nachname *
</label> </label>
<input <input
type="email" type="text"
name="email" name="lastname"
value={formData.email} value={formData.lastname}
onChange={handleChange} onChange={handleChange}
required required
disabled={mode === 'edit'} // Email cannot be changed in edit mode
style={{ style={{
width: '100%', width: '100%',
padding: '10px', padding: '10px',
border: '1px solid #ddd', border: '1px solid #ddd',
borderRadius: '4px', borderRadius: '4px',
fontSize: '16px', fontSize: '16px'
backgroundColor: mode === 'edit' ? '#f8f9fa' : 'white'
}} }}
placeholder="max.mustermann@example.com" placeholder="Mustermann"
/> />
</div> </div>
</div> </div>
{/* Email Preview */}
<div style={{ marginTop: '15px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
E-Mail Adresse (automatisch generiert)
</label>
<div style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px',
backgroundColor: '#f8f9fa',
color: '#6c757d'
}}>
{emailPreview || 'max.mustermann@sp.de'}
</div>
<div style={{ fontSize: '12px', color: '#7f8c8d', marginTop: '5px' }}>
Die E-Mail Adresse wird automatisch aus Vorname und Nachname generiert.
{formData.firstname && formData.lastname && ` Beispiel: ${emailPreview}`}
</div>
</div>
{mode === 'create' && ( {mode === 'create' && (
<div style={{ marginTop: '15px' }}> <div style={{ marginTop: '15px' }}>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}> <label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
@@ -353,7 +412,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
<h3 style={{ margin: '0 0 15px 0', color: '#495057' }}>👥 Mitarbeiter Kategorie</h3> <h3 style={{ margin: '0 0 15px 0', color: '#495057' }}>👥 Mitarbeiter Kategorie</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{EMPLOYEE_TYPE_CONFIG.map(type => ( {Object.values(EMPLOYEE_TYPE_CONFIG).map(type => (
<div <div
key={type.value} key={type.value}
style={{ style={{
@@ -573,7 +632,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
</div> </div>
)} )}
{/* Systemrolle (nur für Admins) */} {/* Systemrollen (nur für Admins) */}
{hasRole(['admin']) && ( {hasRole(['admin']) && (
<div style={{ <div style={{
padding: '20px', padding: '20px',
@@ -581,7 +640,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
borderRadius: '8px', borderRadius: '8px',
border: '1px solid #ffeaa7' border: '1px solid #ffeaa7'
}}> }}>
<h3 style={{ margin: '0 0 15px 0', color: '#856404' }}> Systemrolle</h3> <h3 style={{ margin: '0 0 15px 0', color: '#856404' }}> Systemrollen</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{availableRoles.map(role => ( {availableRoles.map(role => (
@@ -591,29 +650,19 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
display: 'flex', display: 'flex',
alignItems: 'flex-start', alignItems: 'flex-start',
padding: '12px', padding: '12px',
border: `2px solid ${formData.role === role.value ? '#f39c12' : '#e0e0e0'}`, border: `2px solid ${formData.roles.includes(role.value) ? '#f39c12' : '#e0e0e0'}`,
borderRadius: '6px', borderRadius: '6px',
backgroundColor: formData.role === role.value ? '#fef9e7' : 'white', backgroundColor: formData.roles.includes(role.value) ? '#fef9e7' : 'white',
cursor: 'pointer' cursor: 'pointer'
}} }}
onClick={() => { onClick={() => handleRoleChange(role.value, !formData.roles.includes(role.value))}
setFormData(prev => ({
...prev,
role: role.value as 'admin' | 'maintenance' | 'user'
}));
}}
> >
<input <input
type="radio" type="checkbox"
name="role" name="roles"
value={role.value} value={role.value}
checked={formData.role === role.value} checked={formData.roles.includes(role.value)}
onChange={(e) => { onChange={(e) => handleRoleChange(role.value, e.target.checked)}
setFormData(prev => ({
...prev,
role: e.target.value as 'admin' | 'maintenance' | 'user'
}));
}}
style={{ style={{
marginRight: '10px', marginRight: '10px',
marginTop: '2px' marginTop: '2px'
@@ -630,6 +679,9 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
</div> </div>
))} ))}
</div> </div>
<div style={{ fontSize: '12px', color: '#7f8c8d', marginTop: '10px' }}>
<strong>Hinweis:</strong> Ein Mitarbeiter kann mehrere Rollen haben.
</div>
</div> </div>
)} )}

View File

@@ -1,4 +1,3 @@
// frontend/src/pages/Employees/components/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';
@@ -33,11 +32,12 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
if (searchTerm) { if (searchTerm) {
const term = searchTerm.toLowerCase(); const term = searchTerm.toLowerCase();
const fullName = `${employee.firstname} ${employee.lastname}`.toLowerCase();
return ( return (
employee.name.toLowerCase().includes(term) || fullName.includes(term) ||
employee.email.toLowerCase().includes(term) || employee.email.toLowerCase().includes(term) ||
employee.employeeType.toLowerCase().includes(term) || employee.employeeType.toLowerCase().includes(term) ||
employee.role.toLowerCase().includes(term) (employee.roles && employee.roles.some(role => role.toLowerCase().includes(term)))
); );
} }
@@ -51,8 +51,8 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
switch (sortField) { switch (sortField) {
case 'name': case 'name':
aValue = a.name.toLowerCase(); aValue = `${a.firstname} ${a.lastname}`.toLowerCase();
bValue = b.name.toLowerCase(); bValue = `${b.firstname} ${b.lastname}`.toLowerCase();
break; break;
case 'employeeType': case 'employeeType':
aValue = a.employeeType; aValue = a.employeeType;
@@ -63,8 +63,9 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
bValue = b.canWorkAlone; bValue = b.canWorkAlone;
break; break;
case 'role': case 'role':
aValue = a.role; // Use the highest role for sorting
bValue = b.role; aValue = getHighestRole(a.roles || []);
bValue = getHighestRole(b.roles || []);
break; break;
case 'lastLogin': case 'lastLogin':
// Handle null values for lastLogin (put them at the end) // Handle null values for lastLogin (put them at the end)
@@ -82,6 +83,13 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
} }
}); });
// Helper to get highest role for sorting
const getHighestRole = (roles: string[]): string => {
if (roles.includes('admin')) return 'admin';
if (roles.includes('maintenance')) return 'maintenance';
return 'user';
};
const handleSort = (field: SortField) => { const handleSort = (field: SortField) => {
if (sortField === field) { if (sortField === field) {
// Toggle direction if same field // Toggle direction if same field
@@ -102,23 +110,23 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
const canDeleteEmployee = (employee: Employee): boolean => { const canDeleteEmployee = (employee: Employee): boolean => {
if (!hasRole(['admin'])) return false; if (!hasRole(['admin'])) return false;
if (employee.id === currentUser?.id) return false; if (employee.id === currentUser?.id) return false;
if (employee.role === 'admin' && !hasRole(['admin'])) return false; if (employee.roles?.includes('admin') && !hasRole(['admin'])) return false;
return true; return true;
}; };
const canEditEmployee = (employee: Employee): boolean => { const canEditEmployee = (employee: Employee): boolean => {
if (hasRole(['admin'])) return true; if (hasRole(['admin'])) return true;
if (hasRole(['maintenance'])) { if (hasRole(['maintenance'])) {
return employee.role === 'user' || employee.id === currentUser?.id; return !employee.roles?.includes('admin') || employee.id === currentUser?.id;
} }
return false; return false;
}; };
// Using shared configuration for consistent styling // Using shared configuration for consistent styling
type EmployeeType = typeof EMPLOYEE_TYPE_CONFIG[number]['value']; type EmployeeType = 'manager' | 'trainee' | 'experienced';
const getEmployeeTypeBadge = (type: EmployeeType) => { const getEmployeeTypeBadge = (type: EmployeeType) => {
const config = EMPLOYEE_TYPE_CONFIG.find(t => t.value === type)!; const config = EMPLOYEE_TYPE_CONFIG[type];
const bgColor = const bgColor =
type === 'manager' type === 'manager'
@@ -142,19 +150,27 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
: { text: 'Betreuung', color: '#e74c3c', bgColor: '#fadbd8' }; : { text: 'Betreuung', color: '#e74c3c', bgColor: '#fadbd8' };
}; };
type Role = typeof ROLE_CONFIG[number]['value']; type Role = 'admin' | 'maintenance' | 'user';
const getRoleBadge = (role: Role) => { const getRoleBadge = (roles: string[] = []) => {
const { label, color } = ROLE_CONFIG.find(r => r.value === role)!; const highestRole = getHighestRole(roles);
const { label, color } = ROLE_CONFIG.find(r => r.value === highestRole)!;
const bgColor = const bgColor =
role === 'user' highestRole === 'user'
? '#d5f4e6' ? '#d5f4e6'
: role === 'maintenance' : highestRole === 'maintenance'
? '#d6eaf8' ? '#d6eaf8'
: '#fadbd8'; // admin : '#fadbd8'; // admin
return { text: label, color, bgColor }; return { text: label, color, bgColor, roles };
};
const formatRoleDisplay = (roles: string[] = []) => {
if (roles.length === 0) return 'MITARBEITER';
if (roles.includes('admin')) return 'ADMIN';
if (roles.includes('maintenance')) return 'INSTANDHALTER';
return 'MITARBEITER';
}; };
if (employees.length === 0) { if (employees.length === 0) {
@@ -282,7 +298,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
{sortedEmployees.map(employee => { {sortedEmployees.map(employee => {
const employeeType = getEmployeeTypeBadge(employee.employeeType); const employeeType = getEmployeeTypeBadge(employee.employeeType);
const independence = getIndependenceBadge(employee.canWorkAlone); const independence = getIndependenceBadge(employee.canWorkAlone);
const roleColor = getRoleBadge(employee.role); const roleInfo = getRoleBadge(employee.roles);
const status = getStatusBadge(employee.isActive); const status = getStatusBadge(employee.isActive);
const canEdit = canEditEmployee(employee); const canEdit = canEditEmployee(employee);
const canDelete = canDeleteEmployee(employee); const canDelete = canDeleteEmployee(employee);
@@ -302,7 +318,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
{/* Name & E-Mail */} {/* Name & E-Mail */}
<div> <div>
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}> <div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
{employee.name} {employee.firstname} {employee.lastname}
{employee.id === currentUser?.id && ( {employee.id === currentUser?.id && (
<span style={{ <span style={{
marginLeft: '8px', marginLeft: '8px',
@@ -357,8 +373,8 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<span <span
style={{ style={{
backgroundColor: roleColor.bgColor, backgroundColor: roleInfo.bgColor,
color: roleColor.color, color: roleInfo.color,
padding: '6px 12px', padding: '6px 12px',
borderRadius: '15px', borderRadius: '15px',
fontSize: '12px', fontSize: '12px',
@@ -366,9 +382,9 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
display: 'inline-block', display: 'inline-block',
minWidth: '80px' minWidth: '80px'
}} }}
title={employee.roles?.join(', ') || 'user'}
> >
{employee.role === 'admin' ? 'ADMIN' : {formatRoleDisplay(employee.roles)}
employee.role === 'maintenance' ? 'INSTANDHALTER' : 'MITARBEITER'}
</span> </span>
</div> </div>
@@ -406,7 +422,6 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
flexWrap: 'wrap' flexWrap: 'wrap'
}}> }}>
{/* Verfügbarkeit Button */} {/* Verfügbarkeit Button */}
{(employee.role === 'admin' || employee.role === 'maintenance' || employee.role === 'user') && (
<button <button
onClick={() => onManageAvailability(employee)} onClick={() => onManageAvailability(employee)}
style={{ style={{
@@ -424,7 +439,6 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
> >
📅 📅
</button> </button>
)}
{/* Bearbeiten Button */} {/* Bearbeiten Button */}
{canEdit && ( {canEdit && (
@@ -469,7 +483,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
)} )}
{/* Platzhalter für Symmetrie */} {/* Platzhalter für Symmetrie */}
{!canEdit && !canDelete && (employee.role !== 'admin' && employee.role !== 'maintenance') && ( {!canEdit && !canDelete && (
<div style={{ width: '32px', height: '32px' }}></div> <div style={{ width: '32px', height: '32px' }}></div>
)} )}
</div> </div>

View File

@@ -319,7 +319,7 @@ const Settings: React.FC = () => {
</label> </label>
<input <input
type="text" type="text"
value={currentUser.role} value={currentUser.roles}
disabled disabled
style={styles.fieldInputDisabled} style={styles.fieldInputDisabled}
/> />

View File

@@ -1,4 +1,4 @@
// frontend/src/pages/Setup/Setup.tsx - KORRIGIERT // frontend/src/pages/Setup/Setup.tsx - UPDATED
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
@@ -7,7 +7,8 @@ const Setup: React.FC = () => {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
password: '', password: '',
confirmPassword: '', confirmPassword: '',
name: '' firstname: '',
lastname: ''
}); });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
@@ -34,8 +35,12 @@ const Setup: React.FC = () => {
}; };
const validateStep2 = () => { const validateStep2 = () => {
if (!formData.name.trim()) { if (!formData.firstname.trim()) {
setError('Bitte geben Sie einen Namen ein.'); setError('Bitte geben Sie einen Vornamen ein.');
return false;
}
if (!formData.lastname.trim()) {
setError('Bitte geben Sie einen Nachnamen ein.');
return false; return false;
} }
return true; return true;
@@ -62,10 +67,11 @@ const Setup: React.FC = () => {
const payload = { const payload = {
password: formData.password, password: formData.password,
name: formData.name firstname: formData.firstname,
lastname: formData.lastname
}; };
console.log('🚀 Sending setup request...'); console.log('🚀 Sending setup request...', payload);
const response = await fetch('http://localhost:3002/api/setup/admin', { const response = await fetch('http://localhost:3002/api/setup/admin', {
method: 'POST', method: 'POST',
@@ -94,6 +100,17 @@ const Setup: React.FC = () => {
} }
}; };
// Helper to display generated email preview
const getEmailPreview = () => {
if (!formData.firstname.trim() || !formData.lastname.trim()) {
return 'vorname.nachname@sp.de';
}
const cleanFirstname = formData.firstname.toLowerCase().replace(/[^a-z0-9]/g, '');
const cleanLastname = formData.lastname.toLowerCase().replace(/[^a-z0-9]/g, '');
return `${cleanFirstname}.${cleanLastname}@sp.de`;
};
return ( return (
<div style={{ <div style={{
minHeight: '100vh', minHeight: '100vh',
@@ -144,34 +161,6 @@ const Setup: React.FC = () => {
{step === 1 && ( {step === 1 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<div>
<label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Administrator E-Mail
</label>
<div style={{
padding: '0.75rem',
backgroundColor: '#e9ecef',
border: '1px solid #ced4da',
borderRadius: '6px',
color: '#495057',
fontWeight: '500'
}}>
admin@instandhaltung.de
</div>
<div style={{
fontSize: '0.875rem',
color: '#6c757d',
marginTop: '0.25rem'
}}>
Diese E-Mail wird für den Administrator-Account verwendet
</div>
</div>
<div> <div>
<label style={{ <label style={{
display: 'block', display: 'block',
@@ -237,12 +226,12 @@ const Setup: React.FC = () => {
fontWeight: '600', fontWeight: '600',
color: '#495057' color: '#495057'
}}> }}>
Vollständiger Name Vorname
</label> </label>
<input <input
type="text" type="text"
name="name" name="firstname"
value={formData.name} value={formData.firstname}
onChange={handleInputChange} onChange={handleInputChange}
style={{ style={{
width: '100%', width: '100%',
@@ -251,10 +240,65 @@ const Setup: React.FC = () => {
borderRadius: '6px', borderRadius: '6px',
fontSize: '1rem' fontSize: '1rem'
}} }}
placeholder="Max Mustermann" placeholder="Max"
required required
/> />
</div> </div>
<div>
<label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Nachname
</label>
<input
type="text"
name="lastname"
value={formData.lastname}
onChange={handleInputChange}
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid #ced4da',
borderRadius: '6px',
fontSize: '1rem'
}}
placeholder="Mustermann"
required
/>
</div>
<div>
<label style={{
display: 'block',
marginBottom: '0.5rem',
fontWeight: '600',
color: '#495057'
}}>
Automatisch generierte E-Mail
</label>
<div style={{
padding: '0.75rem',
backgroundColor: '#e9ecef',
border: '1px solid #ced4da',
borderRadius: '6px',
color: '#495057',
fontWeight: '500',
fontFamily: 'monospace'
}}>
{getEmailPreview()}
</div>
<div style={{
fontSize: '0.875rem',
color: '#6c757d',
marginTop: '0.25rem'
}}>
Die E-Mail wird automatisch aus Vor- und Nachname generiert
</div>
</div>
</div> </div>
)} )}
@@ -315,7 +359,7 @@ const Setup: React.FC = () => {
border: '1px solid #b6d7e8' border: '1px solid #b6d7e8'
}}> }}>
💡 Nach dem erfolgreichen Setup werden Sie zur Anmeldeseite weitergeleitet, 💡 Nach dem erfolgreichen Setup werden Sie zur Anmeldeseite weitergeleitet,
wo Sie sich mit Ihren Zugangsdaten anmelden können. wo Sie sich mit Ihrer automatisch generierten E-Mail anmelden können.
</div> </div>
)} )}
</div> </div>