Compare commits

...

3 Commits

14 changed files with 789 additions and 784 deletions

View File

@@ -18,17 +18,17 @@ function generateEmail(firstname: string, lastname: string): string {
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`;
} }
export const getEmployees = async (req: AuthRequest, res: Response): Promise<void> => { export const getEmployees = async (req: AuthRequest, res: Response): Promise<void> => {
try { try {
console.log('🔍 Fetching employees - User:', req.user); console.log('🔍 Fetching employees - User:', req.user);
const { includeInactive } = req.query; const { includeInactive } = req.query;
const includeInactiveFlag = includeInactive === 'true'; const includeInactiveFlag = includeInactive === 'true';
let query = ` let query = `
SELECT SELECT
e.id, e.email, e.firstname, e.lastname, e.id, e.email, e.firstname, e.lastname,
@@ -43,13 +43,13 @@ export const getEmployees = async (req: AuthRequest, res: Response): Promise<voi
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
`; `;
if (!includeInactiveFlag) { if (!includeInactiveFlag) {
query += ' WHERE e.is_active = 1'; query += ' WHERE e.is_active = 1';
} }
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 proper field names and roles array // Format employees with proper field names and roles array
@@ -132,12 +132,12 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
password: '***hidden***' password: '***hidden***'
}); });
const { const {
password, password,
firstname, firstname,
lastname, lastname,
roles = ['user'], roles = ['user'],
employeeType, employeeType,
contractType, contractType,
canWorkAlone = false, canWorkAlone = false,
isTrainee = false isTrainee = false
@@ -146,21 +146,21 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
// Validation // Validation
if (!password || !firstname || !lastname || !employeeType) { 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 und employeeType sind erforderlich' error: 'Password, firstname, lastname und employeeType sind erforderlich'
}); });
return; return;
} }
// ✅ ENHANCED: Validate employee type exists and get category info // ✅ ENHANCED: Validate employee type exists and get category info
const employeeTypeInfo = await db.get<{type: string, category: string, has_contract_type: number}>( const employeeTypeInfo = await db.get<{ type: string, category: string, has_contract_type: number }>(
'SELECT type, category, has_contract_type FROM employee_types WHERE type = ?', 'SELECT type, category, has_contract_type FROM employee_types WHERE type = ?',
[employeeType] [employeeType]
); );
if (!employeeTypeInfo) { if (!employeeTypeInfo) {
res.status(400).json({ res.status(400).json({
error: `Ungültiger employeeType: ${employeeType}. Gültige Typen: manager, personell, apprentice, guest` error: `Ungültiger employeeType: ${employeeType}. Gültige Typen: manager, personell, apprentice, guest`
}); });
return; return;
} }
@@ -169,22 +169,22 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
if (employeeTypeInfo.has_contract_type === 1) { if (employeeTypeInfo.has_contract_type === 1) {
// Internal types require contract type // Internal types require contract type
if (!contractType) { if (!contractType) {
res.status(400).json({ res.status(400).json({
error: `contractType ist erforderlich für employeeType: ${employeeType}` error: `contractType ist erforderlich für employeeType: ${employeeType}`
}); });
return; return;
} }
if (!['small', 'large', 'flexible'].includes(contractType)) { if (!['small', 'large', 'flexible'].includes(contractType)) {
res.status(400).json({ res.status(400).json({
error: `Ungültiger contractType: ${contractType}. Gültige Werte: small, large, flexible` error: `Ungültiger contractType: ${contractType}. Gültige Werte: small, large, flexible`
}); });
return; return;
} }
} else { } else {
// External types (guest) should not have contract type // External types (guest) should not have contract type
if (contractType) { if (contractType) {
res.status(400).json({ res.status(400).json({
error: `contractType ist nicht erlaubt für employeeType: ${employeeType}` error: `contractType ist nicht erlaubt für employeeType: ${employeeType}`
}); });
return; return;
} }
@@ -192,8 +192,8 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
// ✅ ENHANCED: isTrainee validation - only applicable for personell type // ✅ ENHANCED: isTrainee validation - only applicable for personell type
if (isTrainee && employeeType !== 'personell') { if (isTrainee && employeeType !== 'personell') {
res.status(400).json({ res.status(400).json({
error: `isTrainee ist nur für employeeType 'personell' erlaubt` error: `isTrainee ist nur für employeeType 'personell' erlaubt`
}); });
return; return;
} }
@@ -204,11 +204,11 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
// Check if generated email already exists // Check if generated email already exists
const existingUser = await db.get<any>('SELECT id FROM employees WHERE email = ? AND is_active = 1', [email]); const existingUser = await db.get<any>('SELECT id FROM employees WHERE email = ? AND is_active = 1', [email]);
if (existingUser) { if (existingUser) {
console.log('❌ Generated email already exists:', email); console.log('❌ Generated email already exists:', email);
res.status(409).json({ res.status(409).json({
error: `Employee with email ${email} already exists. Please use different firstname/lastname.` error: `Employee with email ${email} already exists. Please use different firstname/lastname.`
}); });
return; return;
} }
@@ -228,12 +228,12 @@ export const createEmployee = async (req: AuthRequest, res: Response): Promise<v
is_active, is_trainee is_active, is_trainee
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[ [
employeeId, employeeId,
email, email,
hashedPassword, hashedPassword,
firstname, firstname,
lastname, lastname,
employeeType, employeeType,
contractType, // Will be NULL for external types contractType, // Will be NULL for external types
canWorkAlone ? 1 : 0, canWorkAlone ? 1 : 0,
1, 1,
@@ -302,9 +302,9 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
const { id } = req.params; const { id } = req.params;
const { firstname, lastname, roles, isActive, employeeType, contractType, canWorkAlone, isTrainee } = req.body; const { firstname, lastname, roles, isActive, employeeType, contractType, canWorkAlone, isTrainee } = req.body;
console.log('📝 Update Employee Request:', { console.log('📝 Update Employee Request:', {
id, firstname, lastname, roles, isActive, id, firstname, lastname, roles, isActive,
employeeType, contractType, canWorkAlone, isTrainee employeeType, contractType, canWorkAlone, isTrainee
}); });
// Check if employee exists and get current data // Check if employee exists and get current data
@@ -321,10 +321,10 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
'SELECT role FROM employee_roles WHERE employee_id = ?', 'SELECT role FROM employee_roles WHERE employee_id = ?',
[currentUser.userId] [currentUser.userId]
); );
const isCurrentlyAdmin = currentUserRoles.some(role => role.role === 'admin'); const isCurrentlyAdmin = currentUserRoles.some(role => role.role === 'admin');
const willBeAdmin = roles.includes('admin'); const willBeAdmin = roles.includes('admin');
if (isCurrentlyAdmin && !willBeAdmin) { if (isCurrentlyAdmin && !willBeAdmin) {
res.status(400).json({ error: 'You cannot remove your own admin role' }); res.status(400).json({ error: 'You cannot remove your own admin role' });
return; return;
@@ -372,8 +372,8 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
); );
if (!validEmployeeType) { if (!validEmployeeType) {
res.status(400).json({ res.status(400).json({
error: `Ungültiger employeeType: ${employeeType}` error: `Ungültiger employeeType: ${employeeType}`
}); });
return; return;
} }
@@ -385,16 +385,16 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
const newFirstname = firstname || existingEmployee.firstname; const newFirstname = firstname || existingEmployee.firstname;
const newLastname = lastname || existingEmployee.lastname; const newLastname = lastname || existingEmployee.lastname;
email = generateEmail(newFirstname, newLastname); email = generateEmail(newFirstname, newLastname);
// Check if new email already exists (for another employee) // Check if new email already exists (for another employee)
const emailExists = await db.get<any>( const emailExists = await db.get<any>(
'SELECT id FROM employees WHERE email = ? AND id != ? AND is_active = 1', 'SELECT id FROM employees WHERE email = ? AND id != ? AND is_active = 1',
[email, id] [email, id]
); );
if (emailExists) { if (emailExists) {
res.status(409).json({ res.status(409).json({
error: `Cannot update name - email ${email} already exists for another employee` error: `Cannot update name - email ${email} already exists for another employee`
}); });
return; return;
} }
@@ -423,7 +423,7 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
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]);
// Insert new roles // Insert new roles
for (const role of roles) { for (const role of roles) {
await db.run( await db.run(
@@ -541,18 +541,18 @@ export const deleteEmployee = async (req: AuthRequest, res: Response): Promise<v
try { try {
// 1. Remove availabilities // 1. Remove availabilities
await db.run('DELETE FROM employee_availability WHERE employee_id = ?', [id]); await db.run('DELETE FROM employee_availability WHERE employee_id = ?', [id]);
// 2. Remove from assigned_shifts (JSON field cleanup) // 2. Remove from assigned_shifts (JSON field cleanup)
interface AssignedShift { interface AssignedShift {
id: string; id: string;
assigned_employees: string; assigned_employees: string;
} }
const assignedShifts = await db.all<AssignedShift>( const assignedShifts = await db.all<AssignedShift>(
'SELECT id, assigned_employees FROM scheduled_shifts WHERE json_extract(assigned_employees, "$") LIKE ?', 'SELECT id, assigned_employees FROM scheduled_shifts WHERE json_extract(assigned_employees, "$") LIKE ?',
[`%${id}%`] [`%${id}%`]
); );
for (const shift of assignedShifts) { for (const shift of assignedShifts) {
try { try {
const employeesArray: string[] = JSON.parse(shift.assigned_employees || '[]'); const employeesArray: string[] = JSON.parse(shift.assigned_employees || '[]');
@@ -581,7 +581,7 @@ export const deleteEmployee = async (req: AuthRequest, res: Response): Promise<v
await db.run('COMMIT'); await db.run('COMMIT');
console.log('✅ Successfully deleted employee:', existingEmployee.email); console.log('✅ Successfully deleted employee:', existingEmployee.email);
res.status(204).send(); res.status(204).send();
} catch (error) { } catch (error) {
@@ -655,23 +655,23 @@ export const updateAvailabilities = async (req: AuthRequest, res: Response): Pro
} }
// Validate contract type requirements // Validate contract type requirements
const availableCount = availabilities.filter((avail: any) => const availableCount = availabilities.filter((avail: any) =>
avail.preferenceLevel === 1 || avail.preferenceLevel === 2 avail.preferenceLevel === 1 || avail.preferenceLevel === 2
).length; ).length;
const contractType = existingEmployee.contract_type; const contractType = existingEmployee.contract_type;
// Apply contract type minimum requirements // Apply contract type minimum requirements
if (contractType === 'small' && availableCount < 2) { if (contractType === 'small' && availableCount < 2) {
res.status(400).json({ res.status(400).json({
error: 'Employees with small contract must have at least 2 available shifts' error: 'Employees with small contract must have at least 2 available shifts'
}); });
return; return;
} }
if (contractType === 'large' && availableCount < 3) { if (contractType === 'large' && availableCount < 3) {
res.status(400).json({ res.status(400).json({
error: 'Employees with large contract must have at least 3 available shifts' error: 'Employees with large contract must have at least 3 available shifts'
}); });
return; return;
} }
@@ -742,12 +742,12 @@ export const changePassword = async (req: AuthRequest, res: Response): Promise<v
// Get the current user from the auth middleware // Get the current user from the auth middleware
const currentUser = (req as AuthRequest).user; const currentUser = (req as AuthRequest).user;
// Check if user is changing their own password or is an admin // Check if user is changing their own password or is an admin
if (currentUser?.userId !== id && currentUser?.role !== 'admin') { if (currentUser?.userId !== id && currentUser?.role !== 'admin') {
res.status(403).json({ error: 'You can only change your own password' }); res.status(403).json({ error: 'You can only change your own password' });
return; return;
} }
// Check if employee exists and get password // Check if employee exists and get password
const employee = await db.get<{ password: string }>('SELECT password FROM employees WHERE id = ?', [id]); const employee = await db.get<{ password: string }>('SELECT password FROM employees WHERE id = ?', [id]);
@@ -756,8 +756,8 @@ export const changePassword = async (req: AuthRequest, res: Response): Promise<v
return; return;
} }
// For non-admin users, verify current password // Verify current password
if (currentUser?.role !== 'admin') { if (employee) {
const isValidPassword = await bcrypt.compare(currentPassword, employee.password); const isValidPassword = await bcrypt.compare(currentPassword, employee.password);
if (!isValidPassword) { if (!isValidPassword) {
res.status(400).json({ error: 'Current password is incorrect' }); res.status(400).json({ error: 'Current password is incorrect' });
@@ -766,8 +766,8 @@ export const changePassword = async (req: AuthRequest, res: Response): Promise<v
} }
// Validate new password // Validate new password
if (!newPassword || newPassword.length < 6) { if (!newPassword || newPassword.length < 8) {
res.status(400).json({ error: 'New password must be at least 6 characters long' }); res.status(400).json({ error: 'New password must be at least 8 characters long' });
return; return;
} }
@@ -798,13 +798,13 @@ export const updateLastLogin = async (req: AuthRequest, res: Response): Promise<
// Update last_login with current timestamp // Update last_login with current timestamp
const currentTimestamp = new Date().toISOString(); const currentTimestamp = new Date().toISOString();
await db.run( await db.run(
'UPDATE employees SET last_login = ? WHERE id = ?', 'UPDATE employees SET last_login = ? WHERE id = ?',
[currentTimestamp, id] [currentTimestamp, id]
); );
console.log(`✅ Last login updated for employee ${id}: ${currentTimestamp}`); console.log(`✅ Last login updated for employee ${id}: ${currentTimestamp}`);
res.json({ res.json({
message: 'Last login updated successfully', message: 'Last login updated successfully',
lastLogin: currentTimestamp lastLogin: currentTimestamp
}); });
@@ -825,7 +825,7 @@ const checkAdminCount = async (employeeId: string, newRoles: string[]): Promise<
); );
const currentAdminCount = adminCountResult?.count || 0; const currentAdminCount = adminCountResult?.count || 0;
// Check ALL current roles for the employee // Check ALL current roles for the employee
const currentEmployeeRoles = await db.all<{ role: string }>( const currentEmployeeRoles = await db.all<{ role: string }>(
`SELECT role FROM employee_roles WHERE employee_id = ?`, `SELECT role FROM employee_roles WHERE employee_id = ?`,

View File

@@ -16,7 +16,7 @@ function generateEmail(firstname: string, lastname: string): string {
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`;
} }
@@ -31,15 +31,15 @@ export const checkSetupStatus = async (req: Request, res: Response): Promise<voi
); );
console.log('Admin exists check:', adminExists); console.log('Admin exists check:', adminExists);
const needsSetup = !adminExists || adminExists['COUNT(*)'] === 0; const needsSetup = !adminExists || adminExists['COUNT(*)'] === 0;
res.json({ res.json({
needsSetup: needsSetup needsSetup: needsSetup
}); });
} catch (error) { } catch (error) {
console.error('Error checking setup status:', error); console.error('Error checking setup status:', error);
res.status(500).json({ res.status(500).json({
error: 'Internal server error during setup check' error: 'Internal server error during setup check'
}); });
} }
@@ -75,8 +75,8 @@ export const setupAdmin = async (req: Request, res: Response): Promise<void> =>
} }
// Password length validation // Password length validation
if (password.length < 6) { if (password.length < 8) {
res.status(400).json({ error: 'Das Passwort muss mindestens 6 Zeichen lang sein' }); res.status(400).json({ error: 'Das Passwort muss mindestens 8 Zeichen lang sein' });
return; return;
} }
@@ -125,15 +125,15 @@ export const setupAdmin = async (req: Request, res: Response): Promise<void> =>
} catch (dbError) { } catch (dbError) {
await db.run('ROLLBACK'); await db.run('ROLLBACK');
console.error('❌ Database error during admin creation:', dbError); console.error('❌ Database error during admin creation:', dbError);
res.status(500).json({ res.status(500).json({
error: 'Fehler beim Erstellen des Admin-Accounts' error: 'Fehler beim Erstellen des Admin-Accounts'
}); });
} }
} catch (error) { } catch (error) {
console.error('❌ Error in setup:', error); console.error('❌ Error in setup:', error);
if (!res.headersSent) { if (!res.headersSent) {
res.status(500).json({ res.status(500).json({
error: 'Ein unerwarteter Fehler ist aufgetreten' error: 'Ein unerwarteter Fehler ist aufgetreten'
}); });
} }

View File

@@ -23,7 +23,7 @@
### \[CREATE\] Employee ### \[CREATE\] Employee
* `firstname` 1-100 characters and must not be empty * `firstname` 1-100 characters and must not be empty
* `lastname` 1-100 characters and must not be empty * `lastname` 1-100 characters and must not be empty
* `password` must be at least 6 characters (in create mode) * `password` must be at least 8 characters (in create mode)
* `employeeType` must be `manager`, `personell`, `apprentice`, or `guest` * `employeeType` must be `manager`, `personell`, `apprentice`, or `guest`
* `canWorkAlone` optional boolean * `canWorkAlone` optional boolean
* `isTrainee` optional boolean * `isTrainee` optional boolean

View File

@@ -73,7 +73,7 @@ export const validateEmployee = [
body('contractType') body('contractType')
.custom((value, { req }) => { .custom((value, { req }) => {
const employeeType = req.body.employeeType; const employeeType = req.body.employeeType;
// Manager, apprentice => contractType must be flexible // Manager, apprentice => contractType must be flexible
if (['manager', 'apprentice'].includes(employeeType)) { if (['manager', 'apprentice'].includes(employeeType)) {
if (value !== 'flexible') { if (value !== 'flexible') {
@@ -92,7 +92,7 @@ export const validateEmployee = [
throw new Error(`contractType must be 'small' or 'large' for employeeType: ${employeeType}`); throw new Error(`contractType must be 'small' or 'large' for employeeType: ${employeeType}`);
} }
} }
return true; return true;
}), }),
@@ -156,7 +156,7 @@ export const validateEmployeeUpdate = [
.custom((value, { req }) => { .custom((value, { req }) => {
const employeeType = req.body.employeeType; const employeeType = req.body.employeeType;
if (!employeeType) return true; // Skip if employeeType not provided if (!employeeType) return true; // Skip if employeeType not provided
// Same validation logic as create // Same validation logic as create
if (['manager', 'apprentice'].includes(employeeType)) { if (['manager', 'apprentice'].includes(employeeType)) {
if (value !== 'flexible') { if (value !== 'flexible') {
@@ -173,7 +173,7 @@ export const validateEmployeeUpdate = [
throw new Error(`contractType must be 'small' or 'large' for employeeType: ${employeeType}`); throw new Error(`contractType must be 'small' or 'large' for employeeType: ${employeeType}`);
} }
} }
return true; return true;
}), }),
@@ -209,7 +209,7 @@ export const validateChangePassword = [
.isLength({ min: 1 }) .isLength({ min: 1 })
.withMessage('Current password is required for self-password change'), .withMessage('Current password is required for self-password change'),
body('password') body('newPassword')
.isLength({ min: 8 }) .isLength({ min: 8 })
.withMessage('Password must be at least 8 characters') .withMessage('Password must be at least 8 characters')
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?])/) .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?])/)
@@ -217,7 +217,7 @@ export const validateChangePassword = [
body('confirmPassword') body('confirmPassword')
.custom((value, { req }) => { .custom((value, { req }) => {
if (value !== req.body.password) { if (value !== req.body.newPassword) {
throw new Error('Passwords do not match'); throw new Error('Passwords do not match');
} }
return true; return true;
@@ -465,7 +465,7 @@ export const validateAvailabilities = [
.withMessage('Availabilities must be an array') .withMessage('Availabilities must be an array')
.custom((availabilities, { req }) => { .custom((availabilities, { req }) => {
// Count available shifts (preference level 1 or 2) // Count available shifts (preference level 1 or 2)
const availableCount = availabilities.filter((avail: any) => const availableCount = availabilities.filter((avail: any) =>
avail.preferenceLevel === 1 || avail.preferenceLevel === 2 avail.preferenceLevel === 1 || avail.preferenceLevel === 2
).length; ).length;
@@ -473,7 +473,7 @@ export const validateAvailabilities = [
if (availableCount === 0) { if (availableCount === 0) {
throw new Error('At least one available shift is required'); throw new Error('At least one available shift is required');
} }
return true; return true;
}), }),

View File

@@ -14,7 +14,7 @@ function generateEmail(firstname: string, lastname: string): string {
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`;
} }
@@ -22,8 +22,8 @@ function generateEmail(firstname: string, lastname: string): string {
export function validateEmployeeData(employee: CreateEmployeeRequest): string[] { export function validateEmployeeData(employee: CreateEmployeeRequest): string[] {
const errors: string[] = []; const errors: string[] = [];
if (employee.password?.length < 6) { if (employee.password?.length < 8) {
errors.push('Password must be at least 6 characters long'); errors.push('Password must be at least 8 characters long');
} }
if (!employee.firstname?.trim() || employee.firstname.trim().length < 2) { if (!employee.firstname?.trim() || employee.firstname.trim().length < 2) {
@@ -72,16 +72,16 @@ export function generateEmployeeEmail(firstname: string, lastname: string): stri
} }
// UPDATED: Business logic helpers for new employee types // 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 => export const isPersonell = (employee: Employee): boolean =>
employee.employeeType === 'personell'; employee.employeeType === 'personell';
export const isApprentice = (employee: Employee): boolean => export const isApprentice = (employee: Employee): boolean =>
employee.employeeType === 'apprentice'; employee.employeeType === 'apprentice';
export const isGuest = (employee: Employee): boolean => export const isGuest = (employee: Employee): boolean =>
employee.employeeType === 'guest'; employee.employeeType === 'guest';
export const isInternal = (employee: Employee): boolean => export const isInternal = (employee: Employee): boolean =>
@@ -91,24 +91,24 @@ export const isExternal = (employee: Employee): boolean =>
employee.employeeType === 'guest'; employee.employeeType === 'guest';
// UPDATED: Trainee logic - now based on isTrainee field for personell type // 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 === 'personell' && employee.isTrainee; employee.employeeType === 'personell' && employee.isTrainee;
export const isExperienced = (employee: Employee): boolean => export const isExperienced = (employee: Employee): boolean =>
employee.employeeType === 'personell' && !employee.isTrainee; employee.employeeType === 'personell' && !employee.isTrainee;
// Role-based helpers // 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;
export const isMaintenance = (employee: Employee): boolean => export const isMaintenance = (employee: Employee): boolean =>
employee.roles?.includes('maintenance') || false; employee.roles?.includes('maintenance') || false;
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 // 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 && (isManager(employee) || isExperienced(employee)); employee.canWorkAlone && (isManager(employee) || isExperienced(employee));
// Helper for full name display // Helper for full name display

View File

@@ -11,15 +11,15 @@ import {
changePassword, changePassword,
updateLastLogin updateLastLogin
} from '../controllers/employeeController.js'; } from '../controllers/employeeController.js';
import { import {
handleValidationErrors, handleValidationErrors,
validateEmployee, validateEmployee,
validateEmployeeUpdate, validateEmployeeUpdate,
validateChangePassword, validateChangePassword,
validateId, validateId,
validateEmployeeId, validateEmployeeId,
validateAvailabilities, validateAvailabilities,
validatePagination validatePagination
} from '../middleware/validation.js'; } from '../middleware/validation.js';
const router = express.Router(); const router = express.Router();
@@ -28,18 +28,18 @@ const router = express.Router();
router.use(authMiddleware); router.use(authMiddleware);
// Employee CRUD Routes // Employee CRUD Routes
router.get('/', validatePagination, handleValidationErrors, getEmployees); router.get('/', validatePagination, handleValidationErrors, authMiddleware, getEmployees);
router.get('/:id', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), getEmployee); router.get('/:id', validateId, handleValidationErrors, requireRole(['admin', 'maintenance']), getEmployee);
router.post('/', validateEmployee, handleValidationErrors, requireRole(['admin']), createEmployee); router.post('/', validateEmployee, handleValidationErrors, requireRole(['admin']), createEmployee);
router.put('/:id', validateId, validateEmployeeUpdate, handleValidationErrors, requireRole(['admin', 'maintenance']), updateEmployee); router.put('/:id', validateId, validateEmployeeUpdate, handleValidationErrors, requireRole(['admin', 'maintenance']), updateEmployee);
router.delete('/:id', validateId, handleValidationErrors, requireRole(['admin']), deleteEmployee); router.delete('/:id', validateId, handleValidationErrors, requireRole(['admin']), deleteEmployee);
// Password & Login Routes // Password & Login Routes
router.put('/:id/password', validateId, validateChangePassword, handleValidationErrors, changePassword); router.put('/:id/password', validateId, validateChangePassword, handleValidationErrors, authMiddleware, changePassword);
router.put('/:id/last-login', validateId, handleValidationErrors, updateLastLogin); router.put('/:id/last-login', validateId, handleValidationErrors, authMiddleware, updateLastLogin);
// Availability Routes // Availability Routes
router.get('/:employeeId/availabilities', validateEmployeeId, handleValidationErrors, getAvailabilities); router.get('/:employeeId/availabilities', validateEmployeeId, handleValidationErrors, authMiddleware, getAvailabilities);
router.put('/:employeeId/availabilities', validateEmployeeId, validateAvailabilities, handleValidationErrors, updateAvailabilities); router.put('/:employeeId/availabilities', validateEmployeeId, validateAvailabilities, handleValidationErrors, authMiddleware, updateAvailabilities);
export default router; export default router;

View File

@@ -33,35 +33,19 @@ export const useBackendValidation = () => {
const result = await apiCall(); const result = await apiCall();
return result; return result;
} catch (error: any) { } catch (error: any) {
if (error.validationErrors) { if (error.validationErrors && Array.isArray(error.validationErrors)) {
setValidationErrors(error.validationErrors); setValidationErrors(error.validationErrors);
// Show specific validation error messages
if (error.validationErrors.length > 0) {
// Show the first validation error as the main notification
const firstError = error.validationErrors[0];
showNotification({
type: 'error',
title: 'Validierungsfehler',
message: firstError.message
});
// If there are multiple errors, show additional notifications for each // Show specific validation error messages from backend
if (error.validationErrors.length > 1) { error.validationErrors.forEach((validationError: ValidationError, index: number) => {
// Wait a bit before showing additional notifications to avoid overlap setTimeout(() => {
setTimeout(() => { showNotification({
error.validationErrors.slice(1).forEach((validationError: ValidationError, index: number) => { type: 'error',
setTimeout(() => { title: 'Validierungsfehler',
showNotification({ message: `${validationError.field ? `${validationError.field}: ` : ''}${validationError.message}`
type: 'error', });
title: 'Weiterer Fehler', }, index * 500); // Stagger the notifications
message: validationError.message });
});
}, index * 300); // Stagger the notifications
});
}, 500);
}
}
} else { } else {
// Show notification for other errors // Show notification for other errors
showNotification({ showNotification({

View File

@@ -14,7 +14,7 @@ function generateEmail(firstname: string, lastname: string): string {
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`;
} }
@@ -22,8 +22,8 @@ function generateEmail(firstname: string, lastname: string): string {
export function validateEmployeeData(employee: CreateEmployeeRequest): string[] { export function validateEmployeeData(employee: CreateEmployeeRequest): string[] {
const errors: string[] = []; const errors: string[] = [];
if (employee.password?.length < 6) { if (employee.password?.length < 8) {
errors.push('Password must be at least 6 characters long'); errors.push('Password must be at least 8 characters long');
} }
if (!employee.firstname?.trim() || employee.firstname.trim().length < 2) { if (!employee.firstname?.trim() || employee.firstname.trim().length < 2) {
@@ -72,16 +72,16 @@ export function generateEmployeeEmail(firstname: string, lastname: string): stri
} }
// UPDATED: Business logic helpers for new employee types // 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 => export const isPersonell = (employee: Employee): boolean =>
employee.employeeType === 'personell'; employee.employeeType === 'personell';
export const isApprentice = (employee: Employee): boolean => export const isApprentice = (employee: Employee): boolean =>
employee.employeeType === 'apprentice'; employee.employeeType === 'apprentice';
export const isGuest = (employee: Employee): boolean => export const isGuest = (employee: Employee): boolean =>
employee.employeeType === 'guest'; employee.employeeType === 'guest';
export const isInternal = (employee: Employee): boolean => export const isInternal = (employee: Employee): boolean =>
@@ -91,24 +91,24 @@ export const isExternal = (employee: Employee): boolean =>
employee.employeeType === 'guest'; employee.employeeType === 'guest';
// UPDATED: Trainee logic - now based on isTrainee field for personell type // 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 === 'personell' && employee.isTrainee; employee.employeeType === 'personell' && employee.isTrainee;
export const isExperienced = (employee: Employee): boolean => export const isExperienced = (employee: Employee): boolean =>
employee.employeeType === 'personell' && !employee.isTrainee; employee.employeeType === 'personell' && !employee.isTrainee;
// Role-based helpers // 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;
export const isMaintenance = (employee: Employee): boolean => export const isMaintenance = (employee: Employee): boolean =>
employee.roles?.includes('maintenance') || false; employee.roles?.includes('maintenance') || false;
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 // 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 && (isManager(employee) || isExperienced(employee)); employee.canWorkAlone && (isManager(employee) || isExperienced(employee));
// Helper for full name display // Helper for full name display

View File

@@ -23,12 +23,12 @@ interface EmployeeFormData {
lastname: string; lastname: string;
email: string; email: string;
password: string; password: string;
// Step 2: Mitarbeiterkategorie // Step 2: Mitarbeiterkategorie
employeeType: EmployeeType; employeeType: EmployeeType;
contractType: ContractType | undefined; contractType: ContractType | undefined;
isTrainee: boolean; isTrainee: boolean;
// Step 3: Berechtigungen & Status // Step 3: Berechtigungen & Status
roles: string[]; roles: string[];
canWorkAlone: boolean; canWorkAlone: boolean;
@@ -64,12 +64,12 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
canWorkAlone: false, canWorkAlone: false,
isActive: true isActive: true
}); });
const [passwordForm, setPasswordForm] = useState<PasswordFormData>({ const [passwordForm, setPasswordForm] = useState<PasswordFormData>({
newPassword: '', newPassword: '',
confirmPassword: '' confirmPassword: ''
}); });
const [showPasswordSection, setShowPasswordSection] = useState(false); const [showPasswordSection, setShowPasswordSection] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
@@ -116,7 +116,7 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
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`;
}; };
@@ -177,7 +177,7 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
const goToNextStep = (): void => { const goToNextStep = (): void => {
setError(''); setError('');
clearErrors(); // Clear previous validation errors clearErrors(); // Clear previous validation errors
if (!validateCurrentStep(currentStep)) { if (!validateCurrentStep(currentStep)) {
return; return;
} }
@@ -198,7 +198,7 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
const handleStepChange = (stepIndex: number): void => { const handleStepChange = (stepIndex: number): void => {
setError(''); setError('');
clearErrors(); // Clear validation errors when changing steps clearErrors(); // Clear validation errors when changing steps
// Nur erlauben, zu bereits validierten Schritten zu springen // Nur erlauben, zu bereits validierten Schritten zu springen
if (stepIndex <= currentStep + 1) { if (stepIndex <= currentStep + 1) {
// Vor dem Wechsel validieren // Vor dem Wechsel validieren
@@ -212,7 +212,7 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
// ===== FORM HANDLER ===== // ===== FORM HANDLER =====
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value, type } = e.target; const { name, value, type } = e.target;
setFormData(prev => ({ setFormData(prev => ({
...prev, ...prev,
[name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value [name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value
@@ -264,9 +264,9 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
} }
// Determine if can work alone based on employee type // Determine if can work alone based on employee type
const canWorkAlone = employeeType === 'manager' || const canWorkAlone = employeeType === 'manager' ||
(employeeType === 'personell' && !formData.isTrainee); (employeeType === 'personell' && !formData.isTrainee);
// Reset isTrainee if not personell // Reset isTrainee if not personell
const isTrainee = employeeType === 'personell' ? formData.isTrainee : false; const isTrainee = employeeType === 'personell' ? formData.isTrainee : false;
@@ -311,9 +311,9 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
canWorkAlone: formData.canWorkAlone, canWorkAlone: formData.canWorkAlone,
isTrainee: formData.isTrainee isTrainee: formData.isTrainee
}; };
// Use executeWithValidation ONLY for the API call // Use executeWithValidation ONLY for the API call
await executeWithValidation(() => await executeWithValidation(() =>
employeeService.createEmployee(createData) employeeService.createEmployee(createData)
); );
} else if (employee) { } else if (employee) {
@@ -327,9 +327,9 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
isActive: formData.isActive, isActive: formData.isActive,
isTrainee: formData.isTrainee isTrainee: formData.isTrainee
}; };
// Use executeWithValidation for the update call // Use executeWithValidation for the update call
await executeWithValidation(() => await executeWithValidation(() =>
employeeService.updateEmployee(employee.id, updateData) employeeService.updateEmployee(employee.id, updateData)
); );
@@ -343,12 +343,13 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
await executeWithValidation(() => await executeWithValidation(() =>
employeeService.changePassword(employee.id, { employeeService.changePassword(employee.id, {
currentPassword: '', currentPassword: '',
newPassword: passwordForm.newPassword newPassword: passwordForm.newPassword,
confirmPassword: passwordForm.confirmPassword
}) })
); );
} }
} }
return Promise.resolve(); return Promise.resolve();
} catch (err: any) { } catch (err: any) {
// Only set error if it's not a validation error (validation errors are handled by the hook) // Only set error if it's not a validation error (validation errors are handled by the hook)
@@ -364,9 +365,9 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
const isStepCompleted = (stepIndex: number): boolean => { const isStepCompleted = (stepIndex: number): boolean => {
switch (stepIndex) { switch (stepIndex) {
case 0: case 0:
return !!formData.firstname.trim() && return !!formData.firstname.trim() &&
!!formData.lastname.trim(); !!formData.lastname.trim();
// REMOVE: (mode === 'edit' || formData.password.length >= 6) // REMOVE: (mode === 'edit' || formData.password.length >= 6)
case 1: case 1:
return !!formData.employeeType; return !!formData.employeeType;
case 2: case 2:
@@ -391,7 +392,7 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
validationErrors, validationErrors,
getFieldError, getFieldError,
hasErrors, hasErrors,
// Actions // Actions
goToNextStep, goToNextStep,
goToPrevStep, goToPrevStep,
@@ -405,7 +406,7 @@ const useEmployeeForm = (mode: 'create' | 'edit', employee?: Employee) => {
handleSubmit, handleSubmit,
setShowPasswordSection, setShowPasswordSection,
clearErrors, clearErrors,
// Helpers // Helpers
isStepCompleted isStepCompleted
}; };
@@ -430,8 +431,8 @@ interface StepContentProps {
hasErrors: (fieldName?: string) => boolean; hasErrors: (fieldName?: string) => boolean;
} }
const Step1Content: React.FC<StepContentProps> = ({ const Step1Content: React.FC<StepContentProps> = ({
formData, formData,
onInputChange, onInputChange,
emailPreview, emailPreview,
mode mode
@@ -439,9 +440,9 @@ const Step1Content: React.FC<StepContentProps> = ({
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}> <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem' }}>
<div> <div>
<label style={{ <label style={{
display: 'block', display: 'block',
marginBottom: '0.5rem', marginBottom: '0.5rem',
fontWeight: '600', fontWeight: '600',
color: '#495057' color: '#495057'
}}> }}>
@@ -465,9 +466,9 @@ const Step1Content: React.FC<StepContentProps> = ({
</div> </div>
<div> <div>
<label style={{ <label style={{
display: 'block', display: 'block',
marginBottom: '0.5rem', marginBottom: '0.5rem',
fontWeight: '600', fontWeight: '600',
color: '#495057' color: '#495057'
}}> }}>
@@ -493,17 +494,17 @@ const Step1Content: React.FC<StepContentProps> = ({
{/* Email Preview */} {/* Email Preview */}
<div> <div>
<label style={{ <label style={{
display: 'block', display: 'block',
marginBottom: '0.5rem', marginBottom: '0.5rem',
fontWeight: '600', fontWeight: '600',
color: '#495057' color: '#495057'
}}> }}>
E-Mail Adresse (automatisch generiert) E-Mail Adresse (automatisch generiert)
</label> </label>
<div style={{ <div style={{
padding: '0.75rem', padding: '0.75rem',
backgroundColor: '#e9ecef', backgroundColor: '#e9ecef',
border: '1px solid #ced4da', border: '1px solid #ced4da',
borderRadius: '6px', borderRadius: '6px',
color: '#495057', color: '#495057',
@@ -512,8 +513,8 @@ const Step1Content: React.FC<StepContentProps> = ({
}}> }}>
{emailPreview || 'max.mustermann@sp.de'} {emailPreview || 'max.mustermann@sp.de'}
</div> </div>
<div style={{ <div style={{
fontSize: '0.875rem', fontSize: '0.875rem',
color: '#6c757d', color: '#6c757d',
marginTop: '0.25rem' marginTop: '0.25rem'
}}> }}>
@@ -523,9 +524,9 @@ const Step1Content: React.FC<StepContentProps> = ({
{mode === 'create' && ( {mode === 'create' && (
<div> <div>
<label style={{ <label style={{
display: 'block', display: 'block',
marginBottom: '0.5rem', marginBottom: '0.5rem',
fontWeight: '600', fontWeight: '600',
color: '#495057' color: '#495057'
}}> }}>
@@ -546,8 +547,8 @@ const Step1Content: React.FC<StepContentProps> = ({
}} }}
placeholder="Passwort eingeben" placeholder="Passwort eingeben"
/> />
<div style={{ <div style={{
fontSize: '0.875rem', fontSize: '0.875rem',
color: '#6c757d', color: '#6c757d',
marginTop: '0.25rem' marginTop: '0.25rem'
}}> }}>
@@ -558,7 +559,7 @@ const Step1Content: React.FC<StepContentProps> = ({
</div> </div>
); );
const Step2Content: React.FC<StepContentProps> = ({ const Step2Content: React.FC<StepContentProps> = ({
formData, formData,
onEmployeeTypeChange, onEmployeeTypeChange,
onTraineeChange, onTraineeChange,
@@ -581,11 +582,11 @@ const Step2Content: React.FC<StepContentProps> = ({
{/* Mitarbeiter Kategorie */} {/* Mitarbeiter Kategorie */}
<div> <div>
<h3 style={{ margin: '0 0 1rem 0', color: '#495057' }}>👥 Mitarbeiter Kategorie</h3> <h3 style={{ margin: '0 0 1rem 0', color: '#495057' }}>👥 Mitarbeiter Kategorie</h3>
{employeeTypeError && ( {employeeTypeError && (
<div style={{ <div style={{
color: '#dc3545', color: '#dc3545',
fontSize: '0.875rem', fontSize: '0.875rem',
marginBottom: '1rem', marginBottom: '1rem',
padding: '0.5rem', padding: '0.5rem',
backgroundColor: '#f8d7da', backgroundColor: '#f8d7da',
@@ -595,10 +596,10 @@ const Step2Content: React.FC<StepContentProps> = ({
{employeeTypeError} {employeeTypeError}
</div> </div>
)} )}
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{Object.values(EMPLOYEE_TYPE_CONFIG).map(type => ( {Object.values(EMPLOYEE_TYPE_CONFIG).map(type => (
<div <div
key={type.value} key={type.value}
style={{ style={{
display: 'flex', display: 'flex',
@@ -626,16 +627,16 @@ const Step2Content: React.FC<StepContentProps> = ({
}} }}
/> />
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<div style={{ <div style={{
fontWeight: 'bold', fontWeight: 'bold',
color: '#2c3e50', color: '#2c3e50',
marginBottom: '4px', marginBottom: '4px',
fontSize: '16px' fontSize: '16px'
}}> }}>
{type.label} {type.label}
</div> </div>
<div style={{ <div style={{
fontSize: '14px', fontSize: '14px',
color: '#7f8c8d', color: '#7f8c8d',
lineHeight: '1.4' lineHeight: '1.4'
}}> }}>
@@ -658,10 +659,10 @@ const Step2Content: React.FC<StepContentProps> = ({
{/* Trainee checkbox for personell type */} {/* Trainee checkbox for personell type */}
{formData.employeeType === 'personell' && ( {formData.employeeType === 'personell' && (
<div style={{ <div style={{
marginTop: '1rem', marginTop: '1rem',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '10px', gap: '10px',
padding: '1rem', padding: '1rem',
border: '1px solid #e0e0e0', border: '1px solid #e0e0e0',
@@ -692,11 +693,11 @@ const Step2Content: React.FC<StepContentProps> = ({
{hasRole(['admin']) && showContractType && ( {hasRole(['admin']) && showContractType && (
<div> <div>
<h3 style={{ margin: '0 0 1rem 0', color: '#0c5460' }}>📝 Vertragstyp</h3> <h3 style={{ margin: '0 0 1rem 0', color: '#0c5460' }}>📝 Vertragstyp</h3>
{contractTypeError && ( {contractTypeError && (
<div style={{ <div style={{
color: '#dc3545', color: '#dc3545',
fontSize: '0.875rem', fontSize: '0.875rem',
marginBottom: '1rem', marginBottom: '1rem',
padding: '0.5rem', padding: '0.5rem',
backgroundColor: '#f8d7da', backgroundColor: '#f8d7da',
@@ -706,16 +707,16 @@ const Step2Content: React.FC<StepContentProps> = ({
{contractTypeError} {contractTypeError}
</div> </div>
)} )}
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{contractTypeOptions.map(contract => { {contractTypeOptions.map(contract => {
const isFlexibleDisabled = contract.value === 'flexible' && formData.employeeType === 'personell'; const isFlexibleDisabled = contract.value === 'flexible' && formData.employeeType === 'personell';
const isSmallLargeDisabled = (contract.value === 'small' || contract.value === 'large') && const isSmallLargeDisabled = (contract.value === 'small' || contract.value === 'large') &&
(formData.employeeType === 'manager' || formData.employeeType === 'apprentice'); (formData.employeeType === 'manager' || formData.employeeType === 'apprentice');
const isDisabled = isFlexibleDisabled || isSmallLargeDisabled; const isDisabled = isFlexibleDisabled || isSmallLargeDisabled;
return ( return (
<div <div
key={contract.value} key={contract.value}
style={{ style={{
display: 'flex', display: 'flex',
@@ -745,8 +746,8 @@ const Step2Content: React.FC<StepContentProps> = ({
}} }}
/> />
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<div style={{ <div style={{
fontWeight: 'bold', fontWeight: 'bold',
color: '#2c3e50', color: '#2c3e50',
marginBottom: '4px', marginBottom: '4px',
fontSize: '16px' fontSize: '16px'
@@ -773,8 +774,8 @@ const Step2Content: React.FC<StepContentProps> = ({
</span> </span>
)} )}
</div> </div>
<div style={{ <div style={{
fontSize: '14px', fontSize: '14px',
color: '#7f8c8d', color: '#7f8c8d',
lineHeight: '1.4' lineHeight: '1.4'
}}> }}>
@@ -801,7 +802,7 @@ const Step2Content: React.FC<StepContentProps> = ({
); );
}; };
const Step3Content: React.FC<StepContentProps> = ({ const Step3Content: React.FC<StepContentProps> = ({
formData, formData,
onInputChange, onInputChange,
onRoleChange, onRoleChange,
@@ -816,11 +817,11 @@ const Step3Content: React.FC<StepContentProps> = ({
{/* Eigenständigkeit */} {/* Eigenständigkeit */}
<div> <div>
<h3 style={{ margin: '0 0 1rem 0', color: '#495057' }}>🎯 Eigenständigkeit</h3> <h3 style={{ margin: '0 0 1rem 0', color: '#495057' }}>🎯 Eigenständigkeit</h3>
{canWorkAloneError && ( {canWorkAloneError && (
<div style={{ <div style={{
color: '#dc3545', color: '#dc3545',
fontSize: '0.875rem', fontSize: '0.875rem',
marginBottom: '1rem', marginBottom: '1rem',
padding: '0.5rem', padding: '0.5rem',
backgroundColor: '#f8d7da', backgroundColor: '#f8d7da',
@@ -830,10 +831,10 @@ const Step3Content: React.FC<StepContentProps> = ({
{canWorkAloneError} {canWorkAloneError}
</div> </div>
)} )}
<div style={{ <div style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '15px', gap: '15px',
padding: '1rem', padding: '1rem',
border: '1px solid #e0e0e0', border: '1px solid #e0e0e0',
@@ -847,16 +848,16 @@ const Step3Content: React.FC<StepContentProps> = ({
checked={formData.canWorkAlone} checked={formData.canWorkAlone}
onChange={onInputChange} onChange={onInputChange}
disabled={formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)} disabled={formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)}
style={{ style={{
width: '20px', width: '20px',
height: '20px', height: '20px',
opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.5 : 1 opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.5 : 1
}} }}
/> />
<div style={{ flex: 1 }}> <div style={{ flex: 1 }}>
<label htmlFor="canWorkAlone" style={{ <label htmlFor="canWorkAlone" style={{
fontWeight: 'bold', fontWeight: 'bold',
color: '#2c3e50', color: '#2c3e50',
display: 'block', display: 'block',
opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.5 : 1 opacity: (formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) ? 0.5 : 1
}}> }}>
@@ -864,11 +865,11 @@ const Step3Content: React.FC<StepContentProps> = ({
{(formData.employeeType === 'manager' || (formData.employeeType === 'personell' && formData.isTrainee)) && ' (Automatisch festgelegt)'} {(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 : formData.employeeType === 'personell' && formData.isTrainee
? 'Auszubildende können nicht als eigenständig markiert werden.' ? '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>
</div> </div>
@@ -890,11 +891,11 @@ const Step3Content: React.FC<StepContentProps> = ({
{hasRole(['admin']) && ( {hasRole(['admin']) && (
<div> <div>
<h3 style={{ margin: '0 0 1rem 0', color: '#856404' }}> Systemrollen</h3> <h3 style={{ margin: '0 0 1rem 0', color: '#856404' }}> Systemrollen</h3>
{rolesError && ( {rolesError && (
<div style={{ <div style={{
color: '#dc3545', color: '#dc3545',
fontSize: '0.875rem', fontSize: '0.875rem',
marginBottom: '1rem', marginBottom: '1rem',
padding: '0.5rem', padding: '0.5rem',
backgroundColor: '#f8d7da', backgroundColor: '#f8d7da',
@@ -904,10 +905,10 @@ const Step3Content: React.FC<StepContentProps> = ({
{rolesError} {rolesError}
</div> </div>
)} )}
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{ROLE_CONFIG.map(role => ( {ROLE_CONFIG.map(role => (
<div <div
key={role.value} key={role.value}
style={{ style={{
display: 'flex', display: 'flex',
@@ -951,7 +952,7 @@ const Step3Content: React.FC<StepContentProps> = ({
); );
}; };
const Step4Content: React.FC<StepContentProps> = ({ const Step4Content: React.FC<StepContentProps> = ({
formData, formData,
passwordForm, passwordForm,
onInputChange, onInputChange,
@@ -970,7 +971,7 @@ const Step4Content: React.FC<StepContentProps> = ({
{/* Passwort ändern */} {/* Passwort ändern */}
<div> <div>
<h3 style={{ margin: '0 0 1rem 0', color: '#856404' }}>🔒 Passwort zurücksetzen</h3> <h3 style={{ margin: '0 0 1rem 0', color: '#856404' }}>🔒 Passwort zurücksetzen</h3>
{!showPasswordSection ? ( {!showPasswordSection ? (
<button <button
type="button" type="button"
@@ -1009,10 +1010,10 @@ const Step4Content: React.FC<StepContentProps> = ({
placeholder="Mindestens 6 Zeichen" placeholder="Mindestens 6 Zeichen"
/> />
{newPasswordError && ( {newPasswordError && (
<div style={{ <div style={{
color: '#dc3545', color: '#dc3545',
fontSize: '0.875rem', fontSize: '0.875rem',
marginTop: '0.25rem' marginTop: '0.25rem'
}}> }}>
{newPasswordError} {newPasswordError}
</div> </div>
@@ -1039,10 +1040,10 @@ const Step4Content: React.FC<StepContentProps> = ({
placeholder="Passwort wiederholen" placeholder="Passwort wiederholen"
/> />
{confirmPasswordError && ( {confirmPasswordError && (
<div style={{ <div style={{
color: '#dc3545', color: '#dc3545',
fontSize: '0.875rem', fontSize: '0.875rem',
marginTop: '0.25rem' marginTop: '0.25rem'
}}> }}>
{confirmPasswordError} {confirmPasswordError}
</div> </div>
@@ -1074,9 +1075,9 @@ const Step4Content: React.FC<StepContentProps> = ({
{/* Aktiv Status */} {/* Aktiv Status */}
{mode === 'edit' && ( {mode === 'edit' && (
<div style={{ <div style={{
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '10px', gap: '10px',
padding: '1rem', padding: '1rem',
border: `1px solid ${isActiveError ? '#dc3545' : '#e0e0e0'}`, border: `1px solid ${isActiveError ? '#dc3545' : '#e0e0e0'}`,
@@ -1099,10 +1100,10 @@ const Step4Content: React.FC<StepContentProps> = ({
Inaktive Mitarbeiter können sich nicht anmelden und werden nicht für Schichten eingeplant. Inaktive Mitarbeiter können sich nicht anmelden und werden nicht für Schichten eingeplant.
</div> </div>
{isActiveError && ( {isActiveError && (
<div style={{ <div style={{
color: '#dc3545', color: '#dc3545',
fontSize: '0.875rem', fontSize: '0.875rem',
marginTop: '0.25rem' marginTop: '0.25rem'
}}> }}>
{isActiveError} {isActiveError}
</div> </div>
@@ -1151,9 +1152,9 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
// Inline Step Indicator Komponente (wie in Setup.tsx) // Inline Step Indicator Komponente (wie in Setup.tsx)
const StepIndicator: React.FC = () => ( const StepIndicator: React.FC = () => (
<div style={{ <div style={{
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginBottom: '2.5rem', marginBottom: '2.5rem',
position: 'relative', position: 'relative',
@@ -1169,18 +1170,18 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
backgroundColor: '#e9ecef', backgroundColor: '#e9ecef',
zIndex: 1 zIndex: 1
}} /> }} />
{steps.map((step, index) => { {steps.map((step, index) => {
const isCompleted = index < currentStep; const isCompleted = index < currentStep;
const isCurrent = index === currentStep; const isCurrent = index === currentStep;
const isClickable = index <= currentStep + 1; const isClickable = index <= currentStep + 1;
return ( return (
<div <div
key={step.id} key={step.id}
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
zIndex: 2, zIndex: 2,
position: 'relative', position: 'relative',
@@ -1210,18 +1211,18 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
> >
{index + 1} {index + 1}
</button> </button>
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<div style={{ <div style={{
fontSize: '14px', fontSize: '14px',
fontWeight: isCurrent ? '600' : '400', fontWeight: isCurrent ? '600' : '400',
color: isCurrent ? '#51258f' : '#6c757d' color: isCurrent ? '#51258f' : '#6c757d'
}}> }}>
{step.title} {step.title}
</div> </div>
{step.subtitle && ( {step.subtitle && (
<div style={{ <div style={{
fontSize: '12px', fontSize: '12px',
color: '#6c757d', color: '#6c757d',
marginTop: '2px' marginTop: '2px'
}}> }}>
@@ -1275,8 +1276,8 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
showNotification({ // Changed from addNotification to showNotification showNotification({ // Changed from addNotification to showNotification
type: 'success', type: 'success',
title: 'Erfolg', title: 'Erfolg',
message: mode === 'create' message: mode === 'create'
? 'Mitarbeiter wurde erfolgreich erstellt' ? 'Mitarbeiter wurde erfolgreich erstellt'
: 'Mitarbeiter wurde erfolgreich aktualisiert' : 'Mitarbeiter wurde erfolgreich aktualisiert'
}); });
onSuccess(); onSuccess();
@@ -1287,11 +1288,11 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
const getNextButtonText = (): string => { const getNextButtonText = (): string => {
if (loading) return '⏳ Wird gespeichert...'; if (loading) return '⏳ Wird gespeichert...';
if (currentStep === steps.length - 1) { if (currentStep === steps.length - 1) {
return mode === 'create' ? 'Mitarbeiter erstellen' : 'Änderungen speichern'; return mode === 'create' ? 'Mitarbeiter erstellen' : 'Änderungen speichern';
} }
return 'Weiter →'; return 'Weiter →';
}; };
@@ -1307,8 +1308,8 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
border: '1px solid #e0e0e0', border: '1px solid #e0e0e0',
boxShadow: '0 4px 6px rgba(0,0,0,0.1)' boxShadow: '0 4px 6px rgba(0,0,0,0.1)'
}}> }}>
<h2 style={{ <h2 style={{
margin: '0 0 1.5rem 0', margin: '0 0 1.5rem 0',
color: '#2c3e50', color: '#2c3e50',
borderBottom: '2px solid #f0f0f0', borderBottom: '2px solid #f0f0f0',
paddingBottom: '1rem', paddingBottom: '1rem',
@@ -1322,16 +1323,16 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
{/* Aktueller Schritt Titel und Beschreibung */} {/* Aktueller Schritt Titel und Beschreibung */}
<div style={{ textAlign: 'center', marginBottom: '1.5rem' }}> <div style={{ textAlign: 'center', marginBottom: '1.5rem' }}>
<h3 style={{ <h3 style={{
fontSize: '1.25rem', fontSize: '1.25rem',
fontWeight: 'bold', fontWeight: 'bold',
marginBottom: '0.5rem', marginBottom: '0.5rem',
color: '#2c3e50' color: '#2c3e50'
}}> }}>
{steps[currentStep].title} {steps[currentStep].title}
</h3> </h3>
{steps[currentStep].subtitle && ( {steps[currentStep].subtitle && (
<p style={{ <p style={{
color: '#6c757d', color: '#6c757d',
fontSize: '1rem' fontSize: '1rem'
}}> }}>
@@ -1346,9 +1347,9 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
</div> </div>
{/* Navigations-Buttons */} {/* Navigations-Buttons */}
<div style={{ <div style={{
marginTop: '2rem', marginTop: '2rem',
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center' alignItems: 'center'
}}> }}>
@@ -1368,7 +1369,7 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
> >
{currentStep === 0 ? 'Abbrechen' : '← Zurück'} {currentStep === 0 ? 'Abbrechen' : '← Zurück'}
</button> </button>
<button <button
onClick={isLastStep ? handleFinalSubmit : goToNextStep} onClick={isLastStep ? handleFinalSubmit : goToNextStep}
disabled={loading} disabled={loading}
@@ -1390,16 +1391,16 @@ const EmployeeForm: React.FC<EmployeeFormProps> = ({
{/* Zusätzliche Informationen */} {/* Zusätzliche Informationen */}
{isLastStep && !loading && ( {isLastStep && !loading && (
<div style={{ <div style={{
marginTop: '1.5rem', marginTop: '1.5rem',
textAlign: 'center', textAlign: 'center',
color: '#6c757d', color: '#6c757d',
fontSize: '0.9rem', fontSize: '0.9rem',
padding: '1rem', padding: '1rem',
backgroundColor: '#f8f9fa', backgroundColor: '#f8f9fa',
borderRadius: '6px' borderRadius: '6px'
}}> }}>
{mode === 'create' {mode === 'create'
? 'Überprüfen Sie alle Daten, bevor Sie den Mitarbeiter erstellen' ? 'Überprüfen Sie alle Daten, bevor Sie den Mitarbeiter erstellen'
: 'Überprüfen Sie alle Änderungen, bevor Sie sie speichern' : 'Überprüfen Sie alle Änderungen, bevor Sie sie speichern'
} }

View File

@@ -1,8 +1,9 @@
// frontend/src/pages/Settings/Settings.tsx - UPDATED WITH NEW STYLES // frontend/src/pages/Settings/Settings.tsx - UPDATED WITH VALIDATION STRATEGY
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
import { employeeService } from '../../services/employeeService'; import { employeeService } from '../../services/employeeService';
import { useNotification } from '../../contexts/NotificationContext'; import { useNotification } from '../../contexts/NotificationContext';
import { useBackendValidation } from '../../hooks/useBackendValidation';
import AvailabilityManager from '../Employees/components/AvailabilityManager'; import AvailabilityManager from '../Employees/components/AvailabilityManager';
import { Employee } from '../../models/Employee'; import { Employee } from '../../models/Employee';
import { styles } from './type/SettingsType'; import { styles } from './type/SettingsType';
@@ -10,11 +11,12 @@ import { styles } from './type/SettingsType';
const Settings: React.FC = () => { const Settings: React.FC = () => {
const { user: currentUser, updateUser } = useAuth(); const { user: currentUser, updateUser } = useAuth();
const { showNotification } = useNotification(); const { showNotification } = useNotification();
const { executeWithValidation, clearErrors, isSubmitting } = useBackendValidation();
const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'availability'>('profile'); const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'availability'>('profile');
const [loading, setLoading] = useState(false);
const [showAvailabilityManager, setShowAvailabilityManager] = useState(false); const [showAvailabilityManager, setShowAvailabilityManager] = useState(false);
// Profile form state - updated for firstname/lastname // Profile form state
const [profileForm, setProfileForm] = useState({ const [profileForm, setProfileForm] = useState({
firstname: currentUser?.firstname || '', firstname: currentUser?.firstname || '',
lastname: currentUser?.lastname || '' lastname: currentUser?.lastname || ''
@@ -73,7 +75,7 @@ const Settings: React.FC = () => {
})); }));
}; };
// Password visibility handlers for current password // Password visibility handlers
const handleCurrentPasswordMouseDown = () => { const handleCurrentPasswordMouseDown = () => {
currentPasswordTimeoutRef.current = setTimeout(() => { currentPasswordTimeoutRef.current = setTimeout(() => {
setShowCurrentPassword(true); setShowCurrentPassword(true);
@@ -88,7 +90,6 @@ const Settings: React.FC = () => {
setShowCurrentPassword(false); setShowCurrentPassword(false);
}; };
// Password visibility handlers for new password
const handleNewPasswordMouseDown = () => { const handleNewPasswordMouseDown = () => {
newPasswordTimeoutRef.current = setTimeout(() => { newPasswordTimeoutRef.current = setTimeout(() => {
setShowNewPassword(true); setShowNewPassword(true);
@@ -103,7 +104,6 @@ const Settings: React.FC = () => {
setShowNewPassword(false); setShowNewPassword(false);
}; };
// Password visibility handlers for confirm password
const handleConfirmPasswordMouseDown = () => { const handleConfirmPasswordMouseDown = () => {
confirmPasswordTimeoutRef.current = setTimeout(() => { confirmPasswordTimeoutRef.current = setTimeout(() => {
setShowConfirmPassword(true); setShowConfirmPassword(true);
@@ -129,7 +129,6 @@ const Settings: React.FC = () => {
cleanup(); cleanup();
}; };
// Prevent context menu
const handleContextMenu = (e: React.MouseEvent) => { const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
}; };
@@ -138,40 +137,46 @@ const Settings: React.FC = () => {
e.preventDefault(); e.preventDefault();
if (!currentUser) return; if (!currentUser) return;
// Validation // BASIC FRONTEND VALIDATION: Only check required fields
if (!profileForm.firstname.trim() || !profileForm.lastname.trim()) { if (!profileForm.firstname.trim()) {
showNotification({ showNotification({
type: 'error', type: 'error',
title: 'Fehler', title: 'Fehler',
message: 'Vorname und Nachname sind erforderlich' message: 'Vorname ist erforderlich'
});
return;
}
if (!profileForm.lastname.trim()) {
showNotification({
type: 'error',
title: 'Fehler',
message: 'Nachname ist erforderlich'
}); });
return; return;
} }
try { try {
setLoading(true); // Use executeWithValidation to handle backend validation
await employeeService.updateEmployee(currentUser.id, { await executeWithValidation(async () => {
firstname: profileForm.firstname.trim(), const updatedEmployee = await employeeService.updateEmployee(currentUser.id, {
lastname: profileForm.lastname.trim() firstname: profileForm.firstname.trim(),
}); lastname: profileForm.lastname.trim()
});
// Update the auth context with new user data // Update the auth context with new user data
const updatedUser = await employeeService.getEmployee(currentUser.id); updateUser(updatedEmployee);
updateUser(updatedUser);
showNotification({ showNotification({
type: 'success', type: 'success',
title: 'Erfolg', title: 'Erfolg',
message: 'Profil erfolgreich aktualisiert' message: 'Profil erfolgreich aktualisiert'
});
}); });
} catch (error: any) { } catch (error) {
showNotification({ // Backend validation errors are already handled by executeWithValidation
type: 'error', // We only need to handle unexpected errors here
title: 'Fehler', console.error('Unexpected error:', error);
message: error.message || 'Profil konnte nicht aktualisiert werden'
});
} finally {
setLoading(false);
} }
}; };
@@ -179,12 +184,30 @@ const Settings: React.FC = () => {
e.preventDefault(); e.preventDefault();
if (!currentUser) return; if (!currentUser) return;
// Validation // BASIC FRONTEND VALIDATION: Only check minimum requirements
if (passwordForm.newPassword.length < 6) { if (!passwordForm.currentPassword) {
showNotification({ showNotification({
type: 'error', type: 'error',
title: 'Fehler', title: 'Fehler',
message: 'Das neue Passwort muss mindestens 6 Zeichen lang sein' message: 'Aktuelles Passwort ist erforderlich'
});
return;
}
if (!passwordForm.newPassword) {
showNotification({
type: 'error',
title: 'Fehler',
message: 'Neues Passwort ist erforderlich'
});
return;
}
if (passwordForm.newPassword.length < 8) {
showNotification({
type: 'error',
title: 'Fehler',
message: 'Das neue Passwort muss mindestens 8 Zeichen lang sein'
}); });
return; return;
} }
@@ -199,34 +222,30 @@ const Settings: React.FC = () => {
} }
try { try {
setLoading(true); // Use executeWithValidation to handle backend validation
await executeWithValidation(async () => {
// Use the actual password change endpoint await employeeService.changePassword(currentUser.id, {
await employeeService.changePassword(currentUser.id, { currentPassword: passwordForm.currentPassword,
currentPassword: passwordForm.currentPassword, newPassword: passwordForm.newPassword,
newPassword: passwordForm.newPassword confirmPassword: passwordForm.confirmPassword
}); });
showNotification({ showNotification({
type: 'success', type: 'success',
title: 'Erfolg', title: 'Erfolg',
message: 'Passwort erfolgreich geändert' message: 'Passwort erfolgreich geändert'
}); });
// Clear password form // Clear password form
setPasswordForm({ setPasswordForm({
currentPassword: '', currentPassword: '',
newPassword: '', newPassword: '',
confirmPassword: '' confirmPassword: ''
});
}); });
} catch (error: any) { } catch (error) {
showNotification({ // Backend validation errors are already handled by executeWithValidation
type: 'error', console.error('Unexpected error:', error);
title: 'Fehler',
message: error.message || 'Passwort konnte nicht geändert werden'
});
} finally {
setLoading(false);
} }
}; };
@@ -243,12 +262,18 @@ const Settings: React.FC = () => {
setShowAvailabilityManager(false); setShowAvailabilityManager(false);
}; };
// Clear validation errors when switching tabs
const handleTabChange = (tab: 'profile' | 'password' | 'availability') => {
clearErrors();
setActiveTab(tab);
};
if (!currentUser) { if (!currentUser) {
return <div style={{ return <div style={{
textAlign: 'center', textAlign: 'center',
padding: '3rem', padding: '3rem',
color: '#666', color: '#666',
fontSize: '1.1rem' fontSize: '1.1rem'
}}>Nicht eingeloggt</div>; }}>Nicht eingeloggt</div>;
} }
@@ -270,10 +295,10 @@ const Settings: React.FC = () => {
<h1 style={styles.title}>Einstellungen</h1> <h1 style={styles.title}>Einstellungen</h1>
<div style={styles.subtitle}>Verwalten Sie Ihre Kontoeinstellungen und Präferenzen</div> <div style={styles.subtitle}>Verwalten Sie Ihre Kontoeinstellungen und Präferenzen</div>
</div> </div>
<div style={styles.tabs}> <div style={styles.tabs}>
<button <button
onClick={() => setActiveTab('profile')} onClick={() => handleTabChange('profile')}
style={{ style={{
...styles.tab, ...styles.tab,
...(activeTab === 'profile' ? styles.tabActive : {}) ...(activeTab === 'profile' ? styles.tabActive : {})
@@ -299,9 +324,9 @@ const Settings: React.FC = () => {
<span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Persönliche Informationen</span> <span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Persönliche Informationen</span>
</div> </div>
</button> </button>
<button <button
onClick={() => setActiveTab('password')} onClick={() => handleTabChange('password')}
style={{ style={{
...styles.tab, ...styles.tab,
...(activeTab === 'password' ? styles.tabActive : {}) ...(activeTab === 'password' ? styles.tabActive : {})
@@ -327,9 +352,9 @@ const Settings: React.FC = () => {
<span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Sicherheitseinstellungen</span> <span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Sicherheitseinstellungen</span>
</div> </div>
</button> </button>
<button <button
onClick={() => setActiveTab('availability')} onClick={() => handleTabChange('availability')}
style={{ style={{
...styles.tab, ...styles.tab,
...(activeTab === 'availability' ? styles.tabActive : {}) ...(activeTab === 'availability' ? styles.tabActive : {})
@@ -369,7 +394,7 @@ const Settings: React.FC = () => {
Verwalten Sie Ihre persönlichen Informationen und Kontaktdaten Verwalten Sie Ihre persönlichen Informationen und Kontaktdaten
</p> </p>
</div> </div>
<form onSubmit={handleProfileUpdate} style={{ marginTop: '2rem' }}> <form onSubmit={handleProfileUpdate} style={{ marginTop: '2rem' }}>
<div style={styles.formGrid}> <div style={styles.formGrid}>
{/* Read-only information */} {/* Read-only information */}
@@ -480,28 +505,28 @@ const Settings: React.FC = () => {
<div style={styles.actions}> <div style={styles.actions}>
<button <button
type="submit" type="submit"
disabled={loading || !profileForm.firstname.trim() || !profileForm.lastname.trim()} disabled={isSubmitting || !profileForm.firstname.trim() || !profileForm.lastname.trim()}
style={{ style={{
...styles.button, ...styles.button,
...styles.buttonPrimary, ...styles.buttonPrimary,
...((loading || !profileForm.firstname.trim() || !profileForm.lastname.trim()) ? styles.buttonDisabled : {}) ...((isSubmitting || !profileForm.firstname.trim() || !profileForm.lastname.trim()) ? styles.buttonDisabled : {})
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (!loading && profileForm.firstname.trim() && profileForm.lastname.trim()) { if (!isSubmitting && profileForm.firstname.trim() && profileForm.lastname.trim()) {
e.currentTarget.style.background = styles.buttonPrimaryHover.background; e.currentTarget.style.background = styles.buttonPrimaryHover.background;
e.currentTarget.style.transform = styles.buttonPrimaryHover.transform; e.currentTarget.style.transform = styles.buttonPrimaryHover.transform;
e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow; e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow;
} }
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
if (!loading && profileForm.firstname.trim() && profileForm.lastname.trim()) { if (!isSubmitting && profileForm.firstname.trim() && profileForm.lastname.trim()) {
e.currentTarget.style.background = styles.buttonPrimary.background; e.currentTarget.style.background = styles.buttonPrimary.background;
e.currentTarget.style.transform = 'none'; e.currentTarget.style.transform = 'none';
e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow; e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow;
} }
}} }}
> >
{loading ? '⏳ Wird gespeichert...' : 'Profil aktualisieren'} {isSubmitting ? '⏳ Wird gespeichert...' : 'Profil aktualisieren'}
</button> </button>
</div> </div>
</form> </form>
@@ -517,7 +542,7 @@ const Settings: React.FC = () => {
Aktualisieren Sie Ihr Passwort für erhöhte Sicherheit Aktualisieren Sie Ihr Passwort für erhöhte Sicherheit
</p> </p>
</div> </div>
<form onSubmit={handlePasswordUpdate} style={{ marginTop: '2rem' }}> <form onSubmit={handlePasswordUpdate} style={{ marginTop: '2rem' }}>
<div style={styles.formGridCompact}> <div style={styles.formGridCompact}>
{/* Current Password Field */} {/* Current Password Field */}
@@ -575,9 +600,9 @@ const Settings: React.FC = () => {
value={passwordForm.newPassword} value={passwordForm.newPassword}
onChange={handlePasswordChange} onChange={handlePasswordChange}
required required
minLength={6} minLength={8}
style={styles.fieldInputWithIcon} style={styles.fieldInputWithIcon}
placeholder="Mindestens 6 Zeichen" placeholder="Mindestens 8 Zeichen"
onFocus={(e) => { onFocus={(e) => {
e.target.style.borderColor = '#1a1325'; e.target.style.borderColor = '#1a1325';
e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)'; e.target.style.boxShadow = '0 0 0 3px rgba(26, 19, 37, 0.1)';
@@ -606,7 +631,7 @@ const Settings: React.FC = () => {
</button> </button>
</div> </div>
<div style={styles.fieldHint}> <div style={styles.fieldHint}>
Das Passwort muss mindestens 6 Zeichen lang sein. Das Passwort muss mindestens 8 Zeichen lang sein.
</div> </div>
</div> </div>
@@ -657,28 +682,28 @@ const Settings: React.FC = () => {
<div style={styles.actions}> <div style={styles.actions}>
<button <button
type="submit" type="submit"
disabled={loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword} disabled={isSubmitting || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword}
style={{ style={{
...styles.button, ...styles.button,
...styles.buttonPrimary, ...styles.buttonPrimary,
...((loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) ? styles.buttonDisabled : {}) ...((isSubmitting || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) ? styles.buttonDisabled : {})
}} }}
onMouseEnter={(e) => { onMouseEnter={(e) => {
if (!loading && passwordForm.currentPassword && passwordForm.newPassword && passwordForm.confirmPassword) { if (!isSubmitting && passwordForm.currentPassword && passwordForm.newPassword && passwordForm.confirmPassword) {
e.currentTarget.style.background = styles.buttonPrimaryHover.background; e.currentTarget.style.background = styles.buttonPrimaryHover.background;
e.currentTarget.style.transform = styles.buttonPrimaryHover.transform; e.currentTarget.style.transform = styles.buttonPrimaryHover.transform;
e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow; e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow;
} }
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
if (!loading && passwordForm.currentPassword && passwordForm.newPassword && passwordForm.confirmPassword) { if (!isSubmitting && passwordForm.currentPassword && passwordForm.newPassword && passwordForm.confirmPassword) {
e.currentTarget.style.background = styles.buttonPrimary.background; e.currentTarget.style.background = styles.buttonPrimary.background;
e.currentTarget.style.transform = 'none'; e.currentTarget.style.transform = 'none';
e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow; e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow;
} }
}} }}
> >
{loading ? '⏳ Wird geändert...' : 'Passwort ändern'} {isSubmitting ? '⏳ Wird geändert...' : 'Passwort ändern'}
</button> </button>
</div> </div>
</form> </form>
@@ -694,16 +719,16 @@ const Settings: React.FC = () => {
Legen Sie Ihre persönliche Verfügbarkeit für Schichtpläne fest Legen Sie Ihre persönliche Verfügbarkeit für Schichtpläne fest
</p> </p>
</div> </div>
<div style={styles.availabilityCard}> <div style={styles.availabilityCard}>
<div style={styles.availabilityIcon}>📅</div> <div style={styles.availabilityIcon}>📅</div>
<h3 style={styles.availabilityTitle}>Verfügbarkeit verwalten</h3> <h3 style={styles.availabilityTitle}>Verfügbarkeit verwalten</h3>
<p style={styles.availabilityDescription}> <p style={styles.availabilityDescription}>
Hier können Sie Ihre persönliche Verfügbarkeit für Schichtpläne festlegen. Hier können Sie Ihre persönliche Verfügbarkeit für Schichtpläne festlegen.
Legen Sie für jeden Tag und jede Schicht fest, ob Sie bevorzugt, möglicherweise Legen Sie für jeden Tag und jede Schicht fest, ob Sie bevorzugt, möglicherweise
oder nicht verfügbar sind. oder nicht verfügbar sind.
</p> </p>
<button <button
onClick={() => setShowAvailabilityManager(true)} onClick={() => setShowAvailabilityManager(true)}
style={{ style={{

View File

@@ -1,275 +1,275 @@
// frontend/src/pages/Settings/type/SettingsType.tsx - CORRECTED // frontend/src/pages/Settings/type/SettingsType.tsx
export const styles = { export const styles = {
container: { container: {
display: 'flex', display: 'flex',
minHeight: 'calc(100vh - 120px)', minHeight: 'calc(100vh - 120px)',
background: '#FBFAF6', background: '#FBFAF6',
padding: '2rem', padding: '2rem',
maxWidth: '1200px', maxWidth: '1200px',
margin: '0 auto', margin: '0 auto',
gap: '2rem', gap: '2rem',
}, },
sidebar: { sidebar: {
width: '280px', width: '280px',
background: '#FBFAF6', background: '#FBFAF6',
borderRadius: '16px', borderRadius: '16px',
border: '1px solid rgba(255, 255, 255, 0.8)', border: '1px solid rgba(255, 255, 255, 0.8)',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.04)', boxShadow: '0 4px 20px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.04)',
padding: '1.5rem', padding: '1.5rem',
height: 'fit-content', height: 'fit-content',
position: 'sticky' as const, position: 'sticky' as const,
top: '2rem', top: '2rem',
}, },
header: { header: {
marginBottom: '2rem', marginBottom: '2rem',
paddingBottom: '1.5rem', paddingBottom: '1.5rem',
borderBottom: '1px solid rgba(26, 19, 37, 0.1)', borderBottom: '1px solid rgba(26, 19, 37, 0.1)',
}, },
title: { title: {
fontSize: '1.5rem', fontSize: '1.5rem',
fontWeight: 600, fontWeight: 600,
color: '#161718', color: '#161718',
margin: '0 0 0.5rem 0', margin: '0 0 0.5rem 0',
}, },
subtitle: { subtitle: {
fontSize: '0.95rem', fontSize: '0.95rem',
color: '#666', color: '#666',
fontWeight: 400, fontWeight: 400,
lineHeight: 1.5, lineHeight: 1.5,
}, },
tabs: { tabs: {
display: 'flex', display: 'flex',
flexDirection: 'column' as const, flexDirection: 'column' as const,
gap: '0.5rem', gap: '0.5rem',
}, },
tab: { tab: {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '1rem', gap: '1rem',
padding: '1rem 1.25rem', padding: '1rem 1.25rem',
background: 'transparent', background: 'transparent',
color: '#666', color: '#666',
border: 'none', border: 'none',
borderRadius: '8px', borderRadius: '8px',
cursor: 'pointer', cursor: 'pointer',
fontWeight: 500, fontWeight: 500,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
textAlign: 'left' as const, textAlign: 'left' as const,
width: '100%', width: '100%',
}, },
tabActive: { tabActive: {
background: '#51258f', background: '#51258f',
color: '#FBFAF6', color: '#FBFAF6',
boxShadow: '0 4px 12px rgba(26, 19, 37, 0.15)', boxShadow: '0 4px 12px rgba(26, 19, 37, 0.15)',
}, },
tabHover: { tabHover: {
background: 'rgba(81, 37, 143, 0.1)', background: 'rgba(81, 37, 143, 0.1)',
color: '#1a1325', color: '#1a1325',
transform: 'translateX(4px)', transform: 'translateX(4px)',
}, },
content: { content: {
flex: 1, flex: 1,
background: '#FBFAF6', background: '#FBFAF6',
padding: '2.5rem', padding: '2.5rem',
borderRadius: '16px', borderRadius: '16px',
border: '1px solid rgba(255, 255, 255, 0.8)', border: '1px solid rgba(255, 255, 255, 0.8)',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.04)', boxShadow: '0 4px 20px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.04)',
backdropFilter: 'blur(10px)', backdropFilter: 'blur(10px)',
minHeight: '100px', minHeight: '100px',
}, },
section: { section: {
marginBottom: '2rem', marginBottom: '2rem',
}, },
sectionTitle: { sectionTitle: {
fontSize: '1.75rem', fontSize: '1.75rem',
fontWeight: 600, fontWeight: 600,
color: '#161718', color: '#161718',
margin: '0 0 0.5rem 0', margin: '0 0 0.5rem 0',
}, },
sectionDescription: { sectionDescription: {
color: '#666', color: '#666',
fontSize: '1rem', fontSize: '1rem',
margin: 0, margin: 0,
lineHeight: 1.5, lineHeight: 1.5,
}, },
formGrid: { formGrid: {
display: 'grid', display: 'grid',
gap: '1.5rem', gap: '1.5rem',
}, },
formGridCompact: { formGridCompact: {
display: 'grid', display: 'grid',
gap: '1.5rem', gap: '1.5rem',
maxWidth: '500px', maxWidth: '500px',
}, },
infoCard: { infoCard: {
padding: '1.5rem', padding: '1.5rem',
background: 'rgba(26, 19, 37, 0.02)', background: 'rgba(26, 19, 37, 0.02)',
borderRadius: '12px', borderRadius: '12px',
border: '1px solid rgba(26, 19, 37, 0.1)', border: '1px solid rgba(26, 19, 37, 0.1)',
}, },
infoCardTitle: { infoCardTitle: {
fontSize: '1rem', fontSize: '1rem',
fontWeight: 600, fontWeight: 600,
color: '#1a1325', color: '#1a1325',
margin: '0 0 1rem 0', margin: '0 0 1rem 0',
}, },
infoGrid: { infoGrid: {
display: 'grid', display: 'grid',
gridTemplateColumns: '1fr 1fr', gridTemplateColumns: '1fr 1fr',
gap: '1rem', gap: '1rem',
}, },
field: { field: {
display: 'flex', display: 'flex',
flexDirection: 'column' as const, flexDirection: 'column' as const,
gap: '0.5rem', gap: '0.5rem',
width: '100%', width: '100%',
}, },
fieldLabel: { fieldLabel: {
fontSize: '0.9rem', fontSize: '0.9rem',
fontWeight: 600, fontWeight: 600,
color: '#161718', color: '#161718',
width: '100%', width: '100%',
}, },
fieldInputContainer: { fieldInputContainer: {
position: 'relative' as const, position: 'relative' as const,
width: '100%', width: '100%',
}, },
fieldInput: { fieldInput: {
padding: '0.875rem 1rem', padding: '0.875rem 1rem',
border: '1.5px solid #e8e8e8', border: '1.5px solid #e8e8e8',
borderRadius: '8px', borderRadius: '8px',
fontSize: '0.95rem', fontSize: '0.95rem',
background: '#FBFAF6', background: '#FBFAF6',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
color: '#161718', color: '#161718',
width: '100%', width: '100%',
boxSizing: 'border-box' as const, boxSizing: 'border-box' as const,
}, },
fieldInputWithIcon: { fieldInputWithIcon: {
padding: '0.875rem 1rem', padding: '0.875rem 1rem',
border: '1.5px solid #e8e8e8', border: '1.5px solid #e8e8e8',
borderRadius: '8px', borderRadius: '8px',
fontSize: '0.95rem', fontSize: '0.95rem',
background: '#FBFAF6', background: '#FBFAF6',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
color: '#161718', color: '#161718',
width: '100%', width: '100%',
paddingRight: '40px', paddingRight: '40px',
boxSizing: 'border-box' as const, boxSizing: 'border-box' as const,
}, },
fieldInputDisabled: { fieldInputDisabled: {
padding: '0.875rem 1rem', padding: '0.875rem 1rem',
border: '1.5px solid rgba(26, 19, 37, 0.1)', border: '1.5px solid rgba(26, 19, 37, 0.1)',
borderRadius: '8px', borderRadius: '8px',
fontSize: '0.95rem', fontSize: '0.95rem',
background: 'rgba(26, 19, 37, 0.05)', background: 'rgba(26, 19, 37, 0.05)',
color: '#666', color: '#666',
cursor: 'not-allowed', cursor: 'not-allowed',
width: '100%', width: '100%',
boxSizing: 'border-box' as const, boxSizing: 'border-box' as const,
}, },
fieldHint: { fieldHint: {
fontSize: '0.8rem', fontSize: '0.8rem',
color: '#888', color: '#888',
marginTop: '0.25rem', marginTop: '0.25rem',
width: '100%', width: '100%',
}, },
passwordToggleButton: { passwordToggleButton: {
position: 'absolute' as const, position: 'absolute' as const,
right: '10px', right: '10px',
top: '50%', top: '50%',
transform: 'translateY(-50%)', transform: 'translateY(-50%)',
background: 'none', background: 'none',
border: 'none', border: 'none',
cursor: 'pointer', cursor: 'pointer',
padding: '5px', padding: '5px',
borderRadius: '4px', borderRadius: '4px',
transition: 'background-color 0.2s', transition: 'background-color 0.2s',
userSelect: 'none' as const, userSelect: 'none' as const,
WebkitUserSelect: 'none' as const, WebkitUserSelect: 'none' as const,
touchAction: 'manipulation' as const, touchAction: 'manipulation' as const,
}, },
actions: { actions: {
display: 'flex', display: 'flex',
justifyContent: 'flex-end', justifyContent: 'flex-end',
marginTop: '2.5rem', marginTop: '2.5rem',
paddingTop: '1.5rem', paddingTop: '1.5rem',
borderTop: '1px solid rgba(26, 19, 37, 0.1)', borderTop: '1px solid rgba(26, 19, 37, 0.1)',
}, },
button: { button: {
padding: '0.875rem 2rem', padding: '0.875rem 2rem',
border: 'none', border: 'none',
borderRadius: '8px', borderRadius: '8px',
fontSize: '0.95rem', fontSize: '0.95rem',
fontWeight: 600, fontWeight: 600,
cursor: 'pointer', cursor: 'pointer',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)', transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative' as const, position: 'relative' as const,
overflow: 'hidden' as const, overflow: 'hidden' as const,
}, },
buttonPrimary: { buttonPrimary: {
background: '#1a1325', background: '#1a1325',
color: '#FBFAF6', color: '#FBFAF6',
boxShadow: '0 2px 8px rgba(26, 19, 37, 0.2)', boxShadow: '0 2px 8px rgba(26, 19, 37, 0.2)',
}, },
buttonPrimaryHover: { buttonPrimaryHover: {
background: '#24163a', background: '#24163a',
transform: 'translateY(-1px)', transform: 'translateY(-1px)',
boxShadow: '0 4px 16px rgba(26, 19, 37, 0.3)', boxShadow: '0 4px 16px rgba(26, 19, 37, 0.3)',
}, },
buttonDisabled: { buttonDisabled: {
background: '#ccc', background: '#ccc',
color: '#666', color: '#666',
cursor: 'not-allowed', cursor: 'not-allowed',
transform: 'none', transform: 'none',
boxShadow: 'none', boxShadow: 'none',
}, },
availabilityCard: { availabilityCard: {
padding: '3rem 2rem', padding: '3rem 2rem',
textAlign: 'center' as const, textAlign: 'center' as const,
background: 'rgba(26, 19, 37, 0.03)', background: 'rgba(26, 19, 37, 0.03)',
borderRadius: '16px', borderRadius: '16px',
border: '2px dashed rgba(26, 19, 37, 0.1)', border: '2px dashed rgba(26, 19, 37, 0.1)',
backdropFilter: 'blur(10px)', backdropFilter: 'blur(10px)',
}, },
availabilityIcon: { availabilityIcon: {
fontSize: '3rem', fontSize: '3rem',
marginBottom: '1.5rem', marginBottom: '1.5rem',
opacity: 0.8, opacity: 0.8,
}, },
availabilityTitle: { availabilityTitle: {
fontSize: '1.5rem', fontSize: '1.5rem',
fontWeight: 600, fontWeight: 600,
color: '#161718', color: '#161718',
margin: '0 0 1rem 0', margin: '0 0 1rem 0',
}, },
availabilityDescription: { availabilityDescription: {
color: '#666', color: '#666',
marginBottom: '2rem', marginBottom: '2rem',
lineHeight: 1.6, lineHeight: 1.6,
maxWidth: '500px', maxWidth: '500px',
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
}, },
infoHint: { infoHint: {
padding: '1.25rem', padding: '1.25rem',
background: 'rgba(26, 19, 37, 0.05)', background: 'rgba(26, 19, 37, 0.05)',
border: '1px solid rgba(26, 19, 37, 0.1)', border: '1px solid rgba(26, 19, 37, 0.1)',
borderRadius: '12px', borderRadius: '12px',
fontSize: '0.9rem', fontSize: '0.9rem',
color: '#161718', color: '#161718',
textAlign: 'left' as const, textAlign: 'left' as const,
maxWidth: '400px', maxWidth: '400px',
margin: '0 auto', margin: '0 auto',
}, },
infoList: { infoList: {
margin: '0.75rem 0 0 1rem', margin: '0.75rem 0 0 1rem',
padding: 0, padding: 0,
listStyle: 'none', listStyle: 'none',
}, },
infoListItem: { infoListItem: {
marginBottom: '0.5rem', marginBottom: '0.5rem',
position: 'relative' as const, position: 'relative' as const,
paddingLeft: '1rem', paddingLeft: '1rem',
}, },
}; };

View File

@@ -32,7 +32,7 @@ const useSetup = () => {
const steps: SetupStep[] = [ const steps: SetupStep[] = [
{ {
id: 'profile-setup', id: 'profile-setup',
title: 'Profilinformationen', title: 'Profilinformationen',
subtitle: 'Geben Sie Ihre persönlichen Daten ein' subtitle: 'Geben Sie Ihre persönlichen Daten ein'
}, },
@@ -62,8 +62,8 @@ const useSetup = () => {
}; };
const validateStep2 = (): boolean => { const validateStep2 = (): boolean => {
if (formData.password.length < 6) { if (formData.password.length < 8) {
setError('Das Passwort muss mindestens 6 Zeichen lang sein.'); setError('Das Passwort muss mindestens 8 Zeichen lang sein.');
return false; return false;
} }
if (formData.password !== formData.confirmPassword) { if (formData.password !== formData.confirmPassword) {
@@ -87,7 +87,7 @@ const useSetup = () => {
// ===== NAVIGATIONS-FUNKTIONEN ===== // ===== NAVIGATIONS-FUNKTIONEN =====
const goToNextStep = async (): Promise<void> => { const goToNextStep = async (): Promise<void> => {
setError(''); setError('');
if (!validateCurrentStep(currentStep)) { if (!validateCurrentStep(currentStep)) {
return; return;
} }
@@ -111,7 +111,7 @@ const useSetup = () => {
const handleStepChange = (stepIndex: number): void => { const handleStepChange = (stepIndex: number): void => {
setError(''); setError('');
// Nur erlauben, zu bereits validierten Schritten zu springen // Nur erlauben, zu bereits validierten Schritten zu springen
// oder zum nächsten Schritt nach dem aktuellen // oder zum nächsten Schritt nach dem aktuellen
if (stepIndex <= currentStep + 1) { if (stepIndex <= currentStep + 1) {
@@ -163,7 +163,7 @@ const useSetup = () => {
// Setup Status neu prüfen // Setup Status neu prüfen
await checkSetupStatus(); await checkSetupStatus();
} catch (err: any) { } catch (err: any) {
console.error('❌ Setup error:', err); console.error('❌ Setup error:', err);
setError(err.message || 'Ein unerwarteter Fehler ist aufgetreten'); setError(err.message || 'Ein unerwarteter Fehler ist aufgetreten');
@@ -177,7 +177,7 @@ const useSetup = () => {
if (!formData.firstname.trim() || !formData.lastname.trim()) { if (!formData.firstname.trim() || !formData.lastname.trim()) {
return 'vorname.nachname@sp.de'; return 'vorname.nachname@sp.de';
} }
const cleanFirstname = formData.firstname.toLowerCase().replace(/[^a-z0-9]/g, ''); const cleanFirstname = formData.firstname.toLowerCase().replace(/[^a-z0-9]/g, '');
const cleanLastname = formData.lastname.toLowerCase().replace(/[^a-z0-9]/g, ''); const cleanLastname = formData.lastname.toLowerCase().replace(/[^a-z0-9]/g, '');
return `${cleanFirstname}.${cleanLastname}@sp.de`; return `${cleanFirstname}.${cleanLastname}@sp.de`;
@@ -186,8 +186,8 @@ const useSetup = () => {
const isStepCompleted = (stepIndex: number): boolean => { const isStepCompleted = (stepIndex: number): boolean => {
switch (stepIndex) { switch (stepIndex) {
case 0: case 0:
return formData.password.length >= 6 && return formData.password.length >= 8 &&
formData.password === formData.confirmPassword; formData.password === formData.confirmPassword;
case 1: case 1:
return !!formData.firstname.trim() && !!formData.lastname.trim(); return !!formData.firstname.trim() && !!formData.lastname.trim();
default: default:
@@ -202,13 +202,13 @@ const useSetup = () => {
loading, loading,
error, error,
steps, steps,
// Actions // Actions
goToNextStep, goToNextStep,
goToPrevStep, goToPrevStep,
handleStepChange, handleStepChange,
handleInputChange, handleInputChange,
// Helpers // Helpers
getEmailPreview, getEmailPreview,
isStepCompleted isStepCompleted
@@ -223,16 +223,16 @@ interface StepContentProps {
currentStep: number; currentStep: number;
} }
const Step1Content: React.FC<StepContentProps> = ({ const Step1Content: React.FC<StepContentProps> = ({
formData, formData,
onInputChange, onInputChange,
getEmailPreview getEmailPreview
}) => ( }) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<div> <div>
<label style={{ <label style={{
display: 'block', display: 'block',
marginBottom: '0.5rem', marginBottom: '0.5rem',
fontWeight: '600', fontWeight: '600',
color: '#495057' color: '#495057'
}}> }}>
@@ -257,9 +257,9 @@ const Step1Content: React.FC<StepContentProps> = ({
</div> </div>
<div> <div>
<label style={{ <label style={{
display: 'block', display: 'block',
marginBottom: '0.5rem', marginBottom: '0.5rem',
fontWeight: '600', fontWeight: '600',
color: '#495057' color: '#495057'
}}> }}>
@@ -284,17 +284,17 @@ const Step1Content: React.FC<StepContentProps> = ({
</div> </div>
<div> <div>
<label style={{ <label style={{
display: 'block', display: 'block',
marginBottom: '0.5rem', marginBottom: '0.5rem',
fontWeight: '600', fontWeight: '600',
color: '#495057' color: '#495057'
}}> }}>
Automatisch generierte E-Mail Automatisch generierte E-Mail
</label> </label>
<div style={{ <div style={{
padding: '0.75rem', padding: '0.75rem',
backgroundColor: '#e9ecef', backgroundColor: '#e9ecef',
border: '1px solid #ced4da', border: '1px solid #ced4da',
borderRadius: '6px', borderRadius: '6px',
color: '#495057', color: '#495057',
@@ -303,8 +303,8 @@ const Step1Content: React.FC<StepContentProps> = ({
}}> }}>
{getEmailPreview()} {getEmailPreview()}
</div> </div>
<div style={{ <div style={{
fontSize: '0.875rem', fontSize: '0.875rem',
color: '#6c757d', color: '#6c757d',
marginTop: '0.25rem' marginTop: '0.25rem'
}}> }}>
@@ -315,15 +315,15 @@ const Step1Content: React.FC<StepContentProps> = ({
); );
const Step2Content: React.FC<StepContentProps> = ({ const Step2Content: React.FC<StepContentProps> = ({
formData, formData,
onInputChange onInputChange
}) => ( }) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<div> <div>
<label style={{ <label style={{
display: 'block', display: 'block',
marginBottom: '0.5rem', marginBottom: '0.5rem',
fontWeight: '600', fontWeight: '600',
color: '#495057' color: '#495057'
}}> }}>
@@ -342,16 +342,16 @@ const Step2Content: React.FC<StepContentProps> = ({
fontSize: '1rem', fontSize: '1rem',
transition: 'border-color 0.3s ease' transition: 'border-color 0.3s ease'
}} }}
placeholder="Mindestens 6 Zeichen" placeholder="Mindestens 8 Zeichen"
required required
autoComplete="new-password" autoComplete="new-password"
/> />
</div> </div>
<div> <div>
<label style={{ <label style={{
display: 'block', display: 'block',
marginBottom: '0.5rem', marginBottom: '0.5rem',
fontWeight: '600', fontWeight: '600',
color: '#495057' color: '#495057'
}}> }}>
@@ -378,26 +378,26 @@ const Step2Content: React.FC<StepContentProps> = ({
</div> </div>
); );
const Step3Content: React.FC<StepContentProps> = ({ const Step3Content: React.FC<StepContentProps> = ({
formData, formData,
getEmailPreview getEmailPreview
}) => ( }) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
<div style={{ <div style={{
backgroundColor: '#f8f9fa', backgroundColor: '#f8f9fa',
padding: '1.5rem', padding: '1.5rem',
borderRadius: '8px', borderRadius: '8px',
border: '1px solid #e9ecef' border: '1px solid #e9ecef'
}}> }}>
<h3 style={{ <h3 style={{
marginBottom: '1rem', marginBottom: '1rem',
color: '#2c3e50', color: '#2c3e50',
fontSize: '1.1rem', fontSize: '1.1rem',
fontWeight: '600' fontWeight: '600'
}}> }}>
Zusammenfassung Zusammenfassung
</h3> </h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#6c757d' }}>E-Mail:</span> <span style={{ color: '#6c757d' }}>E-Mail:</span>
@@ -413,15 +413,15 @@ const Step3Content: React.FC<StepContentProps> = ({
</div> </div>
</div> </div>
</div> </div>
<div style={{ <div style={{
padding: '1rem', padding: '1rem',
backgroundColor: '#e7f3ff', backgroundColor: '#e7f3ff',
borderRadius: '6px', borderRadius: '6px',
border: '1px solid #b6d7e8', border: '1px solid #b6d7e8',
color: '#2c3e50' color: '#2c3e50'
}}> }}>
<strong>💡 Wichtig:</strong> Nach dem Setup können Sie sich mit Ihrer <strong>💡 Wichtig:</strong> Nach dem Setup können Sie sich mit Ihrer
automatisch generierten E-Mail anmelden. automatisch generierten E-Mail anmelden.
</div> </div>
</div> </div>
@@ -464,7 +464,7 @@ const Setup: React.FC = () => {
const getNextButtonText = (): string => { const getNextButtonText = (): string => {
if (loading) return '⏳ Wird verarbeitet...'; if (loading) return '⏳ Wird verarbeitet...';
switch (currentStep) { switch (currentStep) {
case 0: case 0:
return 'Weiter →'; return 'Weiter →';
@@ -479,9 +479,9 @@ const Setup: React.FC = () => {
// Inline Step Indicator Komponente // Inline Step Indicator Komponente
const StepIndicator: React.FC = () => ( const StepIndicator: React.FC = () => (
<div style={{ <div style={{
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
marginBottom: '2.5rem', marginBottom: '2.5rem',
position: 'relative', position: 'relative',
@@ -497,18 +497,18 @@ const Setup: React.FC = () => {
backgroundColor: '#e9ecef', backgroundColor: '#e9ecef',
zIndex: 1 zIndex: 1
}} /> }} />
{steps.map((step, index) => { {steps.map((step, index) => {
const isCompleted = index < currentStep; const isCompleted = index < currentStep;
const isCurrent = index === currentStep; const isCurrent = index === currentStep;
const isClickable = index <= currentStep + 1; const isClickable = index <= currentStep + 1;
return ( return (
<div <div
key={step.id} key={step.id}
style={{ style={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
zIndex: 2, zIndex: 2,
position: 'relative', position: 'relative',
@@ -538,10 +538,10 @@ const Setup: React.FC = () => {
> >
{index + 1} {index + 1}
</button> </button>
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>
<div style={{ <div style={{
fontSize: '14px', fontSize: '14px',
fontWeight: isCurrent ? '600' : '400', fontWeight: isCurrent ? '600' : '400',
color: isCurrent ? '#51258f' : '#6c757d' color: isCurrent ? '#51258f' : '#6c757d'
}}> }}>
@@ -555,8 +555,8 @@ const Setup: React.FC = () => {
); );
return ( return (
<div style={{ <div style={{
minHeight: '100vh', minHeight: '100vh',
backgroundColor: '#f8f9fa', backgroundColor: '#f8f9fa',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
@@ -573,15 +573,15 @@ const Setup: React.FC = () => {
border: '1px solid #e9ecef' border: '1px solid #e9ecef'
}}> }}>
<div style={{ textAlign: 'center', marginBottom: '1rem' }}> <div style={{ textAlign: 'center', marginBottom: '1rem' }}>
<h1 style={{ <h1 style={{
fontSize: '2rem', fontSize: '2rem',
fontWeight: 'bold', fontWeight: 'bold',
marginBottom: '0.5rem', marginBottom: '0.5rem',
color: '#2c3e50' color: '#2c3e50'
}}> }}>
🚀 Erstkonfiguration 🚀 Erstkonfiguration
</h1> </h1>
<p style={{ <p style={{
color: '#6c757d', color: '#6c757d',
fontSize: '1.1rem', fontSize: '1.1rem',
marginBottom: '2rem' marginBottom: '2rem'
@@ -592,16 +592,16 @@ const Setup: React.FC = () => {
{/* Aktueller Schritt Titel und Beschreibung */} {/* Aktueller Schritt Titel und Beschreibung */}
<div style={{ textAlign: 'center', marginBottom: '1.5rem' }}> <div style={{ textAlign: 'center', marginBottom: '1.5rem' }}>
<h2 style={{ <h2 style={{
fontSize: '1.5rem', fontSize: '1.5rem',
fontWeight: 'bold', fontWeight: 'bold',
marginBottom: '0.5rem', marginBottom: '0.5rem',
color: '#2c3e50' color: '#2c3e50'
}}> }}>
{steps[currentStep].title} {steps[currentStep].title}
</h2> </h2>
{steps[currentStep].subtitle && ( {steps[currentStep].subtitle && (
<p style={{ <p style={{
color: '#6c757d', color: '#6c757d',
fontSize: '1rem' fontSize: '1rem'
}}> }}>
@@ -633,9 +633,9 @@ const Setup: React.FC = () => {
</div> </div>
{/* Navigations-Buttons */} {/* Navigations-Buttons */}
<div style={{ <div style={{
marginTop: '2rem', marginTop: '2rem',
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center' alignItems: 'center'
}}> }}>
@@ -655,7 +655,7 @@ const Setup: React.FC = () => {
> >
Zurück Zurück
</button> </button>
<button <button
onClick={goToNextStep} onClick={goToNextStep}
disabled={loading} disabled={loading}
@@ -677,10 +677,10 @@ const Setup: React.FC = () => {
{/* Zusätzliche Informationen */} {/* Zusätzliche Informationen */}
{currentStep === 2 && !loading && ( {currentStep === 2 && !loading && (
<div style={{ <div style={{
marginTop: '1.5rem', marginTop: '1.5rem',
textAlign: 'center', textAlign: 'center',
color: '#6c757d', color: '#6c757d',
fontSize: '0.9rem', fontSize: '0.9rem',
padding: '1rem', padding: '1rem',
backgroundColor: '#f8f9fa', backgroundColor: '#f8f9fa',

View File

@@ -1,5 +1,7 @@
// frontend/src/services/authService.ts // frontend/src/services/authService.ts - UPDATED
import { Employee } from '../models/Employee'; import { Employee } from '../models/Employee';
import { ErrorService } from './errorService';
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api'; const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
export interface LoginRequest { export interface LoginRequest {
@@ -23,6 +25,23 @@ export interface AuthResponse {
class AuthService { class AuthService {
private token: string | null = null; private token: string | null = null;
private async handleApiResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const validationErrors = ErrorService.extractValidationErrors(errorData);
if (validationErrors.length > 0) {
const error = new Error('Validation failed');
(error as any).validationErrors = validationErrors;
throw error;
}
throw new Error(errorData.error || errorData.message || 'Authentication failed');
}
return response.json();
}
async login(credentials: LoginRequest): Promise<AuthResponse> { async login(credentials: LoginRequest): Promise<AuthResponse> {
const response = await fetch(`${API_BASE_URL}/auth/login`, { const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST', method: 'POST',
@@ -30,12 +49,7 @@ class AuthService {
body: JSON.stringify(credentials) body: JSON.stringify(credentials)
}); });
if (!response.ok) { const data = await this.handleApiResponse<AuthResponse>(response);
const errorData = await response.json();
throw new Error(errorData.error || 'Login fehlgeschlagen');
}
const data: AuthResponse = await response.json();
this.token = data.token; this.token = data.token;
localStorage.setItem('token', data.token); localStorage.setItem('token', data.token);
localStorage.setItem('employee', JSON.stringify(data.employee)); localStorage.setItem('employee', JSON.stringify(data.employee));
@@ -49,11 +63,7 @@ class AuthService {
body: JSON.stringify(userData) body: JSON.stringify(userData)
}); });
if (!response.ok) { const data = await this.handleApiResponse<AuthResponse>(response);
const errorData = await response.json();
throw new Error(errorData.error || 'Registrierung fehlgeschlagen');
}
return this.login({ return this.login({
email: userData.email, email: userData.email,
password: userData.password password: userData.password
@@ -95,6 +105,7 @@ class AuthService {
this.token = null; this.token = null;
localStorage.removeItem('token'); localStorage.removeItem('token');
localStorage.removeItem('user'); localStorage.removeItem('user');
localStorage.removeItem('employee');
} }
getToken(): string | null { getToken(): string | null {

View File

@@ -17,40 +17,40 @@ export class EmployeeService {
if (!response.ok) { if (!response.ok) {
const errorData = await response.json().catch(() => ({})); const errorData = await response.json().catch(() => ({}));
const validationErrors = ErrorService.extractValidationErrors(errorData); const validationErrors = ErrorService.extractValidationErrors(errorData);
if (validationErrors.length > 0) { if (validationErrors.length > 0) {
const error = new Error('Validation failed'); const error = new Error('Validation failed');
(error as any).validationErrors = validationErrors; (error as any).validationErrors = validationErrors;
throw error; throw error;
} }
throw new Error(errorData.error || `HTTP error! status: ${response.status}`); throw new Error(errorData.error || errorData.message || `HTTP error! status: ${response.status}`);
} }
return response.json(); return response.json();
} }
async getEmployees(includeInactive: boolean = false): Promise<Employee[]> { async getEmployees(includeInactive: boolean = false): Promise<Employee[]> {
console.log('🔄 Fetching employees from API...'); console.log('🔄 Fetching employees from API...');
const token = localStorage.getItem('token'); const token = localStorage.getItem('token');
console.log('🔑 Token exists:', !!token); console.log('🔑 Token exists:', !!token);
const response = await fetch(`${API_BASE_URL}/employees?includeInactive=${includeInactive}`, { const response = await fetch(`${API_BASE_URL}/employees?includeInactive=${includeInactive}`, {
headers: getAuthHeaders(), headers: getAuthHeaders(),
}); });
console.log('📡 Response status:', response.status); console.log('📡 Response status:', response.status);
if (!response.ok) { if (!response.ok) {
const errorText = await response.text(); const errorText = await response.text();
console.error('❌ API Error:', errorText); console.error('❌ API Error:', errorText);
throw new Error('Failed to fetch employees'); throw new Error('Failed to fetch employees');
} }
const employees = await response.json(); const employees = await response.json();
console.log('✅ Employees received:', employees.length); console.log('✅ Employees received:', employees.length);
return employees; return employees;
} }
@@ -58,12 +58,8 @@ export class EmployeeService {
const response = await fetch(`${API_BASE_URL}/employees/${id}`, { const response = await fetch(`${API_BASE_URL}/employees/${id}`, {
headers: getAuthHeaders(), headers: getAuthHeaders(),
}); });
if (!response.ok) { return this.handleApiResponse<Employee>(response);
throw new Error('Failed to fetch employee');
}
return response.json();
} }
async createEmployee(employee: CreateEmployeeRequest): Promise<Employee> { async createEmployee(employee: CreateEmployeeRequest): Promise<Employee> {
@@ -72,7 +68,7 @@ export class EmployeeService {
headers: getAuthHeaders(), headers: getAuthHeaders(),
body: JSON.stringify(employee), body: JSON.stringify(employee),
}); });
return this.handleApiResponse<Employee>(response); return this.handleApiResponse<Employee>(response);
} }
@@ -82,7 +78,7 @@ export class EmployeeService {
headers: getAuthHeaders(), headers: getAuthHeaders(),
body: JSON.stringify(employee), body: JSON.stringify(employee),
}); });
return this.handleApiResponse<Employee>(response); return this.handleApiResponse<Employee>(response);
} }
@@ -91,7 +87,7 @@ export class EmployeeService {
method: 'DELETE', method: 'DELETE',
headers: getAuthHeaders(), headers: getAuthHeaders(),
}); });
if (!response.ok) { if (!response.ok) {
const error = await response.json(); const error = await response.json();
throw new Error(error.error || 'Failed to delete employee'); throw new Error(error.error || 'Failed to delete employee');
@@ -102,12 +98,8 @@ export class EmployeeService {
const response = await fetch(`${API_BASE_URL}/employees/${employeeId}/availabilities`, { const response = await fetch(`${API_BASE_URL}/employees/${employeeId}/availabilities`, {
headers: getAuthHeaders(), headers: getAuthHeaders(),
}); });
if (!response.ok) { return this.handleApiResponse<EmployeeAvailability[]>(response);
throw new Error('Failed to fetch availabilities');
}
return response.json();
} }
async updateAvailabilities(employeeId: string, data: { planId: string, availabilities: Omit<EmployeeAvailability, 'id' | 'employeeId'>[] }): Promise<EmployeeAvailability[]> { async updateAvailabilities(employeeId: string, data: { planId: string, availabilities: Omit<EmployeeAvailability, 'id' | 'employeeId'>[] }): Promise<EmployeeAvailability[]> {
@@ -117,26 +109,18 @@ export class EmployeeService {
headers: getAuthHeaders(), headers: getAuthHeaders(),
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
if (!response.ok) { return this.handleApiResponse<EmployeeAvailability[]>(response);
const error = await response.json();
throw new Error(error.error || 'Failed to update availabilities');
}
return response.json();
} }
async changePassword(id: string, data: { currentPassword: string, newPassword: string }): Promise<void> { async changePassword(id: string, data: { currentPassword: string, newPassword: string, confirmPassword: string }): Promise<void> {
const response = await fetch(`${API_BASE_URL}/employees/${id}/password`, { const response = await fetch(`${API_BASE_URL}/employees/${id}/password`, {
method: 'PUT', method: 'PUT',
headers: getAuthHeaders(), headers: getAuthHeaders(),
body: JSON.stringify(data), body: JSON.stringify(data),
}); });
if (!response.ok) { return this.handleApiResponse<void>(response);
const error = await response.json();
throw new Error(error.error || 'Failed to change password');
}
} }
async updateLastLogin(employeeId: string): Promise<void> { async updateLastLogin(employeeId: string): Promise<void> {