import { useState, useEffect } from 'react'; import { useHasPermission } from '../hooks/usePermissions'; import ProtectedRoute from './ProtectedRoute'; import PageHeader from './PageHeader'; const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:3001'; interface User { id: number; email: string; username: string; display_name: string | null; is_active: boolean; email_verified: boolean; created_at: string; last_login: string | null; roles: Array<{ id: number; name: string; description: string | null }>; } interface Role { id: number; name: string; description: string | null; } export default function UserManagement() { const [users, setUsers] = useState([]); const [roles, setRoles] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const [showCreateModal, setShowCreateModal] = useState(false); const [showPasswordModal, setShowPasswordModal] = useState(false); const [selectedUser, setSelectedUser] = useState(null); const [searchTerm, setSearchTerm] = useState(''); // const [expandedUser, setExpandedUser] = useState(null); // Unused for now const [actionMenuOpen, setActionMenuOpen] = useState(null); const hasManageUsers = useHasPermission('manage_users'); useEffect(() => { if (hasManageUsers) { fetchUsers(); fetchRoles(); } }, [hasManageUsers]); // Close action menu when clicking outside useEffect(() => { const handleClickOutside = () => { setActionMenuOpen(null); }; if (actionMenuOpen !== null) { document.addEventListener('click', handleClickOutside); return () => document.removeEventListener('click', handleClickOutside); } }, [actionMenuOpen]); const fetchUsers = async () => { try { const response = await fetch(`${API_BASE}/api/users`, { credentials: 'include', }); if (!response.ok) throw new Error('Failed to fetch users'); const data = await response.json(); setUsers(data); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load users'); } finally { setIsLoading(false); } }; const fetchRoles = async () => { try { const response = await fetch(`${API_BASE}/api/roles`, { credentials: 'include', }); if (!response.ok) throw new Error('Failed to fetch roles'); const data = await response.json(); setRoles(data); } catch (err) { console.error('Failed to fetch roles:', err); } }; const showSuccess = (message: string) => { setSuccess(message); setTimeout(() => setSuccess(null), 5000); }; const showError = (message: string) => { setError(message); setTimeout(() => setError(null), 5000); }; const handleCreateUser = async (e: React.FormEvent) => { e.preventDefault(); setError(null); const formData = new FormData(e.currentTarget); const email = formData.get('email') as string; const username = formData.get('username') as string; const displayName = formData.get('display_name') as string; const sendInvitation = formData.get('send_invitation') === 'on'; try { const response = await fetch(`${API_BASE}/api/users`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ email, username, display_name: displayName || null, send_invitation: sendInvitation, }), }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || 'Failed to create user'); } setShowCreateModal(false); showSuccess('Gebruiker succesvol aangemaakt'); fetchUsers(); (e.target as HTMLFormElement).reset(); } catch (err) { showError(err instanceof Error ? err.message : 'Failed to create user'); } }; const handleInviteUser = async (userId: number) => { try { const response = await fetch(`${API_BASE}/api/users/${userId}/invite`, { method: 'POST', credentials: 'include', }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || 'Failed to send invitation'); } showSuccess('Uitnodiging succesvol verzonden'); setActionMenuOpen(null); } catch (err) { showError(err instanceof Error ? err.message : 'Failed to send invitation'); } }; const handleToggleActive = async (userId: number, isActive: boolean) => { try { const response = await fetch(`${API_BASE}/api/users/${userId}/activate`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ is_active: !isActive }), }); if (!response.ok) throw new Error('Failed to update user'); showSuccess(`Gebruiker ${!isActive ? 'geactiveerd' : 'gedeactiveerd'}`); fetchUsers(); setActionMenuOpen(null); } catch (err) { showError(err instanceof Error ? err.message : 'Failed to update user'); } }; const handleAssignRole = async (userId: number, roleId: number) => { try { const response = await fetch(`${API_BASE}/api/users/${userId}/roles`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ role_id: roleId }), }); if (!response.ok) throw new Error('Failed to assign role'); showSuccess('Rol toegewezen'); fetchUsers(); } catch (err) { showError(err instanceof Error ? err.message : 'Failed to assign role'); } }; const handleRemoveRole = async (userId: number, roleId: number) => { try { const response = await fetch(`${API_BASE}/api/users/${userId}/roles/${roleId}`, { method: 'DELETE', credentials: 'include', }); if (!response.ok) throw new Error('Failed to remove role'); showSuccess('Rol verwijderd'); fetchUsers(); } catch (err) { showError(err instanceof Error ? err.message : 'Failed to remove role'); } }; const handleDeleteUser = async (userId: number) => { if (!confirm('Weet je zeker dat je deze gebruiker wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.')) return; try { const response = await fetch(`${API_BASE}/api/users/${userId}`, { method: 'DELETE', credentials: 'include', }); if (!response.ok) throw new Error('Failed to delete user'); showSuccess('Gebruiker succesvol verwijderd'); fetchUsers(); setActionMenuOpen(null); } catch (err) { showError(err instanceof Error ? err.message : 'Failed to delete user'); } }; const handleVerifyEmail = async (userId: number) => { if (!confirm('Weet je zeker dat je het e-mailadres van deze gebruiker wilt verifiëren?')) return; try { const response = await fetch(`${API_BASE}/api/users/${userId}/verify-email`, { method: 'PUT', credentials: 'include', }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || 'Failed to verify email'); } showSuccess('E-mailadres succesvol geverifieerd'); fetchUsers(); setActionMenuOpen(null); } catch (err) { showError(err instanceof Error ? err.message : 'Failed to verify email'); } }; const handleSetPassword = async (e: React.FormEvent) => { e.preventDefault(); if (!selectedUser) return; setError(null); const formData = new FormData(e.currentTarget); const password = formData.get('password') as string; const confirmPassword = formData.get('confirm_password') as string; if (password !== confirmPassword) { showError('Wachtwoorden komen niet overeen'); return; } if (password.length < 8) { showError('Wachtwoord moet minimaal 8 tekens lang zijn'); return; } try { const response = await fetch(`${API_BASE}/api/users/${selectedUser.id}/password`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ password }), }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || 'Failed to set password'); } showSuccess('Wachtwoord succesvol ingesteld'); setShowPasswordModal(false); setSelectedUser(null); (e.target as HTMLFormElement).reset(); } catch (err) { showError(err instanceof Error ? err.message : 'Failed to set password'); } }; const filteredUsers = users.filter( (user) => user.email.toLowerCase().includes(searchTerm.toLowerCase()) || user.username.toLowerCase().includes(searchTerm.toLowerCase()) || (user.display_name && user.display_name.toLowerCase().includes(searchTerm.toLowerCase())) ); const getUserInitials = (user: User) => { if (user.display_name) { return user.display_name .split(' ') .map(n => n[0]) .join('') .toUpperCase() .slice(0, 2); } return user.username.substring(0, 2).toUpperCase(); }; const formatDate = (dateString: string | null) => { if (!dateString) return 'Nog niet ingelogd'; return new Date(dateString).toLocaleDateString('nl-NL', { year: 'numeric', month: 'short', day: 'numeric', }); }; if (!hasManageUsers) { return (
Access denied
); } return (
{/* Header */} } actions={ } /> {/* Success/Error Messages */} {success && (

{success}

)} {error && (

{error}

)} {/* Search and Stats */}
setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" />
{filteredUsers.length} van {users.length} gebruikers
{/* Users Grid */} {isLoading ? (

Gebruikers laden...

) : filteredUsers.length === 0 ? (

Geen gebruikers gevonden

{searchTerm ? 'Probeer een andere zoekterm' : 'Maak je eerste gebruiker aan'}

) : (
{filteredUsers.map((user) => (
{/* User Header */}
{getUserInitials(user)}

{user.display_name || user.username}

@{user.username}

{actionMenuOpen === user.id && (
{!user.email_verified && ( )}
)}
{/* Email */}
{user.email}
{/* Status Badges */}
{user.is_active ? 'Actief' : 'Inactief'}
{user.email_verified ? ( E-mail geverifieerd ) : ( E-mail niet geverifieerd )}
{/* Roles */}
Rollen
{user.roles.length > 0 ? ( user.roles.map((role) => ( {role.name} )) ) : ( Geen rollen toegewezen )}
{/* Additional Info */}
Aangemaakt:

{formatDate(user.created_at)}

Laatste login:

{formatDate(user.last_login)}

))}
)}
{/* Create User Modal */} {showCreateModal && (

Nieuwe gebruiker

Voeg een nieuwe gebruiker toe aan het systeem

)} {/* Set Password Modal */} {showPasswordModal && selectedUser && (

Wachtwoord instellen

Stel een nieuw wachtwoord in voor {selectedUser.display_name || selectedUser.username}

)}
); }