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