admin has to confirm current password as well on self password change

This commit is contained in:
2025-10-31 12:30:54 +01:00
parent 6cc8c91317
commit b302c447f8
8 changed files with 522 additions and 518 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' });
@@ -767,7 +767,7 @@ 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 < 6) {
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

@@ -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

@@ -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

@@ -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,7 +184,25 @@ const Settings: React.FC = () => {
e.preventDefault(); e.preventDefault();
if (!currentUser) return; if (!currentUser) return;
// Validation // BASIC FRONTEND VALIDATION: Only check minimum requirements
if (!passwordForm.currentPassword) {
showNotification({
type: 'error',
title: 'Fehler',
message: 'Aktuelles Passwort ist erforderlich'
});
return;
}
if (!passwordForm.newPassword) {
showNotification({
type: 'error',
title: 'Fehler',
message: 'Neues Passwort ist erforderlich'
});
return;
}
if (passwordForm.newPassword.length < 6) { if (passwordForm.newPassword.length < 6) {
showNotification({ showNotification({
type: 'error', type: 'error',
@@ -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 */}
@@ -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

@@ -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> {