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
+153 -166
+153 -166
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,7 +298,8 @@ 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>
<CardContent className="p-4">
<div className="relative max-w-md"> <div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" /> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input <Input
@@ -316,7 +309,8 @@ export default function BLReconciliation() {
className="pl-10 border border-gray-300 shadow-sm" 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,10 +326,29 @@ export default function BLReconciliation() {
</p> </p>
</div> </div>
) : ( ) : (
<div className="bg-white border border-gray-200 shadow-lg overflow-hidden rounded-lg"> <Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Edit className="w-5 h-5" />
Rapprochements Manuels
</CardTitle>
</CardHeader>
<CardContent>
{/* Pagination du haut */}
<Pagination
currentPage={manualCurrentPage}
totalPages={manualTotalPages}
totalItems={manualTotalItems}
itemsPerPage={manualItemsPerPage}
onPageChange={setManualCurrentPage}
onItemsPerPageChange={setManualItemsPerPage}
className="mb-4"
/>
<div className="border border-gray-200 rounded-lg overflow-hidden">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200"> <table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50 border-b border-gray-200"> <thead className="bg-gray-50">
<tr> <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">
Fournisseur Fournisseur
@@ -367,7 +380,7 @@ export default function BLReconciliation() {
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{filteredManualDeliveries.map((delivery: any) => ( {paginatedManualDeliveries.map((delivery: any) => (
<tr <tr
key={delivery.id} key={delivery.id}
className={`hover:bg-gray-50 ${ className={`hover:bg-gray-50 ${
@@ -444,9 +457,7 @@ export default function BLReconciliation() {
</td> </td>
<td className="px-3 py-2 text-sm"> <td className="px-3 py-2 text-sm">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{/* Actions selon le statut de rapprochement et les permissions */}
{!delivery.reconciled ? ( {!delivery.reconciled ? (
// Livraison non rapprochée : Valider + Supprimer
<> <>
{permissions.canValidate('reconciliation') && ( {permissions.canValidate('reconciliation') && (
<button <button
@@ -468,7 +479,6 @@ export default function BLReconciliation() {
)} )}
</> </>
) : ( ) : (
// Livraison rapprochée : Dévalider (admin seulement) + Supprimer
<> <>
{user?.role === 'admin' && ( {user?.role === 'admin' && (
<button <button
@@ -490,7 +500,6 @@ export default function BLReconciliation() {
)} )}
</> </>
)} )}
{/* Bouton Modifier : disponible seulement si pas encore validé */}
{!delivery.reconciled && ( {!delivery.reconciled && (
<button <button
onClick={() => handleOpenModal(delivery)} onClick={() => handleOpenModal(delivery)}
@@ -508,6 +517,19 @@ export default function BLReconciliation() {
</table> </table>
</div> </div>
</div> </div>
{/* Pagination du bas */}
<Pagination
currentPage={manualCurrentPage}
totalPages={manualTotalPages}
totalItems={manualTotalItems}
itemsPerPage={manualItemsPerPage}
onPageChange={setManualCurrentPage}
onItemsPerPageChange={setManualItemsPerPage}
className="mt-4"
/>
</CardContent>
</Card>
)} )}
</TabsContent> </TabsContent>
@@ -541,10 +563,29 @@ export default function BLReconciliation() {
</p> </p>
</div> </div>
) : ( ) : (
<div className="bg-white border border-gray-200 shadow-lg overflow-hidden rounded-lg"> <Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Settings className="w-5 h-5" />
Rapprochements Automatiques
</CardTitle>
</CardHeader>
<CardContent>
{/* Pagination du haut */}
<Pagination
currentPage={autoCurrentPage}
totalPages={autoTotalPages}
totalItems={autoTotalItems}
itemsPerPage={autoItemsPerPage}
onPageChange={setAutoCurrentPage}
onItemsPerPageChange={setAutoItemsPerPage}
className="mb-4"
/>
<div className="border border-gray-200 rounded-lg overflow-hidden">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200"> <table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50 border-b border-gray-200"> <thead className="bg-gray-50">
<tr> <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">
Fournisseur Fournisseur
@@ -559,18 +600,6 @@ export default function BLReconciliation() {
<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">
Date Valid. Date Valid.
</th> </th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Montant BL
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Ref. Facture
</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Montant Fact.
</th>
<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"> <th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Magasin Magasin
</th> </th>
@@ -580,28 +609,18 @@ export default function BLReconciliation() {
</tr> </tr>
</thead> </thead>
<tbody className="bg-white divide-y divide-gray-200"> <tbody className="bg-white divide-y divide-gray-200">
{filteredAutomaticDeliveries.map((delivery: any) => ( {paginatedAutoDeliveries.map((delivery: any) => (
<tr <tr key={delivery.id} className="hover:bg-gray-50 bg-green-50">
key={delivery.id}
className={`hover:bg-gray-50 ${
delivery.reconciled === true
? 'bg-gray-100 opacity-60 text-gray-500'
: delivery.reconciled === false
? 'bg-red-50 border-l-4 border-red-400'
: 'bg-green-50'
}`}
>
<td className="px-3 py-2 text-sm"> <td className="px-3 py-2 text-sm">
<div className="flex items-center space-x-2">
<div className="font-medium text-gray-900 truncate max-w-32"> <div className="font-medium text-gray-900 truncate max-w-32">
{delivery.supplier?.name} {delivery.supplier?.name}
</div> </div>
<Settings className="w-3 h-3 text-blue-500" />
</div>
</td> </td>
<td className="px-3 py-2 text-sm"> <td className="px-3 py-2 text-sm">
<div className="text-gray-900 font-medium"> <div className="text-gray-900">
{delivery.blNumber || '-'} {delivery.blNumber || (
<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">
@@ -611,52 +630,12 @@ export default function BLReconciliation() {
</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.reconciled && delivery.updatedAt ? {delivery.validatedAt ?
safeFormat(delivery.updatedAt, 'dd/MM/yy') : safeFormat(delivery.validatedAt, 'dd/MM/yy HH:mm') :
'-' <span className="text-gray-400 italic text-xs">Non validé</span>
} }
</div> </div>
</td> </td>
<td className="px-3 py-2 text-sm">
<div className="font-medium text-gray-900">
{delivery.blAmount ?
`${parseFloat(delivery.blAmount).toFixed(2)}€` :
'-'
}
</div>
</td>
<td className="px-3 py-2 text-sm">
<div className="text-gray-900">
{delivery.invoiceReference || '-'}
</div>
</td>
<td className="px-3 py-2 text-sm">
<div className="text-gray-900">
{delivery.invoiceAmount ?
`${parseFloat(delivery.invoiceAmount).toFixed(2)}€` :
'-'
}
</div>
</td>
<td className="px-3 py-2 text-sm">
{(() => {
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 (
<div className={`font-medium ${
diff === 0 ? 'text-green-600' :
diffAbs > 10 ? 'text-red-600' : 'text-orange-600'
}`}>
{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"> <td className="px-3 py-2 text-sm">
<div className="text-gray-900"> <div className="text-gray-900">
{delivery.group?.name} {delivery.group?.name}
@@ -664,16 +643,16 @@ export default function BLReconciliation() {
</td> </td>
<td className="px-3 py-2 text-sm"> <td className="px-3 py-2 text-sm">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{user?.role === 'admin' && delivery.reconciled && ( {user?.role === 'admin' && (
<button <button
onClick={() => handleDevalidateAutoReconciliation(delivery.id)} onClick={() => handleDevalidateReconciliation(delivery.id)}
className="text-gray-600 hover:text-orange-600 transition-colors duration-200 p-1 hover:bg-orange-50 rounded" 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" title="Dévalider le rapprochement automatique"
> >
<Ban className="w-4 h-4" /> <Ban className="w-4 h-4" />
</button> </button>
)} )}
{user?.role === 'admin' && ( {permissions.canDelete('reconciliation') && (
<button <button
onClick={() => handleDeleteDelivery(delivery.id)} onClick={() => handleDeleteDelivery(delivery.id)}
className="text-gray-600 hover:text-red-600 transition-colors duration-200 p-1 hover:bg-red-50 rounded" className="text-gray-600 hover:text-red-600 transition-colors duration-200 p-1 hover:bg-red-50 rounded"
@@ -682,16 +661,13 @@ export default function BLReconciliation() {
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</button> </button>
)} )}
{/* Bouton Modifier : disponible seulement si pas encore validé */}
{!delivery.reconciled && (
<button <button
onClick={() => handleOpenModal(delivery)} onClick={() => handleOpenModal(delivery)}
className="text-gray-600 hover:text-blue-600 transition-colors duration-200 p-1 hover:bg-blue-50 rounded" 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" title="Voir les détails"
> >
<Settings className="w-4 h-4" /> <Eye className="w-4 h-4" />
</button> </button>
)}
</div> </div>
</td> </td>
</tr> </tr>
@@ -700,20 +676,31 @@ export default function BLReconciliation() {
</table> </table>
</div> </div>
</div> </div>
{/* Pagination du bas */}
<Pagination
currentPage={autoCurrentPage}
totalPages={autoTotalPages}
totalItems={autoTotalItems}
itemsPerPage={autoItemsPerPage}
onPageChange={setAutoCurrentPage}
onItemsPerPageChange={setAutoItemsPerPage}
className="mt-4"
/>
</CardContent>
</Card>
)} )}
</TabsContent> </TabsContent>
</Tabs> </Tabs>
{/* Modal de rapprochement */} {/* Modal de rapprochement */}
{isModalOpen && selectedDelivery && (
<ReconciliationModal <ReconciliationModal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
delivery={selectedDelivery} delivery={selectedDelivery}
onSave={() => { onSave={handleSaveReconciliation}
// Rafraîchir les données après sauvegarde onClose={handleCloseModal}
queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] });
}}
/> />
)}
</div> </div>
); );
} }