diff --git a/backend/src/controllers/employeeController.ts b/backend/src/controllers/employeeController.ts index 8c8625b..0ee75d9 100644 --- a/backend/src/controllers/employeeController.ts +++ b/backend/src/controllers/employeeController.ts @@ -13,6 +13,7 @@ export const getEmployees = async (req: AuthRequest, res: Response): Promise => { try { + console.log('🔍 Starting employee creation process with data:', { + ...req.body, + password: '***hidden***' + }); + const { email, password, name, role, phone, department } = req.body; // Validierung if (!email || !password || !name || !role) { + console.log('❌ Validation failed: Missing required fields'); res.status(400).json({ error: 'Email, password, name and role are required' }); return; } if (password.length < 6) { + console.log('❌ Validation failed: Password too short'); res.status(400).json({ error: 'Password must be at least 6 characters long' }); return; } - // Check if email already exists - const existingUser = await db.get('SELECT id FROM users WHERE email = ?', [email]); - if (existingUser) { + // First check for ANY user with this email to debug + const allUsersWithEmail = await db.all('SELECT id, email, is_active FROM users WHERE email = ?', [email]); + console.log('🔍 Found existing users with this email:', allUsersWithEmail); + + // Check if email already exists among active users only + const existingActiveUser = await db.get('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' }); return; } @@ -141,16 +156,152 @@ export const updateEmployee = async (req: AuthRequest, res: Response): Promise => { try { const { id } = req.params; + console.log('🗑️ Starting deletion process for employee ID:', id); + + // Get full employee details first + const existingEmployee = await db.get(` + 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) { + console.log('❌ Employee not found for deletion:', id); res.status(404).json({ error: 'Employee not found' }); return; } - // Soft delete - set is_active to false - await db.run('UPDATE users SET is_active = 0 WHERE id = ?', [id]); + console.log('📝 Found employee to delete:', existingEmployee); + + 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(); } catch (error) { diff --git a/backend/src/server.ts b/backend/src/server.ts index b0da954..0337450 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -239,18 +239,32 @@ app.put('/api/employees/:id', async (req: any, res: any) => { app.delete('/api/employees/:id', async (req: any, res: any) => { try { const { id } = req.params; - console.log('🗑️ Deleting employee:', id); + console.log('🗑️ Deleting employee:', id); - // Mitarbeiter finden const employeeIndex = employees.findIndex(emp => emp.id === id); if (employeeIndex === -1) { return res.status(404).json({ error: 'Employee not found' }); } - // Soft delete - employees[employeeIndex].isActive = false; + const employeeToDelete = employees[employeeIndex]; + + // 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(); } catch (error) { diff --git a/backend/src/services/databaseService.ts b/backend/src/services/databaseService.ts index 081ca8a..9c921e6 100644 --- a/backend/src/services/databaseService.ts +++ b/backend/src/services/databaseService.ts @@ -17,20 +17,59 @@ export class DatabaseService { console.error('Database connection error:', err); } else { 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() { + 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 = ` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, - email TEXT UNIQUE NOT NULL, + email TEXT NOT NULL, password TEXT NOT NULL, name TEXT 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 ( @@ -64,8 +103,8 @@ export class DatabaseService { status TEXT CHECK(status IN ('draft', 'published')) DEFAULT 'draft', created_by TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (created_by) REFERENCES users(id), - FOREIGN KEY (template_id) REFERENCES shift_templates(id) + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL, + FOREIGN KEY (template_id) REFERENCES shift_templates(id) ON DELETE SET NULL ); CREATE TABLE IF NOT EXISTS assigned_shifts ( @@ -98,6 +137,15 @@ export class DatabaseService { }); } + exec(sql: string): Promise { + return new Promise((resolve, reject) => { + this.db.exec(sql, (err) => { + if (err) reject(err); + else resolve(); + }); + }); + } + get(sql: string, params: any[] = []): Promise { return new Promise((resolve, reject) => { this.db.get(sql, params, (err, row) => { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2d1ef46..0a5b858 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,9 @@ -// frontend/src/App.tsx - KORRIGIERT +// frontend/src/App.tsx - KORRIGIERTE VERSION import React from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import { AuthProvider, useAuth } from './contexts/AuthContext'; +import { NotificationProvider } from './contexts/NotificationContext'; +import NotificationContainer from './components/Notification/NotificationContainer'; import Layout from './components/Layout/Layout'; import Login from './pages/Auth/Login'; import Dashboard from './pages/Dashboard/Dashboard'; @@ -11,15 +13,13 @@ import EmployeeManagement from './pages/Employees/EmployeeManagement'; import Settings from './pages/Settings/Settings'; 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[] }> = ({ children, roles = ['admin', 'instandhalter', 'user'] }) => { const { user, loading, hasRole } = useAuth(); - console.log('🔒 ProtectedRoute - User:', user?.email, 'Loading:', loading); - if (loading) { return (
@@ -29,12 +29,10 @@ const ProtectedRoute: React.FC<{ children: React.ReactNode; roles?: string[] }> } if (!user) { - console.log('❌ No user, redirecting to login'); return ; } if (!hasRole(roles)) { - console.log('❌ Insufficient permissions for:', user.email); return (
@@ -45,82 +43,58 @@ const ProtectedRoute: React.FC<{ children: React.ReactNode; roles?: string[] }> ); } - console.log('✅ Access granted for:', user.email); return {children}; }; 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 ( -
-
⏳ SchichtPlaner wird geladen...
-
- ); - } - return ( - - - {/* Public Route */} - } /> - - {/* Protected Routes with Layout */} - - - - } /> - - - - - } /> - - - - - } /> - - - - - } /> - - - - - } /> - - - - - } /> - - + + + + + + } /> + + + + + } /> + + + + + } /> + + + + + } /> + + + + + } /> + + + + + } /> + + + + + } /> + + + + ); } -// Hauptkomponente mit AuthProvider -function AppWrapper() { - return ( - - - - ); -} - -export default AppWrapper; \ No newline at end of file +export default App; \ No newline at end of file diff --git a/frontend/src/components/Notification/NotificationContainer.tsx b/frontend/src/components/Notification/NotificationContainer.tsx new file mode 100644 index 0000000..1316dbb --- /dev/null +++ b/frontend/src/components/Notification/NotificationContainer.tsx @@ -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 ( +
+ {notifications.map(notification => ( +
+
+ + {getIcon(notification.type)} + +
+
+ {notification.title} +
+
+ {notification.message} +
+
+
+ +
+ ))} +
+ ); +}; + +export default NotificationContainer; \ No newline at end of file diff --git a/frontend/src/contexts/NotificationContext.tsx b/frontend/src/contexts/NotificationContext.tsx new file mode 100644 index 0000000..4b7caf0 --- /dev/null +++ b/frontend/src/contexts/NotificationContext.tsx @@ -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) => void; + removeNotification: (id: string) => void; + confirmDialog: (options: ConfirmDialogOptions) => Promise; +} + +interface ConfirmDialogOptions { + title: string; + message: string; + confirmText?: string; + cancelText?: string; + type?: NotificationType; +} + +const NotificationContext = createContext(undefined); + +export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [notifications, setNotifications] = useState([]); + + const showNotification = (notification: Omit) => { + 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 => { + 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 ( + + {children} + + ); +}; + +export const useNotification = () => { + const context = useContext(NotificationContext); + if (context === undefined) { + throw new Error('useNotification must be used within a NotificationProvider'); + } + return context; +}; \ No newline at end of file diff --git a/frontend/src/pages/Employees/EmployeeManagement.tsx b/frontend/src/pages/Employees/EmployeeManagement.tsx index f8a85d5..3ee5fe8 100644 --- a/frontend/src/pages/Employees/EmployeeManagement.tsx +++ b/frontend/src/pages/Employees/EmployeeManagement.tsx @@ -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 { Employee } from '../../types/employee'; import { employeeService } from '../../services/employeeService'; @@ -6,16 +6,17 @@ import EmployeeList from './components/EmployeeList'; import EmployeeForm from './components/EmployeeForm'; import AvailabilityManager from './components/AvailabilityManager'; import { useAuth } from '../../contexts/AuthContext'; +import { useNotification } from '../../contexts/NotificationContext'; type ViewMode = 'list' | 'create' | 'edit' | 'availability'; const EmployeeManagement: React.FC = () => { const [employees, setEmployees] = useState([]); const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); const [viewMode, setViewMode] = useState('list'); const [selectedEmployee, setSelectedEmployee] = useState(null); const { hasRole } = useAuth(); + const { showNotification, confirmDialog } = useNotification(); useEffect(() => { loadEmployees(); @@ -24,14 +25,20 @@ const EmployeeManagement: React.FC = () => { const loadEmployees = async () => { try { setLoading(true); - setError(''); - console.log('🔄 Loading employees...'); + console.log('Fetching fresh employee list'); + + // Add cache-busting parameter to prevent browser caching const data = await employeeService.getEmployees(); - console.log('✅ Employees loaded:', data); + console.log('Received employees:', data.length); + setEmployees(data); } catch (err: any) { - console.error('❌ Error loading employees:', err); - setError(err.message || 'Fehler beim Laden der Mitarbeiter'); + console.error('Error loading employees:', err); + showNotification({ + type: 'error', + title: 'Fehler', + message: 'Mitarbeiter konnten nicht geladen werden' + }); } finally { setLoading(false); } @@ -57,39 +64,92 @@ const EmployeeManagement: React.FC = () => { setSelectedEmployee(null); }; - // KORRIGIERT: Explizit Daten neu laden nach Create/Update const handleEmployeeCreated = () => { - console.log('🔄 Reloading employees after creation...'); - loadEmployees(); // Daten neu laden - setViewMode('list'); // Zurück zur Liste + loadEmployees(); + setViewMode('list'); + showNotification({ + type: 'success', + title: 'Erfolg', + message: 'Mitarbeiter wurde erfolgreich erstellt' + }); }; const handleEmployeeUpdated = () => { - console.log('🔄 Reloading employees after update...'); - loadEmployees(); // Daten neu laden - setViewMode('list'); // Zurück zur Liste + loadEmployees(); + setViewMode('list'); + showNotification({ + type: 'success', + title: 'Erfolg', + message: 'Mitarbeiter wurde erfolgreich aktualisiert' + }); }; - const handleDeleteEmployee = async (employeeId: string) => { - if (!window.confirm('Mitarbeiter wirklich löschen?\nDer Mitarbeiter wird deaktiviert und kann keine Schichten mehr zugewiesen bekommen.')) { - return; - } - + // Verbesserte Lösch-Funktion mit Bestätigungs-Dialog + const handleDeleteEmployee = async (employee: Employee) => { try { - await employeeService.deleteEmployee(employeeId); - await loadEmployees(); // Liste aktualisieren + // Bestätigungs-Dialog basierend auf Rolle + 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) { - 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') { return (
@@ -100,7 +160,6 @@ const EmployeeManagement: React.FC = () => { return (
- {/* Header mit Titel und Aktionen */}
{ )}
- {/* Fehleranzeige */} - {error && ( -
- Fehler: {error} - -
- )} - {/* Inhalt basierend auf View Mode */} {viewMode === 'list' && ( diff --git a/frontend/src/pages/Employees/components/EmployeeList.tsx b/frontend/src/pages/Employees/components/EmployeeList.tsx index 4867ce1..33049d0 100644 --- a/frontend/src/pages/Employees/components/EmployeeList.tsx +++ b/frontend/src/pages/Employees/components/EmployeeList.tsx @@ -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 { Employee } from '../../../types/employee'; +import { useAuth } from '../../../contexts/AuthContext'; interface EmployeeListProps { employees: Employee[]; onEdit: (employee: Employee) => void; - onDelete: (employeeId: string) => void; + onDelete: (employee: Employee) => void; // Jetzt mit Employee-Objekt onManageAvailability: (employee: Employee) => void; currentUserRole: 'admin' | 'instandhalter'; } + const EmployeeList: React.FC = ({ employees, onEdit, @@ -17,8 +19,9 @@ const EmployeeList: React.FC = ({ onManageAvailability, currentUserRole }) => { - const [filter, setFilter] = useState<'all' | 'active' | 'inactive'>('all'); + const [filter, setFilter] = useState<'all' | 'active' | 'inactive'>('active'); const [searchTerm, setSearchTerm] = useState(''); + const { user: currentUser } = useAuth(); const filteredEmployees = employees.filter(employee => { // Status-Filter @@ -54,6 +57,33 @@ const EmployeeList: React.FC = ({ : { 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) { return (
= ({
- {/* Mitarbeiter Tabelle */} + {/* Mitarbeiter Tabelle - SYMMETRISCH KORRIGIERT */}
= ({ overflow: 'hidden', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }}> + {/* Tabellen-Header - SYMMETRISCH */}
Name & E-Mail
Abteilung
-
Rolle
-
Status
-
Letzter Login
-
Aktionen
+
Rolle
+
Status
+
Letzter Login
+
Aktionen
{filteredEmployees.map(employee => { - const status = getStatusBadge(employee.isActive); + const status = getStatusBadge(employee.isActive ?? true); + const canEdit = canEditEmployee(employee); + const canDelete = canDeleteEmployee(employee); return (
= ({
{employee.name} + {employee.id === currentUser?.id && ( + + (Sie) + + )}
{employee.email} @@ -174,81 +218,99 @@ const EmployeeList: React.FC = ({
{/* Abteilung */} -
+
{employee.department || ( Nicht zugewiesen )}
- {/* Rolle */} -
+ {/* Rolle - ZENTRIERT */} +
- {employee.role} + {employee.role === 'admin' ? 'ADMIN' : + employee.role === 'instandhalter' ? 'INSTANDHALTER' : 'MITARBEITER'}
- {/* Status */} -
+ {/* Status - ZENTRIERT */} +
{status.text}
- {/* Letzter Login */} -
+ {/* Letzter Login - ZENTRIERT */} +
{employee.lastLogin ? new Date(employee.lastLogin).toLocaleDateString('de-DE') : 'Noch nie' }
- {/* Aktionen */} -
- + {/* Aktionen - ZENTRIERT und SYMMETRISCH */} +
+ {/* Verfügbarkeit Button - immer sichtbar für berechtigte */} + {(currentUserRole === 'admin' || currentUserRole === 'instandhalter') && ( + + )} - {(currentUserRole === 'admin' || employee.role !== 'admin') && ( + {/* Bearbeiten Button */} + {canEdit && ( )} - {currentUserRole === 'admin' && employee.role !== 'admin' && ( + {/* Löschen Button - NUR FÜR ADMINS */} + {canDelete && ( )} + + {/* Platzhalter für Symmetrie wenn keine Aktionen */} + {!canEdit && !canDelete && (currentUserRole !== 'admin' && currentUserRole !== 'instandhalter') && ( +
+ )}
); })}
+ + {/* NEU: Info-Box über Berechtigungen */} +
+ 💡 Informationen zu Berechtigungen: +
    +
  • Admins können alle Benutzer bearbeiten und löschen
  • +
  • Instandhalter können nur Mitarbeiter bearbeiten
  • +
  • Mindestens ein Admin muss immer im System vorhanden sein
  • +
  • Benutzer können sich nicht selbst löschen
  • +
+
); }; diff --git a/frontend/src/services/employeeService.ts b/frontend/src/services/employeeService.ts index bc1cfec..b48884b 100644 --- a/frontend/src/services/employeeService.ts +++ b/frontend/src/services/employeeService.ts @@ -7,9 +7,11 @@ const API_BASE = 'http://localhost:3002/api/employees'; export const employeeService = { // Alle Mitarbeiter abrufen async getEmployees(): Promise { - const response = await fetch(API_BASE, { + const response = await fetch(`${API_BASE}?_=${Date.now()}`, { headers: { 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache', ...authService.getAuthHeaders() } }); @@ -74,7 +76,7 @@ export const employeeService = { return response.json(); }, - // Mitarbeiter löschen (deaktivieren) + // Mitarbeiter permanent löschen async deleteEmployee(id: string): Promise { const response = await fetch(`${API_BASE}/${id}`, { method: 'DELETE', @@ -84,7 +86,8 @@ export const employeeService = { }); 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'); } },