fixed user deletion

This commit is contained in:
2025-10-08 22:02:14 +02:00
parent f4aaac4679
commit bc15e644b8
9 changed files with 716 additions and 206 deletions

View File

@@ -13,6 +13,7 @@ export const getEmployees = async (req: AuthRequest, res: Response): Promise<voi
phone, department, created_at as createdAt, phone, department, created_at as createdAt,
last_login as lastLogin last_login as lastLogin
FROM users FROM users
WHERE is_active = 1
ORDER BY name ORDER BY name
`); `);
@@ -50,22 +51,36 @@ export const getEmployee = async (req: AuthRequest, res: Response): Promise<void
export const createEmployee = async (req: AuthRequest, res: Response): Promise<void> => { export const createEmployee = async (req: AuthRequest, res: Response): Promise<void> => {
try { try {
console.log('🔍 Starting employee creation process with data:', {
...req.body,
password: '***hidden***'
});
const { email, password, name, role, phone, department } = req.body; const { email, password, name, role, phone, department } = req.body;
// Validierung // Validierung
if (!email || !password || !name || !role) { if (!email || !password || !name || !role) {
console.log('❌ Validation failed: Missing required fields');
res.status(400).json({ error: 'Email, password, name and role are required' }); res.status(400).json({ error: 'Email, password, name and role are required' });
return; return;
} }
if (password.length < 6) { if (password.length < 6) {
console.log('❌ Validation failed: Password too short');
res.status(400).json({ error: 'Password must be at least 6 characters long' }); res.status(400).json({ error: 'Password must be at least 6 characters long' });
return; return;
} }
// Check if email already exists // First check for ANY user with this email to debug
const existingUser = await db.get<any>('SELECT id FROM users WHERE email = ?', [email]); const allUsersWithEmail = await db.all<any>('SELECT id, email, is_active FROM users WHERE email = ?', [email]);
if (existingUser) { console.log('🔍 Found existing users with this email:', allUsersWithEmail);
// Check if email already exists among active users only
const existingActiveUser = await db.get<any>('SELECT id, is_active FROM users WHERE email = ? AND is_active = 1', [email]);
console.log('🔍 Checking active users with this email:', existingActiveUser);
if (existingActiveUser) {
console.log('❌ Email exists for active user:', existingActiveUser);
res.status(409).json({ error: 'Email already exists' }); res.status(409).json({ error: 'Email already exists' });
return; return;
} }
@@ -141,16 +156,152 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise<v
export const deleteEmployee = async (req: AuthRequest, res: Response): Promise<void> => { export const deleteEmployee = async (req: AuthRequest, res: Response): Promise<void> => {
try { try {
const { id } = req.params; const { id } = req.params;
console.log('🗑️ Starting deletion process for employee ID:', id);
// Get full employee details first
const existingEmployee = await db.get<any>(`
SELECT id, email, name, is_active, role
FROM users
WHERE id = ?
`, [id]);
// Check if employee exists
const existingEmployee = await db.get('SELECT * FROM users WHERE id = ?', [id]);
if (!existingEmployee) { if (!existingEmployee) {
console.log('❌ Employee not found for deletion:', id);
res.status(404).json({ error: 'Employee not found' }); res.status(404).json({ error: 'Employee not found' });
return; return;
} }
// Soft delete - set is_active to false console.log('📝 Found employee to delete:', existingEmployee);
await db.run('UPDATE users SET is_active = 0 WHERE id = ?', [id]);
try {
// Start transaction
await db.run('BEGIN TRANSACTION');
// Remove all references first
const queries = [
// 1. Remove availabilities
'DELETE FROM employee_availabilities WHERE employee_id = ?',
// 2. Remove from assigned shifts
`UPDATE assigned_shifts
SET assigned_employees = json_remove(
assigned_employees,
'$[' || (
SELECT key
FROM json_each(assigned_employees)
WHERE value = ?
LIMIT 1
) || ']'
)
WHERE json_extract(assigned_employees, '$') LIKE ?`,
// 3. Nullify references
'UPDATE shift_plans SET created_by = NULL WHERE created_by = ?',
'UPDATE shift_templates SET created_by = NULL WHERE created_by = ?',
// 4. Delete the user
'DELETE FROM users WHERE id = ?'
];
// Execute all cleanup queries
for (const query of queries) {
if (query.includes('json_extract')) {
await db.run(query, [id, `%${id}%`]);
} else {
await db.run(query, [id]);
}
}
// Verify the deletion
const verifyDeletion = await db.get<{count: number}>(`
SELECT
(SELECT COUNT(*) FROM users WHERE id = ?) +
(SELECT COUNT(*) FROM employee_availabilities WHERE employee_id = ?) +
(SELECT COUNT(*) FROM assigned_shifts WHERE json_extract(assigned_employees, '$') LIKE ?) as count
`, [id, id, `%${id}%`]);
if ((verifyDeletion?.count ?? 0) > 0) {
throw new Error('Failed to remove all references to the employee');
}
// If we got here, everything worked
await db.run('COMMIT');
console.log('✅ Successfully deleted employee:', existingEmployee.email);
res.status(204).send();
} catch (error) {
console.error('Error during deletion, rolling back:', error);
await db.run('ROLLBACK');
throw error;
}
console.log('Attempting to delete employee:', { id, email: existingEmployee.email });
try {
// Start a transaction to ensure all deletes succeed or none do
await db.run('BEGIN TRANSACTION');
console.log('Starting transaction for deletion of employee:', id);
// First verify the current state
const beforeState = await db.all(`
SELECT
(SELECT COUNT(*) FROM employee_availabilities WHERE employee_id = ?) as avail_count,
(SELECT COUNT(*) FROM shift_templates WHERE created_by = ?) as template_count,
(SELECT COUNT(*) FROM shift_plans WHERE created_by = ?) as plan_count,
(SELECT COUNT(*) FROM users WHERE id = ?) as user_count
`, [id, id, id, id]);
console.log('Before deletion state:', beforeState[0]);
// Clear all references first
await db.run(`DELETE FROM employee_availabilities WHERE employee_id = ?`, [id]);
await db.run(`UPDATE shift_plans SET created_by = NULL WHERE created_by = ?`, [id]);
await db.run(`UPDATE shift_templates SET created_by = NULL WHERE created_by = ?`, [id]);
await db.run(`UPDATE assigned_shifts
SET assigned_employees = json_remove(assigned_employees, '$[' || json_each.key || ']')
FROM json_each(assigned_shifts.assigned_employees)
WHERE json_each.value = ?`, [id]);
// Now delete the user
await db.run('DELETE FROM users WHERE id = ?', [id]);
// Verify the deletion
const verifyAfterDelete = await db.all(`
SELECT
(SELECT COUNT(*) FROM users WHERE id = ?) as user_exists,
(SELECT COUNT(*) FROM users WHERE email = ?) as email_exists,
(SELECT COUNT(*) FROM users WHERE email = ? AND is_active = 1) as active_email_exists
`, [id, existingEmployee.email, existingEmployee.email]);
console.log('🔍 Verification after delete:', verifyAfterDelete[0]);
// Verify the deletion worked
const verifyDeletion = await db.all<{user_count: number, avail_count: number, shift_refs: number}>(`
SELECT
(SELECT COUNT(*) FROM users WHERE id = ?) as user_count,
(SELECT COUNT(*) FROM employee_availabilities WHERE employee_id = ?) as avail_count,
(SELECT COUNT(*) FROM assigned_shifts WHERE json_extract(assigned_employees, '$') LIKE ?) as shift_refs
`, [id, id, `%${id}%`]);
const counts = verifyDeletion[0];
if (!counts || counts.user_count > 0 || counts.avail_count > 0 || counts.shift_refs > 0) {
console.error('Deletion verification failed:', counts);
await db.run('ROLLBACK');
throw new Error('Failed to delete all user data');
}
// If we got here, the deletion was successful
await db.run('COMMIT');
console.log('✅ Deletion committed successfully');
// Final verification after commit
const finalCheck = await db.get('SELECT * FROM users WHERE email = ?', [existingEmployee.email]);
console.log('🔍 Final verification - any user with this email:', finalCheck);
} catch (err) {
console.error('Error during deletion transaction:', err);
await db.run('ROLLBACK');
throw err;
}
res.status(204).send(); res.status(204).send();
} catch (error) { } catch (error) {

View File

@@ -239,18 +239,32 @@ app.put('/api/employees/:id', async (req: any, res: any) => {
app.delete('/api/employees/:id', async (req: any, res: any) => { app.delete('/api/employees/:id', async (req: any, res: any) => {
try { try {
const { id } = req.params; const { id } = req.params;
console.log('🗑️ Deleting employee:', id); console.log('🗑️ Deleting employee:', id);
// Mitarbeiter finden
const employeeIndex = employees.findIndex(emp => emp.id === id); const employeeIndex = employees.findIndex(emp => emp.id === id);
if (employeeIndex === -1) { if (employeeIndex === -1) {
return res.status(404).json({ error: 'Employee not found' }); return res.status(404).json({ error: 'Employee not found' });
} }
// Soft delete const employeeToDelete = employees[employeeIndex];
employees[employeeIndex].isActive = false;
// Admin-Check
if (employeeToDelete.role === 'admin') {
const adminCount = employees.filter(emp =>
emp.role === 'admin' && emp.isActive
).length;
if (adminCount <= 1) {
return res.status(400).json({
error: 'Mindestens ein Administrator muss im System verbleiben'
});
}
}
// Perform hard delete
employees.splice(employeeIndex, 1);
console.log('✅ Employee permanently deleted:', employeeToDelete.name);
console.log('✅ Employee deactivated:', employees[employeeIndex].name);
res.status(204).send(); res.status(204).send();
} catch (error) { } catch (error) {

View File

@@ -17,20 +17,59 @@ export class DatabaseService {
console.error('Database connection error:', err); console.error('Database connection error:', err);
} else { } else {
console.log('Connected to SQLite database'); console.log('Connected to SQLite database');
this.initializeDatabase(); this.enableForeignKeysAndInitialize();
} }
}); });
} }
private async enableForeignKeysAndInitialize() {
try {
// First enable foreign keys
await this.run('PRAGMA foreign_keys = ON');
console.log('Foreign keys enabled');
// Then check if it's actually enabled
const pragma = await this.get('PRAGMA foreign_keys');
console.log('Foreign keys status:', pragma);
// Now initialize the database
await this.initializeDatabase();
} catch (error) {
console.error('Error in database initialization:', error);
}
}
private initializeDatabase() { private initializeDatabase() {
const dropTables = `
DROP TABLE IF EXISTS employee_availabilities;
DROP TABLE IF EXISTS assigned_shifts;
DROP TABLE IF EXISTS shift_plans;
DROP TABLE IF EXISTS template_shifts;
DROP TABLE IF EXISTS shift_templates;
DROP TABLE IF EXISTS users;
`;
this.db.exec(dropTables, (err) => {
if (err) {
console.error('Error dropping tables:', err);
return;
}
console.log('Existing tables dropped');
});
const schema = ` const schema = `
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL, email TEXT NOT NULL,
password TEXT NOT NULL, password TEXT NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
role TEXT CHECK(role IN ('admin', 'instandhalter', 'user')) NOT NULL, role TEXT CHECK(role IN ('admin', 'instandhalter', 'user')) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE,
phone TEXT,
department TEXT,
last_login DATETIME,
CONSTRAINT unique_active_email UNIQUE (email, is_active) WHERE is_active = 1
); );
CREATE TABLE IF NOT EXISTS shift_templates ( CREATE TABLE IF NOT EXISTS shift_templates (
@@ -64,8 +103,8 @@ export class DatabaseService {
status TEXT CHECK(status IN ('draft', 'published')) DEFAULT 'draft', status TEXT CHECK(status IN ('draft', 'published')) DEFAULT 'draft',
created_by TEXT NOT NULL, created_by TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (created_by) REFERENCES users(id), FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
FOREIGN KEY (template_id) REFERENCES shift_templates(id) FOREIGN KEY (template_id) REFERENCES shift_templates(id) ON DELETE SET NULL
); );
CREATE TABLE IF NOT EXISTS assigned_shifts ( CREATE TABLE IF NOT EXISTS assigned_shifts (
@@ -98,6 +137,15 @@ export class DatabaseService {
}); });
} }
exec(sql: string): Promise<void> {
return new Promise((resolve, reject) => {
this.db.exec(sql, (err) => {
if (err) reject(err);
else resolve();
});
});
}
get<T>(sql: string, params: any[] = []): Promise<T | undefined> { get<T>(sql: string, params: any[] = []): Promise<T | undefined> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.db.get(sql, params, (err, row) => { this.db.get(sql, params, (err, row) => {

View File

@@ -1,7 +1,9 @@
// frontend/src/App.tsx - KORRIGIERT // frontend/src/App.tsx - KORRIGIERTE VERSION
import React from 'react'; import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { AuthProvider, useAuth } from './contexts/AuthContext'; import { AuthProvider, useAuth } from './contexts/AuthContext';
import { NotificationProvider } from './contexts/NotificationContext';
import NotificationContainer from './components/Notification/NotificationContainer';
import Layout from './components/Layout/Layout'; import Layout from './components/Layout/Layout';
import Login from './pages/Auth/Login'; import Login from './pages/Auth/Login';
import Dashboard from './pages/Dashboard/Dashboard'; import Dashboard from './pages/Dashboard/Dashboard';
@@ -11,15 +13,13 @@ import EmployeeManagement from './pages/Employees/EmployeeManagement';
import Settings from './pages/Settings/Settings'; import Settings from './pages/Settings/Settings';
import Help from './pages/Help/Help'; import Help from './pages/Help/Help';
// Protected Route Component // Protected Route Component direkt in App.tsx
const ProtectedRoute: React.FC<{ children: React.ReactNode; roles?: string[] }> = ({ const ProtectedRoute: React.FC<{ children: React.ReactNode; roles?: string[] }> = ({
children, children,
roles = ['admin', 'instandhalter', 'user'] roles = ['admin', 'instandhalter', 'user']
}) => { }) => {
const { user, loading, hasRole } = useAuth(); const { user, loading, hasRole } = useAuth();
console.log('🔒 ProtectedRoute - User:', user?.email, 'Loading:', loading);
if (loading) { if (loading) {
return ( return (
<div style={{ textAlign: 'center', padding: '40px' }}> <div style={{ textAlign: 'center', padding: '40px' }}>
@@ -29,12 +29,10 @@ const ProtectedRoute: React.FC<{ children: React.ReactNode; roles?: string[] }>
} }
if (!user) { if (!user) {
console.log('❌ No user, redirecting to login');
return <Login />; return <Login />;
} }
if (!hasRole(roles)) { if (!hasRole(roles)) {
console.log('❌ Insufficient permissions for:', user.email);
return ( return (
<Layout> <Layout>
<div style={{ textAlign: 'center', padding: '40px' }}> <div style={{ textAlign: 'center', padding: '40px' }}>
@@ -45,82 +43,58 @@ const ProtectedRoute: React.FC<{ children: React.ReactNode; roles?: string[] }>
); );
} }
console.log('✅ Access granted for:', user.email);
return <Layout>{children}</Layout>; return <Layout>{children}</Layout>;
}; };
function App() { function App() {
const { user, loading } = useAuth();
console.log('🏠 App Component - User:', user?.email, 'Loading:', loading);
// Während des Ladens zeigen wir einen Loading Screen
if (loading) {
return (
<div style={{
textAlign: 'center',
padding: '100px 20px',
fontSize: '18px'
}}>
<div> SchichtPlaner wird geladen...</div>
</div>
);
}
return ( return (
<Router> <NotificationProvider>
<Routes> <AuthProvider>
{/* Public Route */} <Router>
<Route path="/login" element={<Login />} /> <NotificationContainer />
<Routes>
<Route path="/login" element={<Login />} />
{/* Protected Routes with Layout */} <Route path="/" element={
<Route path="/" element={ <ProtectedRoute>
<ProtectedRoute> <Dashboard />
<Dashboard /> </ProtectedRoute>
</ProtectedRoute> } />
} />
<Route path="/shift-plans" element={ <Route path="/shift-plans" element={
<ProtectedRoute> <ProtectedRoute>
<ShiftPlanList /> <ShiftPlanList />
</ProtectedRoute> </ProtectedRoute>
} /> } />
<Route path="/shift-plans/new" element={ <Route path="/shift-plans/new" element={
<ProtectedRoute roles={['admin', 'instandhalter']}> <ProtectedRoute roles={['admin', 'instandhalter']}>
<ShiftPlanCreate /> <ShiftPlanCreate />
</ProtectedRoute> </ProtectedRoute>
} /> } />
<Route path="/employees" element={ <Route path="/employees" element={
<ProtectedRoute roles={['admin', 'instandhalter']}> <ProtectedRoute roles={['admin', 'instandhalter']}>
<EmployeeManagement /> <EmployeeManagement />
</ProtectedRoute> </ProtectedRoute>
} /> } />
<Route path="/settings" element={ <Route path="/settings" element={
<ProtectedRoute roles={['admin']}> <ProtectedRoute roles={['admin']}>
<Settings /> <Settings />
</ProtectedRoute> </ProtectedRoute>
} /> } />
<Route path="/help" element={ <Route path="/help" element={
<ProtectedRoute> <ProtectedRoute>
<Help /> <Help />
</ProtectedRoute> </ProtectedRoute>
} /> } />
</Routes> </Routes>
</Router> </Router>
</AuthProvider>
</NotificationProvider>
); );
} }
// Hauptkomponente mit AuthProvider export default App;
function AppWrapper() {
return (
<AuthProvider>
<App />
</AuthProvider>
);
}
export default AppWrapper;

View File

@@ -0,0 +1,105 @@
// frontend/src/components/Notification/NotificationContainer.tsx
import React from 'react';
import { useNotification, Notification } from '../../contexts/NotificationContext';
const NotificationContainer: React.FC = () => {
const { notifications, removeNotification } = useNotification();
const getNotificationStyle = (type: Notification['type']) => {
const baseStyle = {
padding: '15px 20px',
marginBottom: '10px',
borderRadius: '8px',
border: '1px solid',
boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
maxWidth: '400px',
minWidth: '300px'
};
const typeStyles = {
info: {
backgroundColor: '#e8f4fd',
borderColor: '#b6d7e8',
color: '#2c3e50'
},
success: {
backgroundColor: '#d5f4e6',
borderColor: '#a3e4c1',
color: '#27ae60'
},
warning: {
backgroundColor: '#fef5e7',
borderColor: '#fadbd8',
color: '#f39c12'
},
error: {
backgroundColor: '#fadbd8',
borderColor: '#f5b7b1',
color: '#e74c3c'
}
};
return { ...baseStyle, ...typeStyles[type] };
};
const getIcon = (type: Notification['type']) => {
switch (type) {
case 'info': return '💡';
case 'success': return '✅';
case 'warning': return '⚠️';
case 'error': return '❌';
default: return '💡';
}
};
if (notifications.length === 0) return null;
return (
<div style={{
position: 'fixed',
top: '20px',
right: '20px',
zIndex: 1000
}}>
{notifications.map(notification => (
<div
key={notification.id}
style={getNotificationStyle(notification.type)}
>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '10px', flex: 1 }}>
<span style={{ fontSize: '18px' }}>
{getIcon(notification.type)}
</span>
<div>
<div style={{ fontWeight: 'bold', marginBottom: '5px' }}>
{notification.title}
</div>
<div style={{ fontSize: '14px' }}>
{notification.message}
</div>
</div>
</div>
<button
onClick={() => removeNotification(notification.id)}
style={{
background: 'none',
border: 'none',
fontSize: '18px',
cursor: 'pointer',
padding: '0',
marginLeft: '10px',
color: 'inherit'
}}
>
×
</button>
</div>
))}
</div>
);
};
export default NotificationContainer;

View File

@@ -0,0 +1,94 @@
// frontend/src/contexts/NotificationContext.tsx
import React, { createContext, useContext, useState } from 'react';
export type NotificationType = 'info' | 'success' | 'warning' | 'error';
export interface Notification {
id: string;
type: NotificationType;
title: string;
message: string;
duration?: number;
}
interface NotificationContextType {
notifications: Notification[];
showNotification: (notification: Omit<Notification, 'id'>) => void;
removeNotification: (id: string) => void;
confirmDialog: (options: ConfirmDialogOptions) => Promise<boolean>;
}
interface ConfirmDialogOptions {
title: string;
message: string;
confirmText?: string;
cancelText?: string;
type?: NotificationType;
}
const NotificationContext = createContext<NotificationContextType | undefined>(undefined);
export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [notifications, setNotifications] = useState<Notification[]>([]);
const showNotification = (notification: Omit<Notification, 'id'>) => {
const id = Date.now().toString();
const newNotification: Notification = {
id,
duration: 5000,
...notification
};
setNotifications(prev => [...prev, newNotification]);
// Auto-remove after duration
if (newNotification.duration) {
setTimeout(() => {
removeNotification(id);
}, newNotification.duration);
}
};
const removeNotification = (id: string) => {
setNotifications(prev => prev.filter(notif => notif.id !== id));
};
const confirmDialog = (options: ConfirmDialogOptions): Promise<boolean> => {
return new Promise((resolve) => {
const {
title,
message,
confirmText = 'OK',
cancelText = 'Abbrechen',
type = 'warning'
} = options;
const userConfirmed = window.confirm(
`${title}\n\n${message}\n\n${confirmText} / ${cancelText}`
);
resolve(userConfirmed);
});
};
const value = {
notifications,
showNotification,
removeNotification,
confirmDialog
};
return (
<NotificationContext.Provider value={value}>
{children}
</NotificationContext.Provider>
);
};
export const useNotification = () => {
const context = useContext(NotificationContext);
if (context === undefined) {
throw new Error('useNotification must be used within a NotificationProvider');
}
return context;
};

View File

@@ -1,4 +1,4 @@
// frontend/src/pages/Employees/EmployeeManagement.tsx // frontend/src/pages/Employees/EmployeeManagement.tsx - MIT NOTIFICATION SYSTEM
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Employee } from '../../types/employee'; import { Employee } from '../../types/employee';
import { employeeService } from '../../services/employeeService'; import { employeeService } from '../../services/employeeService';
@@ -6,16 +6,17 @@ import EmployeeList from './components/EmployeeList';
import EmployeeForm from './components/EmployeeForm'; import EmployeeForm from './components/EmployeeForm';
import AvailabilityManager from './components/AvailabilityManager'; import AvailabilityManager from './components/AvailabilityManager';
import { useAuth } from '../../contexts/AuthContext'; import { useAuth } from '../../contexts/AuthContext';
import { useNotification } from '../../contexts/NotificationContext';
type ViewMode = 'list' | 'create' | 'edit' | 'availability'; type ViewMode = 'list' | 'create' | 'edit' | 'availability';
const EmployeeManagement: React.FC = () => { const EmployeeManagement: React.FC = () => {
const [employees, setEmployees] = useState<Employee[]>([]); const [employees, setEmployees] = useState<Employee[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [viewMode, setViewMode] = useState<ViewMode>('list'); const [viewMode, setViewMode] = useState<ViewMode>('list');
const [selectedEmployee, setSelectedEmployee] = useState<Employee | null>(null); const [selectedEmployee, setSelectedEmployee] = useState<Employee | null>(null);
const { hasRole } = useAuth(); const { hasRole } = useAuth();
const { showNotification, confirmDialog } = useNotification();
useEffect(() => { useEffect(() => {
loadEmployees(); loadEmployees();
@@ -24,14 +25,20 @@ const EmployeeManagement: React.FC = () => {
const loadEmployees = async () => { const loadEmployees = async () => {
try { try {
setLoading(true); setLoading(true);
setError(''); console.log('Fetching fresh employee list');
console.log('🔄 Loading employees...');
// Add cache-busting parameter to prevent browser caching
const data = await employeeService.getEmployees(); const data = await employeeService.getEmployees();
console.log('✅ Employees loaded:', data); console.log('Received employees:', data.length);
setEmployees(data); setEmployees(data);
} catch (err: any) { } catch (err: any) {
console.error('Error loading employees:', err); console.error('Error loading employees:', err);
setError(err.message || 'Fehler beim Laden der Mitarbeiter'); showNotification({
type: 'error',
title: 'Fehler',
message: 'Mitarbeiter konnten nicht geladen werden'
});
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -57,39 +64,92 @@ const EmployeeManagement: React.FC = () => {
setSelectedEmployee(null); setSelectedEmployee(null);
}; };
// KORRIGIERT: Explizit Daten neu laden nach Create/Update
const handleEmployeeCreated = () => { const handleEmployeeCreated = () => {
console.log('🔄 Reloading employees after creation...'); loadEmployees();
loadEmployees(); // Daten neu laden setViewMode('list');
setViewMode('list'); // Zurück zur Liste showNotification({
type: 'success',
title: 'Erfolg',
message: 'Mitarbeiter wurde erfolgreich erstellt'
});
}; };
const handleEmployeeUpdated = () => { const handleEmployeeUpdated = () => {
console.log('🔄 Reloading employees after update...'); loadEmployees();
loadEmployees(); // Daten neu laden setViewMode('list');
setViewMode('list'); // Zurück zur Liste showNotification({
type: 'success',
title: 'Erfolg',
message: 'Mitarbeiter wurde erfolgreich aktualisiert'
});
}; };
const handleDeleteEmployee = async (employeeId: string) => { // Verbesserte Lösch-Funktion mit Bestätigungs-Dialog
if (!window.confirm('Mitarbeiter wirklich löschen?\nDer Mitarbeiter wird deaktiviert und kann keine Schichten mehr zugewiesen bekommen.')) { const handleDeleteEmployee = async (employee: Employee) => {
return;
}
try { try {
await employeeService.deleteEmployee(employeeId); // Bestätigungs-Dialog basierend auf Rolle
await loadEmployees(); // Liste aktualisieren let confirmMessage = `Möchten Sie den Mitarbeiter "${employee.name}" wirklich PERMANENT LÖSCHEN?\n\nDie Daten des Mitarbeiters werden unwiderruflich gelöscht. Diese Aktion kann nicht rückgängig gemacht werden.`;
let confirmTitle = 'Mitarbeiter löschen';
if (employee.role === 'admin') {
const adminCount = employees.filter(emp =>
emp.role === 'admin' && emp.isActive
).length;
if (adminCount <= 1) {
showNotification({
type: 'error',
title: 'Aktion nicht möglich',
message: 'Mindestens ein Administrator muss im System verbleiben'
});
return;
}
confirmTitle = 'Administrator löschen';
confirmMessage = `Möchten Sie den Administrator "${employee.name}" wirklich PERMANENT LÖSCHEN?\n\nAchtung: Diese Aktion ist permanent und kann nicht rückgängig gemacht werden.`;
}
const confirmed = await confirmDialog({
title: confirmTitle,
message: confirmMessage,
confirmText: 'Permanent löschen',
cancelText: 'Abbrechen',
type: 'warning'
});
if (!confirmed) return;
console.log('Starting deletion process for employee:', employee.name);
await employeeService.deleteEmployee(employee.id);
console.log('Employee deleted, reloading list');
// Force a fresh reload of employees
const updatedEmployees = await employeeService.getEmployees();
setEmployees(updatedEmployees);
showNotification({
type: 'success',
title: 'Erfolg',
message: `Mitarbeiter "${employee.name}" wurde erfolgreich gelöscht`
});
} catch (err: any) { } catch (err: any) {
setError(err.message || 'Fehler beim Löschen des Mitarbeiters'); if (err.message.includes('Mindestens ein Administrator')) {
showNotification({
type: 'error',
title: 'Aktion nicht möglich',
message: err.message
});
} else {
showNotification({
type: 'error',
title: 'Fehler',
message: 'Mitarbeiter konnte nicht gelöscht werden'
});
}
} }
}; };
// Debug: Zeige aktuellen State
console.log('📊 Current state:', {
viewMode,
employeesCount: employees.length,
selectedEmployee: selectedEmployee?.name
});
if (loading && viewMode === 'list') { if (loading && viewMode === 'list') {
return ( return (
<div style={{ textAlign: 'center', padding: '40px' }}> <div style={{ textAlign: 'center', padding: '40px' }}>
@@ -100,7 +160,6 @@ const EmployeeManagement: React.FC = () => {
return ( return (
<div> <div>
{/* Header mit Titel und Aktionen */}
<div style={{ <div style={{
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
@@ -154,39 +213,12 @@ const EmployeeManagement: React.FC = () => {
)} )}
</div> </div>
{/* Fehleranzeige */}
{error && (
<div style={{
backgroundColor: '#fee',
border: '1px solid #f5c6cb',
color: '#721c24',
padding: '15px',
borderRadius: '6px',
marginBottom: '20px'
}}>
<strong>Fehler:</strong> {error}
<button
onClick={() => setError('')}
style={{
float: 'right',
background: 'none',
border: 'none',
color: '#721c24',
cursor: 'pointer',
fontWeight: 'bold'
}}
>
×
</button>
</div>
)}
{/* Inhalt basierend auf View Mode */} {/* Inhalt basierend auf View Mode */}
{viewMode === 'list' && ( {viewMode === 'list' && (
<EmployeeList <EmployeeList
employees={employees} employees={employees}
onEdit={handleEditEmployee} onEdit={handleEditEmployee}
onDelete={handleDeleteEmployee} onDelete={handleDeleteEmployee} // Jetzt mit Employee-Objekt
onManageAvailability={handleManageAvailability} onManageAvailability={handleManageAvailability}
currentUserRole={hasRole(['admin']) ? 'admin' : 'instandhalter'} currentUserRole={hasRole(['admin']) ? 'admin' : 'instandhalter'}
/> />

View File

@@ -1,15 +1,17 @@
// frontend/src/pages/Employees/components/EmployeeList.tsx // frontend/src/pages/Employees/components/EmployeeList.tsx - KORRIGIERT
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Employee } from '../../../types/employee'; import { Employee } from '../../../types/employee';
import { useAuth } from '../../../contexts/AuthContext';
interface EmployeeListProps { interface EmployeeListProps {
employees: Employee[]; employees: Employee[];
onEdit: (employee: Employee) => void; onEdit: (employee: Employee) => void;
onDelete: (employeeId: string) => void; onDelete: (employee: Employee) => void; // Jetzt mit Employee-Objekt
onManageAvailability: (employee: Employee) => void; onManageAvailability: (employee: Employee) => void;
currentUserRole: 'admin' | 'instandhalter'; currentUserRole: 'admin' | 'instandhalter';
} }
const EmployeeList: React.FC<EmployeeListProps> = ({ const EmployeeList: React.FC<EmployeeListProps> = ({
employees, employees,
onEdit, onEdit,
@@ -17,8 +19,9 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
onManageAvailability, onManageAvailability,
currentUserRole currentUserRole
}) => { }) => {
const [filter, setFilter] = useState<'all' | 'active' | 'inactive'>('all'); const [filter, setFilter] = useState<'all' | 'active' | 'inactive'>('active');
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const { user: currentUser } = useAuth();
const filteredEmployees = employees.filter(employee => { const filteredEmployees = employees.filter(employee => {
// Status-Filter // Status-Filter
@@ -54,6 +57,33 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
: { text: 'Inaktiv', color: '#e74c3c', bgColor: '#fadbd8' }; : { text: 'Inaktiv', color: '#e74c3c', bgColor: '#fadbd8' };
}; };
// NEU: Kann Benutzer löschen?
const canDeleteEmployee = (employee: Employee): boolean => {
// Nur Admins können löschen
if (currentUserRole !== 'admin') return false;
// Kann sich nicht selbst löschen
if (employee.id === currentUser?.id) return false;
// Admins können nur von Admins gelöscht werden
if (employee.role === 'admin' && currentUserRole !== 'admin') return false;
return true;
};
// NEU: Kann Benutzer bearbeiten?
const canEditEmployee = (employee: Employee): boolean => {
// Admins können alle bearbeiten
if (currentUserRole === 'admin') return true;
// Instandhalter können nur User und sich selbst bearbeiten
if (currentUserRole === 'instandhalter') {
return employee.role === 'user' || employee.id === currentUser?.id;
}
return false;
};
if (employees.length === 0) { if (employees.length === 0) {
return ( return (
<div style={{ <div style={{
@@ -122,7 +152,7 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
</div> </div>
</div> </div>
{/* Mitarbeiter Tabelle */} {/* Mitarbeiter Tabelle - SYMMETRISCH KORRIGIERT */}
<div style={{ <div style={{
backgroundColor: 'white', backgroundColor: 'white',
borderRadius: '8px', borderRadius: '8px',
@@ -130,33 +160,37 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
overflow: 'hidden', overflow: 'hidden',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)' boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
}}> }}>
{/* Tabellen-Header - SYMMETRISCH */}
<div style={{ <div style={{
display: 'grid', display: 'grid',
gridTemplateColumns: '2fr 1.5fr 1fr 1fr 1fr auto', gridTemplateColumns: '2fr 1.5fr 1fr 1fr 1fr 120px', // Feste Breite für Aktionen
gap: '15px', gap: '15px',
padding: '15px 20px', padding: '15px 20px',
backgroundColor: '#f8f9fa', backgroundColor: '#f8f9fa',
borderBottom: '1px solid #dee2e6', borderBottom: '1px solid #dee2e6',
fontWeight: 'bold', fontWeight: 'bold',
color: '#2c3e50' color: '#2c3e50',
alignItems: 'center'
}}> }}>
<div>Name & E-Mail</div> <div>Name & E-Mail</div>
<div>Abteilung</div> <div>Abteilung</div>
<div>Rolle</div> <div style={{ textAlign: 'center' }}>Rolle</div>
<div>Status</div> <div style={{ textAlign: 'center' }}>Status</div>
<div>Letzter Login</div> <div style={{ textAlign: 'center' }}>Letzter Login</div>
<div>Aktionen</div> <div style={{ textAlign: 'center' }}>Aktionen</div>
</div> </div>
{filteredEmployees.map(employee => { {filteredEmployees.map(employee => {
const status = getStatusBadge(employee.isActive); const status = getStatusBadge(employee.isActive ?? true);
const canEdit = canEditEmployee(employee);
const canDelete = canDeleteEmployee(employee);
return ( return (
<div <div
key={employee.id} key={employee.id}
style={{ style={{
display: 'grid', display: 'grid',
gridTemplateColumns: '2fr 1.5fr 1fr 1fr 1fr auto', gridTemplateColumns: '2fr 1.5fr 1fr 1fr 1fr 120px', // Gleiche Spalten wie Header
gap: '15px', gap: '15px',
padding: '15px 20px', padding: '15px 20px',
borderBottom: '1px solid #f0f0f0', borderBottom: '1px solid #f0f0f0',
@@ -167,6 +201,16 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
<div> <div>
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}> <div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
{employee.name} {employee.name}
{employee.id === currentUser?.id && (
<span style={{
marginLeft: '8px',
fontSize: '12px',
color: '#3498db',
fontWeight: 'normal'
}}>
(Sie)
</span>
)}
</div> </div>
<div style={{ color: '#666', fontSize: '14px' }}> <div style={{ color: '#666', fontSize: '14px' }}>
{employee.email} {employee.email}
@@ -174,81 +218,99 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
</div> </div>
{/* Abteilung */} {/* Abteilung */}
<div> <div style={{ color: '#666' }}>
{employee.department || ( {employee.department || (
<span style={{ color: '#999', fontStyle: 'italic' }}>Nicht zugewiesen</span> <span style={{ color: '#999', fontStyle: 'italic' }}>Nicht zugewiesen</span>
)} )}
</div> </div>
{/* Rolle */} {/* Rolle - ZENTRIERT */}
<div> <div style={{ textAlign: 'center' }}>
<span <span
style={{ style={{
backgroundColor: getRoleBadgeColor(employee.role), backgroundColor: getRoleBadgeColor(employee.role),
color: 'white', color: 'white',
padding: '4px 8px', padding: '6px 12px',
borderRadius: '12px', borderRadius: '15px',
fontSize: '12px', fontSize: '12px',
fontWeight: 'bold' fontWeight: 'bold',
display: 'inline-block',
minWidth: '80px'
}} }}
> >
{employee.role} {employee.role === 'admin' ? 'ADMIN' :
employee.role === 'instandhalter' ? 'INSTANDHALTER' : 'MITARBEITER'}
</span> </span>
</div> </div>
{/* Status */} {/* Status - ZENTRIERT */}
<div> <div style={{ textAlign: 'center' }}>
<span <span
style={{ style={{
backgroundColor: status.bgColor, backgroundColor: status.bgColor,
color: status.color, color: status.color,
padding: '4px 8px', padding: '6px 12px',
borderRadius: '12px', borderRadius: '15px',
fontSize: '12px', fontSize: '12px',
fontWeight: 'bold' fontWeight: 'bold',
display: 'inline-block',
minWidth: '70px'
}} }}
> >
{status.text} {status.text}
</span> </span>
</div> </div>
{/* Letzter Login */} {/* Letzter Login - ZENTRIERT */}
<div style={{ fontSize: '14px', color: '#666' }}> <div style={{ textAlign: 'center', fontSize: '14px', color: '#666' }}>
{employee.lastLogin {employee.lastLogin
? new Date(employee.lastLogin).toLocaleDateString('de-DE') ? new Date(employee.lastLogin).toLocaleDateString('de-DE')
: 'Noch nie' : 'Noch nie'
} }
</div> </div>
{/* Aktionen */} {/* Aktionen - ZENTRIERT und SYMMETRISCH */}
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}> <div style={{
<button display: 'flex',
onClick={() => onManageAvailability(employee)} gap: '8px',
style={{ justifyContent: 'center',
padding: '6px 12px', flexWrap: 'wrap'
backgroundColor: '#3498db', }}>
color: 'white', {/* Verfügbarkeit Button - immer sichtbar für berechtigte */}
border: 'none', {(currentUserRole === 'admin' || currentUserRole === 'instandhalter') && (
borderRadius: '4px', <button
cursor: 'pointer', onClick={() => onManageAvailability(employee)}
fontSize: '12px' style={{
}} padding: '6px 8px',
title="Verfügbarkeit verwalten" backgroundColor: '#3498db',
> color: 'white',
📅 border: 'none',
</button> borderRadius: '4px',
cursor: 'pointer',
fontSize: '12px',
minWidth: '32px',
height: '32px'
}}
title="Verfügbarkeit verwalten"
>
📅
</button>
)}
{(currentUserRole === 'admin' || employee.role !== 'admin') && ( {/* Bearbeiten Button */}
{canEdit && (
<button <button
onClick={() => onEdit(employee)} onClick={() => onEdit(employee)}
style={{ style={{
padding: '6px 12px', padding: '6px 8px',
backgroundColor: '#f39c12', backgroundColor: '#f39c12',
color: 'white', color: 'white',
border: 'none', border: 'none',
borderRadius: '4px', borderRadius: '4px',
cursor: 'pointer', cursor: 'pointer',
fontSize: '12px' fontSize: '12px',
minWidth: '32px',
height: '32px'
}} }}
title="Mitarbeiter bearbeiten" title="Mitarbeiter bearbeiten"
> >
@@ -256,28 +318,55 @@ const EmployeeList: React.FC<EmployeeListProps> = ({
</button> </button>
)} )}
{currentUserRole === 'admin' && employee.role !== 'admin' && ( {/* Löschen Button - NUR FÜR ADMINS */}
{canDelete && (
<button <button
onClick={() => onDelete(employee.id)} onClick={() => onDelete(employee)}
style={{ style={{
padding: '6px 12px', padding: '6px 8px',
backgroundColor: '#e74c3c', backgroundColor: '#e74c3c',
color: 'white', color: 'white',
border: 'none', border: 'none',
borderRadius: '4px', borderRadius: '4px',
cursor: 'pointer', cursor: 'pointer',
fontSize: '12px' fontSize: '12px',
minWidth: '32px',
height: '32px'
}} }}
title="Mitarbeiter löschen" title="Mitarbeiter deaktivieren"
> >
🗑 🗑
</button> </button>
)} )}
{/* Platzhalter für Symmetrie wenn keine Aktionen */}
{!canEdit && !canDelete && (currentUserRole !== 'admin' && currentUserRole !== 'instandhalter') && (
<div style={{ width: '32px', height: '32px' }}></div>
)}
</div> </div>
</div> </div>
); );
})} })}
</div> </div>
{/* NEU: Info-Box über Berechtigungen */}
<div style={{
marginTop: '20px',
padding: '15px',
backgroundColor: '#e8f4fd',
border: '1px solid #b6d7e8',
borderRadius: '6px',
fontSize: '14px',
color: '#2c3e50'
}}>
<strong>💡 Informationen zu Berechtigungen:</strong>
<ul style={{ margin: '8px 0 0 20px', padding: 0 }}>
<li><strong>Admins</strong> können alle Benutzer bearbeiten und löschen</li>
<li><strong>Instandhalter</strong> können nur Mitarbeiter bearbeiten</li>
<li>Mindestens <strong>ein Admin</strong> muss immer im System vorhanden sein</li>
<li>Benutzer können sich <strong>nicht selbst löschen</strong></li>
</ul>
</div>
</div> </div>
); );
}; };

View File

@@ -7,9 +7,11 @@ const API_BASE = 'http://localhost:3002/api/employees';
export const employeeService = { export const employeeService = {
// Alle Mitarbeiter abrufen // Alle Mitarbeiter abrufen
async getEmployees(): Promise<Employee[]> { async getEmployees(): Promise<Employee[]> {
const response = await fetch(API_BASE, { const response = await fetch(`${API_BASE}?_=${Date.now()}`, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
...authService.getAuthHeaders() ...authService.getAuthHeaders()
} }
}); });
@@ -74,7 +76,7 @@ export const employeeService = {
return response.json(); return response.json();
}, },
// Mitarbeiter löschen (deaktivieren) // Mitarbeiter permanent löschen
async deleteEmployee(id: string): Promise<void> { async deleteEmployee(id: string): Promise<void> {
const response = await fetch(`${API_BASE}/${id}`, { const response = await fetch(`${API_BASE}/${id}`, {
method: 'DELETE', method: 'DELETE',
@@ -84,7 +86,8 @@ export const employeeService = {
}); });
if (!response.ok) { if (!response.ok) {
throw new Error('Fehler beim Löschen des Mitarbeiters'); const error = await response.json().catch(() => ({ error: 'Fehler beim Löschen des Mitarbeiters' }));
throw new Error(error.error || 'Fehler beim Löschen des Mitarbeiters');
} }
}, },