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' }); 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 // 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 { try {
const { templatePlanId, name, startDate, endDate, description } = req.body; const { templatePlanId, name, startDate, endDate, description } = req.body;
const userId = (req as AuthRequest).user?.userId; 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); console.error('Error creating plan from template:', error);
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
} }
}; };*/

View File

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

View File

@@ -8,7 +8,7 @@ import {
updateShiftPlan, updateShiftPlan,
deleteShiftPlan, deleteShiftPlan,
getTemplates, getTemplates,
createFromTemplate, //createFromTemplate,
createFromPreset createFromPreset
} from '../controllers/shiftPlanController.js'; } from '../controllers/shiftPlanController.js';
@@ -31,7 +31,7 @@ router.get('/:id', getShiftPlan);
router.post('/', requireRole(['admin', 'instandhalter']), createShiftPlan); router.post('/', requireRole(['admin', 'instandhalter']), createShiftPlan);
// POST create new plan from template // 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 // POST create new plan from preset
router.post('/from-preset', requireRole(['admin', 'instandhalter']), createFromPreset); 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 React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { Employee } from '../models/Employee'; import { Employee } from '../models/Employee';
@@ -15,6 +16,7 @@ interface AuthContextType {
refreshUser: () => void; refreshUser: () => void;
needsSetup: boolean; needsSetup: boolean;
checkSetupStatus: () => Promise<void>; checkSetupStatus: () => Promise<void>;
updateUser: (userData: Employee) => void; // Add this line
} }
const AuthContext = createContext<AuthContextType | undefined>(undefined); const AuthContext = createContext<AuthContextType | undefined>(undefined);
@@ -26,7 +28,7 @@ interface AuthProviderProps {
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => { export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [user, setUser] = useState<Employee | null>(null); const [user, setUser] = useState<Employee | null>(null);
const [loading, setLoading] = useState(true); 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 // Token aus localStorage laden
const getStoredToken = (): string | null => { const getStoredToken = (): string | null => {
@@ -55,7 +57,7 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
setNeedsSetup(data.needsSetup === true); setNeedsSetup(data.needsSetup === true);
} catch (error) { } catch (error) {
console.error('❌ Error checking setup status:', 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> => { const login = async (credentials: LoginRequest): Promise<void> => {
try { try {
console.log('🔐 Attempting login for:', credentials.email); console.log('🔐 Attempting login for:', credentials.email);
@@ -112,7 +120,6 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const data = await response.json(); const data = await response.json();
console.log('✅ Login successful, storing token'); console.log('✅ Login successful, storing token');
// Token persistent speichern
setStoredToken(data.token); setStoredToken(data.token);
setUser(data.user); setUser(data.user);
} catch (error) { } catch (error) {
@@ -156,8 +163,9 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
hasRole, hasRole,
loading, loading,
refreshUser, refreshUser,
needsSetup: needsSetup === null ? true : needsSetup, // Falls null, true annehmen needsSetup: needsSetup === null ? true : needsSetup,
checkSetupStatus, checkSetupStatus,
updateUser, // Add this to the context value
}; };
return ( return (

View File

@@ -1,27 +1,542 @@
// frontend/src/pages/Settings/Settings.tsx // 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 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 ( return (
<div> <AvailabilityManager
employee={currentUser as Employee}
onSave={handleAvailabilitySave}
onCancel={handleAvailabilityCancel}
/>
);
}
return (
<div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}>
<h1> Einstellungen</h1> <h1> Einstellungen</h1>
{/* Tab Navigation */}
<div style={{ <div style={{
padding: '40px', display: 'flex',
borderBottom: '1px solid #e0e0e0',
marginBottom: '30px'
}}>
<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', textAlign: 'center',
backgroundColor: '#f8f9fa', backgroundColor: '#f8f9fa',
borderRadius: '8px', borderRadius: '8px',
border: '2px dashed #dee2e6', border: '2px dashed #dee2e6'
marginTop: '20px'
}}> }}>
<div style={{ fontSize: '48px', marginBottom: '20px' }}></div> <div style={{ fontSize: '48px', marginBottom: '20px' }}>📅</div>
<h3>System Einstellungen</h3> <h3 style={{ color: '#2c3e50' }}>Verfügbarkeit verwalten</h3>
<p>Hier können Sie Systemweite Einstellungen vornehmen.</p> <p style={{ color: '#6c757d', marginBottom: '25px' }}>
<p style={{ fontSize: '14px', color: '#6c757d' }}> Hier können Sie Ihre persönliche Verfügbarkeit für Schichtpläne festlegen.
Diese Seite wird demnächst mit Funktionen gefüllt. Legen Sie für jeden Tag und jede Schicht fest, ob Sie bevorzugt, möglicherweise
oder nicht verfügbar sind.
</p> </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>
</div>
)}
</div>
); );
}; };

View File

@@ -117,6 +117,19 @@ export class EmployeeService {
return response.json(); 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(); export const employeeService = new EmployeeService();

View File

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