import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useAuthUnified } from "@/hooks/useAuthUnified"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Pagination, usePagination } from "@/components/ui/pagination"; import { Plus, Edit, Trash2, Phone, PhoneCall, Printer, Eye, Package, AlertCircle } from "lucide-react"; import JsBarcode from 'jsbarcode'; import { safeFormat, safeDate } from "@/lib/dateUtils"; import { format } from "date-fns"; import { fr } from "date-fns/locale"; import { apiRequest } from "@/lib/queryClient"; import { toast } from "@/hooks/use-toast"; import type { CustomerOrderWithRelations, Group } from "@shared/schema"; import { CustomerOrderForm } from "@/components/CustomerOrderForm"; import { CustomerOrderDetails } from "@/components/CustomerOrderDetails"; import { ConfirmationModal } from "@/components/ConfirmationModal"; import ClientCallsModal from "@/components/modals/ClientCallsModal"; import { useStore } from "@/contexts/StoreContext"; import { usePermissions } from "@shared/permissions"; export default function CustomerOrders() { const { user } = useAuthUnified(); const { selectedStoreId } = useStore(); const permissions = usePermissions(user?.role); const queryClient = useQueryClient(); const [showCreateModal, setShowCreateModal] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [showDetailsModal, setShowDetailsModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [showStatusModal, setShowStatusModal] = useState(false); const [showClientCallsModal, setShowClientCallsModal] = useState(false); const [selectedOrder, setSelectedOrder] = useState(null); const [searchTerm, setSearchTerm] = useState(""); const [sortBy, setSortBy] = useState<"date" | "status" | "supplier">("date"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); const [filterSupplier, setFilterSupplier] = useState("all"); const [filterStatus, setFilterStatus] = useState("all"); // Fetch groups for store filter const { data: groups = [] } = useQuery({ queryKey: ['/api/groups'], }); // Query pour les appels clients en attente const { data: pendingCalls = [], isLoading: isPendingCallsLoading } = useQuery({ queryKey: ['/api/customer-orders/pending-calls', selectedStoreId], queryFn: async () => { const pendingCallsUrl = `/api/customer-orders/pending-calls${selectedStoreId ? `?storeId=${selectedStoreId}` : ''}`; const response = await fetch(pendingCallsUrl, { credentials: 'include' }); if (!response.ok) { throw new Error('Failed to fetch pending calls'); } const data = await response.json(); console.log('📞 Customer Orders - Pending calls received:', Array.isArray(data) ? data.length : 'NOT_ARRAY', 'items'); return data; }, refetchInterval: 30000, // Refetch every 30 seconds }); // Fetch suppliers for filter const { data: suppliers = [] } = useQuery({ queryKey: ['/api/suppliers'], }); // Fetch customer orders with store filtering for admins const customerOrdersUrl = `/api/customer-orders${selectedStoreId && user?.role === 'admin' ? `?storeId=${selectedStoreId}` : ''}`; const { data: customerOrders = [], isLoading } = useQuery({ queryKey: [customerOrdersUrl, selectedStoreId], }); // Create mutation const createMutation = useMutation({ mutationFn: (data: any) => { console.log("🔥 CREATE MUTATION STARTED with data:", data); return apiRequest('/api/customer-orders', 'POST', data); }, onSuccess: (result) => { console.log("✅ CREATE MUTATION SUCCESS:", result); // Force refresh of the query with store context queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0]?.toString()?.includes('/api/customer-orders') ?? false }); queryClient.refetchQueries({ predicate: (query) => query.queryKey[0]?.toString()?.includes('/api/customer-orders') ?? false }); setShowCreateModal(false); toast({ title: "Succès", description: "Commande client créée avec succès", }); }, onError: (error: any) => { console.error("❌ CREATE MUTATION ERROR:", error); toast({ title: "Erreur", description: error.message || "Erreur lors de la création de la commande", variant: "destructive", }); }, }); // Update mutation const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: any }) => apiRequest(`/api/customer-orders/${id}`, 'PUT', data), onSuccess: () => { queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0]?.toString()?.includes('/api/customer-orders') ?? false }); setShowEditModal(false); setSelectedOrder(null); toast({ title: "Succès", description: "Commande client mise à jour avec succès", }); }, onError: (error: any) => { toast({ title: "Erreur", description: error.message || "Erreur lors de la mise à jour de la commande", variant: "destructive", }); }, }); // Delete mutation const deleteMutation = useMutation({ mutationFn: (id: number) => apiRequest(`/api/customer-orders/${id}`, 'DELETE'), onSuccess: () => { queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0]?.toString()?.includes('/api/customer-orders') ?? false }); setShowDeleteModal(false); setSelectedOrder(null); toast({ title: "Succès", description: "Commande client supprimée avec succès", }); }, onError: (error: any) => { toast({ title: "Erreur", description: error.message || "Erreur lors de la suppression de la commande", variant: "destructive", }); }, }); // Status mutation const statusMutation = useMutation({ mutationFn: ({ id, status }: { id: number; status: string }) => apiRequest(`/api/customer-orders/${id}`, 'PUT', { status }), onSuccess: () => { queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0]?.toString()?.includes('/api/customer-orders') ?? false }); toast({ title: "Succès", description: "Statut de la commande mis à jour", }); }, }); // Notification mutation const notificationMutation = useMutation({ mutationFn: ({ id, customerNotified }: { id: number; customerNotified: boolean }) => apiRequest(`/api/customer-orders/${id}`, 'PUT', { customerNotified }), onSuccess: () => { queryClient.invalidateQueries({ predicate: (query) => query.queryKey[0]?.toString()?.includes('/api/customer-orders') ?? false }); toast({ title: "Succès", description: "Statut de notification mis à jour", }); }, }); const getStatusColor = (status: string) => { switch (status) { case "En attente de Commande": return "bg-yellow-50 text-yellow-700 border border-yellow-200"; case "Commande en Cours": return "bg-blue-50 text-blue-700 border border-blue-200"; case "Disponible": return "bg-green-50 text-green-700 border border-green-200"; case "Retiré": return "bg-gray-50 text-gray-700 border border-gray-200"; case "Annulé": return "bg-red-50 text-red-700 border border-red-200"; default: return "bg-gray-50 text-gray-700 border border-gray-200"; } }; const isGrayedOut = (status: string) => { return status === "Retiré" || status === "Annulé"; }; const canShowButtons = (status: string) => { return status === "Disponible"; }; const statusOptions = [ "En attente de Commande", "Commande en Cours", "Disponible", "Retiré", "Annulé" ]; const handleStatusChange = (id: number, newStatus: string) => { statusMutation.mutate({ id, status: newStatus }); }; const handleCreateOrder = (data: any) => { console.log("🎯 HANDLE CREATE ORDER CALLED with data:", data); console.log("🎯 createMutation state:", { isIdle: createMutation.isIdle, isPending: createMutation.isPending, isError: createMutation.isError, isSuccess: createMutation.isSuccess }); createMutation.mutate(data); console.log("🎯 createMutation.mutate CALLED"); }; const handleEditOrder = (data: any) => { if (selectedOrder) { updateMutation.mutate({ id: selectedOrder.id, data }); } }; const openEditModal = (order: CustomerOrderWithRelations) => { setSelectedOrder(order); setShowEditModal(true); }; const openDetailsModal = (order: CustomerOrderWithRelations) => { setSelectedOrder(order); setShowDetailsModal(true); }; const openDeleteModal = (order: CustomerOrderWithRelations) => { setSelectedOrder(order); setShowDeleteModal(true); }; const openStatusModal = (order: CustomerOrderWithRelations) => { setSelectedOrder(order); setShowStatusModal(true); }; const handleNotificationToggle = (order: CustomerOrderWithRelations) => { notificationMutation.mutate({ id: order.id, customerNotified: !order.customerNotified }); }; // Fonction pour calculer le checksum EAN13 const calculateEAN13Checksum = (code: string): string => { let sum = 0; for (let i = 0; i < 12; i++) { const digit = parseInt(code[i]); sum += i % 2 === 0 ? digit : digit * 3; } const checksum = (10 - (sum % 10)) % 10; return checksum.toString(); }; // Fonction pour générer un code-barres scannable const generateEAN13Barcode = (code: string): string => { try { // Créer un canvas temporaire const canvas = document.createElement('canvas'); let processedCode = code.replace(/[^0-9]/g, ''); // Garder que les chiffres // Gérer différentes longueurs de codes if (processedCode.length < 12) { processedCode = processedCode.padStart(12, '0'); } else if (processedCode.length > 13) { processedCode = processedCode.substring(0, 12); } else if (processedCode.length === 13) { processedCode = processedCode.substring(0, 12); } // Ajouter le checksum pour avoir un EAN13 valide const checksum = calculateEAN13Checksum(processedCode); const ean13 = processedCode + checksum; console.log('Code original:', code, 'EAN13 généré:', ean13); // Générer le code-barres avec jsbarcode JsBarcode(canvas, ean13, { format: "EAN13", width: 2, height: 60, displayValue: false, background: "#ffffff", lineColor: "#000000", margin: 5 }); // Retourner l'image en base64 return canvas.toDataURL('image/png'); } catch (error) { console.error('Erreur génération code-barres:', error); // Fallback SVG professionnel return 'data:image/svg+xml;base64,' + btoa(` ${code} `); } }; const handlePrintLabel = (order: CustomerOrderWithRelations) => { // Ouvrir une nouvelle fenêtre pour imprimer l'étiquette const printWindow = window.open('', '_blank'); if (printWindow) { const barcodeDisplay = order.gencode ? generateEAN13Barcode(order.gencode) : ''; const isImageBarcode = barcodeDisplay.startsWith('data:'); printWindow.document.write(` Commande #${order.id}
Commande #${order.id}
${order.status}
Créée le ${safeFormat(order.createdAt, 'dd MMMM yyyy')} à ${safeFormat(order.createdAt, 'HH:mm')}
Informations Client
Nom: ${order.customerName}
📞 Téléphone: ${order.customerPhone}
Informations Produit
Désignation:
${order.productDesignation}
${order.productReference ? `
Référence: ${order.productReference}
` : ''}
Quantité: ${order.quantity || 1}
${order.deposit && parseFloat(order.deposit) > 0 ? `
💰 Acompte versé: ${parseFloat(order.deposit).toFixed(2)}€
` : ''} ${order.isPromotionalPrice ? `
Prix: 🏷️ PRIX PUBLICITÉ
` : ''} ${order.gencode ? `
Code à barres EAN13:
${isImageBarcode ? `Code-barres EAN13` : `
${barcodeDisplay}
` }
${order.gencode}
` : ''}
Informations Commande
Prise par: ${order.orderTaker}
🏪 Magasin: ${order.group.name}
📅 Date de création: ${safeFormat(order.createdAt, 'dd/MM/yyyy')} à ${safeFormat(order.createdAt, 'HH:mm')}
`); } }; // Filter orders based on search term, supplier, and status const filteredOrders = Array.isArray(customerOrders) ? customerOrders.filter(order => { // Search term filter const matchesSearch = !searchTerm || order.customerName.toLowerCase().includes(searchTerm.toLowerCase()) || order.productDesignation.toLowerCase().includes(searchTerm.toLowerCase()) || order.customerPhone.includes(searchTerm) || (order.productReference && order.productReference.toLowerCase().includes(searchTerm.toLowerCase())) || (order.gencode && order.gencode.toLowerCase().includes(searchTerm.toLowerCase())) || (order.supplier && order.supplier.name.toLowerCase().includes(searchTerm.toLowerCase())); // Supplier filter const matchesSupplier = filterSupplier === "all" || (order.supplier && order.supplier.id.toString() === filterSupplier); // Status filter const matchesStatus = filterStatus === "all" || order.status === filterStatus; return matchesSearch && matchesSupplier && matchesStatus; }) : []; // Sort orders const sortedOrders = [...filteredOrders].sort((a, b) => { let aValue: string | number; let bValue: string | number; switch (sortBy) { case "date": const dateA = safeDate(a.createdAt); const dateB = safeDate(b.createdAt); aValue = dateA ? dateA.getTime() : 0; bValue = dateB ? dateB.getTime() : 0; break; case "status": aValue = a.status; bValue = b.status; break; case "supplier": aValue = a.supplier?.name || ""; bValue = b.supplier?.name || ""; break; default: const defaultDateA = safeDate(a.createdAt); const defaultDateB = safeDate(b.createdAt); aValue = defaultDateA ? defaultDateA.getTime() : 0; bValue = defaultDateB ? defaultDateB.getTime() : 0; } if (sortOrder === "desc") { return aValue > bValue ? -1 : aValue < bValue ? 1 : 0; } else { return aValue < bValue ? -1 : aValue > bValue ? 1 : 0; } }); // Pagination const { currentPage, setCurrentPage, itemsPerPage, setItemsPerPage, totalPages, paginatedData: paginatedOrders, totalItems } = usePagination(sortedOrders, 10); return (
{/* Alerte pour les appels clients en attente */} {!isPendingCallsLoading && pendingCalls.length > 0 && ( setShowClientCallsModal(true)} data-testid="alert-client-calls" > 📞 {pendingCalls.length} client{pendingCalls.length > 1 ? 's' : ''} à appeler Produits disponibles - Cliquez pour voir la liste des clients à contacter )} {/* Header */}

Commandes Client

Gestion des commandes clients et étiquettes

{permissions.canCreate('customer-orders') && ( )}
{/* Search and Filters */} Recherche et Filtres
setSearchTerm(e.target.value)} className="max-w-md" />
{/* Orders Table */} Commandes ({totalItems}) {isLoading ? (
Chargement...
) : ( Client Téléphone Produit Quantité Fournisseur Gencode Statut Date Actions {paginatedOrders.map((order) => ( {order.customerName} {order.customerPhone}
{order.productDesignation}
{order.productReference && (
Ref: {order.productReference}
)}
{order.quantity || 1}
{order.supplier?.name || "N/A"}
{order.gencode || "-"} openStatusModal(order)} > {order.status} {safeFormat(order.createdAt, 'dd/MM/yyyy')}
{canShowButtons(order.status) && ( <> )} {permissions.canDelete('customer-orders') && ( )}
))}
)} {/* Pagination */} {totalItems > 0 && ( )}
{/* Create Modal */} Nouvelle Commande Client
setShowCreateModal(false)} isLoading={createMutation.isPending} />
{/* Edit Modal */} Modifier la Commande
{selectedOrder && ( setShowEditModal(false)} isLoading={updateMutation.isPending} /> )}
{/* Details Modal */} Détails de la Commande {selectedOrder && ( )} {/* Status Change Modal */} Changer le statut Sélectionnez un nouveau statut pour cette commande client.

Commande: {selectedOrder?.productDesignation}

Client: {selectedOrder?.customerName}

{statusOptions.map((status) => ( ))}
{/* Delete Confirmation Modal */} selectedOrder && deleteMutation.mutate(selectedOrder.id)} title="Supprimer la commande" description={`Êtes-vous sûr de vouloir supprimer la commande de ${selectedOrder?.customerName} ? Cette action est irréversible.`} confirmText="Supprimer" isLoading={deleteMutation.isPending} /> {/* Modal pour les appels clients */} setShowClientCallsModal(false)} pendingCalls={pendingCalls} />
); }