Merge pull request #20 from R0m1k3/1.1

Add pagination and card design to the reconciliation page
This commit is contained in:
LogiFlow authored and GitHub committed 2025-08-12 14:11:24 +02:00
commit 6914b4998d
1 file changed
+408 -421
+408 -421
View File
@@ -4,13 +4,15 @@ import { safeFormat } from "@/lib/dateUtils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge"; 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { apiRequest } from "@/lib/queryClient"; import { apiRequest } from "@/lib/queryClient";
import { useStore } from "@/components/Layout"; import { useStore } from "@/components/Layout";
import { useAuthUnified } from "@/hooks/useAuthUnified"; import { useAuthUnified } from "@/hooks/useAuthUnified";
import { usePermissions } from "@shared/permissions"; import { usePermissions } from "@shared/permissions";
import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban } from "lucide-react"; import { Pagination, usePagination } from "@/components/ui/pagination";
import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban, Filter } from "lucide-react";
import ReconciliationModal from "@/components/modals/ReconciliationModal"; import ReconciliationModal from "@/components/modals/ReconciliationModal";
export default function BLReconciliation() { export default function BLReconciliation() {
@@ -73,50 +75,52 @@ export default function BLReconciliation() {
return filtered.sort((a: any, b: any) => new Date(b.deliveredDate || b.updatedAt).getTime() - new Date(a.deliveredDate || a.updatedAt).getTime()); return filtered.sort((a: any, b: any) => new Date(b.deliveredDate || b.updatedAt).getTime() - new Date(a.deliveredDate || a.updatedAt).getTime());
}, },
enabled: !!user
}); });
// Séparer les livraisons selon le mode de rapprochement du fournisseur // Séparer les livraisons par mode de rapprochement
const manualReconciliationDeliveries = deliveriesWithBL.filter((delivery: any) => { const manualReconciliationDeliveries = deliveriesWithBL.filter((delivery: any) => {
const supplier = suppliers.find((s: any) => s.id === delivery.supplierId); const supplier = suppliers.find(s => s.id === delivery.supplierId);
return !supplier?.automaticReconciliation; return !supplier?.isAutoReconciliation;
}); });
const automaticReconciliationDeliveries = deliveriesWithBL.filter((delivery: any) => { const automaticReconciliationDeliveries = deliveriesWithBL.filter((delivery: any) => {
const supplier = suppliers.find((s: any) => s.id === delivery.supplierId); const supplier = suppliers.find(s => s.id === delivery.supplierId);
return supplier?.automaticReconciliation; return supplier?.isAutoReconciliation;
}); });
// Fonction pour ouvrir le modal d'édition // Fonctions de gestion
const handleOpenModal = (delivery: any) => { const handleOpenModal = (delivery: any) => {
setSelectedDelivery(delivery); setSelectedDelivery(delivery);
setIsModalOpen(true); setIsModalOpen(true);
}; };
// Fonction pour valider un rapprochement rapidement (avec validation des données) const handleCloseModal = () => {
setSelectedDelivery(null);
setIsModalOpen(false);
};
const handleSaveReconciliation = async (data: any) => {
try {
await apiRequest(`/api/deliveries/${selectedDelivery.id}`, "PUT", data);
toast({
title: "Succès",
description: "Données de rapprochement mises à jour",
});
queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] });
handleCloseModal();
} catch (error) {
toast({
title: "Erreur",
description: "Impossible de mettre à jour les données",
variant: "destructive",
});
}
};
const handleQuickValidate = async (delivery: any) => { 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 { try {
await apiRequest(`/api/deliveries/${delivery.id}`, "PUT", { await apiRequest(`/api/deliveries/${delivery.id}`, "PUT", {
reconciled: true, reconciled: true,
@@ -138,7 +142,6 @@ export default function BLReconciliation() {
} }
}; };
// Fonction pour dévalider un rapprochement (admins uniquement)
const handleDevalidateReconciliation = async (deliveryId: number) => { const handleDevalidateReconciliation = async (deliveryId: number) => {
if (user?.role !== 'admin') { if (user?.role !== 'admin') {
toast({ toast({
@@ -174,7 +177,6 @@ export default function BLReconciliation() {
} }
}; };
// Fonction pour supprimer une livraison (admins uniquement)
const handleDeleteDelivery = async (deliveryId: number) => { const handleDeleteDelivery = async (deliveryId: number) => {
if (!permissions.canDelete('reconciliation')) { if (!permissions.canDelete('reconciliation')) {
toast({ toast({
@@ -207,42 +209,6 @@ export default function BLReconciliation() {
} }
}; };
// 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 // Filtrage des livraisons par recherche
const filterDeliveries = (deliveries: any[]) => { const filterDeliveries = (deliveries: any[]) => {
return deliveries.filter((delivery: any) => { return deliveries.filter((delivery: any) => {
@@ -258,11 +224,37 @@ export default function BLReconciliation() {
const filteredManualDeliveries = filterDeliveries(manualReconciliationDeliveries); const filteredManualDeliveries = filterDeliveries(manualReconciliationDeliveries);
const filteredAutomaticDeliveries = filterDeliveries(automaticReconciliationDeliveries); 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'; const canModify = user?.role === 'directeur' || user?.role === 'admin';
if (isLoading) {
return <div className="flex justify-center items-center h-64">Chargement...</div>;
}
return ( return (
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
{/* Header avec onglets */} {/* Header */}
<div className="bg-white border-b border-gray-200 p-6 shadow-sm -m-6 mb-6"> <div className="bg-white border-b border-gray-200 p-6 shadow-sm -m-6 mb-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div> <div>
@@ -306,17 +298,19 @@ export default function BLReconciliation() {
</div> </div>
{/* Filtre de recherche */} {/* Filtre de recherche */}
<div className="bg-gray-50 border border-gray-200 p-4 rounded-lg"> <Card>
<div className="relative max-w-md"> <CardContent className="p-4">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" /> <div className="relative max-w-md">
<Input <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
placeholder="Rechercher par fournisseur, BL ou facture..." <Input
value={searchTerm} placeholder="Rechercher par fournisseur, BL ou facture..."
onChange={(e) => setSearchTerm(e.target.value)} value={searchTerm}
className="pl-10 border border-gray-300 shadow-sm" onChange={(e) => setSearchTerm(e.target.value)}
/> className="pl-10 border border-gray-300 shadow-sm"
</div> />
</div> </div>
</CardContent>
</Card>
{/* Contenu des onglets */} {/* Contenu des onglets */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full"> <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
@@ -332,182 +326,210 @@ export default function BLReconciliation() {
</p> </p>
</div> </div>
) : ( ) : (
<div className="bg-white border border-gray-200 shadow-lg overflow-hidden rounded-lg"> <Card>
<div className="overflow-x-auto"> <CardHeader>
<table className="min-w-full divide-y divide-gray-200"> <CardTitle className="flex items-center gap-2">
<thead className="bg-gray-50 border-b border-gray-200"> <Edit className="w-5 h-5" />
<tr> Rapprochements Manuels
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> </CardTitle>
Fournisseur </CardHeader>
</th> <CardContent>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> {/* Pagination du haut */}
N° BL <Pagination
</th> currentPage={manualCurrentPage}
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> totalPages={manualTotalPages}
Date Livr. totalItems={manualTotalItems}
</th> itemsPerPage={manualItemsPerPage}
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> onPageChange={setManualCurrentPage}
Montant BL onItemsPerPageChange={setManualItemsPerPage}
</th> className="mb-4"
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> />
Ref. Facture
</th> <div className="border border-gray-200 rounded-lg overflow-hidden">
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <div className="overflow-x-auto">
Montant Fact. <table className="min-w-full divide-y divide-gray-200">
</th> <thead className="bg-gray-50">
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <tr>
Écart <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th> Fournisseur
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> </th>
Magasin <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th> N° BL
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> </th>
Actions <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th> Date Livr.
</tr> </th>
</thead> <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<tbody className="bg-white divide-y divide-gray-200"> Montant BL
{filteredManualDeliveries.map((delivery: any) => ( </th>
<tr <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
key={delivery.id} Ref. Facture
className={`hover:bg-gray-50 ${ </th>
delivery.reconciled === true <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
? 'bg-gray-100 opacity-60 text-gray-500' Montant Fact.
: delivery.reconciled === false </th>
? 'bg-red-50 border-l-4 border-red-400' <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
: '' Écart
}`} </th>
> <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<td className="px-3 py-2 text-sm"> Magasin
<div className="font-medium text-gray-900 truncate max-w-32"> </th>
{delivery.supplier?.name} <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</div> Actions
</td> </th>
<td className="px-3 py-2 text-sm"> </tr>
<div className="text-gray-900"> </thead>
{delivery.blNumber || ( <tbody className="bg-white divide-y divide-gray-200">
<span className="text-gray-400 italic text-xs">Non renseigné</span> {paginatedManualDeliveries.map((delivery: any) => (
)} <tr
</div> key={delivery.id}
</td> className={`hover:bg-gray-50 ${
<td className="px-3 py-2 text-sm"> delivery.reconciled === true
<div className="text-gray-900"> ? 'bg-gray-100 opacity-60 text-gray-500'
{safeFormat(delivery.scheduledDate, 'dd/MM/yy')} : delivery.reconciled === false
</div> ? 'bg-red-50 border-l-4 border-red-400'
</td> : ''
<td className="px-3 py-2 text-sm"> }`}
<div className="font-medium text-gray-900"> >
{delivery.blAmount ? <td className="px-3 py-2 text-sm">
`${parseFloat(delivery.blAmount).toFixed(2)}€` : <div className="font-medium text-gray-900 truncate max-w-32">
<span className="text-gray-400 italic text-xs">Non renseigné</span> {delivery.supplier?.name}
} </div>
</div> </td>
</td> <td className="px-3 py-2 text-sm">
<td className="px-3 py-2 text-sm"> <div className="text-gray-900">
<div className="text-gray-900"> {delivery.blNumber || (
{delivery.invoiceReference || ( <span className="text-gray-400 italic text-xs">Non renseigné</span>
<span className="text-gray-400 italic text-xs">Non renseigné</span> )}
)} </div>
</div> </td>
</td> <td className="px-3 py-2 text-sm">
<td className="px-3 py-2 text-sm"> <div className="text-gray-900">
<div className="text-gray-900"> {safeFormat(delivery.scheduledDate, 'dd/MM/yy')}
{delivery.invoiceAmount ? </div>
`${parseFloat(delivery.invoiceAmount).toFixed(2)}€` : </td>
<span className="text-gray-400 italic text-xs">Non renseigné</span> <td className="px-3 py-2 text-sm">
} <div className="font-medium text-gray-900">
</div> {delivery.blAmount ?
</td> `${parseFloat(delivery.blAmount).toFixed(2)}€` :
<td className="px-3 py-2 text-sm"> <span className="text-gray-400 italic text-xs">Non renseigné</span>
{(() => { }
const blAmount = delivery.blAmount ? parseFloat(delivery.blAmount) : 0; </div>
const invoiceAmount = delivery.invoiceAmount ? parseFloat(delivery.invoiceAmount) : 0; </td>
if (blAmount && invoiceAmount) { <td className="px-3 py-2 text-sm">
const diff = blAmount - invoiceAmount; <div className="text-gray-900">
const diffAbs = Math.abs(diff); {delivery.invoiceReference || (
return ( <span className="text-gray-400 italic text-xs">Non renseigné</span>
<div className={`font-medium ${ )}
diff === 0 ? 'text-green-600' : </div>
diffAbs > 10 ? 'text-red-600' : 'text-orange-600' </td>
}`}> <td className="px-3 py-2 text-sm">
{diff > 0 ? '+' : ''}{diff.toFixed(2)}€ <div className="text-gray-900">
</div> {delivery.invoiceAmount ?
); `${parseFloat(delivery.invoiceAmount).toFixed(2)}€` :
} <span className="text-gray-400 italic text-xs">Non renseigné</span>
return <span className="text-gray-400 italic text-xs">-</span>; }
})()} </div>
</td> </td>
<td className="px-3 py-2 text-sm"> <td className="px-3 py-2 text-sm">
<div className="text-gray-900"> {(() => {
{delivery.group?.name} const blAmount = delivery.blAmount ? parseFloat(delivery.blAmount) : 0;
</div> const invoiceAmount = delivery.invoiceAmount ? parseFloat(delivery.invoiceAmount) : 0;
</td> if (blAmount && invoiceAmount) {
<td className="px-3 py-2 text-sm"> const diff = blAmount - invoiceAmount;
<div className="flex items-center space-x-2"> const diffAbs = Math.abs(diff);
{/* Actions selon le statut de rapprochement et les permissions */} return (
{!delivery.reconciled ? ( <div className={`font-medium ${
// Livraison non rapprochée : Valider + Supprimer diff === 0 ? 'text-green-600' :
<> diffAbs > 10 ? 'text-red-600' : 'text-orange-600'
{permissions.canValidate('reconciliation') && ( }`}>
{diff > 0 ? '+' : ''}{diff.toFixed(2)}€
</div>
);
}
return <span className="text-gray-400 italic text-xs">-</span>;
})()}
</td>
<td className="px-3 py-2 text-sm">
<div className="text-gray-900">
{delivery.group?.name}
</div>
</td>
<td className="px-3 py-2 text-sm">
<div className="flex items-center space-x-2">
{!delivery.reconciled ? (
<>
{permissions.canValidate('reconciliation') && (
<button
onClick={() => handleQuickValidate(delivery)}
className="text-gray-600 hover:text-green-600 transition-colors duration-200 p-1 hover:bg-green-50 rounded"
title="Valider le rapprochement"
>
<Check className="w-4 h-4" />
</button>
)}
{permissions.canDelete('reconciliation') && (
<button
onClick={() => handleDeleteDelivery(delivery.id)}
className="text-gray-600 hover:text-red-600 transition-colors duration-200 p-1 hover:bg-red-50 rounded"
title="Supprimer la livraison"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</>
) : (
<>
{user?.role === 'admin' && (
<button
onClick={() => handleDevalidateReconciliation(delivery.id)}
className="text-gray-600 hover:text-orange-600 transition-colors duration-200 p-1 hover:bg-orange-50 rounded"
title="Dévalider le rapprochement"
>
<Ban className="w-4 h-4" />
</button>
)}
{permissions.canDelete('reconciliation') && (
<button
onClick={() => handleDeleteDelivery(delivery.id)}
className="text-gray-600 hover:text-red-600 transition-colors duration-200 p-1 hover:bg-red-50 rounded"
title="Supprimer la livraison"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</>
)}
{!delivery.reconciled && (
<button <button
onClick={() => handleQuickValidate(delivery)} onClick={() => handleOpenModal(delivery)}
className="text-gray-600 hover:text-green-600 transition-colors duration-200 p-1 hover:bg-green-50 rounded" className="text-gray-600 hover:text-blue-600 transition-colors duration-200 p-1 hover:bg-blue-50 rounded"
title="Valider le rapprochement" title="Modifier les données de rapprochement"
> >
<Check className="w-4 h-4" /> <Settings className="w-4 h-4" />
</button> </button>
)} )}
{permissions.canDelete('reconciliation') && ( </div>
<button </td>
onClick={() => handleDeleteDelivery(delivery.id)} </tr>
className="text-gray-600 hover:text-red-600 transition-colors duration-200 p-1 hover:bg-red-50 rounded" ))}
title="Supprimer la livraison" </tbody>
> </table>
<Trash2 className="w-4 h-4" /> </div>
</button> </div>
)}
</> {/* Pagination du bas */}
) : ( <Pagination
// Livraison rapprochée : Dévalider (admin seulement) + Supprimer currentPage={manualCurrentPage}
<> totalPages={manualTotalPages}
{user?.role === 'admin' && ( totalItems={manualTotalItems}
<button itemsPerPage={manualItemsPerPage}
onClick={() => handleDevalidateReconciliation(delivery.id)} onPageChange={setManualCurrentPage}
className="text-gray-600 hover:text-orange-600 transition-colors duration-200 p-1 hover:bg-orange-50 rounded" onItemsPerPageChange={setManualItemsPerPage}
title="Dévalider le rapprochement" className="mt-4"
> />
<Ban className="w-4 h-4" /> </CardContent>
</button> </Card>
)}
{permissions.canDelete('reconciliation') && (
<button
onClick={() => handleDeleteDelivery(delivery.id)}
className="text-gray-600 hover:text-red-600 transition-colors duration-200 p-1 hover:bg-red-50 rounded"
title="Supprimer la livraison"
>
<Trash2 className="w-4 h-4" />
</button>
)}
</>
)}
{/* Bouton Modifier : disponible seulement si pas encore validé */}
{!delivery.reconciled && (
<button
onClick={() => handleOpenModal(delivery)}
className="text-gray-600 hover:text-blue-600 transition-colors duration-200 p-1 hover:bg-blue-50 rounded"
title="Modifier les données de rapprochement"
>
<Settings className="w-4 h-4" />
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)} )}
</TabsContent> </TabsContent>
@@ -541,179 +563,144 @@ export default function BLReconciliation() {
</p> </p>
</div> </div>
) : ( ) : (
<div className="bg-white border border-gray-200 shadow-lg overflow-hidden rounded-lg"> <Card>
<div className="overflow-x-auto"> <CardHeader>
<table className="min-w-full divide-y divide-gray-200"> <CardTitle className="flex items-center gap-2">
<thead className="bg-gray-50 border-b border-gray-200"> <Settings className="w-5 h-5" />
<tr> Rapprochements Automatiques
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> </CardTitle>
Fournisseur </CardHeader>
<Badge variant="secondary" className="ml-2 text-xs">AUTO</Badge> <CardContent>
</th> {/* Pagination du haut */}
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <Pagination
N° BL currentPage={autoCurrentPage}
</th> totalPages={autoTotalPages}
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> totalItems={autoTotalItems}
Date Livr. itemsPerPage={autoItemsPerPage}
</th> onPageChange={setAutoCurrentPage}
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> onItemsPerPageChange={setAutoItemsPerPage}
Date Valid. className="mb-4"
</th> />
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Montant BL <div className="border border-gray-200 rounded-lg overflow-hidden">
</th> <div className="overflow-x-auto">
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <table className="min-w-full divide-y divide-gray-200">
Ref. Facture <thead className="bg-gray-50">
</th> <tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Montant Fact. Fournisseur
</th> <Badge variant="secondary" className="ml-2 text-xs">AUTO</Badge>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> </th>
Écart <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th> N° BL
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> </th>
Magasin <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th> Date Livr.
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> </th>
Actions <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th> Date Valid.
</tr> </th>
</thead> <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<tbody className="bg-white divide-y divide-gray-200"> Magasin
{filteredAutomaticDeliveries.map((delivery: any) => ( </th>
<tr <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
key={delivery.id} Actions
className={`hover:bg-gray-50 ${ </th>
delivery.reconciled === true </tr>
? 'bg-gray-100 opacity-60 text-gray-500' </thead>
: delivery.reconciled === false <tbody className="bg-white divide-y divide-gray-200">
? 'bg-red-50 border-l-4 border-red-400' {paginatedAutoDeliveries.map((delivery: any) => (
: 'bg-green-50' <tr key={delivery.id} className="hover:bg-gray-50 bg-green-50">
}`} <td className="px-3 py-2 text-sm">
> <div className="font-medium text-gray-900 truncate max-w-32">
<td className="px-3 py-2 text-sm"> {delivery.supplier?.name}
<div className="flex items-center space-x-2"> </div>
<div className="font-medium text-gray-900 truncate max-w-32"> </td>
{delivery.supplier?.name} <td className="px-3 py-2 text-sm">
</div> <div className="text-gray-900">
<Settings className="w-3 h-3 text-blue-500" /> {delivery.blNumber || (
</div> <span className="text-gray-400 italic text-xs">Non renseigné</span>
</td> )}
<td className="px-3 py-2 text-sm"> </div>
<div className="text-gray-900 font-medium"> </td>
{delivery.blNumber || '-'} <td className="px-3 py-2 text-sm">
</div> <div className="text-gray-900">
</td> {safeFormat(delivery.scheduledDate, 'dd/MM/yy')}
<td className="px-3 py-2 text-sm"> </div>
<div className="text-gray-900"> </td>
{safeFormat(delivery.scheduledDate, 'dd/MM/yy')} <td className="px-3 py-2 text-sm">
</div> <div className="text-gray-900">
</td> {delivery.validatedAt ?
<td className="px-3 py-2 text-sm"> safeFormat(delivery.validatedAt, 'dd/MM/yy HH:mm') :
<div className="text-gray-900"> <span className="text-gray-400 italic text-xs">Non validé</span>
{delivery.reconciled && delivery.updatedAt ? }
safeFormat(delivery.updatedAt, 'dd/MM/yy') : </div>
'-' </td>
} <td className="px-3 py-2 text-sm">
</div> <div className="text-gray-900">
</td> {delivery.group?.name}
<td className="px-3 py-2 text-sm"> </div>
<div className="font-medium text-gray-900"> </td>
{delivery.blAmount ? <td className="px-3 py-2 text-sm">
`${parseFloat(delivery.blAmount).toFixed(2)}€` : <div className="flex items-center space-x-2">
'-' {user?.role === 'admin' && (
} <button
</div> onClick={() => handleDevalidateReconciliation(delivery.id)}
</td> className="text-gray-600 hover:text-orange-600 transition-colors duration-200 p-1 hover:bg-orange-50 rounded"
<td className="px-3 py-2 text-sm"> title="Dévalider le rapprochement automatique"
<div className="text-gray-900"> >
{delivery.invoiceReference || '-'} <Ban className="w-4 h-4" />
</div> </button>
</td> )}
<td className="px-3 py-2 text-sm"> {permissions.canDelete('reconciliation') && (
<div className="text-gray-900"> <button
{delivery.invoiceAmount ? onClick={() => handleDeleteDelivery(delivery.id)}
`${parseFloat(delivery.invoiceAmount).toFixed(2)}€` : className="text-gray-600 hover:text-red-600 transition-colors duration-200 p-1 hover:bg-red-50 rounded"
'-' title="Supprimer la livraison"
} >
</div> <Trash2 className="w-4 h-4" />
</td> </button>
<td className="px-3 py-2 text-sm"> )}
{(() => { <button
const blAmount = delivery.blAmount ? parseFloat(delivery.blAmount) : 0; onClick={() => handleOpenModal(delivery)}
const invoiceAmount = delivery.invoiceAmount ? parseFloat(delivery.invoiceAmount) : 0; className="text-gray-600 hover:text-blue-600 transition-colors duration-200 p-1 hover:bg-blue-50 rounded"
if (blAmount && invoiceAmount) { title="Voir les détails"
const diff = blAmount - invoiceAmount; >
const diffAbs = Math.abs(diff); <Eye className="w-4 h-4" />
return ( </button>
<div className={`font-medium ${ </div>
diff === 0 ? 'text-green-600' : </td>
diffAbs > 10 ? 'text-red-600' : 'text-orange-600' </tr>
}`}> ))}
{diff > 0 ? '+' : ''}{diff.toFixed(2)}€ </tbody>
</div> </table>
); </div>
} </div>
return <span className="text-gray-400 italic text-xs">-</span>;
})()} {/* Pagination du bas */}
</td> <Pagination
<td className="px-3 py-2 text-sm"> currentPage={autoCurrentPage}
<div className="text-gray-900"> totalPages={autoTotalPages}
{delivery.group?.name} totalItems={autoTotalItems}
</div> itemsPerPage={autoItemsPerPage}
</td> onPageChange={setAutoCurrentPage}
<td className="px-3 py-2 text-sm"> onItemsPerPageChange={setAutoItemsPerPage}
<div className="flex items-center space-x-2"> className="mt-4"
{user?.role === 'admin' && delivery.reconciled && ( />
<button </CardContent>
onClick={() => handleDevalidateAutoReconciliation(delivery.id)} </Card>
className="text-gray-600 hover:text-orange-600 transition-colors duration-200 p-1 hover:bg-orange-50 rounded"
title="Dévalider le rapprochement automatique"
>
<Ban className="w-4 h-4" />
</button>
)}
{user?.role === 'admin' && (
<button
onClick={() => handleDeleteDelivery(delivery.id)}
className="text-gray-600 hover:text-red-600 transition-colors duration-200 p-1 hover:bg-red-50 rounded"
title="Supprimer la livraison"
>
<Trash2 className="w-4 h-4" />
</button>
)}
{/* Bouton Modifier : disponible seulement si pas encore validé */}
{!delivery.reconciled && (
<button
onClick={() => handleOpenModal(delivery)}
className="text-gray-600 hover:text-blue-600 transition-colors duration-200 p-1 hover:bg-blue-50 rounded"
title="Modifier les données de rapprochement"
>
<Settings className="w-4 h-4" />
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)} )}
</TabsContent> </TabsContent>
</Tabs> </Tabs>
{/* Modal de rapprochement */} {/* Modal de rapprochement */}
<ReconciliationModal {isModalOpen && selectedDelivery && (
isOpen={isModalOpen} <ReconciliationModal
onClose={() => setIsModalOpen(false)} delivery={selectedDelivery}
delivery={selectedDelivery} onSave={handleSaveReconciliation}
onSave={() => { onClose={handleCloseModal}
// Rafraîchir les données après sauvegarde />
queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); )}
}}
/>
</div> </div>
); );
} }