mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-21 14:08:15 +02:00
Merge pull request #20 from R0m1k3/1.1
Add pagination and card design to the reconciliation page
This commit is contained in:
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