import React, { useState, useEffect } 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 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.

) : (
{/* Pagination du haut */}
{currentManualData.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') && ( )}
{/* Pagination du bas */}
)}
{/* 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° 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') && ( )}
{/* Pagination du bas */}
)}
); }