diff --git a/client/src/components/Administration.tsx b/client/src/components/Administration.tsx index 982fc0a..17eef92 100644 --- a/client/src/components/Administration.tsx +++ b/client/src/components/Administration.tsx @@ -6,14 +6,15 @@ import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription } from "@/components/ui/dialog"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Badge } from "@/components/ui/badge"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useToast } from "@/hooks/use-toast"; import { apiRequest } from "@/lib/queryClient"; import { insertUserSchema, insertStoreSchema, type User, type Store } from "@shared/schema"; -import { Users, Building2, Plus, Edit } from "lucide-react"; +import { Users, Building2, Plus, Edit, Trash2, Settings } from "lucide-react"; import { z } from "zod"; const createUserSchema = insertUserSchema.extend({ @@ -23,35 +24,67 @@ const createUserSchema = insertUserSchema.extend({ path: ["confirmPassword"], }); +const editUserSchema = insertUserSchema.partial().extend({ + id: z.number(), + password: z.string().optional(), + confirmPassword: z.string().optional(), +}).refine((data) => { + if (data.password || data.confirmPassword) { + return data.password === data.confirmPassword; + } + return true; +}, { + message: "Les mots de passe ne correspondent pas", + path: ["confirmPassword"], +}); + +const editStoreSchema = insertStoreSchema.extend({ + id: z.number(), +}); + type CreateUserData = z.infer; +type EditUserData = z.infer; +type EditStoreData = z.infer; export default function Administration() { const { toast } = useToast(); const queryClient = useQueryClient(); const [isUserDialogOpen, setIsUserDialogOpen] = useState(false); const [isStoreDialogOpen, setIsStoreDialogOpen] = useState(false); + const [editingUser, setEditingUser] = useState(null); + const [editingStore, setEditingStore] = useState(null); // Queries const { data: users = [], isLoading: usersLoading } = useQuery({ queryKey: ['/api/admin/users'], + queryFn: async () => { + const response = await apiRequest('/api/admin/users'); + return response.json(); + }, }); const { data: stores = [], isLoading: storesLoading } = useQuery({ queryKey: ['/api/admin/stores'], + queryFn: async () => { + const response = await apiRequest('/api/admin/stores'); + return response.json(); + }, }); - // Mutations + // User mutations const createUserMutation = useMutation({ - mutationFn: (userData: CreateUserData) => { + mutationFn: async (userData: CreateUserData) => { const { confirmPassword, ...data } = userData; - return apiRequest('/api/admin/users', { + const response = await apiRequest('/api/admin/users', { method: 'POST', body: JSON.stringify(data), }); + return response.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/admin/users'] }); setIsUserDialogOpen(false); + userForm.reset(); toast({ title: "Utilisateur créé avec succès" }); }, onError: () => { @@ -59,15 +92,52 @@ export default function Administration() { }, }); + const updateUserMutation = useMutation({ + mutationFn: async (userData: EditUserData) => { + const { id, confirmPassword, ...data } = userData; + const response = await apiRequest(`/api/admin/users/${id}`, { + method: 'PUT', + body: JSON.stringify(data), + }); + return response.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/admin/users'] }); + setEditingUser(null); + editUserForm.reset(); + toast({ title: "Utilisateur modifié avec succès" }); + }, + onError: () => { + toast({ title: "Erreur", description: "Impossible de modifier l'utilisateur", variant: "destructive" }); + }, + }); + + const deleteUserMutation = useMutation({ + mutationFn: async (userId: number) => { + await apiRequest(`/api/admin/users/${userId}`, { method: 'DELETE' }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/admin/users'] }); + toast({ title: "Utilisateur supprimé avec succès" }); + }, + onError: () => { + toast({ title: "Erreur", description: "Impossible de supprimer l'utilisateur", variant: "destructive" }); + }, + }); + + // Store mutations const createStoreMutation = useMutation({ - mutationFn: (storeData: z.infer) => - apiRequest('/api/admin/stores', { + mutationFn: async (storeData: z.infer) => { + const response = await apiRequest('/api/admin/stores', { method: 'POST', body: JSON.stringify(storeData), - }), + }); + return response.json(); + }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/admin/stores'] }); setIsStoreDialogOpen(false); + storeForm.reset(); toast({ title: "Magasin créé avec succès" }); }, onError: () => { @@ -75,6 +145,39 @@ export default function Administration() { }, }); + const updateStoreMutation = useMutation({ + mutationFn: async (storeData: EditStoreData) => { + const { id, ...data } = storeData; + const response = await apiRequest(`/api/admin/stores/${id}`, { + method: 'PUT', + body: JSON.stringify(data), + }); + return response.json(); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/admin/stores'] }); + setEditingStore(null); + editStoreForm.reset(); + toast({ title: "Magasin modifié avec succès" }); + }, + onError: () => { + toast({ title: "Erreur", description: "Impossible de modifier le magasin", variant: "destructive" }); + }, + }); + + const deleteStoreMutation = useMutation({ + mutationFn: async (storeId: number) => { + await apiRequest(`/api/admin/stores/${storeId}`, { method: 'DELETE' }); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['/api/admin/stores'] }); + toast({ title: "Magasin supprimé avec succès" }); + }, + onError: () => { + toast({ title: "Erreur", description: "Impossible de supprimer le magasin", variant: "destructive" }); + }, + }); + // Forms const userForm = useForm({ resolver: zodResolver(createUserSchema), @@ -91,6 +194,10 @@ export default function Administration() { }, }); + const editUserForm = useForm({ + resolver: zodResolver(editUserSchema), + }); + const storeForm = useForm>({ resolver: zodResolver(insertStoreSchema), defaultValues: { @@ -102,45 +209,107 @@ export default function Administration() { }, }); - const onCreateUser = (data: CreateUserData) => { - createUserMutation.mutate(data); + const editStoreForm = useForm({ + resolver: zodResolver(editStoreSchema), + }); + + // Handlers + const onCreateUser = async (data: CreateUserData) => { + await createUserMutation.mutateAsync(data); }; - const onCreateStore = (data: z.infer) => { - createStoreMutation.mutate(data); + const onEditUser = async (data: EditUserData) => { + await updateUserMutation.mutateAsync(data); + }; + + const onCreateStore = async (data: z.infer) => { + await createStoreMutation.mutateAsync(data); + }; + + const onEditStore = async (data: EditStoreData) => { + await updateStoreMutation.mutateAsync(data); + }; + + const handleEditUser = (user: User) => { + setEditingUser(user); + editUserForm.reset({ + id: user.id, + username: user.username, + email: user.email || "", + firstName: user.firstName || "", + lastName: user.lastName || "", + role: user.role, + storeId: user.storeId || undefined, + isActive: user.isActive, + password: "", + confirmPassword: "", + }); + }; + + const handleEditStore = (store: Store) => { + setEditingStore(store); + editStoreForm.reset({ + id: store.id, + name: store.name, + address: store.address || "", + phone: store.phone || "", + email: store.email || "", + isActive: store.isActive, + }); + }; + + const handleDeleteUser = async (user: User) => { + if (window.confirm(`Êtes-vous sûr de vouloir supprimer l'utilisateur "${user.username}" ?`)) { + await deleteUserMutation.mutateAsync(user.id); + } + }; + + const handleDeleteStore = async (store: Store) => { + if (window.confirm(`Êtes-vous sûr de vouloir supprimer le magasin "${store.name}" ?`)) { + await deleteStoreMutation.mutateAsync(store.id); + } }; const getRoleBadge = (role: string) => { - const colors = { - admin: "bg-red-100 text-red-800", - manager: "bg-blue-100 text-blue-800", - employee: "bg-green-100 text-green-800", + const variants = { + administrator: "destructive" as const, + manager: "default" as const, + employee: "secondary" as const, }; - return colors[role as keyof typeof colors] || "bg-gray-100 text-gray-800"; + const labels = { + administrator: "Administrateur", + manager: "Manager", + employee: "Employé", + }; + return ( + + {labels[role as keyof typeof labels] || role} + + ); }; return ( -
-
-

Administration

-
+ + + + + Administration + + + + + + Utilisateurs + Magasins + - - - - - Utilisateurs - - - - Magasins - - - - - - - Gestion des Utilisateurs + {/* Users Tab */} + +
+

+ + Gestion des Utilisateurs +

- + - Créer un Utilisateur + Créer un nouvel utilisateur + + Ajoutez un nouvel utilisateur au système +
- ( - - Nom d'utilisateur - - - - - - )} - /> +
+ ( + + Nom d'utilisateur * + + + + + + )} + /> + ( + + Email + + + + + + )} + /> +
- ( - - Email - - - - - - )} - /> - ( - - Rôle - - - Administrateur - Manager - Employé - - - - - )} - /> - ( - - Magasin - - - {stores.map((store) => ( - - {store.name} - - ))} - - - - - )} - /> - ( - - Mot de passe - - - - - - )} - /> - ( - - Confirmer le mot de passe - - - - - - )} - /> - + + + )} + /> +
+
+ ( + + Rôle * + + + + )} + /> + ( + + Magasin + + + + )} + /> +
+
+ + +
-
- - {usersLoading ? ( -

Chargement...

- ) : ( - - - - Utilisateur - Email - Rôle - Magasin - Statut - - - - {users.map((user) => ( - - -
-

{user.firstName} {user.lastName}

-

{user.username}

-
-
- {user.email} - - - {user.role === 'admin' ? 'Administrateur' : - user.role === 'manager' ? 'Manager' : 'Employé'} - - - - {stores.find(s => s.id === user.storeId)?.name || '-'} - - - {user.isActive ? ( - Actif - ) : ( - Inactif - )} - -
- ))} -
-
- )} -
-
-
+
- - - - Gestion des Magasins + {usersLoading ? ( +
Chargement...
+ ) : ( + + + + Utilisateur + Email + Rôle + Magasin + Statut + Actions + + + + {users.map((user) => ( + + +
+
{user.username}
+ {(user.firstName || user.lastName) && ( +
+ {user.firstName} {user.lastName} +
+ )} +
+
+ {user.email || "-"} + {getRoleBadge(user.role)} + + {stores.find(s => s.id === user.storeId)?.name || "-"} + + + + {user.isActive ? "Actif" : "Inactif"} + + + +
+ + +
+
+
+ ))} +
+
+ )} + + {/* Edit User Dialog */} + !open && setEditingUser(null)}> + + + Modifier l'utilisateur + + Modifiez les informations de l'utilisateur + + + {editingUser && ( +
+ +
+ ( + + Nom d'utilisateur + + + + + + )} + /> + ( + + Email + + + + + + )} + /> +
+
+ ( + + Prénom + + + + + + )} + /> + ( + + Nom + + + + + + )} + /> +
+
+ ( + + Nouveau mot de passe (optionnel) + + + + + + )} + /> + ( + + Confirmer le mot de passe + + + + + + )} + /> +
+
+ ( + + Rôle + + + + )} + /> + ( + + Magasin + + + + )} + /> +
+
+ + +
+
+ + )} +
+
+
+ + {/* Stores Tab */} + +
+

+ + Gestion des Magasins +

- + - Créer un Magasin + Créer un nouveau magasin + + Ajoutez un nouveau magasin au système +
@@ -359,7 +721,7 @@ export default function Administration() { name="name" render={({ field }) => ( - Nom du magasin + Nom du magasin * @@ -380,12 +742,116 @@ export default function Administration() { )} /> +
+ ( + + Téléphone + + + + + + )} + /> + ( + + Email + + + + + + )} + /> +
+
+ + +
+
+ +
+
+
+ + {storesLoading ? ( +
Chargement...
+ ) : ( + + + + Nom + Adresse + Téléphone + Email + Statut + Actions + + + + {stores.map((store) => ( + + {store.name} + {store.address || "-"} + {store.phone || "-"} + {store.email || "-"} + + + {store.isActive ? "Actif" : "Inactif"} + + + +
+ + +
+
+
+ ))} +
+
+ )} + + {/* Edit Store Dialog */} + !open && setEditingStore(null)}> + + + Modifier le magasin + + Modifiez les informations du magasin + + + {editingStore && ( +
+ ( - Téléphone + Nom du magasin @@ -394,63 +860,62 @@ export default function Administration() { )} /> ( - Email + Adresse - + )} /> - +
+ ( + + Téléphone + + + + + + )} + /> + ( + + Email + + + + + + )} + /> +
+
+ + +
-
-
- - - {storesLoading ? ( -

Chargement...

- ) : ( - - - - Nom - Adresse - Téléphone - Email - Statut - - - - {stores.map((store) => ( - - {store.name} - {store.address} - {store.phone} - {store.email} - - {store.isActive ? ( - Actif - ) : ( - Inactif - )} - - - ))} - -
- )} -
- -
- - + )} + + + + + + ); } \ No newline at end of file diff --git a/server/routes.ts b/server/routes.ts index fde706e..e15bd67 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -217,6 +217,27 @@ export async function registerRoutes(app: Express): Promise { } }); + app.delete('/api/admin/users/:id', requireRole(['admin']), async (req, res) => { + try { + const userId = parseInt(req.params.id); + + // Don't allow deletion of the last administrator + const users = await storage.getAllUsers(); + const adminUsers = users.filter(u => u.role === 'administrator'); + const userToDelete = users.find(u => u.id === userId); + + if (userToDelete?.role === 'administrator' && adminUsers.length <= 1) { + return res.status(400).json({ error: "Cannot delete the last administrator" }); + } + + await storage.deleteUser(userId); + res.json({ message: "User deleted successfully" }); + } catch (error) { + console.error('Delete user error:', error); + res.status(500).json({ error: "Failed to delete user" }); + } + }); + // Admin routes - Stores app.get('/api/admin/stores', requireRole(['admin']), async (req, res) => { try { @@ -260,6 +281,29 @@ export async function registerRoutes(app: Express): Promise { } }); + app.delete('/api/admin/stores/:id', requireRole(['admin']), async (req, res) => { + try { + const storeId = parseInt(req.params.id); + + // Check if there are users assigned to this store + const users = await storage.getAllUsers(); + const usersInStore = users.filter(u => u.storeId === storeId); + + if (usersInStore.length > 0) { + return res.status(400).json({ + error: "Cannot delete store with assigned users", + details: `${usersInStore.length} user(s) are assigned to this store` + }); + } + + await storage.deleteStore(storeId); + res.json({ message: "Store deleted successfully" }); + } catch (error) { + console.error('Delete store error:', error); + res.status(500).json({ error: "Failed to delete store" }); + } + }); + const httpServer = createServer(app); return httpServer; } diff --git a/server/storage.ts b/server/storage.ts index d40c9fb..caf4b29 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -19,6 +19,7 @@ export interface IStorage { getUserByUsername(username: string): Promise; createUser(user: InsertUser): Promise; updateUser(id: number, user: Partial): Promise; + deleteUser(id: number): Promise; getAllUsers(): Promise; // Store operations @@ -26,6 +27,7 @@ export interface IStorage { createStore(store: InsertStore): Promise; getAllStores(): Promise; updateStore(id: number, store: Partial): Promise; + deleteStore(id: number): Promise; // Sale operations getSale(id: number): Promise; @@ -82,6 +84,10 @@ export class DatabaseStorage implements IStorage { return await db.select().from(users).orderBy(desc(users.createdAt)); } + async deleteUser(id: number): Promise { + await db.delete(users).where(eq(users.id, id)); + } + // Store operations async getStore(id: number): Promise { const [store] = await db.select().from(stores).where(eq(stores.id, id)); @@ -109,6 +115,10 @@ export class DatabaseStorage implements IStorage { return store; } + async deleteStore(id: number): Promise { + await db.delete(stores).where(eq(stores.id, id)); + } + // Sale operations async getSale(id: number): Promise { const [sale] = await db.select().from(sales).where(eq(sales.id, id));