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); // É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()); const [autoVerifiedDeliveries, setAutoVerifiedDeliveries] = 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 })); // Pas de toast - affichage silencieux des coches vertes/rouges // Auto-remplissage si facture trouvée (référence facture OU numéro BL) if (result.exists) { console.log('🔍 DEBUG - Résultat complet de vérification:', { deliveryId: variables.deliveryId, exists: result.exists, matchType: result.matchType, invoiceReference: result.invoiceReference, invoiceAmount: result.invoiceAmount, dueDate: result.dueDate, hasInvoiceRef: !!result.invoiceReference, hasAmount: result.invoiceAmount !== undefined && result.invoiceAmount !== null, hasDueDate: !!result.dueDate }); // Auto-remplir les champs dans la livraison via API const updateData: any = {}; // Ajouter la référence de facture SEULEMENT si trouvée via BL (pas déjà renseignée) if (result.invoiceReference && result.matchType === 'bl_number') { updateData.invoiceReference = result.invoiceReference; console.log('✅ Ajout invoiceReference:', result.invoiceReference); } else { console.log('⚠️ Pas d\'ajout invoiceReference:', { hasRef: !!result.invoiceReference, matchType: result.matchType }); } // TOUJOURS mettre à jour le montant si disponible (peu importe le matchType) if (result.invoiceAmount !== undefined && result.invoiceAmount !== null) { updateData.invoiceAmount = result.invoiceAmount; console.log('✅ Ajout invoiceAmount:', result.invoiceAmount); } else { console.log('⚠️ Pas d\'ajout invoiceAmount:', { amount: result.invoiceAmount }); } // TOUJOURS mettre à jour la date d'échéance si disponible (peu importe le matchType) if (result.dueDate) { updateData.dueDate = result.dueDate; console.log('✅ Ajout dueDate:', result.dueDate); } else { console.log('⚠️ Pas d\'ajout dueDate:', { dueDate: result.dueDate }); } console.log('📝 Données finales à sauvegarder:', { deliveryId: variables.deliveryId, updateData, matchType: result.matchType }); // Ne faire l'appel que si on a des données à mettre à jour if (Object.keys(updateData).length > 0) { apiRequest(`/api/deliveries/${variables.deliveryId}`, "PUT", updateData) .then(() => { console.log('✅ Données sauvegardées avec succès'); // Invalider les caches (pas de refetch pour éviter boucles) queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); queryClient.invalidateQueries({ queryKey: ['/api/deliveries'] }); }) .catch((error) => { console.error('❌ Erreur auto-remplissage:', error); }); } else { console.log('⚠️ Aucune donnée à sauvegarder'); } } 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 = manualNotValidatedDeliveries.filter((delivery: any) => (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: any, index: number) => { // 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); } // CAS SPÉCIAL : Livraisons réconciliées (✅) avec cellules vides // Si reconciled=true ET (cellules vides) ET blNumber existe → auto-remplir deliveriesWithBL.forEach((delivery: any) => { if (delivery.reconciled) { const hasEmptyCells = !delivery.invoiceReference || !delivery.invoiceAmount || !delivery.dueDate; const hasBLNumber = delivery.blNumber?.trim(); const notAlreadyAutoVerified = !autoVerifiedDeliveries.has(delivery.id); const notCurrentlyVerifying = !verifyingDeliveries.has(delivery.id); if (hasEmptyCells && hasBLNumber && notAlreadyAutoVerified && notCurrentlyVerifying) { // Livraison réconciliée avec cellules vides → vérifier pour auto-remplir (UNE SEULE FOIS) if (import.meta.env.DEV) { console.log(`🔄 Livraison réconciliée #${delivery.id} avec cellules vides, auto-vérification (première tentative)...`); } // Marquer comme auto-vérifiée AVANT de lancer pour éviter les doublons setAutoVerifiedDeliveries(prev => new Set(prev).add(delivery.id)); // Lancer la vérification sans délai handleVerifyInvoice(delivery, false); } return; // Autres livraisons réconciliées = AUCUNE vérification nécessaire } // VÉRIFICATION AUTOMATIQUE pour afficher les coches // NE vérifier QUE les factures qui n'ont PAS encore de montant renseigné (pas encore trouvées) const hasVerifiableData = delivery.invoiceReference || delivery.blNumber; const hasNoInvoiceAmount = !delivery.invoiceAmount; // Pas encore trouvée dans NocoDB const notAlreadyVerified = !verificationResults[delivery.id]; const notCurrentlyVerifying = !verifyingDeliveries.has(delivery.id); // Ne vérifier que si : a des données ET pas de montant (pas encore trouvée) ET pas déjà vérifiée if (hasVerifiableData && hasNoInvoiceAmount && notAlreadyVerified && notCurrentlyVerifying) { if (import.meta.env.DEV) { console.log(`🔍 Vérification initiale ${delivery.id} (pas encore trouvée):`, { invoiceRef: delivery.invoiceReference, blNumber: delivery.blNumber }); } // Délai pour éviter de surcharger le serveur setTimeout(() => { handleVerifyInvoice(delivery, false); }, Math.random() * 1000); } // Si la facture a déjà un montant → marquer comme trouvée (coche verte) sans vérifier if (hasVerifiableData && delivery.invoiceAmount && !verificationResults[delivery.id]) { newVerificationResults[delivery.id] = { exists: true, matchType: delivery.invoiceReference ? 'invoice_reference' : 'bl_number', fromCache: true, permanent: true, invoiceAmount: delivery.invoiceAmount }; hasNewReconciledResults = true; } }); }, [deliveriesWithBL, suppliers, verificationResults, verifyingDeliveries]); // Séparer les livraisons : non validées manuelles et toutes les validées const manualNotValidatedDeliveries = deliveriesWithBL.filter((delivery: any) => { const supplier = suppliers.find(s => s.id === delivery.supplierId); const isManual = supplier?.automaticReconciliation !== true; const isNotValidated = delivery.reconciled !== true && delivery.reconciled !== 1; return isManual && isNotValidated; }); const allValidatedDeliveries = deliveriesWithBL.filter((delivery: any) => { return delivery.reconciled === true || delivery.reconciled === 1; }); // Fonctions de gestion const handleOpenModal = (delivery: any) => { setSelectedDelivery(delivery); setIsModalOpen(true); }; const handleCloseModal = () => { setSelectedDelivery(null); setIsModalOpen(false); }; const handleSaveReconciliation = async () => { try { // Force refetch immédiat pour mettre à jour l'affichage await queryClient.refetchQueries({ queryKey: ['/api/deliveries/bl'] }); await queryClient.refetchQueries({ queryKey: ['/api/deliveries'] }); 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('webhookUrl', selectedDeliveryForInvoice.group.webhookUrl); formData.append('supplier', selectedDeliveryForInvoice.supplier?.name || ''); formData.append('blNumber', selectedDeliveryForInvoice.blNumber || ''); formData.append('type', 'Facture'); // Envoyer via route backend proxy (résout problèmes CORS) const response = await fetch('/api/reconciliation/send-invoice', { method: 'POST', body: formData, credentials: 'include' }); if (!response.ok) { const errorData = await response.json().catch(() => ({ error: 'Erreur inconnue' })); throw new Error(errorData.error || `Erreur ${response.status}`); } 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", }); // Mise à jour optimiste du cache local pour disparition immédiate queryClient.setQueryData(['/api/deliveries/bl', selectedStoreId], (oldData: any) => { if (!oldData) return oldData; return oldData.map((d: any) => d.id === delivery.id ? { ...d, reconciled: true, validatedAt: new Date().toISOString() } : d ); }); // Force refetch pour synchroniser avec le serveur queryClient.refetchQueries({ queryKey: ['/api/deliveries/bl'] }); queryClient.refetchQueries({ queryKey: ['/api/deliveries'] }); } catch (error) { // En cas d'erreur, refetch pour annuler la mise à jour optimiste queryClient.refetchQueries({ queryKey: ['/api/deliveries/bl'] }); 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", }); // Force refetch immédiat pour déplacer la facture dans l'onglet manuel await queryClient.refetchQueries({ queryKey: ['/api/deliveries/bl'] }); await queryClient.refetchQueries({ queryKey: ['/api/deliveries'] }); } 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", }); // Mise à jour optimiste du cache local pour disparition immédiate queryClient.setQueryData(['/api/deliveries/bl', selectedStoreId], (oldData: any) => { if (!oldData) return oldData; return oldData.filter((d: any) => d.id !== deliveryId); }); // Force refetch pour synchroniser avec le serveur queryClient.refetchQueries({ queryKey: ['/api/deliveries/bl'] }); queryClient.refetchQueries({ queryKey: ['/api/deliveries'] }); } catch (error) { // En cas d'erreur, refetch pour annuler la mise à jour optimiste queryClient.refetchQueries({ queryKey: ['/api/deliveries/bl'] }); toast({ title: "Erreur", description: "Impossible de supprimer la livraison", variant: "destructive", }); } }; // Filtrage des livraisons par recherche uniquement const filterDeliveries = (deliveries: any[]) => { if (!searchTerm) return deliveries; const searchLower = searchTerm.toLowerCase(); return deliveries.filter((delivery: any) => { return ( delivery.supplier?.name?.toLowerCase().includes(searchLower) || delivery.blNumber?.toLowerCase().includes(searchLower) || delivery.invoiceReference?.toLowerCase().includes(searchLower) || delivery.group?.name?.toLowerCase().includes(searchLower) ); }); }; const filteredManualDeliveries = filterDeliveries(manualNotValidatedDeliveries); const filteredValidatedDeliveries = filterDeliveries(allValidatedDeliveries); // 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 livraisons validées const { currentPage: validatedCurrentPage, setCurrentPage: setValidatedCurrentPage, itemsPerPage: validatedItemsPerPage, setItemsPerPage: setValidatedItemsPerPage, totalPages: validatedTotalPages, paginatedData: paginatedValidatedDeliveries, totalItems: validatedTotalItems } = usePagination(filteredValidatedDeliveries, 20); const canModify = user?.role === 'directeur' || user?.role === 'admin'; if (isLoading) { return
Chargement...
; } return (
{/* Header */}

Rapprochement BL/Factures

Gestion des rapprochements manuels et automatiques

{manualNotValidatedDeliveries.length} à traiter {allValidatedDeliveries.length} validées
{/* Onglets */} Rapprochement Manuel {manualNotValidatedDeliveries.length} Livraisons Validées {allValidatedDeliveries.length}
{/* Barre de recherche */}
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. Échéance É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é }
{delivery.dueDate ? ( {safeFormat(delivery.dueDate, 'dd/MM/yyyy')} ) : ( - )}
{(() => { 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') && verificationResults[delivery.id]?.exists === true ? ( ) : permissions.canValidate('reconciliation') && verificationResults[delivery.id]?.exists === false ? ( ) : permissions.canValidate('reconciliation') ? ( ) : null} {permissions.canDelete('reconciliation') && ( )} ) : ( <> {user?.role === 'admin' && ( )} {permissions.canDelete('reconciliation') && ( )} )}
{/* Pagination du bas */}
)}
{/* Message d'information */}

Livraisons validées

Cette section regroupe toutes les livraisons validées (manuellement ou automatiquement). {(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." )}

{filteredValidatedDeliveries.length === 0 ? (

Aucune livraison validée trouvée

Les livraisons validées apparaîtront ici.

) : (
{/* Pagination du haut */}
{paginatedValidatedDeliveries.map((delivery: any) => { const supplier = suppliers.find(s => s.id === delivery.supplierId); const isAutomatic = supplier?.automaticReconciliation === true; const ecart = delivery.blAmount && delivery.invoiceAmount ? ((parseFloat(delivery.invoiceAmount) - parseFloat(delivery.blAmount)) / parseFloat(delivery.blAmount) * 100).toFixed(1) : null; // Log pour déboguer le problème des commentaires if (import.meta.env.DEV && delivery.reconciliationCommentsCount) { console.log('🔍 Validated delivery comments:', { deliveryId: delivery.id, commentsCount: delivery.reconciliationCommentsCount, hasComments: delivery.reconciliationCommentsCount > 0 }); } return ( ); })}
Fournisseur N° BL Date Livr. Ref. Facture Montant BL Montant Fact. Échéance Écart Magasin Actions
{delivery.supplier?.name}
{isAutomatic && ( AUTO )}
{delivery.blNumber || ( Non renseigné )}
{safeFormat(delivery.scheduledDate, 'dd/MM/yy')}
{delivery.invoiceReference || ( Non renseignée )}
{delivery.blAmount ? `${parseFloat(delivery.blAmount).toFixed(2)}€` : Non renseigné }
{delivery.invoiceAmount ? `${parseFloat(delivery.invoiceAmount).toFixed(2)}€` : Non renseigné }
{delivery.dueDate ? ( {safeFormat(delivery.dueDate, 'dd/MM/yyyy')} ) : ( - )}
{ecart !== null ? ( 5 ? "destructive" : "secondary"} className="text-xs" > {parseFloat(ecart) > 0 ? '+' : ''}{ecart}% ) : ( - )}
{delivery.group?.name}
{(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 */}
)}
); }