mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-20 05:28:22 +02:00
Add pagination and card design to the reconciliation page
Update the BLReconciliation page to include pagination controls at the top and bottom of the table, limit to 20 items per page, and refactor the UI to use a card-based layout. Replit-Commit-Author: Agent Replit-Commit-Session-Id: b163d4c0-de5e-4f4e-a9c0-aed4c7049718 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/b163d4c0-de5e-4f4e-a9c0-aed4c7049718/pf3Qeu9
This commit is contained in:
1 parent
0fa48697a1
commit
aee6f7cf5c
1 file changed
+408
-421
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user