added changing password frontend backend

This commit is contained in:
2025-10-13 11:57:27 +02:00
parent 6de3216dcd
commit dec92daf7c
8 changed files with 595 additions and 25 deletions

View File

@@ -366,3 +366,35 @@ export const updateAvailabilities = async (req: AuthRequest, res: Response): Pro
res.status(500).json({ error: 'Internal server error' });
}
};
export const changePassword = async (req: AuthRequest, res: Response): Promise<void> => {
try {
const { id } = req.params;
const { currentPassword, newPassword } = req.body;
// Check if employee exists and get password
const employee = await db.get<{ password: string }>('SELECT password FROM employees WHERE id = ?', [id]);
if (!employee) {
res.status(404).json({ error: 'Employee not found' });
return;
}
// Verify current password
const isValidPassword = await bcrypt.compare(currentPassword, employee.password);
if (!isValidPassword) {
res.status(400).json({ error: 'Current password is incorrect' });
return;
}
// Hash new password
const hashedPassword = await bcrypt.hash(newPassword, 10);
// Update password
await db.run('UPDATE employees SET password = ? WHERE id = ?', [hashedPassword, id]);
res.json({ message: 'Password updated successfully' });
} catch (error) {
console.error('Error changing password:', error);
res.status(500).json({ error: 'Internal server error' });
}
};

View File

@@ -710,7 +710,7 @@ export const getTemplates = async (req: Request, res: Response): Promise<void> =
};
// Neue Funktion: Create from Template
export const createFromTemplate = async (req: Request, res: Response): Promise<void> => {
/*export const createFromTemplate = async (req: Request, res: Response): Promise<void> => {
try {
const { templatePlanId, name, startDate, endDate, description } = req.body;
const userId = (req as AuthRequest).user?.userId;
@@ -800,4 +800,4 @@ export const createFromTemplate = async (req: Request, res: Response): Promise<v
console.error('Error creating plan from template:', error);
res.status(500).json({ error: 'Internal server error' });
}
};
};*/

View File

@@ -8,7 +8,8 @@ import {
updateEmployee,
deleteEmployee,
getAvailabilities,
updateAvailabilities
updateAvailabilities,
changePassword
} from '../controllers/employeeController.js';
const router = express.Router();
@@ -22,6 +23,7 @@ router.get('/:id', requireRole(['admin', 'instandhalter']), getEmployee);
router.post('/', requireRole(['admin']), createEmployee);
router.put('/:id', requireRole(['admin']), updateEmployee);
router.delete('/:id', requireRole(['admin']), deleteEmployee);
router.put('/:id/password', requireRole(['admin']), changePassword);
// Availability Routes
router.get('/:employeeId/availabilities', requireRole(['admin', 'instandhalter']), getAvailabilities);

View File

@@ -8,7 +8,7 @@ import {
updateShiftPlan,
deleteShiftPlan,
getTemplates,
createFromTemplate,
//createFromTemplate,
createFromPreset
} from '../controllers/shiftPlanController.js';
@@ -31,7 +31,7 @@ router.get('/:id', getShiftPlan);
router.post('/', requireRole(['admin', 'instandhalter']), createShiftPlan);
// POST create new plan from template
router.post('/from-template', requireRole(['admin', 'instandhalter']), createFromTemplate);
//router.post('/from-template', requireRole(['admin', 'instandhalter']), createFromTemplate);
// POST create new plan from preset
router.post('/from-preset', requireRole(['admin', 'instandhalter']), createFromPreset);

View File

@@ -1,3 +1,4 @@
// frontend/src/contexts/AuthContext.tsx
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { Employee } from '../models/Employee';
@@ -15,6 +16,7 @@ interface AuthContextType {
refreshUser: () => void;
needsSetup: boolean;
checkSetupStatus: () => Promise<void>;
updateUser: (userData: Employee) => void; // Add this line
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
@@ -26,7 +28,7 @@ interface AuthProviderProps {
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUser] = useState<Employee | null>(null);
const [loading, setLoading] = useState(true);
const [needsSetup, setNeedsSetup] = useState<boolean | null>(null); // ← Start mit null
const [needsSetup, setNeedsSetup] = useState<boolean | null>(null);
// Token aus localStorage laden
const getStoredToken = (): string | null => {
@@ -55,7 +57,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
setNeedsSetup(data.needsSetup === true);
} catch (error) {
console.error('❌ Error checking setup status:', error);
setNeedsSetup(true); // Bei Fehler Setup annehmen
setNeedsSetup(true);
}
};
@@ -92,6 +94,12 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
}
};
// Add the updateUser function
const updateUser = (userData: Employee) => {
console.log('🔄 Updating user in auth context:', userData);
setUser(userData);
};
const login = async (credentials: LoginRequest): Promise<void> => {
try {
console.log('🔐 Attempting login for:', credentials.email);
@@ -112,7 +120,6 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const data = await response.json();
console.log('✅ Login successful, storing token');
// Token persistent speichern
setStoredToken(data.token);
setUser(data.user);
} catch (error) {
@@ -156,8 +163,9 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
hasRole,
loading,
refreshUser,
needsSetup: needsSetup === null ? true : needsSetup, // Falls null, true annehmen
needsSetup: needsSetup === null ? true : needsSetup,
checkSetupStatus,
updateUser, // Add this to the context value
};
return (

View File

@@ -1,26 +1,541 @@
// frontend/src/pages/Settings/Settings.tsx
import React from 'react';
import React, { useState, useEffect } from 'react';
import { useAuth } from '../../contexts/AuthContext';
import { employeeService } from '../../services/employeeService';
import { useNotification } from '../../contexts/NotificationContext';
import AvailabilityManager from '../Employees/components/AvailabilityManager';
import { Employee } from '../../models/Employee';
const Settings: React.FC = () => {
const { user: currentUser, updateUser } = useAuth();
const { showNotification } = useNotification();
const [activeTab, setActiveTab] = useState<'profile' | 'password' | 'availability'>('profile');
const [loading, setLoading] = useState(false);
const [showAvailabilityManager, setShowAvailabilityManager] = useState(false);
// Profile form state
const [profileForm, setProfileForm] = useState({
name: currentUser?.name || ''
});
// Password form state
const [passwordForm, setPasswordForm] = useState({
currentPassword: '',
newPassword: '',
confirmPassword: ''
});
useEffect(() => {
if (currentUser) {
setProfileForm({
name: currentUser.name
});
}
}, [currentUser]);
const handleProfileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setProfileForm(prev => ({
...prev,
[name]: value
}));
};
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setPasswordForm(prev => ({
...prev,
[name]: value
}));
};
const handleProfileUpdate = async (e: React.FormEvent) => {
e.preventDefault();
if (!currentUser) return;
try {
setLoading(true);
await employeeService.updateEmployee(currentUser.id, {
name: profileForm.name.trim()
});
// Update the auth context with new user data
const updatedUser = await employeeService.getEmployee(currentUser.id);
updateUser(updatedUser);
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);
}
};
const handlePasswordUpdate = async (e: React.FormEvent) => {
e.preventDefault();
if (!currentUser) return;
// Validation
if (passwordForm.newPassword.length < 6) {
showNotification({
type: 'error',
title: 'Fehler',
message: 'Das neue Passwort muss mindestens 6 Zeichen lang sein'
});
return;
}
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
showNotification({
type: 'error',
title: 'Fehler',
message: 'Die Passwörter stimmen nicht überein'
});
return;
}
try {
setLoading(true);
// Use the actual password change endpoint
await employeeService.changePassword(currentUser.id, {
currentPassword: passwordForm.currentPassword,
newPassword: passwordForm.newPassword
});
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Passwort erfolgreich geändert'
});
// 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);
}
};
const handleAvailabilitySave = () => {
setShowAvailabilityManager(false);
showNotification({
type: 'success',
title: 'Erfolg',
message: 'Verfügbarkeit erfolgreich gespeichert'
});
};
const handleAvailabilityCancel = () => {
setShowAvailabilityManager(false);
};
if (!currentUser) {
return <div>Nicht eingeloggt</div>;
}
if (showAvailabilityManager) {
return (
<AvailabilityManager
employee={currentUser as Employee}
onSave={handleAvailabilitySave}
onCancel={handleAvailabilityCancel}
/>
);
}
return (
<div>
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
<h1> Einstellungen</h1>
{/* Tab Navigation */}
<div style={{
padding: '40px',
textAlign: 'center',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
border: '2px dashed #dee2e6',
marginTop: '20px'
display: 'flex',
borderBottom: '1px solid #e0e0e0',
marginBottom: '30px'
}}>
<div style={{ fontSize: '48px', marginBottom: '20px' }}></div>
<h3>System Einstellungen</h3>
<p>Hier können Sie Systemweite Einstellungen vornehmen.</p>
<p style={{ fontSize: '14px', color: '#6c757d' }}>
Diese Seite wird demnächst mit Funktionen gefüllt.
</p>
<button
onClick={() => setActiveTab('profile')}
style={{
padding: '12px 24px',
backgroundColor: activeTab === 'profile' ? '#3498db' : 'transparent',
color: activeTab === 'profile' ? 'white' : '#333',
border: 'none',
borderBottom: activeTab === 'profile' ? '3px solid #3498db' : 'none',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
👤 Profil
</button>
<button
onClick={() => setActiveTab('password')}
style={{
padding: '12px 24px',
backgroundColor: activeTab === 'password' ? '#3498db' : 'transparent',
color: activeTab === 'password' ? 'white' : '#333',
border: 'none',
borderBottom: activeTab === 'password' ? '3px solid #3498db' : 'none',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
🔒 Passwort
</button>
<button
onClick={() => setActiveTab('availability')}
style={{
padding: '12px 24px',
backgroundColor: activeTab === 'availability' ? '#3498db' : 'transparent',
color: activeTab === 'availability' ? 'white' : '#333',
border: 'none',
borderBottom: activeTab === 'availability' ? '3px solid #3498db' : 'none',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
📅 Verfügbarkeit
</button>
</div>
{/* Profile Tab */}
{activeTab === 'profile' && (
<div style={{
backgroundColor: 'white',
padding: '30px',
borderRadius: '8px',
border: '1px solid #e0e0e0',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<h2 style={{ marginTop: 0, color: '#2c3e50' }}>Profilinformationen</h2>
<form onSubmit={handleProfileUpdate}>
<div style={{ display: 'grid', gap: '20px' }}>
{/* Read-only information */}
<div style={{
padding: '15px',
backgroundColor: '#f8f9fa',
borderRadius: '6px',
border: '1px solid #e9ecef'
}}>
<h4 style={{ margin: '0 0 15px 0', color: '#495057' }}>Systeminformationen</h4>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '15px' }}>
<div>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
E-Mail
</label>
<input
type="email"
value={currentUser.email}
disabled
style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#f8f9fa',
color: '#666'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
Rolle
</label>
<input
type="text"
value={currentUser.role}
disabled
style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#f8f9fa',
color: '#666'
}}
/>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '15px', marginTop: '15px' }}>
<div>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
Mitarbeiter Typ
</label>
<input
type="text"
value={currentUser.employeeType}
disabled
style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#f8f9fa',
color: '#666'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold', color: '#2c3e50' }}>
Vertragstyp
</label>
<input
type="text"
value={currentUser.contractType}
disabled
style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
backgroundColor: '#f8f9fa',
color: '#666'
}}
/>
</div>
</div>
</div>
{/* Editable name */}
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
Vollständiger Name *
</label>
<input
type="text"
name="name"
value={profileForm.name}
onChange={handleProfileChange}
required
style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="Ihr vollständiger Name"
/>
</div>
</div>
<div style={{
display: 'flex',
gap: '15px',
justifyContent: 'flex-end',
marginTop: '30px',
paddingTop: '20px',
borderTop: '1px solid #f0f0f0'
}}>
<button
type="submit"
disabled={loading || !profileForm.name.trim()}
style={{
padding: '12px 24px',
backgroundColor: loading ? '#bdc3c7' : (!profileForm.name.trim() ? '#95a5a6' : '#27ae60'),
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: (loading || !profileForm.name.trim()) ? 'not-allowed' : 'pointer',
fontWeight: 'bold'
}}
>
{loading ? '⏳ Wird gespeichert...' : 'Profil aktualisieren'}
</button>
</div>
</form>
</div>
)}
{/* Password Tab */}
{activeTab === 'password' && (
<div style={{
backgroundColor: 'white',
padding: '30px',
borderRadius: '8px',
border: '1px solid #e0e0e0',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<h2 style={{ marginTop: 0, color: '#2c3e50' }}>Passwort ändern</h2>
<form onSubmit={handlePasswordUpdate}>
<div style={{ display: 'grid', gap: '20px', maxWidth: '400px' }}>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
Aktuelles Passwort *
</label>
<input
type="password"
name="currentPassword"
value={passwordForm.currentPassword}
onChange={handlePasswordChange}
required
style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="Aktuelles Passwort"
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
Neues Passwort *
</label>
<input
type="password"
name="newPassword"
value={passwordForm.newPassword}
onChange={handlePasswordChange}
required
minLength={6}
style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="Mindestens 6 Zeichen"
/>
<div style={{ fontSize: '12px', color: '#7f8c8d', marginTop: '5px' }}>
Das Passwort muss mindestens 6 Zeichen lang sein.
</div>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold', color: '#2c3e50' }}>
Neues Passwort bestätigen *
</label>
<input
type="password"
name="confirmPassword"
value={passwordForm.confirmPassword}
onChange={handlePasswordChange}
required
style={{
width: '100%',
padding: '10px',
border: '1px solid #ddd',
borderRadius: '4px',
fontSize: '16px'
}}
placeholder="Passwort wiederholen"
/>
</div>
</div>
<div style={{
display: 'flex',
gap: '15px',
justifyContent: 'flex-end',
marginTop: '30px',
paddingTop: '20px',
borderTop: '1px solid #f0f0f0'
}}>
<button
type="submit"
disabled={loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword}
style={{
padding: '12px 24px',
backgroundColor: loading ? '#bdc3c7' : (!passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword ? '#95a5a6' : '#3498db'),
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: (loading || !passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) ? 'not-allowed' : 'pointer',
fontWeight: 'bold'
}}
>
{loading ? '⏳ Wird geändert...' : 'Passwort ändern'}
</button>
</div>
</form>
</div>
)}
{/* Availability Tab */}
{activeTab === 'availability' && (
<div style={{
backgroundColor: 'white',
padding: '30px',
borderRadius: '8px',
border: '1px solid #e0e0e0',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}>
<h2 style={{ marginTop: 0, color: '#2c3e50' }}>Meine Verfügbarkeit</h2>
<div style={{
padding: '30px',
textAlign: 'center',
backgroundColor: '#f8f9fa',
borderRadius: '8px',
border: '2px dashed #dee2e6'
}}>
<div style={{ fontSize: '48px', marginBottom: '20px' }}>📅</div>
<h3 style={{ color: '#2c3e50' }}>Verfügbarkeit verwalten</h3>
<p style={{ color: '#6c757d', marginBottom: '25px' }}>
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
oder nicht verfügbar sind.
</p>
<button
onClick={() => setShowAvailabilityManager(true)}
style={{
padding: '12px 24px',
backgroundColor: '#3498db',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontWeight: 'bold',
fontSize: '16px'
}}
>
Verfügbarkeit bearbeiten
</button>
<div style={{
marginTop: '20px',
padding: '15px',
backgroundColor: '#e8f4fd',
border: '1px solid #b6d7e8',
borderRadius: '6px',
fontSize: '14px',
color: '#2c3e50',
textAlign: 'left'
}}>
<strong>💡 Informationen:</strong>
<ul style={{ margin: '8px 0 0 20px', padding: 0 }}>
<li><strong>Bevorzugt:</strong> Sie möchten diese Schicht arbeiten</li>
<li><strong>Möglich:</strong> Sie können diese Schicht arbeiten</li>
<li><strong>Nicht möglich:</strong> Sie können diese Schicht nicht arbeiten</li>
</ul>
</div>
</div>
</div>
)}
</div>
);
};

View File

@@ -117,6 +117,19 @@ export class EmployeeService {
return response.json();
}
async changePassword(id: string, data: { currentPassword: string, newPassword: 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');
}
}
}
export const employeeService = new EmployeeService();

View File

@@ -138,14 +138,14 @@ export const shiftPlanService = {
},
// Create plan from template
createFromTemplate: async (data: CreateShiftFromTemplateRequest): Promise<ShiftPlan> => {
/*createFromTemplate: async (data: CreateShiftFromTemplateRequest): Promise<ShiftPlan> => {
const response = await fetch(`${API_BASE}/from-template`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify(data),
});
return handleResponse(response);
},
},*/
// Create new plan
createPlan: async (data: CreateShiftPlanRequest): Promise<ShiftPlan> => {