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) => ( ))}
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') && ( 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° 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') && ( <> 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 */}
)}
)}
); }