import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { X, Plus, Edit2, Trash2, Save, Loader2, Users as UsersIcon, Shield, ArrowLeft } from 'lucide-react'; import { api } from '../api/client'; import type { UserCreate, UserUpdate, UserResponse } from '../api/client'; import { useAuth } from '../contexts/AuthContext'; import { useToast } from '../contexts/ToastContext'; import { Button } from '../components/Button'; import { Card, CardContent, CardHeader } from '../components/Card'; import { ConfirmModal } from '../components/ConfirmModal'; interface FormData extends UserCreate { group_ids: number[]; confirmPassword: string; } export function UsersPage() { const navigate = useNavigate(); const { t } = useTranslation(); const { user: currentUser, hasPermission } = useAuth(); const { showToast } = useToast(); const queryClient = useQueryClient(); const [showCreateModal, setShowCreateModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [editingUserId, setEditingUserId] = useState(null); const [deleteUserId, setDeleteUserId] = useState(null); const [formData, setFormData] = useState({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [], }); // Close modal on Escape key useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { if (showCreateModal) { setShowCreateModal(false); setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] }); } if (showEditModal) { setShowEditModal(false); setEditingUserId(null); setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] }); } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [showCreateModal, showEditModal]); const { data: users = [], isLoading } = useQuery({ queryKey: ['users'], queryFn: () => api.getUsers(), enabled: hasPermission('users:read'), }); const { data: groups = [] } = useQuery({ queryKey: ['groups'], queryFn: () => api.getGroups(), enabled: hasPermission('groups:read'), }); const createMutation = useMutation({ mutationFn: (data: UserCreate) => api.createUser(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }); queryClient.invalidateQueries({ queryKey: ['groups'] }); setShowCreateModal(false); setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] }); showToast(t('users.toast.created')); }, onError: (error: Error) => { showToast(error.message, 'error'); }, }); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: UserUpdate }) => api.updateUser(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }); queryClient.invalidateQueries({ queryKey: ['groups'] }); setShowEditModal(false); setEditingUserId(null); setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] }); showToast(t('users.toast.updated')); }, onError: (error: Error) => { showToast(error.message, 'error'); }, }); const deleteMutation = useMutation({ mutationFn: (id: number) => api.deleteUser(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['users'] }); showToast(t('users.toast.deleted')); }, onError: (error: Error) => { showToast(error.message, 'error'); }, }); const handleCreate = () => { if (!formData.username || !formData.password) { showToast(t('users.toast.fillRequired'), 'error'); return; } if (formData.password !== formData.confirmPassword) { showToast(t('users.toast.passwordsDoNotMatch'), 'error'); return; } if (formData.password.length < 6) { showToast(t('users.toast.passwordTooShort'), 'error'); return; } createMutation.mutate({ username: formData.username, password: formData.password, role: formData.role, group_ids: formData.group_ids.length > 0 ? formData.group_ids : undefined, }); }; const handleUpdate = (id: number) => { // Validate password confirmation if a new password is being set if (formData.password) { if (formData.password !== formData.confirmPassword) { showToast(t('users.toast.passwordsDoNotMatch'), 'error'); return; } if (formData.password.length < 6) { showToast(t('users.toast.passwordTooShort'), 'error'); return; } } const updateData: UserUpdate = { username: formData.username || undefined, password: formData.password || undefined, role: formData.role, group_ids: formData.group_ids, }; // Remove password if empty if (!updateData.password) { delete updateData.password; } updateMutation.mutate({ id, data: updateData }); }; const handleDelete = (id: number) => { setDeleteUserId(id); }; const startEdit = (user: UserResponse) => { setEditingUserId(user.id); setFormData({ username: user.username, password: '', confirmPassword: '', role: user.role, group_ids: user.groups?.map(g => g.id) || [], }); setShowEditModal(true); }; const closeEditModal = () => { setShowEditModal(false); setEditingUserId(null); setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] }); }; const toggleGroup = (groupId: number) => { setFormData(prev => ({ ...prev, group_ids: prev.group_ids.includes(groupId) ? prev.group_ids.filter(id => id !== groupId) : [...prev.group_ids, groupId], })); }; if (!hasPermission('users:read')) { return (

{t('users.noPermission')}

); } return (

{t('users.title')}

{t('users.subtitle')}

{isLoading ? (
) : (
{users.map((user) => ( ))}
{t('users.table.username')} {t('users.table.groups')} {t('users.table.status')} {t('users.table.actions')}
{user.username}
{user.is_admin && ( {t('users.admin')} )} {user.groups?.map(group => ( {group.name} ))} {(!user.groups || user.groups.length === 0) && !user.is_admin && ( {t('users.noGroups')} )}
{user.is_active ? t('users.active') : t('users.inactive')}
{user.id !== currentUser?.id && ( )}
)} {/* Create User Modal */} {showCreateModal && (
{ setShowCreateModal(false); setFormData({ username: '', password: '', confirmPassword: '', role: 'user', group_ids: [] }); }} > e.stopPropagation()} >

{t('users.modal.createUser')}

setFormData({ ...formData, username: e.target.value })} className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors" placeholder={t('users.form.usernamePlaceholder')} autoComplete="username" />
setFormData({ ...formData, password: e.target.value })} className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors" placeholder={t('users.form.passwordPlaceholder')} autoComplete="new-password" minLength={6} />
setFormData({ ...formData, confirmPassword: e.target.value })} className={`w-full px-4 py-3 bg-bambu-dark-secondary border rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors ${ formData.confirmPassword && formData.password !== formData.confirmPassword ? 'border-red-500' : 'border-bambu-dark-tertiary' }`} placeholder={t('users.form.confirmPasswordPlaceholder')} autoComplete="new-password" minLength={6} /> {formData.confirmPassword && formData.password !== formData.confirmPassword && (

{t('users.toast.passwordsDoNotMatch')}

)}
{groups.map(group => ( ))} {groups.length === 0 && (

{t('users.noGroupsAvailable')}

)}
)} {/* Edit User Modal */} {showEditModal && editingUserId !== null && (
e.stopPropagation()} >

{t('users.modal.editUser')}

setFormData({ ...formData, username: e.target.value })} className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors" placeholder={t('users.form.usernamePlaceholder')} autoComplete="username" />
setFormData({ ...formData, password: e.target.value, confirmPassword: '' })} className="w-full px-4 py-3 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors" placeholder={t('users.form.newPasswordPlaceholder')} autoComplete="new-password" minLength={6} />
{formData.password && (
setFormData({ ...formData, confirmPassword: e.target.value })} className={`w-full px-4 py-3 bg-bambu-dark-secondary border rounded-lg text-white placeholder-bambu-gray focus:outline-none focus:ring-2 focus:ring-bambu-green/50 focus:border-bambu-green transition-colors ${ formData.confirmPassword && formData.password !== formData.confirmPassword ? 'border-red-500' : 'border-bambu-dark-tertiary' }`} placeholder={t('users.form.confirmNewPasswordPlaceholder')} autoComplete="new-password" minLength={6} /> {formData.confirmPassword && formData.password !== formData.confirmPassword && (

{t('users.toast.passwordsDoNotMatch')}

)}
)}
{groups.map(group => ( ))} {groups.length === 0 && (

{t('users.noGroupsAvailable')}

)}
)} {/* Delete Confirmation Modal */} {deleteUserId !== null && ( { deleteMutation.mutate(deleteUserId); setDeleteUserId(null); }} onCancel={() => setDeleteUserId(null)} /> )}
); }