Files
LogiFlow/client/src/pages/Users.tsx
T
michaelschal e782580ab8 Integrate group assignment into user editing for a streamlined experience
Removes the separate group management modal and integrates group assignment directly into the user edit form. Updates the user role selection to include "directeur". Also, refactors the user management UI by removing the separate role display and associated text.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: a8a78c07-e900-425c-a577-5b4c5894379d
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/a8a78c07-e900-425c-a577-5b4c5894379d/yF4FWrX
2025-08-11 19:33:30 +00:00

1167 lines
44 KiB
TypeScript

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<UserWithGroups | null>(null);
const [userGroups, setUserGroups] = useState<number[]>([]);
const [showRoleModal, setShowRoleModal] = useState(false);
const [selectedUserForRole, setSelectedUserForRole] = useState<UserWithGroups | null>(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<UserWithGroups[]>({
queryKey: ['/api/users'],
enabled: user?.role === 'admin',
});
const { data: groups = [] } = useQuery<Group[]>({
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 <Crown className={`w-4 h-4 ${iconClass}`} />;
case 'manager':
case 'directeur':
return <Shield className={`w-4 h-4 ${iconClass}`} />;
case 'employee':
return <User className={`w-4 h-4 ${iconClass}`} />;
default:
return <User className={`w-4 h-4 ${iconClass}`} />;
}
};
const getRoleBadge = (role: string) => {
const { badgeClass } = getRoleTailwindClasses(role);
const displayName = getRoleDisplayName(role);
return <Badge className={badgeClass}>{displayName}</Badge>;
};
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 (
<div className="flex-1 flex flex-col items-center justify-center">
<div className="text-center">
<UserCog className="w-16 h-16 text-gray-400 mx-auto mb-4" />
<h2 className="text-xl font-semibold text-gray-900 mb-2">
Accès restreint
</h2>
<p className="text-gray-600">
Seuls les administrateurs peuvent accéder à la gestion des utilisateurs.
</p>
</div>
</div>
);
}
return (
<div className="flex-1 flex flex-col overflow-hidden">
{/* Header */}
<div className="bg-white border-b border-gray-200 p-6 shadow-sm">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-semibold text-gray-900 flex items-center">
<UserCog className="w-6 h-6 mr-3 text-blue-600" />
Gestion des Utilisateurs
</h2>
<p className="text-gray-600 mt-1">
{Array.isArray(filteredUsers) ? filteredUsers.length : 0} utilisateur{Array.isArray(filteredUsers) && filteredUsers.length !== 1 ? 's' : ''}
</p>
</div>
<Button
onClick={handleCreateUser}
className="bg-blue-600 hover:bg-blue-700 text-white shadow-md"
>
<Plus className="w-4 h-4 mr-2" />
Créer un utilisateur
</Button>
</div>
</div>
{/* Filters */}
<div className="bg-gray-50 border-b border-gray-200 p-4">
<div className="flex flex-wrap items-center gap-4">
<div className="flex-1 min-w-64">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="Rechercher un utilisateur..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 border border-gray-300 shadow-sm"
/>
</div>
</div>
<Select value={roleFilter} onValueChange={setRoleFilter}>
<SelectTrigger className="w-48 border border-gray-300 shadow-sm">
<SelectValue placeholder="Filtrer par rôle" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Tous les rôles</SelectItem>
<SelectItem value="admin">Administrateur</SelectItem>
<SelectItem value="manager">Manager</SelectItem>
<SelectItem value="employee">Employé</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* Users List */}
<div className="flex-1 overflow-auto">
{usersLoading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
) : (!Array.isArray(filteredUsers) || filteredUsers.length === 0) ? (
<div className="flex flex-col items-center justify-center h-64 text-gray-500">
<Users className="w-16 h-16 mb-4 text-gray-300" />
<h3 className="text-lg font-medium mb-2">Aucun utilisateur trouvé</h3>
<p className="text-center max-w-md">
{searchTerm || roleFilter !== "all"
? "Aucun utilisateur ne correspond à vos critères de recherche."
: "Aucun utilisateur n'est encore enregistré dans le système."}
</p>
</div>
) : (
<div className="p-6">
<div className="bg-white border border-gray-200 shadow-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Utilisateur
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Email
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Rôle
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Groupes
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{Array.isArray(filteredUsers) && filteredUsers.map((userData) => (
<tr key={userData.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="w-10 h-10 bg-primary rounded-full flex items-center justify-center mr-3">
{userData.profileImageUrl ? (
<img
src={userData.profileImageUrl}
alt="Profile"
className="w-full h-full rounded-full object-cover"
/>
) : (
<span className="text-white font-medium">
{getInitials(
userData.firstName || userData.name?.split(' ')[0] || '',
userData.lastName || userData.name?.split(' ').slice(1).join(' ') || ''
)}
</span>
)}
</div>
<div>
<div className="text-sm font-medium text-gray-900 flex items-center">
{(userData.firstName && userData.lastName)
? `${userData.firstName} ${userData.lastName}`.trim()
: (userData.firstName || userData.lastName || userData.name || 'Nom non renseigné')
}
{userData.id === user?.id && (
<Badge variant="outline" className="ml-2 text-xs">Vous</Badge>
)}
</div>
<div className="text-sm text-gray-500">
ID: {userData.id}
</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{userData.email || 'Non renseigné'}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
{getRoleIcon(userData.role)}
<span className="ml-2">{getRoleBadge(userData.role)}</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex flex-wrap gap-1">
{!Array.isArray(userData.userGroups) || userData.userGroups.length === 0 ? (
<span className="text-sm text-gray-500">Aucun groupe</span>
) : (
userData.userGroups.map((userGroup) => (
<Badge
key={userGroup.groupId}
variant="outline"
className="text-xs"
>
<div className="flex items-center">
<div
className="w-2 h-2 rounded-full mr-1"
style={{ backgroundColor: userGroup.group.color }}
/>
{userGroup.group.name}
</div>
</Badge>
))
)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex items-center justify-end space-x-2">
{/* Sélecteur de rôle rapide */}
{userData.id !== user?.id && (
<Select
value={userData.role || ''}
onValueChange={(newRole) => handleRoleChange(userData.id, newRole)}
disabled={updateUserRoleMutation.isPending}
>
<SelectTrigger className="w-28 h-8 text-xs border-purple-200 hover:border-purple-300">
<SelectValue placeholder="Rôle" />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Administrateur</SelectItem>
<SelectItem value="directeur">Directeur</SelectItem>
<SelectItem value="manager">Manager</SelectItem>
<SelectItem value="employee">Employé</SelectItem>
</SelectContent>
</Select>
)}
<Button
variant="outline"
size="sm"
onClick={() => handleEditUser(userData)}
title="Modifier l'utilisateur"
className="text-blue-600 hover:text-blue-700 border-blue-200 hover:border-blue-300 hover:bg-blue-50"
>
<Edit className="w-4 h-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDeleteUser(userData)}
disabled={userData.id === user?.id || deleteUserMutation.isPending}
className="text-red-600 hover:text-red-700 border-red-200 hover:border-red-300 hover:bg-red-50"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
)}
</div>
{/* Edit User Modal */}
{showEditModal && selectedUser && (
<Dialog open={showEditModal} onOpenChange={() => {
setShowEditModal(false);
setSelectedUser(null);
}}>
<DialogContent className="sm:max-w-lg" aria-describedby="edit-user-modal-description">
<DialogHeader>
<DialogTitle className="text-xl font-medium">
Modifier l'utilisateur
</DialogTitle>
<p id="edit-user-modal-description" className="text-sm text-gray-600 mt-1">
Modifier les informations de l'utilisateur {selectedUser.name || selectedUser.firstName + ' ' + selectedUser.lastName}
</p>
</DialogHeader>
<form onSubmit={handleSubmitEdit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="edit-firstName">Prénom</Label>
<Input
id="edit-firstName"
value={editForm.firstName}
onChange={(e) => setEditForm({...editForm, firstName: e.target.value})}
placeholder="Prénom (optionnel)"
/>
</div>
<div>
<Label htmlFor="edit-lastName">Nom</Label>
<Input
id="edit-lastName"
value={editForm.lastName}
onChange={(e) => setEditForm({...editForm, lastName: e.target.value})}
placeholder="Nom (optionnel)"
/>
</div>
</div>
<div>
<Label htmlFor="edit-username">Identifiant *</Label>
<Input
id="edit-username"
value={editForm.username}
onChange={(e) => setEditForm({...editForm, username: e.target.value})}
placeholder="Identifiant de connexion"
required
/>
</div>
<div>
<Label htmlFor="edit-email">Email</Label>
<Input
id="edit-email"
type="email"
value={editForm.email}
onChange={(e) => setEditForm({...editForm, email: e.target.value})}
placeholder="email@exemple.com (optionnel)"
/>
</div>
<div>
<Label htmlFor="edit-password">Nouveau mot de passe (optionnel)</Label>
<Input
id="edit-password"
type="password"
value={editForm.password}
onChange={(e) => setEditForm({...editForm, password: e.target.value})}
placeholder="Laisser vide pour ne pas changer"
/>
<p className="text-sm text-gray-500 mt-1">
Minimum 6 caractères. Laissez vide pour conserver le mot de passe actuel.
</p>
</div>
{/* Sélection du rôle */}
<div>
<Label>Rôle</Label>
<Select
value={editForm.role}
onValueChange={(role: any) => setEditForm({...editForm, role})}
>
<SelectTrigger>
<SelectValue placeholder="Sélectionner un rôle" />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Administrateur</SelectItem>
<SelectItem value="directeur">Directeur</SelectItem>
<SelectItem value="manager">Manager</SelectItem>
<SelectItem value="employee">Employé</SelectItem>
</SelectContent>
</Select>
</div>
{/* Gestion des groupes/magasins */}
<div>
<Label>Magasins/Groupes</Label>
<div className="space-y-2 mt-2 max-h-32 overflow-y-auto">
{safeGroups.map((group) => {
const isAssigned = selectedUser?.userGroups?.some(ug => ug.groupId === group.id) || false;
return (
<div key={group.id} className="flex items-center justify-between p-2 border rounded">
<div className="flex items-center space-x-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: group.color }}
/>
<span className="text-sm">{group.name}</span>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={() => handleToggleGroupInEdit(group.id, isAssigned)}
className={isAssigned ? "bg-green-50 text-green-700 border-green-200" : ""}
>
{isAssigned ? "Retirer" : "Assigner"}
</Button>
</div>
);
})}
{safeGroups.length === 0 && (
<p className="text-sm text-gray-500">Aucun groupe disponible</p>
)}
</div>
</div>
<div className="flex justify-end space-x-2 pt-4">
<Button
type="button"
variant="outline"
onClick={() => {
setShowEditModal(false);
setSelectedUser(null);
}}
>
Annuler
</Button>
<Button
type="submit"
disabled={updateUserMutation.isPending}
className="bg-blue-600 hover:bg-blue-700 text-white"
>
{updateUserMutation.isPending ? "Mise à jour..." : "Mettre à jour"}
</Button>
</div>
</form>
</DialogContent>
</Dialog>
)}
{/* Create User Modal */}
{showCreateModal && (
<Dialog open={showCreateModal} onOpenChange={setShowCreateModal}>
<DialogContent className="max-w-md" aria-describedby="create-user-modal-description">
<DialogHeader>
<DialogTitle className="flex items-center">
<Plus className="w-5 h-5 mr-2" />
Créer un nouvel utilisateur
</DialogTitle>
<p id="create-user-modal-description" className="text-sm text-gray-600 mt-1">
Créer un nouveau compte utilisateur avec ses permissions
</p>
</DialogHeader>
<div className="space-y-4">
<div>
<Label htmlFor="identifier">Identifiant *</Label>
<Input
id="identifier"
value={newUser.username || ''}
onChange={(e) => setNewUser({...newUser, username: e.target.value})}
placeholder="Identifiant unique (ex: ff0292)"
required
/>
<p className="text-sm text-muted-foreground mt-1">
Identifiant unique pour se connecter à l'application
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="firstName">Prénom</Label>
<Input
id="firstName"
value={newUser.firstName || ''}
onChange={(e) => setNewUser({...newUser, firstName: e.target.value})}
placeholder="Prénom (optionnel)"
/>
</div>
<div>
<Label htmlFor="lastName">Nom</Label>
<Input
id="lastName"
value={newUser.lastName || ''}
onChange={(e) => setNewUser({...newUser, lastName: e.target.value})}
placeholder="Nom (optionnel)"
/>
</div>
</div>
<div>
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={newUser.email || ''}
onChange={(e) => setNewUser({...newUser, email: e.target.value})}
placeholder="email@exemple.com (optionnel)"
/>
</div>
<div>
<Label htmlFor="password">Mot de passe *</Label>
<Input
id="password"
type="password"
value={newUser.password}
onChange={(e) => setNewUser({...newUser, password: e.target.value})}
placeholder="••••••••"
required
/>
<p className="text-sm text-muted-foreground mt-1">
Mot de passe pour se connecter à l'application
</p>
</div>
<div>
<Label htmlFor="role">Rôle</Label>
<Select
value={newUser.role}
onValueChange={(value: "admin" | "manager" | "employee") =>
setNewUser({...newUser, role: value})
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="employee">Employé</SelectItem>
<SelectItem value="manager">Manager</SelectItem>
<SelectItem value="admin">Administrateur</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Magasins assignés</Label>
<div className="space-y-2 mt-2 max-h-40 overflow-y-auto">
{groups.map((group) => (
<div key={group.id} className="flex items-center space-x-3">
<Checkbox
id={`group-${group.id}`}
checked={userGroups.includes(group.id)}
onCheckedChange={(checked) => {
if (checked) {
setUserGroups([...userGroups, group.id]);
} else {
setUserGroups(userGroups.filter(id => id !== group.id));
}
}}
/>
<div className="flex items-center space-x-2">
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: group.color }}
/>
<Label htmlFor={`group-${group.id}`} className="text-sm">
{group.name}
</Label>
</div>
</div>
))}
</div>
</div>
<div className="flex items-center space-x-3 pt-4">
<Button
variant="outline"
onClick={() => {
setShowCreateModal(false);
setNewUser({ email: "", firstName: "", lastName: "", username: "", password: "", role: "employee" });
setUserGroups([]);
}}
>
Annuler
</Button>
<Button
onClick={handleSubmitCreateUser}
disabled={createUserMutation.isPending}
className="flex-1"
>
{createUserMutation.isPending ? "Création..." : "Créer l'utilisateur"}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
)}
{/* Role Change Confirmation Modal */}
{showRoleConfirmModal && pendingRoleChange && (
<Dialog open={showRoleConfirmModal} onOpenChange={cancelRoleChange}>
<DialogContent className="sm:max-w-md" aria-describedby="role-change-modal-description">
<DialogHeader>
<DialogTitle className="text-xl font-medium flex items-center">
<Shield className="w-5 h-5 mr-2 text-orange-500" />
Confirmer le changement de rôle
</DialogTitle>
<p id="role-change-modal-description" className="text-sm text-gray-600 mt-2">
Cette action modifiera les permissions de l'utilisateur dans l'application.
</p>
</DialogHeader>
<div className="py-4">
<div className="bg-orange-50 border border-orange-200 rounded-lg p-4 mb-4">
<div className="flex items-center mb-2">
<Shield className="w-4 h-4 mr-2 text-orange-600" />
<span className="text-sm font-medium text-orange-800">Attention</span>
</div>
<p className="text-sm text-orange-700">
Vous êtes sur le point de changer le rôle de cet utilisateur vers{" "}
<span className="font-semibold">{pendingRoleChange.newRoleDisplay}</span>.
Cette action prendra effet immédiatement.
</p>
</div>
<p className="text-sm text-gray-600 mb-4">
Êtes-vous sûr de vouloir continuer ?
</p>
</div>
<div className="flex items-center justify-end space-x-3">
<Button
variant="outline"
onClick={cancelRoleChange}
disabled={updateUserRoleMutation.isPending}
>
Annuler
</Button>
<Button
onClick={confirmRoleChange}
disabled={updateUserRoleMutation.isPending}
className="bg-orange-600 hover:bg-orange-700 text-white"
>
{updateUserRoleMutation.isPending ? "Modification..." : "Confirmer"}
</Button>
</div>
</DialogContent>
</Dialog>
)}
</div>
);
}