diff --git a/client/src/components/RouterProduction.tsx b/client/src/components/RouterProduction.tsx index 5905855..832f9f1 100644 --- a/client/src/components/RouterProduction.tsx +++ b/client/src/components/RouterProduction.tsx @@ -28,6 +28,7 @@ import WeatherSettings from "@/pages/WeatherSettings"; import Analytics from "@/pages/Analytics"; import SalesAnalysisPage from "@/pages/SalesAnalysisPage"; import PaymentSchedulePage from "@/pages/PaymentSchedulePage"; +import Contacts from "@/pages/Contacts"; import Layout from "@/components/Layout"; // Mobile pages @@ -114,6 +115,7 @@ function RouterProduction() { {/* Pages sans version mobile - utiliser version desktop pour l'instant */} + @@ -153,6 +155,7 @@ function RouterProduction() { + diff --git a/client/src/components/Sidebar.tsx b/client/src/components/Sidebar.tsx index 5ede545..d883be7 100644 --- a/client/src/components/Sidebar.tsx +++ b/client/src/components/Sidebar.tsx @@ -36,7 +36,8 @@ import { Send, Receipt, TrendingUp, - CreditCard + CreditCard, + BookUser, } from "lucide-react"; export default function Sidebar() { @@ -331,6 +332,12 @@ export default function Sidebar() { icon: Wrench, roles: ["admin", "directeur", "manager", "employee"] }, + { + path: "/contacts", + label: "Contacts", + icon: BookUser, + roles: ["admin", "directeur", "manager", "employee"] + }, ] }, ]; diff --git a/client/src/pages/Contacts.tsx b/client/src/pages/Contacts.tsx new file mode 100644 index 0000000..6e0543b --- /dev/null +++ b/client/src/pages/Contacts.tsx @@ -0,0 +1,510 @@ +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 { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { useAuthUnified } from "@/hooks/useAuthUnified"; +import { useStore } from "@/contexts/StoreContext"; +import { useToast } from "@/hooks/use-toast"; +import { apiRequest } from "@/lib/queryClient"; +import { + Plus, + Search, + Building, + Phone, + Mail, + User, + Edit, + Trash2, + BookUser, + StickyNote, +} from "lucide-react"; +import type { Supplier, Contact, Group } from "@shared/schema"; + +const CAN_EDIT_ROLES = ["admin", "directeur", "manager"]; + +export default function Contacts() { + const { user } = useAuthUnified(); + const { toast } = useToast(); + const queryClient = useQueryClient(); + const { stores, selectedStoreId } = useStore(); + + const canEdit = user?.role && CAN_EDIT_ROLES.includes(user.role); + const isAdmin = user?.role === "admin"; + + // Pour les admins, permettre de choisir un magasin + const [adminGroupId, setAdminGroupId] = useState(null); + const effectiveGroupId = isAdmin ? adminGroupId : selectedStoreId; + + const [supplierSearch, setSupplierSearch] = useState(""); + const [contactSearch, setContactSearch] = useState(""); + + const [showModal, setShowModal] = useState(false); + const [selectedContact, setSelectedContact] = useState(null); + const [formData, setFormData] = useState({ + groupId: effectiveGroupId ?? 0, + name: "", + role: "", + phone: "", + email: "", + notes: "", + }); + + const { data: suppliers = [] } = useQuery({ + queryKey: ["/api/suppliers"], + }); + + const contactsQueryKey = effectiveGroupId + ? [`/api/contacts?groupId=${effectiveGroupId}`] + : ["/api/contacts"]; + + const { data: contacts = [] } = useQuery({ + queryKey: contactsQueryKey, + queryFn: async () => { + const url = effectiveGroupId + ? `/api/contacts?groupId=${effectiveGroupId}` + : "/api/contacts"; + return await apiRequest(url, "GET"); + }, + }); + + const createMutation = useMutation({ + mutationFn: (data: any) => apiRequest("/api/contacts", "POST", data), + onSuccess: () => { + toast({ title: "Succès", description: "Contact créé avec succès" }); + queryClient.invalidateQueries({ queryKey: ["/api/contacts"] }); + closeModal(); + }, + onError: () => { + toast({ title: "Erreur", description: "Impossible de créer le contact", variant: "destructive" }); + }, + }); + + const updateMutation = useMutation({ + mutationFn: (data: any) => apiRequest(`/api/contacts/${selectedContact?.id}`, "PUT", data), + onSuccess: () => { + toast({ title: "Succès", description: "Contact modifié avec succès" }); + queryClient.invalidateQueries({ queryKey: ["/api/contacts"] }); + closeModal(); + }, + onError: () => { + toast({ title: "Erreur", description: "Impossible de modifier le contact", variant: "destructive" }); + }, + }); + + const deleteMutation = useMutation({ + mutationFn: (id: number) => apiRequest(`/api/contacts/${id}`, "DELETE"), + onSuccess: () => { + toast({ title: "Succès", description: "Contact supprimé" }); + queryClient.invalidateQueries({ queryKey: ["/api/contacts"] }); + }, + onError: () => { + toast({ title: "Erreur", description: "Impossible de supprimer le contact", variant: "destructive" }); + }, + }); + + const openCreate = () => { + setSelectedContact(null); + setFormData({ + groupId: effectiveGroupId ?? 0, + name: "", + role: "", + phone: "", + email: "", + notes: "", + }); + setShowModal(true); + }; + + const openEdit = (contact: Contact) => { + setSelectedContact(contact); + setFormData({ + groupId: contact.groupId, + name: contact.name, + role: contact.role || "", + phone: contact.phone || "", + email: contact.email || "", + notes: contact.notes || "", + }); + setShowModal(true); + }; + + const closeModal = () => { + setShowModal(false); + setSelectedContact(null); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const payload = { ...formData, groupId: effectiveGroupId ?? formData.groupId }; + if (selectedContact) { + updateMutation.mutate(payload); + } else { + createMutation.mutate(payload); + } + }; + + const handleDelete = (contact: Contact) => { + if (confirm(`Supprimer le contact "${contact.name}" ?`)) { + deleteMutation.mutate(contact.id); + } + }; + + const filteredSuppliers = suppliers.filter((s) => + s.name.toLowerCase().includes(supplierSearch.toLowerCase()) || + (s.contact || "").toLowerCase().includes(supplierSearch.toLowerCase()) + ); + + const filteredContacts = contacts.filter((c) => + c.name.toLowerCase().includes(contactSearch.toLowerCase()) || + (c.role || "").toLowerCase().includes(contactSearch.toLowerCase()) + ); + + const groupName = (groupId: number) => + stores.find((s) => s.id === groupId)?.name ?? `Magasin #${groupId}`; + + return ( +
+ {/* Header */} +
+
+

+ + Contacts +

+

+ Fournisseurs et contacts par magasin +

+
+ + {canEdit && effectiveGroupId && ( + + )} +
+ + {/* Sélecteur de magasin (admin uniquement) */} + {isAdmin && ( +
+ + +
+ )} + + {/* Deux colonnes */} +
+ {/* Colonne Fournisseurs */} +
+
+ +

+ Fournisseurs ({filteredSuppliers.length}) +

+
+ +
+ + setSupplierSearch(e.target.value)} + className="pl-10" + /> +
+ +
+ {filteredSuppliers.length === 0 ? ( +
+ + Aucun fournisseur trouvé +
+ ) : ( + filteredSuppliers.map((supplier) => ( +
+
+
+
+ +
+
+

{supplier.name}

+ {supplier.contact && ( +

+ + {supplier.contact} +

+ )} +
+
+
+
+ {supplier.phone && ( + + + {supplier.phone} + + )} + {(supplier as any).email && ( + + + {(supplier as any).email} + + )} + {!supplier.phone && !(supplier as any).email && ( +

Aucune coordonnée renseignée

+ )} +
+
+ )) + )} +
+
+ + {/* Colonne Autres contacts */} +
+
+ +

+ Autres contacts ({filteredContacts.length}) +

+
+ +
+ + setContactSearch(e.target.value)} + className="pl-10" + /> +
+ + {!effectiveGroupId && !isAdmin && ( +
+ + Sélectionnez un magasin pour voir les contacts +
+ )} + + {(effectiveGroupId || isAdmin) && ( +
+ {filteredContacts.length === 0 ? ( +
+ + Aucun contact + {canEdit && effectiveGroupId && ( +
+ +
+ )} +
+ ) : ( + filteredContacts.map((contact) => ( +
+
+
+
+ +
+
+

{contact.name}

+ {contact.role && ( +

{contact.role}

+ )} + {isAdmin && ( +

{groupName(contact.groupId)}

+ )} +
+
+ {canEdit && ( +
+ + +
+ )} +
+
+ {contact.phone && ( + + + {contact.phone} + + )} + {contact.email && ( + + + {contact.email} + + )} + {contact.notes && ( +

+ + {contact.notes} +

+ )} +
+
+ )) + )} +
+ )} +
+
+ + {/* Modal créer / modifier contact */} + + + + + {selectedContact ? "Modifier le contact" : "Nouveau contact"} + + + +
+ {isAdmin && !selectedContact && ( +
+ + +
+ )} + +
+ + setFormData((p) => ({ ...p, name: e.target.value }))} + placeholder="Nom du contact" + required + /> +
+ +
+ + setFormData((p) => ({ ...p, role: e.target.value }))} + placeholder="Ex: Directeur régional, Commercial..." + /> +
+ +
+ + setFormData((p) => ({ ...p, phone: e.target.value }))} + placeholder="Numéro de téléphone" + /> +
+ +
+ + setFormData((p) => ({ ...p, email: e.target.value }))} + placeholder="Adresse email" + /> +
+ +
+ + setFormData((p) => ({ ...p, notes: e.target.value }))} + placeholder="Notes libres..." + /> +
+ +
+ + +
+
+
+
+
+ ); +} diff --git a/client/src/pages/Suppliers.tsx b/client/src/pages/Suppliers.tsx index d4fb951..9b48437 100644 --- a/client/src/pages/Suppliers.tsx +++ b/client/src/pages/Suppliers.tsx @@ -16,11 +16,12 @@ import { useAuthUnified } from "@/hooks/useAuthUnified"; import { useToast } from "@/hooks/use-toast"; import { apiRequest } from "@/lib/queryClient"; import { isUnauthorizedError } from "@/lib/authUtils"; -import { - Plus, - Search, - Building, - Phone, +import { + Plus, + Search, + Building, + Phone, + Mail, Edit, Trash2, Package, @@ -43,6 +44,7 @@ export default function Suppliers() { name: "", contact: "", phone: "", + email: "", hasDlc: false, automaticReconciliation: false, requiresControl: false, @@ -198,6 +200,7 @@ export default function Suppliers() { name: "", contact: "", phone: "", + email: "", hasDlc: false, automaticReconciliation: false, requiresControl: false, @@ -236,6 +239,7 @@ export default function Suppliers() { name: supplier.name || "", contact: supplier.contact || "", phone: supplier.phone || "", + email: (supplier as any).email || "", hasDlc: supplier.hasDlc || false, automaticReconciliation: supplier.automaticReconciliation || false, requiresControl: supplier.requiresControl || false, @@ -409,6 +413,12 @@ export default function Suppliers() { {supplier.phone} )} + {(supplier as any).email && ( +
+ + {(supplier as any).email} +
+ )} {supplier.paymentMethod && (
@@ -528,6 +538,17 @@ export default function Suppliers() { />
+
+ + handleChange('email', e.target.value)} + placeholder="Email du contact fournisseur" + /> +
+