import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { useAuthUnified } from "@/hooks/useAuthUnified"; import { useToast } from "@/hooks/use-toast"; import { apiRequest } from "@/lib/queryClient"; import { getRoleTailwindClasses, getRoleDisplayName } from "@/lib/roleUtils"; import { usePermissions } from "@/lib/permissions"; import { isUnauthorizedError } from "@/lib/authUtils"; import { Search, UserCog, Plus, Users, Edit, Trash2, Crown, Shield, User } from "lucide-react"; import type { UserWithGroups, Group } from "@shared/schema"; export default function UsersPage() { const { user } = useAuthUnified(); const { toast } = useToast(); const queryClient = useQueryClient(); const [searchTerm, setSearchTerm] = useState(""); const [roleFilter, setRoleFilter] = useState("all"); const [showEditModal, setShowEditModal] = useState(false); // Removed showGroupModal - now integrated in edit form const [showCreateModal, setShowCreateModal] = useState(false); const [selectedUser, setSelectedUser] = useState(null); const [userGroups, setUserGroups] = useState([]); const [showRoleModal, setShowRoleModal] = useState(false); const [selectedUserForRole, setSelectedUserForRole] = useState(null); const [showRoleConfirmModal, setShowRoleConfirmModal] = useState(false); const [pendingRoleChange, setPendingRoleChange] = useState<{userId: string, newRoleName: string, newRoleDisplay: string} | null>(null); // Edit form state const [editForm, setEditForm] = useState({ username: "", firstName: "", lastName: "", email: "", password: "", role: "employee" as "admin" | "manager" | "employee" | "directeur", }); // Form states for creating user const [newUser, setNewUser] = useState({ email: "", firstName: "", lastName: "", username: "", password: "", role: "employee" as "admin" | "manager" | "employee" | "directeur", }); const USE_LOCAL_AUTH = import.meta.env.VITE_USE_LOCAL_AUTH === 'true' || import.meta.env.MODE === 'development'; const { data: users = [], isLoading: usersLoading } = useQuery({ queryKey: ['/api/users'], enabled: user?.role === 'admin', }); const { data: groups = [] } = useQuery({ queryKey: ['/api/groups'], enabled: user?.role === 'admin', }); // Protection React Error #310 - Vérification Array pour users et groups console.log('👥 Users page data:', { usersLoading, usersCount: Array.isArray(users) ? users.length : 'NOT_ARRAY', groupsCount: Array.isArray(groups) ? groups.length : 'NOT_ARRAY', usersType: typeof users, groupsType: typeof groups }); // Protection: s'assurer que users et groups sont des arrays const safeUsers = Array.isArray(users) ? users : []; const safeGroups = Array.isArray(groups) ? groups : []; const updateUserRoleMutation = useMutation({ mutationFn: async (data: { userId: string; role: string }) => { await apiRequest(`/api/users/${data.userId}`, "PUT", { role: data.role }); }, onSuccess: () => { toast({ title: "Succès", description: "Rôle utilisateur mis à jour avec succès", }); queryClient.invalidateQueries({ queryKey: ['/api/users'] }); }, onError: (error) => { if (isUnauthorizedError(error)) { toast({ title: "Non autorisé", description: "Vous êtes déconnecté. Reconnexion...", variant: "destructive", }); setTimeout(() => { window.location.href = "/api/login"; }, 500); return; } toast({ title: "Erreur", description: "Impossible de mettre à jour le rôle", variant: "destructive", }); }, }); const updateUserMutation = useMutation({ mutationFn: async (data: { id: string; updates: any }) => { console.log('🔄 Frontend updateUser mutation:', data); // Nettoyer les données côté frontend avant envoi const cleanedUpdates: any = {}; for (const [key, value] of Object.entries(data.updates)) { // For name fields (firstName, lastName, email), allow empty strings to clear the fields if (key === 'firstName' || key === 'lastName' || key === 'email') { cleanedUpdates[key] = value || ''; } else if (value !== undefined && value !== null && value !== '') { cleanedUpdates[key] = value; } } console.log('📤 Sending cleaned data:', cleanedUpdates); const response = await apiRequest(`/api/users/${data.id}`, "PUT", cleanedUpdates); return response; }, onSuccess: (updatedUser) => { toast({ title: "Succès", description: "Utilisateur mis à jour avec succès", }); // Update the form with the response data before closing if (updatedUser) { setEditForm({ firstName: updatedUser.firstName || '', lastName: updatedUser.lastName || '', username: updatedUser.username || '', email: updatedUser.email || '', role: updatedUser.role || 'employee', password: '' }); // Update selected user as well setSelectedUser(prev => prev ? {...prev, ...updatedUser} : null); } queryClient.invalidateQueries({ queryKey: ['/api/users'] }); // Delay modal close to show the updated data setTimeout(() => { setShowEditModal(false); setSelectedUser(null); }, 1000); }, onError: (error: any) => { if (isUnauthorizedError(error)) { toast({ title: "Non autorisé", description: "Vous êtes déconnecté. Reconnexion...", variant: "destructive", }); setTimeout(() => { window.location.href = "/api/login"; }, 500); return; } // Show specific error message from API if available const errorMessage = error?.response?.data?.message || "Impossible de mettre à jour l'utilisateur"; toast({ title: "Erreur", description: errorMessage, variant: "destructive", }); }, }); const createUserMutation = useMutation({ mutationFn: async (userData: typeof newUser) => { const payload = { ...userData, id: userData.email.split('@')[0] + '_' + Date.now(), // Simple ID generation }; // Only include password for local auth if (USE_LOCAL_AUTH && userData.password) { payload.password = userData.password; } const response = await apiRequest("/api/users", "POST", payload); return response; }, onError: (error: any) => { if (isUnauthorizedError(error)) { toast({ title: "Non autorisé", description: "Vous êtes déconnecté. Reconnexion...", variant: "destructive", }); setTimeout(() => { window.location.href = "/api/login"; }, 500); return; } // Show specific error message from API if available const errorMessage = error?.response?.data?.message || "Impossible de créer l'utilisateur"; toast({ title: "Erreur", description: errorMessage, variant: "destructive", }); }, }); const assignGroupMutation = useMutation({ mutationFn: async (data: { userId: string; groupId: number }) => { console.log('📤 Assigning group:', data); const response = await apiRequest( `/api/users/${data.userId}/groups`, "POST", { groupId: data.groupId } ); console.log('✅ Group assignment response:', response); return response; }, onSuccess: () => { console.log('✅ Group assigned successfully'); toast({ title: "Succès", description: "Utilisateur assigné au groupe avec succès", }); // Invalidation complète du cache queryClient.invalidateQueries({ queryKey: ['/api/users'] }); queryClient.invalidateQueries({ queryKey: ['/api/groups'] }); }, onError: (error: any) => { console.error('❌ Error assigning group:', error); if (isUnauthorizedError(error)) { toast({ title: "Non autorisé", description: "Vous êtes déconnecté. Reconnexion...", variant: "destructive", }); setTimeout(() => { window.location.href = "/api/login"; }, 500); return; } // Show specific error message from API if available const errorMessage = error?.response?.data?.message || "Impossible d'assigner l'utilisateur au groupe"; toast({ title: "Erreur", description: errorMessage, variant: "destructive", }); }, }); const removeGroupMutation = useMutation({ mutationFn: async (data: { userId: string; groupId: number }) => { console.log('🗑️ Removing group:', data); const response = await apiRequest( `/api/users/${data.userId}/groups/${data.groupId}`, "DELETE" ); console.log('✅ Group removal response:', response); return response; }, onSuccess: () => { console.log('✅ Group removed successfully'); toast({ title: "Succès", description: "Utilisateur retiré du groupe avec succès", }); // Invalidation complète du cache queryClient.invalidateQueries({ queryKey: ['/api/users'] }); queryClient.invalidateQueries({ queryKey: ['/api/groups'] }); }, onError: (error: any) => { console.error('❌ Error removing group:', error); if (isUnauthorizedError(error)) { toast({ title: "Non autorisé", description: "Vous êtes déconnecté. Reconnexion...", variant: "destructive", }); setTimeout(() => { window.location.href = "/api/login"; }, 500); return; } // Show specific error message from API if available const errorMessage = error?.response?.data?.message || "Impossible de retirer l'utilisateur du groupe"; toast({ title: "Erreur", description: errorMessage, variant: "destructive", }); }, }); const deleteUserMutation = useMutation({ mutationFn: async (userId: string) => { await apiRequest(`/api/users/${userId}`, "DELETE"); }, onSuccess: () => { toast({ title: "Succès", description: "Utilisateur supprimé avec succès", }); queryClient.invalidateQueries({ queryKey: ['/api/users'] }); }, onError: (error) => { if (isUnauthorizedError(error)) { toast({ title: "Non autorisé", description: "Vous êtes déconnecté. Reconnexion...", variant: "destructive", }); setTimeout(() => { window.location.href = "/api/login"; }, 500); return; } toast({ title: "Erreur", description: "Impossible de supprimer l'utilisateur", variant: "destructive", }); }, }); const filteredUsers = Array.isArray(users) ? users.filter(u => { const matchesSearch = u.firstName?.toLowerCase().includes(searchTerm.toLowerCase()) || u.lastName?.toLowerCase().includes(searchTerm.toLowerCase()) || u.email?.toLowerCase().includes(searchTerm.toLowerCase()); const matchesRole = roleFilter === "all" || u.role === roleFilter; return matchesSearch && matchesRole; }) : []; const getRoleIcon = (role: string) => { const { iconClass } = getRoleTailwindClasses(role); switch (role) { case 'admin': return ; case 'manager': case 'directeur': return ; case 'employee': return ; default: return ; } }; const getRoleBadge = (role: string) => { const { badgeClass } = getRoleTailwindClasses(role); const displayName = getRoleDisplayName(role); return {displayName}; }; const handleCreateUser = () => { setShowCreateModal(true); setNewUser({ email: "", firstName: "", lastName: "", username: "", password: "", role: "employee" }); setUserGroups([]); }; const handleSubmitCreateUser = async () => { if (!newUser.username || !newUser.username.trim()) { toast({ title: "Erreur", description: "L'identifiant est obligatoire", variant: "destructive", }); return; } if (!newUser.password) { toast({ title: "Erreur", description: "Le mot de passe est obligatoire", variant: "destructive", }); return; } // First create the user try { const createdUser = await createUserMutation.mutateAsync(newUser); // Then assign groups if any selected if (userGroups.length > 0) { await Promise.all( userGroups.map(groupId => assignGroupMutation.mutateAsync({ userId: createdUser.id, groupId }) ) ); } toast({ title: "Succès", description: `Utilisateur créé et assigné à ${userGroups.length} magasin(s)`, }); setShowCreateModal(false); setNewUser({ email: "", firstName: "", lastName: "", username: "", password: "", role: "employee" }); setUserGroups([]); queryClient.invalidateQueries({ queryKey: ['/api/users'] }); } catch (error) { // Error handling is already done in the mutations } }; const handleEditUser = (userData: UserWithGroups) => { // Priority: use firstName/lastName if available, fallback to splitting 'name' field let firstName = userData.firstName || ''; let lastName = userData.lastName || ''; // If no firstName/lastName but we have a 'name' field, try to split it if (!firstName && !lastName && userData.name) { const [firstPart = '', ...lastNameParts] = userData.name.split(' '); firstName = firstPart; lastName = lastNameParts.join(' '); } const userWithNames = { ...userData, firstName, lastName }; setSelectedUser(userWithNames); setEditForm({ username: userData.username || "", firstName: firstName, lastName: lastName, email: userData.email || "", password: "", role: userData.role as "admin" | "manager" | "employee" | "directeur", }); setShowEditModal(true); }; // Function to handle group assignment in edit form const handleToggleGroupInEdit = async (groupId: number, isCurrentlyAssigned: boolean) => { if (!selectedUser) return; try { if (isCurrentlyAssigned) { await removeGroupMutation.mutateAsync({ userId: selectedUser.id, groupId }); } else { await assignGroupMutation.mutateAsync({ userId: selectedUser.id, groupId }); } } catch (error) { // Error handling is done in mutations } }; const handleSubmitEdit = (e: React.FormEvent) => { e.preventDefault(); if (!selectedUser) return; const updates: any = { username: editForm.username, firstName: editForm.firstName, lastName: editForm.lastName, email: editForm.email, role: editForm.role, }; // Only include password if it's not empty if (editForm.password.trim()) { updates.password = editForm.password; } updateUserMutation.mutate({ id: selectedUser.id, updates }); }; const handleRoleChange = (userId: string, newRoleName: string) => { if (userId === user?.id) { toast({ title: "Erreur", description: "Vous ne pouvez pas modifier votre propre rôle", variant: "destructive", }); return; } // Préparer la confirmation et ouvrir le modal setPendingRoleChange({ userId, newRoleName, newRoleDisplay: newRoleName }); setShowRoleConfirmModal(true); } const confirmRoleChange = () => { if (!pendingRoleChange) return; updateUserRoleMutation.mutate({ userId: pendingRoleChange.userId, role: pendingRoleChange.newRoleName }); setShowRoleConfirmModal(false); setPendingRoleChange(null); } const cancelRoleChange = () => { setShowRoleConfirmModal(false); setPendingRoleChange(null); } // Gestion des rôles centralisée dans Administration > Gestion des Rôles const handleToggleGroup = (userId: string, groupId: number, isAssigned: boolean) => { if (isAssigned) { removeGroupMutation.mutate({ userId, groupId }); } else { assignGroupMutation.mutate({ userId, groupId }); } }; const handleDeleteUser = (userToDelete: UserWithGroups) => { if (userToDelete.id === user?.id) { toast({ title: "Erreur", description: "Vous ne pouvez pas supprimer votre propre compte", variant: "destructive", }); return; } if (window.confirm(`Êtes-vous sûr de vouloir supprimer l'utilisateur ${userToDelete.firstName} ${userToDelete.lastName} ? Cette action est irréversible.`)) { deleteUserMutation.mutate(userToDelete.id); } }; const getInitials = (firstName?: string, lastName?: string) => { if (!firstName && !lastName) return "U"; return `${firstName?.[0] || ""}${lastName?.[0] || ""}`.toUpperCase(); }; const canManage = user?.role === 'admin'; if (!canManage) { return (

Accès restreint

Seuls les administrateurs peuvent accéder à la gestion des utilisateurs.

); } return (
{/* Header */}

Gestion des Utilisateurs

{Array.isArray(filteredUsers) ? filteredUsers.length : 0} utilisateur{Array.isArray(filteredUsers) && filteredUsers.length !== 1 ? 's' : ''}

{/* Filters */}
setSearchTerm(e.target.value)} className="pl-10 border border-gray-300 shadow-sm" />
{/* Users List */}
{usersLoading ? (
) : (!Array.isArray(filteredUsers) || filteredUsers.length === 0) ? (

Aucun utilisateur trouvé

{searchTerm || roleFilter !== "all" ? "Aucun utilisateur ne correspond à vos critères de recherche." : "Aucun utilisateur n'est encore enregistré dans le système."}

) : (
{Array.isArray(filteredUsers) && filteredUsers.map((userData) => ( ))}
Utilisateur Email RĂ´le Groupes Actions
{userData.profileImageUrl ? ( Profile ) : ( {getInitials( userData.firstName || userData.name?.split(' ')[0] || '', userData.lastName || userData.name?.split(' ').slice(1).join(' ') || '' )} )}
{(userData.firstName && userData.lastName) ? `${userData.firstName} ${userData.lastName}`.trim() : (userData.firstName || userData.lastName || userData.name || 'Nom non renseigné') } {userData.id === user?.id && ( Vous )}
ID: {userData.id}
{userData.email || 'Non renseigné'}
{getRoleIcon(userData.role)} {getRoleBadge(userData.role)}
{!Array.isArray(userData.userGroups) || userData.userGroups.length === 0 ? ( Aucun groupe ) : ( userData.userGroups.map((userGroup) => (
{userGroup.group.name}
)) )}
{/* Sélecteur de rôle rapide */} {userData.id !== user?.id && ( )}
)}
{/* Edit User Modal */} {showEditModal && selectedUser && ( { setShowEditModal(false); setSelectedUser(null); }}> Modifier l'utilisateur

Modifier les informations de l'utilisateur {selectedUser.name || selectedUser.firstName + ' ' + selectedUser.lastName}

setEditForm({...editForm, firstName: e.target.value})} placeholder="Prénom (optionnel)" />
setEditForm({...editForm, lastName: e.target.value})} placeholder="Nom (optionnel)" />
setEditForm({...editForm, username: e.target.value})} placeholder="Identifiant de connexion" required />
setEditForm({...editForm, email: e.target.value})} placeholder="email@exemple.com (optionnel)" />
setEditForm({...editForm, password: e.target.value})} placeholder="Laisser vide pour ne pas changer" />

Minimum 6 caractères. Laissez vide pour conserver le mot de passe actuel.

{/* Sélection du rôle */}
{/* Gestion des groupes/magasins */}
{safeGroups.map((group) => { const isAssigned = selectedUser?.userGroups?.some(ug => ug.groupId === group.id) || false; return (
{group.name}
); })} {safeGroups.length === 0 && (

Aucun groupe disponible

)}
)} {/* Create User Modal */} {showCreateModal && ( Créer un nouvel utilisateur

Créer un nouveau compte utilisateur avec ses permissions

setNewUser({...newUser, username: e.target.value})} placeholder="Identifiant unique (ex: ff0292)" required />

Identifiant unique pour se connecter Ă  l'application

setNewUser({...newUser, firstName: e.target.value})} placeholder="Prénom (optionnel)" />
setNewUser({...newUser, lastName: e.target.value})} placeholder="Nom (optionnel)" />
setNewUser({...newUser, email: e.target.value})} placeholder="email@exemple.com (optionnel)" />
setNewUser({...newUser, password: e.target.value})} placeholder="••••••••" required />

Mot de passe pour se connecter Ă  l'application

{groups.map((group) => (
{ if (checked) { setUserGroups([...userGroups, group.id]); } else { setUserGroups(userGroups.filter(id => id !== group.id)); } }} />
))}
)} {/* Role Change Confirmation Modal */} {showRoleConfirmModal && pendingRoleChange && ( Confirmer le changement de rĂ´le

Cette action modifiera les permissions de l'utilisateur dans l'application.

Attention

Vous êtes sur le point de changer le rôle de cet utilisateur vers{" "} {pendingRoleChange.newRoleDisplay}. Cette action prendra effet immédiatement.

Êtes-vous sûr de vouloir continuer ?

)}
); }