From 99ef70c4f9c94c41fef7dda2e5df479d71ef1253 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Tue, 12 Aug 2025 10:38:10 +0000 Subject: [PATCH] Add pagination to reconciliation tables for better data display Introduces pagination to the BL Reconciliation page, displaying 20 items per page with controls at the top and bottom of the tables. Replit-Commit-Author: Agent Replit-Commit-Session-Id: b163d4c0-de5e-4f4e-a9c0-aed4c7049718 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/b163d4c0-de5e-4f4e-a9c0-aed4c7049718/BTW2G9M --- client/src/pages/BLReconciliation.tsx | 144 ++++- client/src/pages/BLReconciliation_old.tsx | 742 ++++++++++++++++++++++ 2 files changed, 883 insertions(+), 3 deletions(-) create mode 100644 client/src/pages/BLReconciliation_old.tsx diff --git a/client/src/pages/BLReconciliation.tsx b/client/src/pages/BLReconciliation.tsx index c8de9b8..01c0f14 100644 --- a/client/src/pages/BLReconciliation.tsx +++ b/client/src/pages/BLReconciliation.tsx @@ -10,7 +10,7 @@ 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 } from "lucide-react"; +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() { @@ -43,6 +43,8 @@ export default function BLReconciliation() { 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({ @@ -209,11 +211,111 @@ export default function BLReconciliation() { 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 + React.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 + const PaginationControls = ({ 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 + +
+
+ + +
+ {Array.from({ length: totalPages }, (_, i) => i + 1) + .filter(page => { + // Afficher la première page, la dernière page, et les pages autour de la page actuelle + return page === 1 || + page === totalPages || + (page >= currentPage - 2 && page <= currentPage + 2); + }) + .map((page, index, array) => { + // Ajouter des points de suspension si nécessaire + const prevPage = array[index - 1]; + const showEllipsis = prevPage && page > prevPage + 1; + + return ( + + {showEllipsis && ( + ... + )} + + + ); + })} +
+ + +
+
+ ); + }; + if (isLoading || suppliersLoading) { return (
@@ -298,6 +400,15 @@ export default function BLReconciliation() {
) : (
+ {/* Pagination du haut */} + +
@@ -329,7 +440,7 @@ export default function BLReconciliation() { - {filteredManualDeliveries.map((delivery: any) => ( + {currentManualData.map((delivery: any) => (
+ + {/* Pagination du bas */} +
)} @@ -485,6 +605,15 @@ export default function BLReconciliation() { ) : (
+ {/* Pagination du haut */} + +
@@ -517,7 +646,7 @@ export default function BLReconciliation() { - {filteredAutomaticDeliveries.map((delivery: any) => ( + {currentAutomaticData.map((delivery: any) => (
+ + {/* Pagination du bas */} +
)} diff --git a/client/src/pages/BLReconciliation_old.tsx b/client/src/pages/BLReconciliation_old.tsx new file mode 100644 index 0000000..b13faba --- /dev/null +++ b/client/src/pages/BLReconciliation_old.tsx @@ -0,0 +1,742 @@ +import React, { useState } from "react"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { safeFormat } from "@/lib/dateUtils"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +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; + }); + + // Mutation pour valider un rapprochement manuel + 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", + }); + } + }); + + // Mutation pour dévalider un rapprochement (admins uniquement) + 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", + }); + } + }); + + // Mutation pour supprimer une ligne (admins uniquement) + 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 + React.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 + const PaginationControls = ({ 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 + +
+
+ + +
+ {Array.from({ length: totalPages }, (_, i) => i + 1) + .filter(page => { + // Afficher la première page, la dernière page, et les pages autour de la page actuelle + return page === 1 || + page === totalPages || + (page >= currentPage - 2 && page <= currentPage + 2); + }) + .map((page, index, array) => { + // Ajouter des points de suspension si nécessaire + const prevPage = array[index - 1]; + const showEllipsis = prevPage && page > prevPage + 1; + + return ( + + {showEllipsis && ( + ... + )} + + + ); + })} +
+ + +
+
+ ); + }; + + 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 */} + + + + + Rapprochement Manuel + + {manualReconciliationDeliveries.length} + + + + + Rapprochement Automatique + + {automaticReconciliationDeliveries.length} + + + + +
+ + {/* Filtre de recherche */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-10 border border-gray-300 shadow-sm" + /> +
+
+ + {/* Contenu des onglets */} + + + {filteredManualDeliveries.length === 0 ? ( +
+ +

+ Aucun rapprochement manuel trouvé +

+

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

+
+ ) : ( +
+
+ + + + + + + + + + + + + + + {filteredManualDeliveries.map((delivery: any) => ( + + + + + + + + + + + ))} + +
+ Fournisseur + + N° BL + + Date Livr. + + Montant BL + + Ref. Facture + + Montant Fact. + + Statut + + Actions +
+
+ {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') && ( + + )} + + {delivery.reconciled && permissions.canEdit('reconciliation') && user?.role === 'admin' && ( + + )} + + {permissions.canDelete('reconciliation') && ( + + )} +
+
+
+
+ )} +
+ + + {/* 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. +

+
+ ) : ( +
+
+ + + + + + + + + + + + + + + {filteredAutomaticDeliveries.map((delivery: any) => ( + + + + + + + + + + + ))} + +
+ Fournisseur + AUTO + + N° BL + + Date Livr. + + Date Valid. + + Montant BL + + Ref. Facture + + Statut + + Actions +
+
+
+ {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') && ( + <> + + + + )} + + {!isDevalidated(delivery) && permissions.canEdit('reconciliation') && ( + + )} + + {permissions.canDelete('reconciliation') && ( + + )} +
+
+
+
+ )} +
+
+
+ ); +} \ No newline at end of file