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

@@ -33,35 +33,19 @@ export const useBackendValidation = () => {
const result = await apiCall();
return result;
} catch (error: any) {
if (error.validationErrors) {
if (error.validationErrors && Array.isArray(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
if (error.validationErrors.length > 1) {
// Wait a bit before showing additional notifications to avoid overlap
setTimeout(() => {
error.validationErrors.slice(1).forEach((validationError: ValidationError, index: number) => {
setTimeout(() => {
showNotification({
type: 'error',
title: 'Weiterer Fehler',
message: validationError.message
});
}, index * 300); // Stagger the notifications
});
}, 500);
}
}
// Show specific validation error messages from backend
error.validationErrors.forEach((validationError: ValidationError, index: number) => {
setTimeout(() => {
showNotification({
type: 'error',
title: 'Validierungsfehler',
message: `${validationError.field ? `${validationError.field}: ` : ''}${validationError.message}`
});
}, index * 500); // Stagger the notifications
});
} else {
// Show notification for other errors
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 { useAuth } from '../../contexts/AuthContext';
import { employeeService } from '../../services/employeeService';
import { useNotification } from '../../contexts/NotificationContext';
import { useBackendValidation } from '../../hooks/useBackendValidation';
import AvailabilityManager from '../Employees/components/AvailabilityManager';
import { Employee } from '../../models/Employee';
import { styles } from './type/SettingsType';
@@ -10,11 +11,12 @@ import { styles } from './type/SettingsType';
const Settings: React.FC = () => {
const { user: currentUser, updateUser } = useAuth();
const { showNotification } = useNotification();
const { executeWithValidation, clearErrors, isSubmitting } = useBackendValidation();
const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'availability'>('profile');
const [loading, setLoading] = useState(false);
const [showAvailabilityManager, setShowAvailabilityManager] = useState(false);
// Profile form state - updated for firstname/lastname
// Profile form state
const [profileForm, setProfileForm] = useState({
firstname: currentUser?.firstname || '',
lastname: currentUser?.lastname || ''
@@ -73,7 +75,7 @@ const Settings: React.FC = () => {
}));
};
// Password visibility handlers for current password
// Password visibility handlers
const handleCurrentPasswordMouseDown = () => {
currentPasswordTimeoutRef.current = setTimeout(() => {
setShowCurrentPassword(true);
@@ -88,7 +90,6 @@ const Settings: React.FC = () => {
setShowCurrentPassword(false);
};
// Password visibility handlers for new password
const handleNewPasswordMouseDown = () => {
newPasswordTimeoutRef.current = setTimeout(() => {
setShowNewPassword(true);
@@ -103,7 +104,6 @@ const Settings: React.FC = () => {
setShowNewPassword(false);
};
// Password visibility handlers for confirm password
const handleConfirmPasswordMouseDown = () => {
confirmPasswordTimeoutRef.current = setTimeout(() => {
setShowConfirmPassword(true);
@@ -129,7 +129,6 @@ const Settings: React.FC = () => {
cleanup();
};
// Prevent context menu
const handleContextMenu = (e: React.MouseEvent) => {
e.preventDefault();
};
@@ -138,40 +137,46 @@ const Settings: React.FC = () => {
e.preventDefault();
if (!currentUser) return;
// Validation
if (!profileForm.firstname.trim() || !profileForm.lastname.trim()) {
// BASIC FRONTEND VALIDATION: Only check required fields
if (!profileForm.firstname.trim()) {
showNotification({
type: 'error',
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;
}
try {
setLoading(true);
await employeeService.updateEmployee(currentUser.id, {
firstname: profileForm.firstname.trim(),
lastname: profileForm.lastname.trim()
});
// Use executeWithValidation to handle backend validation
await executeWithValidation(async () => {
const updatedEmployee = await employeeService.updateEmployee(currentUser.id, {
firstname: profileForm.firstname.trim(),
lastname: profileForm.lastname.trim()
});
// Update the auth context with new user data
const updatedUser = await employeeService.getEmployee(currentUser.id);
updateUser(updatedUser);
// Update the auth context with new user data
updateUser(updatedEmployee);
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Profil erfolgreich aktualisiert'
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Profil erfolgreich aktualisiert'
});
});
} catch (error: any) {
showNotification({
type: 'error',
title: 'Fehler',
message: error.message || 'Profil konnte nicht aktualisiert werden'
});
} finally {
setLoading(false);
} catch (error) {
// Backend validation errors are already handled by executeWithValidation
// We only need to handle unexpected errors here
console.error('Unexpected error:', error);
}
};
@@ -179,7 +184,25 @@ const Settings: React.FC = () => {
e.preventDefault();
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) {
showNotification({
type: 'error',
@@ -199,34 +222,30 @@ const Settings: React.FC = () => {
}
try {
setLoading(true);
// Use the actual password change endpoint
await employeeService.changePassword(currentUser.id, {
currentPassword: passwordForm.currentPassword,
newPassword: passwordForm.newPassword
});
// Use executeWithValidation to handle backend validation
await executeWithValidation(async () => {
await employeeService.changePassword(currentUser.id, {
currentPassword: passwordForm.currentPassword,
newPassword: passwordForm.newPassword,
confirmPassword: passwordForm.confirmPassword
});
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Passwort erfolgreich geändert'
});
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Passwort erfolgreich geändert'
});
// Clear password form
setPasswordForm({
currentPassword: '',
newPassword: '',
confirmPassword: ''
// Clear password form
setPasswordForm({
currentPassword: '',
newPassword: '',
confirmPassword: ''
});
});
} catch (error: any) {
showNotification({
type: 'error',
title: 'Fehler',
message: error.message || 'Passwort konnte nicht geändert werden'
});
} finally {
setLoading(false);
} catch (error) {
// Backend validation errors are already handled by executeWithValidation
console.error('Unexpected error:', error);
}
};
@@ -243,12 +262,18 @@ const Settings: React.FC = () => {
setShowAvailabilityManager(false);
};
// Clear validation errors when switching tabs
const handleTabChange = (tab: 'profile' | 'password' | 'availability') => {
clearErrors();
setActiveTab(tab);
};
if (!currentUser) {
return <div style={{
textAlign: 'center',
padding: '3rem',
color: '#666',
fontSize: '1.1rem'
return <div style={{
textAlign: 'center',
padding: '3rem',
color: '#666',
fontSize: '1.1rem'
}}>Nicht eingeloggt</div>;
}
@@ -270,10 +295,10 @@ const Settings: React.FC = () => {
<h1 style={styles.title}>Einstellungen</h1>
<div style={styles.subtitle}>Verwalten Sie Ihre Kontoeinstellungen und Präferenzen</div>
</div>
<div style={styles.tabs}>
<button
onClick={() => setActiveTab('profile')}
onClick={() => handleTabChange('profile')}
style={{
...styles.tab,
...(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>
</div>
</button>
<button
onClick={() => setActiveTab('password')}
onClick={() => handleTabChange('password')}
style={{
...styles.tab,
...(activeTab === 'password' ? styles.tabActive : {})
@@ -327,9 +352,9 @@ const Settings: React.FC = () => {
<span style={{ fontSize: '0.8rem', opacity: 0.7, marginTop: '2px' }}>Sicherheitseinstellungen</span>
</div>
</button>
<button
onClick={() => setActiveTab('availability')}
onClick={() => handleTabChange('availability')}
style={{
...styles.tab,
...(activeTab === 'availability' ? styles.tabActive : {})
@@ -369,7 +394,7 @@ const Settings: React.FC = () => {
Verwalten Sie Ihre persönlichen Informationen und Kontaktdaten
</p>
</div>
<form onSubmit={handleProfileUpdate} style={{ marginTop: '2rem' }}>
<div style={styles.formGrid}>
{/* Read-only information */}
@@ -480,28 +505,28 @@ const Settings: React.FC = () => {
<div style={styles.actions}>
<button
type="submit"
disabled={loading || !profileForm.firstname.trim() || !profileForm.lastname.trim()}
disabled={isSubmitting || !profileForm.firstname.trim() || !profileForm.lastname.trim()}
style={{
...styles.button,
...styles.buttonPrimary,
...((loading || !profileForm.firstname.trim() || !profileForm.lastname.trim()) ? styles.buttonDisabled : {})
...((isSubmitting || !profileForm.firstname.trim() || !profileForm.lastname.trim()) ? styles.buttonDisabled : {})
}}
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.transform = styles.buttonPrimaryHover.transform;
e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow;
}
}}
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.transform = 'none';
e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow;
}
}}
>
{loading ? '⏳ Wird gespeichert...' : 'Profil aktualisieren'}
{isSubmitting ? '⏳ Wird gespeichert...' : 'Profil aktualisieren'}
</button>
</div>
</form>
@@ -517,7 +542,7 @@ const Settings: React.FC = () => {
Aktualisieren Sie Ihr Passwort für erhöhte Sicherheit
</p>
</div>
<form onSubmit={handlePasswordUpdate} style={{ marginTop: '2rem' }}>
<div style={styles.formGridCompact}>
{/* Current Password Field */}
@@ -657,28 +682,28 @@ const Settings: React.FC = () => {
<div style={styles.actions}>
<button
type="submit"
disabled={loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword}
disabled={isSubmitting || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword}
style={{
...styles.button,
...styles.buttonPrimary,
...((loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) ? styles.buttonDisabled : {})
...((isSubmitting || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) ? styles.buttonDisabled : {})
}}
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.transform = styles.buttonPrimaryHover.transform;
e.currentTarget.style.boxShadow = styles.buttonPrimaryHover.boxShadow;
}
}}
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.transform = 'none';
e.currentTarget.style.boxShadow = styles.buttonPrimary.boxShadow;
}
}}
>
{loading ? '⏳ Wird geändert...' : 'Passwort ändern'}
{isSubmitting ? '⏳ Wird geändert...' : 'Passwort ändern'}
</button>
</div>
</form>
@@ -694,16 +719,16 @@ const Settings: React.FC = () => {
Legen Sie Ihre persönliche Verfügbarkeit für Schichtpläne fest
</p>
</div>
<div style={styles.availabilityCard}>
<div style={styles.availabilityIcon}>📅</div>
<h3 style={styles.availabilityTitle}>Verfügbarkeit verwalten</h3>
<p style={styles.availabilityDescription}>
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.
</p>
<button
onClick={() => setShowAvailabilityManager(true)}
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 = {
container: {
display: 'flex',
minHeight: 'calc(100vh - 120px)',
background: '#FBFAF6',
padding: '2rem',
maxWidth: '1200px',
margin: '0 auto',
gap: '2rem',
},
sidebar: {
width: '280px',
background: '#FBFAF6',
borderRadius: '16px',
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)',
padding: '1.5rem',
height: 'fit-content',
position: 'sticky' as const,
top: '2rem',
},
header: {
marginBottom: '2rem',
paddingBottom: '1.5rem',
borderBottom: '1px solid rgba(26, 19, 37, 0.1)',
},
title: {
fontSize: '1.5rem',
fontWeight: 600,
color: '#161718',
margin: '0 0 0.5rem 0',
},
subtitle: {
fontSize: '0.95rem',
color: '#666',
fontWeight: 400,
lineHeight: 1.5,
},
tabs: {
display: 'flex',
flexDirection: 'column' as const,
gap: '0.5rem',
},
tab: {
display: 'flex',
alignItems: 'center',
gap: '1rem',
padding: '1rem 1.25rem',
background: 'transparent',
color: '#666',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
fontWeight: 500,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
textAlign: 'left' as const,
width: '100%',
},
tabActive: {
background: '#51258f',
color: '#FBFAF6',
boxShadow: '0 4px 12px rgba(26, 19, 37, 0.15)',
},
tabHover: {
background: 'rgba(81, 37, 143, 0.1)',
color: '#1a1325',
transform: 'translateX(4px)',
},
content: {
flex: 1,
background: '#FBFAF6',
padding: '2.5rem',
borderRadius: '16px',
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)',
backdropFilter: 'blur(10px)',
minHeight: '100px',
},
section: {
marginBottom: '2rem',
},
sectionTitle: {
fontSize: '1.75rem',
fontWeight: 600,
color: '#161718',
margin: '0 0 0.5rem 0',
},
sectionDescription: {
color: '#666',
fontSize: '1rem',
margin: 0,
lineHeight: 1.5,
},
formGrid: {
display: 'grid',
gap: '1.5rem',
},
formGridCompact: {
display: 'grid',
gap: '1.5rem',
maxWidth: '500px',
},
infoCard: {
padding: '1.5rem',
background: 'rgba(26, 19, 37, 0.02)',
borderRadius: '12px',
border: '1px solid rgba(26, 19, 37, 0.1)',
},
infoCardTitle: {
fontSize: '1rem',
fontWeight: 600,
color: '#1a1325',
margin: '0 0 1rem 0',
},
infoGrid: {
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '1rem',
},
field: {
display: 'flex',
flexDirection: 'column' as const,
gap: '0.5rem',
width: '100%',
},
fieldLabel: {
fontSize: '0.9rem',
fontWeight: 600,
color: '#161718',
width: '100%',
},
fieldInputContainer: {
position: 'relative' as const,
width: '100%',
},
fieldInput: {
padding: '0.875rem 1rem',
border: '1.5px solid #e8e8e8',
borderRadius: '8px',
fontSize: '0.95rem',
background: '#FBFAF6',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
color: '#161718',
width: '100%',
boxSizing: 'border-box' as const,
},
fieldInputWithIcon: {
padding: '0.875rem 1rem',
border: '1.5px solid #e8e8e8',
borderRadius: '8px',
fontSize: '0.95rem',
background: '#FBFAF6',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
color: '#161718',
width: '100%',
paddingRight: '40px',
boxSizing: 'border-box' as const,
},
fieldInputDisabled: {
padding: '0.875rem 1rem',
border: '1.5px solid rgba(26, 19, 37, 0.1)',
borderRadius: '8px',
fontSize: '0.95rem',
background: 'rgba(26, 19, 37, 0.05)',
color: '#666',
cursor: 'not-allowed',
width: '100%',
boxSizing: 'border-box' as const,
},
fieldHint: {
fontSize: '0.8rem',
color: '#888',
marginTop: '0.25rem',
width: '100%',
},
passwordToggleButton: {
position: 'absolute' as const,
right: '10px',
top: '50%',
transform: 'translateY(-50%)',
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '5px',
borderRadius: '4px',
transition: 'background-color 0.2s',
userSelect: 'none' as const,
WebkitUserSelect: 'none' as const,
touchAction: 'manipulation' as const,
},
actions: {
display: 'flex',
justifyContent: 'flex-end',
marginTop: '2.5rem',
paddingTop: '1.5rem',
borderTop: '1px solid rgba(26, 19, 37, 0.1)',
},
button: {
padding: '0.875rem 2rem',
border: 'none',
borderRadius: '8px',
fontSize: '0.95rem',
fontWeight: 600,
cursor: 'pointer',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative' as const,
overflow: 'hidden' as const,
},
buttonPrimary: {
background: '#1a1325',
color: '#FBFAF6',
boxShadow: '0 2px 8px rgba(26, 19, 37, 0.2)',
},
buttonPrimaryHover: {
background: '#24163a',
transform: 'translateY(-1px)',
boxShadow: '0 4px 16px rgba(26, 19, 37, 0.3)',
},
buttonDisabled: {
background: '#ccc',
color: '#666',
cursor: 'not-allowed',
transform: 'none',
boxShadow: 'none',
},
availabilityCard: {
padding: '3rem 2rem',
textAlign: 'center' as const,
background: 'rgba(26, 19, 37, 0.03)',
borderRadius: '16px',
border: '2px dashed rgba(26, 19, 37, 0.1)',
backdropFilter: 'blur(10px)',
},
availabilityIcon: {
fontSize: '3rem',
marginBottom: '1.5rem',
opacity: 0.8,
},
availabilityTitle: {
fontSize: '1.5rem',
fontWeight: 600,
color: '#161718',
margin: '0 0 1rem 0',
},
availabilityDescription: {
color: '#666',
marginBottom: '2rem',
lineHeight: 1.6,
maxWidth: '500px',
marginLeft: 'auto',
marginRight: 'auto',
},
infoHint: {
padding: '1.25rem',
background: 'rgba(26, 19, 37, 0.05)',
border: '1px solid rgba(26, 19, 37, 0.1)',
borderRadius: '12px',
fontSize: '0.9rem',
color: '#161718',
textAlign: 'left' as const,
maxWidth: '400px',
margin: '0 auto',
},
infoList: {
margin: '0.75rem 0 0 1rem',
padding: 0,
listStyle: 'none',
},
infoListItem: {
marginBottom: '0.5rem',
position: 'relative' as const,
paddingLeft: '1rem',
},
};
container: {
display: 'flex',
minHeight: 'calc(100vh - 120px)',
background: '#FBFAF6',
padding: '2rem',
maxWidth: '1200px',
margin: '0 auto',
gap: '2rem',
},
sidebar: {
width: '280px',
background: '#FBFAF6',
borderRadius: '16px',
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)',
padding: '1.5rem',
height: 'fit-content',
position: 'sticky' as const,
top: '2rem',
},
header: {
marginBottom: '2rem',
paddingBottom: '1.5rem',
borderBottom: '1px solid rgba(26, 19, 37, 0.1)',
},
title: {
fontSize: '1.5rem',
fontWeight: 600,
color: '#161718',
margin: '0 0 0.5rem 0',
},
subtitle: {
fontSize: '0.95rem',
color: '#666',
fontWeight: 400,
lineHeight: 1.5,
},
tabs: {
display: 'flex',
flexDirection: 'column' as const,
gap: '0.5rem',
},
tab: {
display: 'flex',
alignItems: 'center',
gap: '1rem',
padding: '1rem 1.25rem',
background: 'transparent',
color: '#666',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
fontWeight: 500,
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
textAlign: 'left' as const,
width: '100%',
},
tabActive: {
background: '#51258f',
color: '#FBFAF6',
boxShadow: '0 4px 12px rgba(26, 19, 37, 0.15)',
},
tabHover: {
background: 'rgba(81, 37, 143, 0.1)',
color: '#1a1325',
transform: 'translateX(4px)',
},
content: {
flex: 1,
background: '#FBFAF6',
padding: '2.5rem',
borderRadius: '16px',
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)',
backdropFilter: 'blur(10px)',
minHeight: '100px',
},
section: {
marginBottom: '2rem',
},
sectionTitle: {
fontSize: '1.75rem',
fontWeight: 600,
color: '#161718',
margin: '0 0 0.5rem 0',
},
sectionDescription: {
color: '#666',
fontSize: '1rem',
margin: 0,
lineHeight: 1.5,
},
formGrid: {
display: 'grid',
gap: '1.5rem',
},
formGridCompact: {
display: 'grid',
gap: '1.5rem',
maxWidth: '500px',
},
infoCard: {
padding: '1.5rem',
background: 'rgba(26, 19, 37, 0.02)',
borderRadius: '12px',
border: '1px solid rgba(26, 19, 37, 0.1)',
},
infoCardTitle: {
fontSize: '1rem',
fontWeight: 600,
color: '#1a1325',
margin: '0 0 1rem 0',
},
infoGrid: {
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '1rem',
},
field: {
display: 'flex',
flexDirection: 'column' as const,
gap: '0.5rem',
width: '100%',
},
fieldLabel: {
fontSize: '0.9rem',
fontWeight: 600,
color: '#161718',
width: '100%',
},
fieldInputContainer: {
position: 'relative' as const,
width: '100%',
},
fieldInput: {
padding: '0.875rem 1rem',
border: '1.5px solid #e8e8e8',
borderRadius: '8px',
fontSize: '0.95rem',
background: '#FBFAF6',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
color: '#161718',
width: '100%',
boxSizing: 'border-box' as const,
},
fieldInputWithIcon: {
padding: '0.875rem 1rem',
border: '1.5px solid #e8e8e8',
borderRadius: '8px',
fontSize: '0.95rem',
background: '#FBFAF6',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
color: '#161718',
width: '100%',
paddingRight: '40px',
boxSizing: 'border-box' as const,
},
fieldInputDisabled: {
padding: '0.875rem 1rem',
border: '1.5px solid rgba(26, 19, 37, 0.1)',
borderRadius: '8px',
fontSize: '0.95rem',
background: 'rgba(26, 19, 37, 0.05)',
color: '#666',
cursor: 'not-allowed',
width: '100%',
boxSizing: 'border-box' as const,
},
fieldHint: {
fontSize: '0.8rem',
color: '#888',
marginTop: '0.25rem',
width: '100%',
},
passwordToggleButton: {
position: 'absolute' as const,
right: '10px',
top: '50%',
transform: 'translateY(-50%)',
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '5px',
borderRadius: '4px',
transition: 'background-color 0.2s',
userSelect: 'none' as const,
WebkitUserSelect: 'none' as const,
touchAction: 'manipulation' as const,
},
actions: {
display: 'flex',
justifyContent: 'flex-end',
marginTop: '2.5rem',
paddingTop: '1.5rem',
borderTop: '1px solid rgba(26, 19, 37, 0.1)',
},
button: {
padding: '0.875rem 2rem',
border: 'none',
borderRadius: '8px',
fontSize: '0.95rem',
fontWeight: 600,
cursor: 'pointer',
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
position: 'relative' as const,
overflow: 'hidden' as const,
},
buttonPrimary: {
background: '#1a1325',
color: '#FBFAF6',
boxShadow: '0 2px 8px rgba(26, 19, 37, 0.2)',
},
buttonPrimaryHover: {
background: '#24163a',
transform: 'translateY(-1px)',
boxShadow: '0 4px 16px rgba(26, 19, 37, 0.3)',
},
buttonDisabled: {
background: '#ccc',
color: '#666',
cursor: 'not-allowed',
transform: 'none',
boxShadow: 'none',
},
availabilityCard: {
padding: '3rem 2rem',
textAlign: 'center' as const,
background: 'rgba(26, 19, 37, 0.03)',
borderRadius: '16px',
border: '2px dashed rgba(26, 19, 37, 0.1)',
backdropFilter: 'blur(10px)',
},
availabilityIcon: {
fontSize: '3rem',
marginBottom: '1.5rem',
opacity: 0.8,
},
availabilityTitle: {
fontSize: '1.5rem',
fontWeight: 600,
color: '#161718',
margin: '0 0 1rem 0',
},
availabilityDescription: {
color: '#666',
marginBottom: '2rem',
lineHeight: 1.6,
maxWidth: '500px',
marginLeft: 'auto',
marginRight: 'auto',
},
infoHint: {
padding: '1.25rem',
background: 'rgba(26, 19, 37, 0.05)',
border: '1px solid rgba(26, 19, 37, 0.1)',
borderRadius: '12px',
fontSize: '0.9rem',
color: '#161718',
textAlign: 'left' as const,
maxWidth: '400px',
margin: '0 auto',
},
infoList: {
margin: '0.75rem 0 0 1rem',
padding: 0,
listStyle: 'none',
},
infoListItem: {
marginBottom: '0.5rem',
position: 'relative' as const,
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 { ErrorService } from './errorService';
const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
export interface LoginRequest {
@@ -23,6 +25,23 @@ export interface AuthResponse {
class AuthService {
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> {
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST',
@@ -30,12 +49,7 @@ class AuthService {
body: JSON.stringify(credentials)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Login fehlgeschlagen');
}
const data: AuthResponse = await response.json();
const data = await this.handleApiResponse<AuthResponse>(response);
this.token = data.token;
localStorage.setItem('token', data.token);
localStorage.setItem('employee', JSON.stringify(data.employee));
@@ -49,11 +63,7 @@ class AuthService {
body: JSON.stringify(userData)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Registrierung fehlgeschlagen');
}
const data = await this.handleApiResponse<AuthResponse>(response);
return this.login({
email: userData.email,
password: userData.password
@@ -95,6 +105,7 @@ class AuthService {
this.token = null;
localStorage.removeItem('token');
localStorage.removeItem('user');
localStorage.removeItem('employee');
}
getToken(): string | null {

View File

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