diff --git a/attached_assets/{78C105AD-BD53-4D7E-BF93-C1EF197A0988}_1754995961515.png b/attached_assets/{78C105AD-BD53-4D7E-BF93-C1EF197A0988}_1754995961515.png new file mode 100644 index 0000000..52f8721 Binary files /dev/null and b/attached_assets/{78C105AD-BD53-4D7E-BF93-C1EF197A0988}_1754995961515.png differ diff --git a/client/src/pages/BLReconciliationNative.tsx b/client/src/pages/BLReconciliationNative.tsx new file mode 100644 index 0000000..682cc04 --- /dev/null +++ b/client/src/pages/BLReconciliationNative.tsx @@ -0,0 +1,778 @@ +import { useState, useEffect } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { safeFormat } from "@/lib/dateUtils"; +import { useToast } from "@/hooks/use-toast"; +import { apiRequest } from "@/lib/queryClient"; +import { useStore } from "@/components/Layout"; +import { useAuthUnified } from "@/hooks/useAuthUnified"; +import { usePermissions } from "@shared/permissions"; +import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban, ChevronLeft, ChevronRight } from "lucide-react"; +import type { Supplier } from "@shared/schema"; + +export default function BLReconciliation() { + const { user } = useAuthUnified(); + const { selectedStoreId } = useStore(); + const { toast } = useToast(); + const queryClient = useQueryClient(); + const permissions = usePermissions(user?.role); + + // Vérification des permissions d'accès au module + if (!permissions.canView('reconciliation')) { + return ( +
+
+
+
+ +
+
+

+ Accès restreint
+ Seuls les directeurs et administrateurs peuvent accéder au module de rapprochement BL/Factures. +

+
+
+
+
+ ); + } + + const [activeTab, setActiveTab] = useState("manual"); + const [searchTerm, setSearchTerm] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const itemsPerPage = 20; + + // Récupérer les fournisseurs pour la logique automatique + const { data: suppliers = [], isLoading: suppliersLoading } = useQuery({ + queryKey: ['/api/suppliers'], + }); + + // Récupérer les livraisons validées avec BL + const { data: deliveriesWithBL = [], isLoading } = useQuery({ + queryKey: ['/api/deliveries/bl', selectedStoreId], + queryFn: async () => { + const params = new URLSearchParams({}); + if (selectedStoreId && user?.role === 'admin') { + params.append('storeId', selectedStoreId.toString()); + } + + const response = await fetch(`/api/deliveries?${params.toString()}`, { + credentials: 'include' + }); + + if (!response.ok) { + throw new Error('Failed to fetch deliveries'); + } + + const deliveries = await response.json(); + const filtered = Array.isArray(deliveries) ? deliveries.filter((d: any) => d.status === 'delivered') : []; + + return filtered.sort((a: any, b: any) => new Date(b.deliveredDate || b.updatedAt).getTime() - new Date(a.deliveredDate || a.updatedAt).getTime()); + }, + }); + + // Séparer les livraisons selon le mode de rapprochement du fournisseur + const manualReconciliationDeliveries = deliveriesWithBL.filter((delivery: any) => { + const supplier = suppliers.find((s: any) => s.id === delivery.supplierId); + return !supplier?.automaticReconciliation; + }); + + const automaticReconciliationDeliveries = deliveriesWithBL.filter((delivery: any) => { + const supplier = suppliers.find((s: any) => s.id === delivery.supplierId); + return supplier?.automaticReconciliation; + }); + + // Mutations pour les actions + const validateManualMutation = useMutation({ + mutationFn: async (deliveryId: number) => { + return await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", { + reconciled: true, + validatedAt: new Date().toISOString() + }); + }, + onSuccess: () => { + toast({ + title: "Succès", + description: "Rapprochement validé avec succès", + }); + queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); + }, + onError: () => { + toast({ + title: "Erreur", + description: "Impossible de valider le rapprochement", + variant: "destructive", + }); + } + }); + + const devalidateMutation = useMutation({ + mutationFn: async (deliveryId: number) => { + return await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", { + reconciled: false, + validatedAt: null + }); + }, + onSuccess: () => { + toast({ + title: "Succès", + description: "Rapprochement dévalidé avec succès", + }); + queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); + }, + onError: () => { + toast({ + title: "Erreur", + description: "Impossible de dévalider le rapprochement", + variant: "destructive", + }); + } + }); + + const deleteMutation = useMutation({ + mutationFn: async (deliveryId: number) => { + return await apiRequest(`/api/deliveries/${deliveryId}`, "DELETE"); + }, + onSuccess: () => { + toast({ + title: "Succès", + description: "Ligne supprimée avec succès", + }); + queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); + }, + onError: () => { + toast({ + title: "Erreur", + description: "Impossible de supprimer la ligne", + variant: "destructive", + }); + } + }); + + // Fonctions de gestion des actions + const handleValidateManual = (deliveryId: number) => { + if (!permissions.canValidate('reconciliation')) { + toast({ + title: "Accès refusé", + description: "Vous n'avez pas les permissions pour valider les rapprochements", + variant: "destructive", + }); + return; + } + validateManualMutation.mutate(deliveryId); + }; + + const handleDevalidate = (deliveryId: number) => { + if (!permissions.canEdit('reconciliation')) { + toast({ + title: "Accès refusé", + description: "Seuls les administrateurs peuvent dévalider les rapprochements", + variant: "destructive", + }); + return; + } + if (confirm("Êtes-vous sûr de vouloir dévalider ce rapprochement ?")) { + devalidateMutation.mutate(deliveryId); + } + }; + + const handleDelete = (deliveryId: number) => { + if (!permissions.canDelete('reconciliation')) { + toast({ + title: "Accès refusé", + description: "Seuls les administrateurs peuvent supprimer les lignes", + variant: "destructive", + }); + return; + } + if (confirm("Êtes-vous sûr de vouloir supprimer cette ligne ? Cette action est irréversible.")) { + deleteMutation.mutate(deliveryId); + } + }; + + // Filtrage des livraisons par recherche + const filterDeliveries = (deliveries: any[]) => { + return deliveries.filter((delivery: any) => { + const searchLower = searchTerm.toLowerCase(); + return ( + delivery.supplier?.name?.toLowerCase().includes(searchLower) || + delivery.blNumber?.toLowerCase().includes(searchLower) || + delivery.invoiceReference?.toLowerCase().includes(searchLower) + ); + }); + }; + + const filteredManualDeliveries = filterDeliveries(manualReconciliationDeliveries); + const filteredAutomaticDeliveries = filterDeliveries(automaticReconciliationDeliveries); + + // Pagination + const getCurrentPageData = (data: any[]) => { + const startIndex = (currentPage - 1) * itemsPerPage; + const endIndex = startIndex + itemsPerPage; + return data.slice(startIndex, endIndex); + }; + + const getTotalPages = (data: any[]) => { + return Math.ceil(data.length / itemsPerPage); + }; + + const currentManualData = getCurrentPageData(filteredManualDeliveries); + const currentAutomaticData = getCurrentPageData(filteredAutomaticDeliveries); + + const totalManualPages = getTotalPages(filteredManualDeliveries); + const totalAutomaticPages = getTotalPages(filteredAutomaticDeliveries); + + // Reset page when changing tabs or searching + useEffect(() => { + setCurrentPage(1); + }, [activeTab, searchTerm]); + + // Vérifier si une ligne est dévalidée (pour la coloration) + const isDevalidated = (delivery: any) => { + return delivery.reconciled === false && delivery.validatedAt === null; + }; + + // Composant de pagination simplifiée + const NativePagination = ({ currentPage, totalPages, onPageChange, totalItems, itemsPerPage }: { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; + totalItems: number; + itemsPerPage: number; + }) => { + if (totalPages <= 1) return null; + + const startItem = (currentPage - 1) * itemsPerPage + 1; + const endItem = Math.min(currentPage * itemsPerPage, totalItems); + + return ( +
+
+ + Affichage de {startItem} à {endItem} sur {totalItems} éléments + +
+
+ + + + Page {currentPage} sur {totalPages} + + + +
+
+ ); + }; + + // Badge native + const NativeBadge = ({ children, variant = "default", className = "" }: { children: React.ReactNode, variant?: string, className?: string }) => { + const baseClasses = "inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"; + const variantClasses = { + default: "bg-blue-100 text-blue-800", + secondary: "bg-gray-100 text-gray-800", + destructive: "bg-red-100 text-red-800", + outline: "border border-gray-300 text-gray-700 bg-white" + }; + + return ( + + {children} + + ); + }; + + // Button native + const NativeButton = ({ children, onClick, disabled = false, variant = "default", size = "default", className = "", title = "" }: { + children: React.ReactNode, + onClick?: () => void, + disabled?: boolean, + variant?: string, + size?: string, + className?: string, + title?: string + }) => { + const baseClasses = "inline-flex items-center justify-center font-medium rounded transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2"; + const variantClasses = { + default: "bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500", + ghost: "text-gray-600 hover:text-gray-900 hover:bg-gray-100", + outline: "border border-gray-300 bg-white text-gray-700 hover:bg-gray-50" + }; + const sizeClasses = { + default: "px-4 py-2 text-sm", + sm: "px-2 py-1 text-xs" + }; + + return ( + + ); + }; + + if (isLoading || suppliersLoading) { + return ( +
+
+
+
+
+
+ ); + } + + return ( +
+ {/* Header avec onglets */} +
+
+
+

+ + Rapprochement BL/Factures +

+

+ Gestion des rapprochements manuels et automatiques +

+
+
+ + {manualReconciliationDeliveries.length} rapprochements manuels + + + {automaticReconciliationDeliveries.length} rapprochements automatiques + +
+
+ + {/* Onglets natifs */} +
+ +
+
+ + {/* Filtre de recherche */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-10 pr-4 py-2 w-full border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ + {/* Contenu des onglets */} + {activeTab === "manual" && ( +
+ {filteredManualDeliveries.length === 0 ? ( +
+ +

+ Aucun rapprochement manuel trouvé +

+

+ Les livraisons de fournisseurs en mode manuel apparaîtront ici. +

+
+ ) : ( +
+ {/* Pagination du haut */} + + +
+ + + + + + + + + + + + + + + {currentManualData.map((delivery: any) => ( + + + + + + + + + + + ))} + +
FournisseurN° BLDate Livr.Montant BLRef. FactureMontant Fact.StatutActions
+
+ {delivery.supplier?.name} +
+
+
+ {delivery.blNumber || ( + Non renseigné + )} +
+
+
+ {safeFormat(delivery.scheduledDate, 'dd/MM/yy')} +
+
+
+ {delivery.blAmount ? + `${parseFloat(delivery.blAmount).toFixed(2)}€` : + Non renseigné + } +
+
+
+ {delivery.invoiceReference || ( + Non renseigné + )} +
+
+
+ {delivery.invoiceAmount ? + `${parseFloat(delivery.invoiceAmount).toFixed(2)}€` : + Non renseigné + } +
+
+ {delivery.reconciled ? ( + + Validé + + ) : isDevalidated(delivery) ? ( + + Dévalidé + + ) : ( + + En attente + + )} + +
+ {permissions.canEdit('reconciliation') && ( + + + + )} + + {!delivery.reconciled && permissions.canValidate('reconciliation') && ( + handleValidateManual(delivery.id)} + disabled={validateManualMutation.isPending} + className="text-green-600 hover:text-green-700 p-1" + title="Valider" + > + + + )} + + {delivery.reconciled && permissions.canEdit('reconciliation') && user?.role === 'admin' && ( + handleDevalidate(delivery.id)} + disabled={devalidateMutation.isPending} + className="text-orange-600 hover:text-orange-700 p-1" + title="Dévalider" + > + + + )} + + {permissions.canDelete('reconciliation') && ( + handleDelete(delivery.id)} + disabled={deleteMutation.isPending} + className="text-red-600 hover:text-red-700 p-1" + title="Supprimer" + > + + + )} +
+
+
+ + {/* Pagination du bas */} + +
+ )} +
+ )} + + {activeTab === "automatic" && ( +
+ {/* Message d'information */} +
+
+ +
+

Mode rapprochement automatique

+

+ Les livraisons de fournisseurs en mode automatique sont validées automatiquement lorsqu'elles ont le statut "delivered" et un numéro de BL. + {permissions.canEdit('reconciliation') ? ( + " Vous pouvez dévalider ces rapprochements si nécessaire." + ) : ( + " Seuls les directeurs et administrateurs peuvent modifier ces rapprochements." + )} +

+
+
+
+ + {filteredAutomaticDeliveries.length === 0 ? ( +
+ +

+ Aucun rapprochement automatique trouvé +

+

+ Les livraisons de fournisseurs en mode automatique apparaîtront ici. +

+
+ ) : ( +
+ {/* Pagination du haut */} + + +
+ + + + + + + + + + + + + + + {currentAutomaticData.map((delivery: any) => ( + + + + + + + + + + + ))} + +
+ Fournisseur + AUTO + N° BLDate Livr.Date Valid.Montant BLRef. FactureStatutActions
+
+
+ {delivery.supplier?.name} +
+
+
+
+
+ {delivery.blNumber || '-'} +
+
+
+ {safeFormat(delivery.scheduledDate, 'dd/MM/yy')} +
+
+
+ {delivery.reconciled && delivery.validatedAt ? + safeFormat(delivery.validatedAt, 'dd/MM/yy') : + '-' + } +
+
+
+ {delivery.blAmount ? + `${parseFloat(delivery.blAmount).toFixed(2)}€` : + '-' + } +
+
+
+ {delivery.invoiceReference || '-'} +
+
+ {isDevalidated(delivery) ? ( + + Dévalidé + + ) : ( + + Auto-validé + + )} + +
+ {isDevalidated(delivery) && permissions.canEdit('reconciliation') && ( + <> + + + + handleValidateManual(delivery.id)} + disabled={validateManualMutation.isPending} + className="text-green-600 hover:text-green-700 p-1" + title="Valider" + > + + + + )} + + {!isDevalidated(delivery) && permissions.canEdit('reconciliation') && ( + handleDevalidate(delivery.id)} + disabled={devalidateMutation.isPending} + className="text-orange-600 hover:text-orange-700 p-1" + title="Dévalider" + > + + + )} + + {permissions.canDelete('reconciliation') && ( + handleDelete(delivery.id)} + disabled={deleteMutation.isPending} + className="text-red-600 hover:text-red-700 p-1" + title="Supprimer" + > + + + )} +
+
+
+ + {/* Pagination du bas */} + +
+ )} +
+ )} +
+ ); +} \ No newline at end of file