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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useToast } from "@/hooks/use-toast"; import { apiRequest } from "@/lib/queryClient"; import { useStore } from "@/contexts/StoreContext"; import { useAuthUnified } from "@/hooks/useAuthUnified"; import { usePermissions } from "@shared/permissions"; import { Pagination, usePagination } from "@/components/ui/pagination"; import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban, Filter, Upload, CheckCircle, XCircle, Clock, MessageSquare } from "lucide-react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import ReconciliationComments from "@/components/ReconciliationComments"; import ReconciliationModal from "@/components/modals/ReconciliationModal"; export default function BLReconciliation() { const { user } = useAuthUnified(); const { selectedStoreId } = useStore(); const { toast } = useToast(); const queryClient = useQueryClient(); const permissions = usePermissions(user?.role); // Redirection pour les employés if (user?.role === 'employee') { return (

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

); } const [activeTab, setActiveTab] = useState("manual"); const [searchTerm, setSearchTerm] = useState(""); const [selectedDelivery, setSelectedDelivery] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [filterStatus, setFilterStatus] = useState<"all" | "validated" | "not_validated">("all"); // État pour le modal d'envoi de facture const [showInvoiceModal, setShowInvoiceModal] = useState(false); const [selectedDeliveryForInvoice, setSelectedDeliveryForInvoice] = useState(null); const [selectedFile, setSelectedFile] = useState(null); const [isUploading, setIsUploading] = useState(false); // État pour le modal d'attente du webhook const [showWaitingModal, setShowWaitingModal] = useState(false); const [processingSeconds, setProcessingSeconds] = useState(0); const [processingTimeout, setProcessingTimeout] = useState(null); // État pour le système de vérification de facture const [verificationResults, setVerificationResults] = useState>({}); const [verifyingDeliveries, setVerifyingDeliveries] = useState>(new Set()); // État pour le modal de commentaire const [showCommentModal, setShowCommentModal] = useState(false); const [selectedDeliveryForComment, setSelectedDeliveryForComment] = useState(null); // Récupérer les fournisseurs pour la logique automatique const { data: suppliers = [] } = useQuery({ queryKey: ['/api/suppliers'], }); // Fonction de vérification de facture const verifyInvoiceMutation = useMutation({ mutationFn: async ({ deliveryId, invoiceReference, blNumber, forceRefresh }: { deliveryId: number; invoiceReference?: string; blNumber?: string; forceRefresh?: boolean }) => { try { const result = await apiRequest(`/api/deliveries/${deliveryId}/verify-invoice`, 'POST', { invoiceReference, blNumber, forceRefresh: forceRefresh || false }); return result; } catch (error: any) { console.error('Erreur API vérification:', error); throw new Error(error.message || 'Erreur de vérification'); } }, onSuccess: (result, variables) => { setVerificationResults(prev => ({ ...prev, [variables.deliveryId]: result })); // Auto-remplissage si facture trouvée via BL if (result.exists && result.matchType === 'bl_number' && result.invoiceReference) { // Auto-remplir les champs dans la livraison via API apiRequest(`/api/deliveries/${variables.deliveryId}`, "PUT", { invoiceReference: result.invoiceReference, invoiceAmount: result.invoiceAmount }).then(() => { queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); queryClient.invalidateQueries({ queryKey: ['/api/deliveries'] }); }).catch((error) => { console.error('Erreur auto-remplissage:', error); }); } setVerifyingDeliveries(prev => { const newSet = new Set(prev); newSet.delete(variables.deliveryId); return newSet; }); }, onError: (error, variables) => { console.error('Erreur vérification facture:', error); setVerificationResults(prev => ({ ...prev, [variables.deliveryId]: { exists: false, matchType: 'none', errorMessage: error instanceof Error ? error.message : 'Erreur inconnue' } })); setVerifyingDeliveries(prev => { const newSet = new Set(prev); newSet.delete(variables.deliveryId); return newSet; }); toast({ title: "Erreur de vérification", description: error instanceof Error ? error.message : 'Erreur inconnue', variant: "destructive", }); } }); // Fonction pour déclencher la vérification const handleVerifyInvoice = (delivery: any, forceRefresh: boolean = false) => { // Accepter soit une référence de facture soit un numéro BL const hasInvoiceRef = delivery.invoiceReference?.trim(); const hasBlNumber = delivery.blNumber?.trim(); if (!hasInvoiceRef && !hasBlNumber) { toast({ title: "Référence manquante", description: "Veuillez saisir une référence de facture ou un numéro BL avant la vérification", variant: "destructive", }); return; } if (!delivery.group?.nocodbTableName && !delivery.group?.nocodbConfigId && !delivery.group?.webhookUrl) { toast({ title: "Vérification non disponible", description: "Ce magasin n'a pas de configuration NocoDB", variant: "destructive", }); return; } console.log('🔍 Déclenchement vérification:', { deliveryId: delivery.id, hasInvoiceRef, hasBlNumber, invoiceReference: delivery.invoiceReference, blNumber: delivery.blNumber, supplier: delivery.supplier?.name }); setVerifyingDeliveries(prev => new Set(prev).add(delivery.id)); verifyInvoiceMutation.mutate({ deliveryId: delivery.id, invoiceReference: delivery.invoiceReference, blNumber: delivery.blNumber, forceRefresh }); }; // Fonction pour vérifier toutes les factures avec un bouton const handleVerifyAllInvoices = () => { const deliveriesToVerify = manualReconciliationDeliveries.filter(delivery => (delivery.invoiceReference?.trim() || delivery.blNumber?.trim()) && (delivery.group?.nocodbTableName || delivery.group?.nocodbConfigId || delivery.group?.webhookUrl) ); if (deliveriesToVerify.length === 0) { toast({ title: "Aucune facture à vérifier", description: "Aucune livraison avec référence de facture ou numéro BL trouvée", }); return; } deliveriesToVerify.forEach((delivery, index) => { // Délai échelonné pour éviter la surcharge setTimeout(() => { handleVerifyInvoice(delivery, true); // Force refresh pour toutes }, index * 200); // 200ms entre chaque vérification }); toast({ title: "Vérification lancée", description: `Vérification de ${deliveriesToVerify.length} facture(s)/BL en cours...`, }); }; // Récupérer les livraisons validées avec BL - CACHE INVALIDÉ après modifications const { data: deliveriesWithBL = [], isLoading, refetch } = useQuery({ queryKey: ['/api/deliveries/bl', selectedStoreId], queryFn: async () => { const params = new URLSearchParams({}); if (selectedStoreId && (user?.role === 'admin' || user?.role === 'directeur')) { 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(); // Debug désactivé en production pour éviter latence if (import.meta.env.DEV && deliveries[0]) { console.log('🔍 DEBUG - Première livraison:', deliveries[0]); } 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()); }, enabled: !!user, staleTime: 0 // Éviter la mise en cache pour toujours avoir les dernières données BL }); // VÉRIFICATION AUTOMATIQUE AU CHARGEMENT avec système de cache useEffect(() => { if (!deliveriesWithBL.length || !suppliers.length) return; if (import.meta.env.DEV) { console.log('🔄 Déclenchement vérifications automatiques...'); } // Pré-populer les résultats pour les livraisons déjà réconciliées const newVerificationResults = { ...verificationResults }; let hasNewReconciledResults = false; deliveriesWithBL.forEach((delivery: any) => { // Si la livraison est déjà réconciliée, marquer comme vérifiée avec succès if (delivery.reconciled && !verificationResults[delivery.id]) { newVerificationResults[delivery.id] = { exists: true, matchType: delivery.invoiceReference ? 'invoice_reference' : 'bl_number', fromCache: true, permanent: true, reconciled: true }; hasNewReconciledResults = true; if (import.meta.env.DEV) { console.log(`✅ Livraison ${delivery.id} déjà réconciliée, marquée comme vérifiée`); } } }); // Mettre à jour les résultats si on a de nouvelles livraisons réconciliées if (hasNewReconciledResults) { setVerificationResults(newVerificationResults); } deliveriesWithBL.forEach((delivery: any) => { // ⛔ EXCLURE COMPLÈTEMENT les livraisons déjà réconciliées // Une fois validées avec coche verte, on ne contrôle plus JAMAIS if (delivery.reconciled) { return; // Livraison déjà validée = AUCUNE vérification nécessaire } // Vérifier seulement les livraisons NON validées const hasVerifiableData = delivery.invoiceReference || delivery.blNumber; const notAlreadyProcessed = !verificationResults[delivery.id] && !verifyingDeliveries.has(delivery.id); if (hasVerifiableData && notAlreadyProcessed) { if (import.meta.env.DEV) { console.log(`🔍 Vérification auto pour livraison NON réconciliée ${delivery.id}:`, { invoiceRef: delivery.invoiceReference, blNumber: delivery.blNumber, reconciled: delivery.reconciled }); } // Délai pour éviter de surcharger le serveur setTimeout(() => { handleVerifyInvoice(delivery, false); }, Math.random() * 1000); } }); }, [deliveriesWithBL, suppliers, verificationResults, verifyingDeliveries]); // Séparer les livraisons par mode de rapprochement const manualReconciliationDeliveries = deliveriesWithBL.filter((delivery: any) => { const supplier = suppliers.find(s => s.id === delivery.supplierId); return supplier?.automaticReconciliation !== true; }); const automaticReconciliationDeliveries = deliveriesWithBL.filter((delivery: any) => { const supplier = suppliers.find(s => s.id === delivery.supplierId); return supplier?.automaticReconciliation === true; }); // Fonctions de gestion const handleOpenModal = (delivery: any) => { setSelectedDelivery(delivery); setIsModalOpen(true); }; const handleCloseModal = () => { setSelectedDelivery(null); setIsModalOpen(false); }; const handleSaveReconciliation = async () => { try { // Invalidation cache + refetch forcé pour mise à jour immédiate queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); queryClient.invalidateQueries({ queryKey: ['/api/deliveries'] }); refetch(); handleCloseModal(); } catch (error) { toast({ title: "Erreur", description: "Impossible de mettre à jour les données", variant: "destructive", }); } }; // Nouvelles fonctions pour le modal d'envoi de facture const handleOpenInvoiceModal = (delivery: any) => { setSelectedDeliveryForInvoice(delivery); setSelectedFile(null); setShowInvoiceModal(true); }; // Fonctions pour le modal de commentaire const handleOpenCommentModal = (delivery: any) => { setSelectedDeliveryForComment(delivery); setShowCommentModal(true); }; const handleCloseCommentModal = () => { setShowCommentModal(false); setSelectedDeliveryForComment(null); }; const handleCloseInvoiceModal = () => { setShowInvoiceModal(false); setSelectedDeliveryForInvoice(null); setSelectedFile(null); setIsUploading(false); }; const handleCloseWaitingModal = () => { setShowWaitingModal(false); setProcessingSeconds(0); if (processingTimeout) { clearTimeout(processingTimeout); setProcessingTimeout(null); } }; const startProcessingTimer = () => { setProcessingSeconds(0); const interval = setInterval(() => { setProcessingSeconds(prev => { if (prev >= 60) { clearInterval(interval); return 60; } return prev + 1; }); }, 1000); setProcessingTimeout(interval); }; const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file && file.type === 'application/pdf') { setSelectedFile(file); } else { toast({ title: "Erreur", description: "Veuillez sélectionner un fichier PDF", variant: "destructive", }); } }; const handleSendInvoice = async () => { if (!selectedDeliveryForInvoice || !selectedFile) { toast({ title: "Erreur", description: "Veuillez sélectionner un fichier PDF", variant: "destructive", }); return; } if (!selectedDeliveryForInvoice.group?.webhookUrl) { toast({ title: "Erreur", description: "Aucun webhook configuré pour ce magasin", variant: "destructive", }); return; } // Fermer le modal de sélection et ouvrir le modal d'attente setShowInvoiceModal(false); setShowWaitingModal(true); setIsUploading(true); startProcessingTimer(); try { const formData = new FormData(); formData.append('file', selectedFile); formData.append('supplier', selectedDeliveryForInvoice.supplier?.name || ''); formData.append('blNumber', selectedDeliveryForInvoice.blNumber || ''); formData.append('type', 'Facture'); // Créer un AbortController pour gérer le timeout const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 secondes const response = await fetch(selectedDeliveryForInvoice.group.webhookUrl, { method: 'POST', body: formData, signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`Erreur ${response.status}: ${response.statusText}`); } handleCloseWaitingModal(); toast({ title: "Succès", description: "Facture traitée avec succès via le webhook", }); // Reset des données setSelectedDeliveryForInvoice(null); setSelectedFile(null); // Relancer la vérification de la facture qui vient d'être traitée try { // Invalidation des caches pour forcer le rechargement des données queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); queryClient.invalidateQueries({ queryKey: ['/api/deliveries'] }); // Recharger les données await refetch(); // Attendre un peu que les données soient à jour, puis vérifier cette livraison spécifique setTimeout(() => { handleVerifyInvoice(selectedDeliveryForInvoice, true); }, 1000); console.log('🔄 Vérification automatique relancée pour la livraison traitée par webhook'); } catch (error) { console.error('Erreur lors de la relance de la vérification:', error); } } catch (error: any) { handleCloseWaitingModal(); let errorMessage = "Impossible d'envoyer la facture"; if (error.name === 'AbortError') { errorMessage = "Le traitement a pris trop de temps (timeout de 60 secondes)"; } else if (error.message) { errorMessage = `Impossible d'envoyer la facture: ${error.message}`; } toast({ title: "Erreur", description: errorMessage, variant: "destructive", }); // Réouvrir le modal de sélection en cas d'erreur setShowInvoiceModal(true); } finally { setIsUploading(false); } }; // Fonction pour déterminer si le bouton "Envoyer Facture" doit être affiché const shouldShowInvoiceButton = (delivery: any) => { // Conditions : ligne non validée OU pas de RefFacture renseignée const isNotValidated = !delivery.reconciled; const hasNoInvoiceReference = !delivery.invoiceReference; // Et il faut qu'il y ait un magasin assigné avec un webhook const hasValidGroup = delivery.group && delivery.group.webhookUrl; // Debug uniquement en développement if (import.meta.env.DEV && delivery && delivery.supplier?.name) { console.log(`🔍 Debug bouton facture pour ${delivery.supplier.name}:`, { isNotValidated, hasNoInvoiceReference, hasValidGroup, shouldShow: (isNotValidated || hasNoInvoiceReference) && hasValidGroup }); } return (isNotValidated || hasNoInvoiceReference) && hasValidGroup; }; const handleQuickValidate = async (delivery: any) => { try { await apiRequest(`/api/deliveries/${delivery.id}`, "PUT", { reconciled: true, validatedAt: new Date().toISOString() }); toast({ title: "Succès", description: "Rapprochement validé avec succès", }); // Invalidation cache + refetch forcé pour mise à jour immédiate queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); queryClient.invalidateQueries({ queryKey: ['/api/deliveries'] }); refetch(); // Force recharge immédiate des données BL } catch (error) { toast({ title: "Erreur", description: "Impossible de valider le rapprochement", variant: "destructive", }); } }; const handleDevalidateReconciliation = async (deliveryId: number) => { if (!permissions.canEdit('reconciliation') && !permissions.canValidate('reconciliation')) { toast({ title: "Accès refusé", description: "Vous n'avez pas les permissions nécessaires pour dévalider les rapprochements", variant: "destructive", }); return; } if (!window.confirm("Êtes-vous sûr de vouloir dévalider ce rapprochement ?")) { return; } try { await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", { reconciled: false, validatedAt: null }); toast({ title: "Succès", description: "Rapprochement dévalidé avec succès", }); // Invalidation cache + refetch forcé pour mise à jour immédiate queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); refetch(); } catch (error) { toast({ title: "Erreur", description: "Impossible de dévalider le rapprochement", variant: "destructive", }); } }; const handleDeleteDelivery = async (deliveryId: number) => { if (!permissions.canDelete('reconciliation')) { toast({ title: "Accès refusé", description: "Seuls les administrateurs peuvent supprimer les livraisons", variant: "destructive", }); return; } if (!window.confirm("Êtes-vous sûr de vouloir supprimer cette livraison ? Cette action est irréversible.")) { return; } try { await apiRequest(`/api/deliveries/${deliveryId}`, "DELETE"); toast({ title: "Succès", description: "Livraison supprimée avec succès", }); queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); } catch (error) { toast({ title: "Erreur", description: "Impossible de supprimer la livraison", variant: "destructive", }); } }; // Filtrage des livraisons par recherche et statut de validation const filterDeliveries = (deliveries: any[]) => { return deliveries.filter((delivery: any) => { // Conversion sûre de reconciled - gestion de tous les cas possibles const isReconciled = delivery.reconciled === true || delivery.reconciled === 1 || delivery.reconciled === "t" || delivery.reconciled === "true" || (typeof delivery.reconciled === 'string' && delivery.reconciled.toLowerCase() === 'true'); // Vérifier aussi si une facture a été trouvée ET validée automatiquement // (certaines livraisons ont une coche verte mais reconciled = false) const hasVerifiedInvoice = verificationResults[delivery.id]?.exists === true; const isValidated = isReconciled || (hasVerifiedInvoice && verificationResults[delivery.id]?.reconciled === true); // Filtre par statut validé if (filterStatus === "validated") { // Afficher seulement les livraisons vraiment validées (reconciled = true) return isReconciled; } if (filterStatus === "not_validated") { // Afficher seulement les livraisons non validées (reconciled = false) return !isReconciled; } // Filtre par recherche 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 pour les rapprochements manuels const { currentPage: manualCurrentPage, setCurrentPage: setManualCurrentPage, itemsPerPage: manualItemsPerPage, setItemsPerPage: setManualItemsPerPage, totalPages: manualTotalPages, paginatedData: paginatedManualDeliveries, totalItems: manualTotalItems } = usePagination(filteredManualDeliveries, 20); // Pagination pour les rapprochements automatiques const { currentPage: autoCurrentPage, setCurrentPage: setAutoCurrentPage, itemsPerPage: autoItemsPerPage, setItemsPerPage: setAutoItemsPerPage, totalPages: autoTotalPages, paginatedData: paginatedAutoDeliveries, totalItems: autoTotalItems } = usePagination(filteredAutomaticDeliveries, 20); const canModify = user?.role === 'directeur' || user?.role === 'admin'; if (isLoading) { return
Chargement...
; } return (
{/* Header */}

Rapprochement BL/Factures

Gestion des rapprochements manuels et automatiques

{manualReconciliationDeliveries.length} manuels {automaticReconciliationDeliveries.length} automatiques
{/* Onglets */} Rapprochement Manuel {manualReconciliationDeliveries.length} Rapprochement Automatique {automaticReconciliationDeliveries.length}
{/* Filters */}
setSearchTerm(e.target.value)} className="pl-10 border border-gray-300 shadow-sm w-full" />
{/* Contenu des onglets */} {filteredManualDeliveries.length === 0 ? (

Aucun rapprochement manuel trouvé

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

) : (
{/* Pagination du haut */}
{paginatedManualDeliveries.map((delivery: any) => ( ))}
Fournisseur N° BL Date Livr. Montant BL Ref. Facture Montant Fact. Écart Magasin Actions
{delivery.supplier?.name}
{delivery.blNumber || ( handleOpenModal(delivery)} title="Cliquer pour modifier" > Non renseigné )}
{safeFormat(delivery.scheduledDate, 'dd/MM/yy')}
{delivery.blAmount ? `${parseFloat(delivery.blAmount).toFixed(2)}€` : handleOpenModal(delivery)} title="Cliquer pour modifier" > Non renseigné }
{/* Icône de vérification de facture */} {(delivery.group?.nocodbTableName || delivery.group?.nocodbConfigId || delivery.group?.webhookUrl) && (
{verifyingDeliveries.has(delivery.id) ? ( ) : verificationResults[delivery.id] ? ( verificationResults[delivery.id].exists ? ( ) : ( ) ) : ( )}
)}
{delivery.invoiceReference || ( handleOpenModal(delivery)} title="Cliquer pour modifier" > Non renseigné )}
{delivery.invoiceAmount ? `${parseFloat(delivery.invoiceAmount).toFixed(2)}€` : handleOpenModal(delivery)} title="Cliquer pour modifier" > Non renseigné }
{(() => { const blAmount = delivery.blAmount ? parseFloat(delivery.blAmount) : 0; const invoiceAmount = delivery.invoiceAmount ? parseFloat(delivery.invoiceAmount) : 0; if (blAmount && invoiceAmount) { const diff = invoiceAmount - blAmount; const diffAbs = Math.abs(diff); return (
10 ? 'text-red-600' : 'text-orange-600' }`}> {diff > 0 ? '+' : ''}{diff.toFixed(2)}€
); } return -; })()}
{delivery.group?.name}
{shouldShowInvoiceButton(delivery) && ( )} {!delivery.reconciled ? ( <> {permissions.canValidate('reconciliation') && ( )} {permissions.canDelete('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') || permissions.canValidate('reconciliation')) ? ( " Vous pouvez dévalider ces rapprochements si nécessaire." ) : ( " Seuls les utilisateurs autorisés peuvent dévalider ces rapprochements." )}

{filteredAutomaticDeliveries.length === 0 ? (

Aucun rapprochement automatique trouvé

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

) : (
{/* Pagination du haut */}
{paginatedAutoDeliveries.map((delivery: any) => ( ))}
Fournisseur AUTO N° BL Date Livr. Date Valid. Magasin Actions
{delivery.supplier?.name}
{delivery.blNumber || ( handleOpenModal(delivery)} title="Cliquer pour modifier" > Non renseigné )}
{safeFormat(delivery.scheduledDate, 'dd/MM/yy')}
{delivery.validatedAt ? safeFormat(delivery.validatedAt, 'dd/MM/yy HH:mm') : Non validé }
{delivery.group?.name}
{shouldShowInvoiceButton(delivery) && ( )} {(permissions.canEdit('reconciliation') || permissions.canValidate('reconciliation')) && ( )} {permissions.canDelete('reconciliation') && ( )}
{/* Pagination du bas */}
)}
{/* Modal de rapprochement */} {selectedDelivery && ( )} {/* Modal d'envoi de facture */} Envoyer Facture
{selectedDeliveryForInvoice && (
{selectedDeliveryForInvoice.supplier?.name}
BL: {selectedDeliveryForInvoice.blNumber || 'Non renseigné'}
Magasin: {selectedDeliveryForInvoice.group?.name}
)}
{selectedFile && (
Fichier sélectionné: {selectedFile.name}
)}
{selectedDeliveryForInvoice?.group?.webhookUrl && (
Envoi via: {selectedDeliveryForInvoice.group.webhookUrl}
)}
{/* Modal d'attente pour le traitement du webhook */} {}}> Traitement en cours
{processingSeconds}s

Traitement de votre facture en cours...

Le workflow peut prendre jusqu'à 1 minute.
Veuillez patienter.

{processingSeconds < 60 ? `${60 - processingSeconds}s restantes (max)` : 'Finalisation...'}
{selectedDeliveryForInvoice && (
{selectedDeliveryForInvoice.supplier?.name}
BL: {selectedDeliveryForInvoice.blNumber || 'Non renseigné'}
Fichier: {selectedFile?.name}
)}
{/* Modal de commentaires de rapprochement */} Commentaires de rapprochement {selectedDeliveryForComment && (
{selectedDeliveryForComment.supplier?.name}
BL: {selectedDeliveryForComment.blNumber || 'Non renseigné'} | Facture: {selectedDeliveryForComment.invoiceReference || 'Non renseignée'}
Date prévue: {selectedDeliveryForComment.scheduledDate ? new Date(selectedDeliveryForComment.scheduledDate).toLocaleDateString('fr-FR') : 'Non renseignée'}
{/* Composant de commentaires de rapprochement */}
)}
); }