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 } from "lucide-react"; 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); // Récupérer les fournisseurs pour la logique automatique const { data: suppliers = [] } = 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; }); // Fonction pour ouvrir le modal d'édition const handleOpenModal = (delivery: any) => { setSelectedDelivery(delivery); setIsModalOpen(true); }; // Fonction pour valider un rapprochement rapidement (avec validation des données) const handleQuickValidate = async (delivery: any) => { if (!permissions.canValidate('reconciliation')) { toast({ title: "Accès refusé", description: "Vous n'avez pas les permissions pour valider les rapprochements", variant: "destructive", }); return; } // Vérifier que les données minimales sont présentes if (!delivery.blNumber || delivery.blNumber.trim() === '') { toast({ title: "Données manquantes", description: "Le numéro de BL est requis. Cliquez sur 'Voir les détails' pour le renseigner.", variant: "destructive", }); return; } if (!window.confirm("Êtes-vous sûr de vouloir valider ce rapprochement ?")) { return; } try { await apiRequest(`/api/deliveries/${delivery.id}`, "PUT", { reconciled: true, validatedAt: new Date().toISOString() }); toast({ title: "Succès", description: "Rapprochement validé avec succès", }); queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); } catch (error) { toast({ title: "Erreur", description: "Impossible de valider le rapprochement", variant: "destructive", }); } }; // Fonction pour dévalider un rapprochement (admins uniquement) const handleDevalidateReconciliation = async (deliveryId: number) => { if (user?.role !== 'admin') { toast({ title: "Accès refusé", description: "Seuls les administrateurs peuvent 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", }); queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); } catch (error) { toast({ title: "Erreur", description: "Impossible de dévalider le rapprochement", variant: "destructive", }); } }; // Fonction pour supprimer une livraison (admins uniquement) 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", }); } }; // Fonction pour dévalider un rapprochement automatique (admins uniquement) const handleDevalidateAutoReconciliation = async (deliveryId: number) => { if (user?.role !== 'admin') { toast({ title: "Accès refusé", description: "Seuls les administrateurs peuvent dévalider les rapprochements automatiques", variant: "destructive", }); return; } if (!window.confirm("Êtes-vous sûr de vouloir dévalider ce rapprochement automatique ?")) { return; } try { await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", { reconciled: false, validatedAt: null }); toast({ title: "Succès", description: "Rapprochement automatique dévalidé avec succès", }); queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); } catch (error) { toast({ title: "Erreur", description: "Impossible de dévalider le rapprochement", variant: "destructive", }); } }; // 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); const canModify = user?.role === 'directeur' || user?.role === 'admin'; 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. Écart Magasin 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é }
{(() => { const blAmount = delivery.blAmount ? parseFloat(delivery.blAmount) : 0; const invoiceAmount = delivery.invoiceAmount ? parseFloat(delivery.invoiceAmount) : 0; if (blAmount && invoiceAmount) { const diff = blAmount - invoiceAmount; const diffAbs = Math.abs(diff); return (
10 ? 'text-red-600' : 'text-orange-600' }`}> {diff > 0 ? '+' : ''}{diff.toFixed(2)}€
); } return -; })()}
{delivery.group?.name}
{/* Actions selon le statut de rapprochement et les permissions */} {!delivery.reconciled ? ( // Livraison non rapprochée : Valider + Supprimer <> {permissions.canValidate('reconciliation') && ( )} {permissions.canDelete('reconciliation') && ( )} ) : ( // Livraison rapprochée : Dévalider (admin seulement) + Supprimer <> {user?.role === 'admin' && ( )} {permissions.canDelete('reconciliation') && ( )} )} {/* Bouton Modifier : disponible seulement si pas encore validé */} {!delivery.reconciled && ( )}
)}
{/* 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. {user?.role === 'admin' ? ( " Vous pouvez dévalider ces rapprochements si nécessaire." ) : ( " Seuls les administrateurs peuvent dévalider 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 Montant Fact. Écart Magasin Actions
{delivery.supplier?.name}
{delivery.blNumber || '-'}
{safeFormat(delivery.scheduledDate, 'dd/MM/yy')}
{delivery.reconciled && delivery.updatedAt ? safeFormat(delivery.updatedAt, 'dd/MM/yy') : '-' }
{delivery.blAmount ? `${parseFloat(delivery.blAmount).toFixed(2)}€` : '-' }
{delivery.invoiceReference || '-'}
{delivery.invoiceAmount ? `${parseFloat(delivery.invoiceAmount).toFixed(2)}€` : '-' }
{(() => { const blAmount = delivery.blAmount ? parseFloat(delivery.blAmount) : 0; const invoiceAmount = delivery.invoiceAmount ? parseFloat(delivery.invoiceAmount) : 0; if (blAmount && invoiceAmount) { const diff = blAmount - invoiceAmount; const diffAbs = Math.abs(diff); return (
10 ? 'text-red-600' : 'text-orange-600' }`}> {diff > 0 ? '+' : ''}{diff.toFixed(2)}€
); } return -; })()}
{delivery.group?.name}
{user?.role === 'admin' && delivery.reconciled && ( )} {user?.role === 'admin' && ( )} {/* Bouton Modifier : disponible seulement si pas encore validé */} {!delivery.reconciled && ( )}
)}
{/* Modal de rapprochement */} setIsModalOpen(false)} delivery={selectedDelivery} onSave={() => { // Rafraîchir les données après sauvegarde queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); }} />
); }