mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-18 20:57:40 +02:00
Restore stable version of reconciliation page and update notifications
Rollback BLReconciliation component to a previous stable state and switch to standard shadcn/ui Toaster, resolving React error #310. 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/J7uLHW5
This commit is contained in:
1 parent
69c3d6805a
commit
2f1ed03fa1
4 files changed
+1088
-530
No files matched your search
@@ -0,0 +1,96 @@
|
|||||||
|
# Rollback Solution - React Error #310
|
||||||
|
|
||||||
|
## ✅ Problème Identifié
|
||||||
|
L'erreur React #310 en production est apparue **après l'ajout des actions sur les lignes et des 2 onglets**. La version précédente (backup) fonctionnait parfaitement.
|
||||||
|
|
||||||
|
## 🔄 Action Effectuée
|
||||||
|
**Rollback vers la version fonctionnelle :**
|
||||||
|
- `BLReconciliation_backup.tsx` → `BLReconciliation.tsx` (restauré)
|
||||||
|
- `BLReconciliation.tsx` récente → `BLReconciliation_recent_broken.tsx` (sauvegardé)
|
||||||
|
- Restauration du système de notifications shadcn/ui standard
|
||||||
|
|
||||||
|
## 📊 Comparaison des Versions
|
||||||
|
|
||||||
|
### Version Backup (Fonctionnelle) - 21KB
|
||||||
|
✅ **Caractéristiques :**
|
||||||
|
- Interface simple avec 2 onglets (Manuel/Automatique)
|
||||||
|
- Actions limitées : Voir détails + Dévalider automatique uniquement
|
||||||
|
- Pas de pagination complexe
|
||||||
|
- Une seule fonction d'action : `handleDevalidateAutoReconciliation`
|
||||||
|
- shadcn/ui standard (Button, Tabs, Badge, Input)
|
||||||
|
|
||||||
|
### Version Récente (Cassée) - 35KB
|
||||||
|
❌ **Problèmes introduits :**
|
||||||
|
- Actions complexes : Valider/Dévalider/Supprimer
|
||||||
|
- Système de pagination complet avec mutations multiples
|
||||||
|
- Composants UI personnalisés (RobustButton, RobustInput, etc.)
|
||||||
|
- Système de permissions complexe
|
||||||
|
- Gestion d'états multiples
|
||||||
|
|
||||||
|
## 🎯 Fonctionnalités de la Version Restaurée
|
||||||
|
|
||||||
|
### Interface
|
||||||
|
- ✅ Onglets Manuel vs Automatique
|
||||||
|
- ✅ Recherche par fournisseur/BL/facture
|
||||||
|
- ✅ Affichage des livraisons avec statut "delivered"
|
||||||
|
- ✅ Design cohérent shadcn/ui
|
||||||
|
|
||||||
|
### Actions Disponibles
|
||||||
|
- ✅ **Voir les détails** (icône œil)
|
||||||
|
- ✅ **Dévalider rapprochement automatique** (directeurs/admins uniquement)
|
||||||
|
- ✅ Filtrage automatique selon mode fournisseur
|
||||||
|
- ✅ Notifications toast
|
||||||
|
|
||||||
|
### Permissions
|
||||||
|
- ✅ Accès restreint : managers, directeurs, admins uniquement
|
||||||
|
- ✅ Actions de dévalidation : directeurs et admins uniquement
|
||||||
|
- ✅ Redirection automatique pour employés
|
||||||
|
|
||||||
|
## 🚀 Plan de Réimplémentation Progressive
|
||||||
|
|
||||||
|
### Phase 1 : Test de la Version Backup ✅
|
||||||
|
- [ ] Déployer la version backup en production
|
||||||
|
- [ ] Vérifier absence d'erreur React #310
|
||||||
|
- [ ] Confirmer fonctionnement des onglets
|
||||||
|
- [ ] Tester l'action de dévalidation
|
||||||
|
|
||||||
|
### Phase 2 : Ajout Progressif des Fonctionnalités
|
||||||
|
1. **Pagination simple** (si nécessaire)
|
||||||
|
2. **Action Valider** (une à la fois)
|
||||||
|
3. **Action Supprimer** (après validation des précédentes)
|
||||||
|
4. **Améliorations UI** (en dernier)
|
||||||
|
|
||||||
|
### Phase 3 : Test à Chaque Étape
|
||||||
|
- Test en développement
|
||||||
|
- Test en production
|
||||||
|
- Rollback immédiat si erreur React #310
|
||||||
|
|
||||||
|
## 📁 Fichiers de Sauvegarde
|
||||||
|
|
||||||
|
```
|
||||||
|
client/src/pages/BLReconciliation.tsx # ✅ Version backup restaurée
|
||||||
|
client/src/pages/BLReconciliation_recent_broken.tsx # ❌ Version avec erreur React #310
|
||||||
|
client/src/pages/BLReconciliation_shadcn_broken.tsx # ❌ Autre version problématique
|
||||||
|
client/src/hooks/use-toast-robust.ts # 🔧 Composants robustes (inutilisés)
|
||||||
|
client/src/components/ToasterRobust.tsx # 🔧 Composants robustes (inutilisés)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔍 Points de Test Production
|
||||||
|
|
||||||
|
**À vérifier immédiatement :**
|
||||||
|
- [ ] Page `/bl-reconciliation` se charge sans erreur React #310
|
||||||
|
- [ ] Onglets Manuel/Automatique switchent correctement
|
||||||
|
- [ ] Recherche fonctionne
|
||||||
|
- [ ] Action "Dévalider automatique" fonctionne (directeur/admin)
|
||||||
|
- [ ] Notifications s'affichent
|
||||||
|
- [ ] Autres modules restent inchangés
|
||||||
|
|
||||||
|
## 💡 Conclusion
|
||||||
|
|
||||||
|
Cette approche de **rollback progressif** permet de :
|
||||||
|
1. **Éliminer immédiatement** l'erreur React #310
|
||||||
|
2. **Conserver les fonctionnalités essentielles** du module
|
||||||
|
3. **Réajouter les features** de manière contrôlée
|
||||||
|
4. **Identifier précisément** quelle modification cause l'erreur
|
||||||
|
|
||||||
|
**Status : ✅ Version backup restaurée - Prêt pour test production**
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { queryClient } from "./lib/queryClient";
|
import { queryClient } from "./lib/queryClient";
|
||||||
import { QueryClientProvider } from "@tanstack/react-query";
|
import { QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { ToasterRobust } from "@/components/ToasterRobust";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import RouterProduction from "@/components/RouterProduction";
|
import RouterProduction from "@/components/RouterProduction";
|
||||||
import ErrorBoundary from "@/components/ErrorBoundary";
|
import ErrorBoundary from "@/components/ErrorBoundary";
|
||||||
@@ -10,7 +10,7 @@ function App() {
|
|||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<ToasterRobust />
|
<Toaster />
|
||||||
<RouterProduction />
|
<RouterProduction />
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
|
|||||||
@@ -1,134 +1,24 @@
|
|||||||
import { useState, useEffect } from "react";
|
import React, { useState } from "react";
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { safeFormat } from "@/lib/dateUtils";
|
import { safeFormat } from "@/lib/dateUtils";
|
||||||
import { useToast } from "@/hooks/use-toast-robust";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { apiRequest } from "@/lib/queryClient";
|
import { 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 { Search, Edit, FileText, Settings, Eye, AlertTriangle, X } from "lucide-react";
|
||||||
import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban, ChevronLeft, ChevronRight } from "lucide-react";
|
|
||||||
import type { Supplier } from "@shared/schema";
|
|
||||||
|
|
||||||
// Composants UI robustes sans dépendances shadcn/ui problématiques
|
|
||||||
const RobustButton = ({ children, onClick, disabled = false, variant = "default", size = "default", className = "", title = "" }: {
|
|
||||||
children: React.ReactNode,
|
|
||||||
onClick?: () => void,
|
|
||||||
disabled?: boolean,
|
|
||||||
variant?: "default" | "ghost" | "outline" | "destructive",
|
|
||||||
size?: "default" | "sm" | "lg",
|
|
||||||
className?: string,
|
|
||||||
title?: string
|
|
||||||
}) => {
|
|
||||||
const variantStyles = {
|
|
||||||
default: "bg-blue-600 text-white hover:bg-blue-700 border-blue-600",
|
|
||||||
ghost: "text-gray-600 hover:text-gray-900 hover:bg-gray-100 border-transparent",
|
|
||||||
outline: "border-gray-300 bg-white text-gray-700 hover:bg-gray-50",
|
|
||||||
destructive: "bg-red-600 text-white hover:bg-red-700 border-red-600"
|
|
||||||
};
|
|
||||||
|
|
||||||
const sizeStyles = {
|
|
||||||
default: "px-4 py-2 text-sm",
|
|
||||||
sm: "px-3 py-1.5 text-xs",
|
|
||||||
lg: "px-6 py-3 text-base"
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
disabled={disabled}
|
|
||||||
title={title}
|
|
||||||
className={`inline-flex items-center justify-center font-medium rounded-md border transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${variantStyles[variant]} ${sizeStyles[size]} ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className}`}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const RobustInput = ({ placeholder, value, onChange, className = "" }: {
|
|
||||||
placeholder?: string,
|
|
||||||
value: string,
|
|
||||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
|
|
||||||
className?: string
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={placeholder}
|
|
||||||
value={value}
|
|
||||||
onChange={onChange}
|
|
||||||
className={`w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${className}`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const RobustBadge = ({ children, variant = "default", className = "" }: {
|
|
||||||
children: React.ReactNode,
|
|
||||||
variant?: "default" | "secondary" | "destructive" | "outline",
|
|
||||||
className?: string
|
|
||||||
}) => {
|
|
||||||
const variantStyles = {
|
|
||||||
default: "bg-blue-100 text-blue-800 border-blue-200",
|
|
||||||
secondary: "bg-gray-100 text-gray-800 border-gray-200",
|
|
||||||
destructive: "bg-red-100 text-red-800 border-red-200",
|
|
||||||
outline: "bg-white text-gray-700 border-gray-300"
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${variantStyles[variant]} ${className}`}>
|
|
||||||
{children}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const RobustTabs = ({ value, onValueChange, children }: {
|
|
||||||
value: string,
|
|
||||||
onValueChange: (value: string) => void,
|
|
||||||
children: React.ReactNode
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div data-tab-value={value} onChange={() => onValueChange}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const RobustTabsList = ({ children }: { children: React.ReactNode }) => {
|
|
||||||
return (
|
|
||||||
<div className="inline-flex h-10 items-center justify-center rounded-md bg-gray-100 p-1">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const RobustTabsTrigger = ({ value, children, isActive, onClick }: {
|
|
||||||
value: string,
|
|
||||||
children: React.ReactNode,
|
|
||||||
isActive: boolean,
|
|
||||||
onClick: () => void
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
className={`inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all focus:outline-none ${
|
|
||||||
isActive
|
|
||||||
? 'bg-white text-gray-900 shadow-sm'
|
|
||||||
: 'text-gray-600 hover:text-gray-900'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function BLReconciliation() {
|
export default function BLReconciliation() {
|
||||||
const { user } = useAuthUnified();
|
const { user } = useAuthUnified();
|
||||||
const { selectedStoreId } = useStore();
|
const { selectedStoreId } = useStore();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const permissions = usePermissions(user?.role);
|
|
||||||
|
|
||||||
// Vérification des permissions d'accès au module
|
// Redirection pour les employés
|
||||||
if (!permissions.canView('reconciliation')) {
|
if (user?.role === 'employee') {
|
||||||
return (
|
return (
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="bg-orange-50 border-l-4 border-orange-400 p-4">
|
<div className="bg-orange-50 border-l-4 border-orange-400 p-4">
|
||||||
@@ -139,7 +29,7 @@ export default function BLReconciliation() {
|
|||||||
<div className="ml-3">
|
<div className="ml-3">
|
||||||
<p className="text-sm text-orange-700">
|
<p className="text-sm text-orange-700">
|
||||||
<strong>Accès restreint</strong><br />
|
<strong>Accès restreint</strong><br />
|
||||||
Seuls les directeurs et administrateurs peuvent accéder au module de rapprochement BL/Factures.
|
Seuls les managers et administrateurs peuvent accéder au module de rapprochement BL/Factures.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -150,11 +40,9 @@ export default function BLReconciliation() {
|
|||||||
|
|
||||||
const [activeTab, setActiveTab] = useState("manual");
|
const [activeTab, setActiveTab] = useState("manual");
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const itemsPerPage = 20;
|
|
||||||
|
|
||||||
// Récupérer les fournisseurs pour la logique automatique
|
// Récupérer les fournisseurs pour la logique automatique
|
||||||
const { data: suppliers = [], isLoading: suppliersLoading } = useQuery<Supplier[]>({
|
const { data: suppliers = [] } = useQuery<any[]>({
|
||||||
queryKey: ['/api/suppliers'],
|
queryKey: ['/api/suppliers'],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -193,112 +81,36 @@ export default function BLReconciliation() {
|
|||||||
return supplier?.automaticReconciliation;
|
return supplier?.automaticReconciliation;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mutations pour les actions
|
// Fonction pour dévalider un rapprochement automatique (directeurs et admins uniquement)
|
||||||
const validateManualMutation = useMutation({
|
const handleDevalidateAutoReconciliation = async (deliveryId: number) => {
|
||||||
mutationFn: async (deliveryId: number) => {
|
if (user?.role !== 'directeur' && user?.role !== 'admin') {
|
||||||
return await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", {
|
|
||||||
reconciled: true,
|
|
||||||
validatedAt: new Date().toISOString()
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
toast({
|
toast({
|
||||||
title: "Succès",
|
title: "Accès refusé",
|
||||||
description: "Rapprochement validé avec succès",
|
description: "Seuls les directeurs et administrateurs peuvent dévalider les rapprochements automatiques",
|
||||||
});
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] });
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
toast({
|
|
||||||
title: "Erreur",
|
|
||||||
description: "Impossible de valider le rapprochement",
|
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const devalidateMutation = useMutation({
|
try {
|
||||||
mutationFn: async (deliveryId: number) => {
|
await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", {
|
||||||
return await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", {
|
|
||||||
reconciled: false,
|
reconciled: false,
|
||||||
validatedAt: null
|
validatedAt: null
|
||||||
});
|
});
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
toast({
|
toast({
|
||||||
title: "Succès",
|
title: "Succès",
|
||||||
description: "Rapprochement dévalidé avec succès",
|
description: "Rapprochement automatique dévalidé avec succès",
|
||||||
});
|
});
|
||||||
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] });
|
queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] });
|
||||||
},
|
} catch (error) {
|
||||||
onError: () => {
|
|
||||||
toast({
|
toast({
|
||||||
title: "Erreur",
|
title: "Erreur",
|
||||||
description: "Impossible de dévalider le rapprochement",
|
description: "Impossible de dévalider le rapprochement",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const deleteMutation = useMutation({
|
|
||||||
mutationFn: async (deliveryId: number) => {
|
|
||||||
return await apiRequest(`/api/deliveries/${deliveryId}`, "DELETE");
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
toast({
|
|
||||||
title: "Succès",
|
|
||||||
description: "Ligne supprimée avec succès",
|
|
||||||
});
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] });
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
toast({
|
|
||||||
title: "Erreur",
|
|
||||||
description: "Impossible de supprimer la ligne",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fonctions de gestion des actions
|
|
||||||
const handleValidateManual = (deliveryId: number) => {
|
|
||||||
if (!permissions.canValidate('reconciliation')) {
|
|
||||||
toast({
|
|
||||||
title: "Accès refusé",
|
|
||||||
description: "Vous n'avez pas les permissions pour valider les rapprochements",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
validateManualMutation.mutate(deliveryId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDevalidate = (deliveryId: number) => {
|
|
||||||
if (!permissions.canEdit('reconciliation')) {
|
|
||||||
toast({
|
|
||||||
title: "Accès refusé",
|
|
||||||
description: "Seuls les administrateurs peuvent dévalider les rapprochements",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (confirm("Êtes-vous sûr de vouloir dévalider ce rapprochement ?")) {
|
|
||||||
devalidateMutation.mutate(deliveryId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (deliveryId: number) => {
|
|
||||||
if (!permissions.canDelete('reconciliation')) {
|
|
||||||
toast({
|
|
||||||
title: "Accès refusé",
|
|
||||||
description: "Seuls les administrateurs peuvent supprimer les lignes",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (confirm("Êtes-vous sûr de vouloir supprimer cette ligne ? Cette action est irréversible.")) {
|
|
||||||
deleteMutation.mutate(deliveryId);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Filtrage des livraisons par recherche
|
// Filtrage des livraisons par recherche
|
||||||
@@ -316,94 +128,7 @@ export default function BLReconciliation() {
|
|||||||
const filteredManualDeliveries = filterDeliveries(manualReconciliationDeliveries);
|
const filteredManualDeliveries = filterDeliveries(manualReconciliationDeliveries);
|
||||||
const filteredAutomaticDeliveries = filterDeliveries(automaticReconciliationDeliveries);
|
const filteredAutomaticDeliveries = filterDeliveries(automaticReconciliationDeliveries);
|
||||||
|
|
||||||
// Pagination
|
const canModify = user?.role === 'directeur' || user?.role === 'admin';
|
||||||
const getCurrentPageData = (data: any[]) => {
|
|
||||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
|
||||||
const endIndex = startIndex + itemsPerPage;
|
|
||||||
return data.slice(startIndex, endIndex);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTotalPages = (data: any[]) => {
|
|
||||||
return Math.ceil(data.length / itemsPerPage);
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentManualData = getCurrentPageData(filteredManualDeliveries);
|
|
||||||
const currentAutomaticData = getCurrentPageData(filteredAutomaticDeliveries);
|
|
||||||
|
|
||||||
const totalManualPages = getTotalPages(filteredManualDeliveries);
|
|
||||||
const totalAutomaticPages = getTotalPages(filteredAutomaticDeliveries);
|
|
||||||
|
|
||||||
// Reset page when changing tabs or searching
|
|
||||||
useEffect(() => {
|
|
||||||
setCurrentPage(1);
|
|
||||||
}, [activeTab, searchTerm]);
|
|
||||||
|
|
||||||
// Vérifier si une ligne est dévalidée (pour la coloration)
|
|
||||||
const isDevalidated = (delivery: any) => {
|
|
||||||
return delivery.reconciled === false && delivery.validatedAt === null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Composant de pagination robuste
|
|
||||||
const RobustPagination = ({ currentPage, totalPages, onPageChange, totalItems, itemsPerPage }: {
|
|
||||||
currentPage: number;
|
|
||||||
totalPages: number;
|
|
||||||
onPageChange: (page: number) => void;
|
|
||||||
totalItems: number;
|
|
||||||
itemsPerPage: number;
|
|
||||||
}) => {
|
|
||||||
if (totalPages <= 1) return null;
|
|
||||||
|
|
||||||
const startItem = (currentPage - 1) * itemsPerPage + 1;
|
|
||||||
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between px-4 py-3 bg-white border-t border-gray-200">
|
|
||||||
<div className="flex items-center text-sm text-gray-700">
|
|
||||||
<span>
|
|
||||||
Affichage de {startItem} à {endItem} sur {totalItems} éléments
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<RobustButton
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onPageChange(currentPage - 1)}
|
|
||||||
disabled={currentPage === 1}
|
|
||||||
className="flex items-center space-x-1"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="w-4 h-4" />
|
|
||||||
<span>Précédent</span>
|
|
||||||
</RobustButton>
|
|
||||||
|
|
||||||
<span className="px-3 py-1 text-sm bg-gray-100 rounded">
|
|
||||||
Page {currentPage} sur {totalPages}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<RobustButton
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onPageChange(currentPage + 1)}
|
|
||||||
disabled={currentPage === totalPages}
|
|
||||||
className="flex items-center space-x-1"
|
|
||||||
>
|
|
||||||
<span>Suivant</span>
|
|
||||||
<ChevronRight className="w-4 h-4" />
|
|
||||||
</RobustButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isLoading || suppliersLoading) {
|
|
||||||
return (
|
|
||||||
<div className="p-6">
|
|
||||||
<div className="animate-pulse space-y-4">
|
|
||||||
<div className="h-8 bg-gray-200 rounded w-1/4"></div>
|
|
||||||
<div className="h-64 bg-gray-200 rounded"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
@@ -420,64 +145,52 @@ export default function BLReconciliation() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<RobustBadge variant="outline" className="text-sm border border-gray-300">
|
<Badge variant="outline" className="text-sm border border-gray-300">
|
||||||
{manualReconciliationDeliveries.length} rapprochements manuels
|
{manualReconciliationDeliveries.length} rapprochements manuels
|
||||||
</RobustBadge>
|
</Badge>
|
||||||
<RobustBadge variant="outline" className="text-sm border border-gray-300 bg-blue-50">
|
<Badge variant="outline" className="text-sm border border-gray-300 bg-blue-50">
|
||||||
{automaticReconciliationDeliveries.length} rapprochements automatiques
|
{automaticReconciliationDeliveries.length} rapprochements automatiques
|
||||||
</RobustBadge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Onglets robustes */}
|
{/* Onglets */}
|
||||||
<div className="border-b border-gray-200">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
<nav className="-mb-px flex space-x-8">
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
<RobustTabsTrigger
|
<TabsTrigger value="manual" className="flex items-center space-x-2">
|
||||||
value="manual"
|
<Edit className="w-4 h-4" />
|
||||||
isActive={activeTab === "manual"}
|
<span>Rapprochement Manuel</span>
|
||||||
onClick={() => setActiveTab("manual")}
|
<Badge variant="secondary" className="ml-2">
|
||||||
>
|
{manualReconciliationDeliveries.length}
|
||||||
<div className="flex items-center space-x-2">
|
</Badge>
|
||||||
<Edit className="w-4 h-4" />
|
</TabsTrigger>
|
||||||
<span>Rapprochement Manuel</span>
|
<TabsTrigger value="automatic" className="flex items-center space-x-2">
|
||||||
<RobustBadge variant="secondary" className="ml-2">
|
<Settings className="w-4 h-4" />
|
||||||
{manualReconciliationDeliveries.length}
|
<span>Rapprochement Automatique</span>
|
||||||
</RobustBadge>
|
<Badge variant="secondary" className="ml-2">
|
||||||
</div>
|
{automaticReconciliationDeliveries.length}
|
||||||
</RobustTabsTrigger>
|
</Badge>
|
||||||
<RobustTabsTrigger
|
</TabsTrigger>
|
||||||
value="automatic"
|
</TabsList>
|
||||||
isActive={activeTab === "automatic"}
|
</Tabs>
|
||||||
onClick={() => setActiveTab("automatic")}
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Settings className="w-4 h-4" />
|
|
||||||
<span>Rapprochement Automatique</span>
|
|
||||||
<RobustBadge variant="secondary" className="ml-2">
|
|
||||||
{automaticReconciliationDeliveries.length}
|
|
||||||
</RobustBadge>
|
|
||||||
</div>
|
|
||||||
</RobustTabsTrigger>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filtre de recherche */}
|
{/* Filtre de recherche */}
|
||||||
<div className="bg-gray-50 border border-gray-200 p-4 rounded-lg">
|
<div className="bg-gray-50 border border-gray-200 p-4 rounded-lg">
|
||||||
<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" />
|
||||||
<RobustInput
|
<Input
|
||||||
placeholder="Rechercher par fournisseur, BL ou facture..."
|
placeholder="Rechercher par fournisseur, BL ou facture..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
className="pl-10"
|
className="pl-10 border border-gray-300 shadow-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contenu des onglets */}
|
{/* Contenu des onglets */}
|
||||||
{activeTab === "manual" && (
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||||
<div className="space-y-6">
|
<TabsContent value="manual" className="space-y-6">
|
||||||
{filteredManualDeliveries.length === 0 ? (
|
{filteredManualDeliveries.length === 0 ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<FileText className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
<FileText className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
@@ -490,38 +203,39 @@ export default function BLReconciliation() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-white border border-gray-200 shadow-lg overflow-hidden rounded-lg">
|
<div className="bg-white border border-gray-200 shadow-lg overflow-hidden rounded-lg">
|
||||||
{/* Pagination du haut */}
|
|
||||||
<RobustPagination
|
|
||||||
currentPage={currentPage}
|
|
||||||
totalPages={totalManualPages}
|
|
||||||
onPageChange={setCurrentPage}
|
|
||||||
totalItems={filteredManualDeliveries.length}
|
|
||||||
itemsPerPage={itemsPerPage}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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 border-b border-gray-200">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Fournisseur</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">N° BL</th>
|
Fournisseur
|
||||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date Livr.</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">
|
||||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ref. Facture</th>
|
N° BL
|
||||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Montant Fact.</th>
|
</th>
|
||||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Statut</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">Actions</th>
|
Date Livr.
|
||||||
|
</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">
|
||||||
|
Magasin
|
||||||
|
</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
{currentManualData.map((delivery: any) => (
|
{filteredManualDeliveries.map((delivery: any) => (
|
||||||
<tr
|
<tr key={delivery.id} className="hover:bg-gray-50">
|
||||||
key={delivery.id}
|
|
||||||
className={`hover:bg-gray-50 ${
|
|
||||||
delivery.reconciled ? 'bg-green-50' :
|
|
||||||
isDevalidated(delivery) ? 'bg-red-50' : ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<td className="px-3 py-2 text-sm">
|
<td className="px-3 py-2 text-sm">
|
||||||
<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}
|
||||||
@@ -563,71 +277,18 @@ export default function BLReconciliation() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-sm">
|
<td className="px-3 py-2 text-sm">
|
||||||
{delivery.reconciled ? (
|
<div className="text-gray-900">
|
||||||
<RobustBadge variant="default" className="bg-green-600 text-white border-green-600">
|
{delivery.group?.name}
|
||||||
Validé
|
</div>
|
||||||
</RobustBadge>
|
|
||||||
) : isDevalidated(delivery) ? (
|
|
||||||
<RobustBadge variant="destructive">
|
|
||||||
Dévalidé
|
|
||||||
</RobustBadge>
|
|
||||||
) : (
|
|
||||||
<RobustBadge variant="outline">
|
|
||||||
En attente
|
|
||||||
</RobustBadge>
|
|
||||||
)}
|
|
||||||
</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">
|
||||||
{permissions.canEdit('reconciliation') && (
|
<button
|
||||||
<RobustButton
|
className="text-gray-600 hover:text-blue-600 transition-colors duration-200 p-1 hover:bg-blue-50"
|
||||||
variant="ghost"
|
title="Modifier"
|
||||||
size="sm"
|
>
|
||||||
className="text-gray-600 hover:text-blue-600 p-1"
|
<Edit className="w-4 h-4" />
|
||||||
title="Modifier"
|
</button>
|
||||||
>
|
|
||||||
<Edit className="w-4 h-4" />
|
|
||||||
</RobustButton>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!delivery.reconciled && permissions.canValidate('reconciliation') && (
|
|
||||||
<RobustButton
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleValidateManual(delivery.id)}
|
|
||||||
disabled={validateManualMutation.isPending}
|
|
||||||
className="text-green-600 hover:text-green-700 p-1"
|
|
||||||
title="Valider"
|
|
||||||
>
|
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
</RobustButton>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{delivery.reconciled && permissions.canEdit('reconciliation') && user?.role === 'admin' && (
|
|
||||||
<RobustButton
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleDevalidate(delivery.id)}
|
|
||||||
disabled={devalidateMutation.isPending}
|
|
||||||
className="text-orange-600 hover:text-orange-700 p-1"
|
|
||||||
title="Dévalider"
|
|
||||||
>
|
|
||||||
<Ban className="w-4 h-4" />
|
|
||||||
</RobustButton>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{permissions.canDelete('reconciliation') && (
|
|
||||||
<RobustButton
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleDelete(delivery.id)}
|
|
||||||
disabled={deleteMutation.isPending}
|
|
||||||
className="text-red-600 hover:text-red-700 p-1"
|
|
||||||
title="Supprimer"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</RobustButton>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -635,22 +296,11 @@ export default function BLReconciliation() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination du bas */}
|
|
||||||
<RobustPagination
|
|
||||||
currentPage={currentPage}
|
|
||||||
totalPages={totalManualPages}
|
|
||||||
onPageChange={setCurrentPage}
|
|
||||||
totalItems={filteredManualDeliveries.length}
|
|
||||||
itemsPerPage={itemsPerPage}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</TabsContent>
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === "automatic" && (
|
<TabsContent value="automatic" className="space-y-6">
|
||||||
<div className="space-y-6">
|
|
||||||
{/* Message d'information */}
|
{/* Message d'information */}
|
||||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
<div className="flex items-start space-x-3">
|
<div className="flex items-start space-x-3">
|
||||||
@@ -659,7 +309,7 @@ export default function BLReconciliation() {
|
|||||||
<h4 className="text-sm font-medium text-blue-900">Mode rapprochement automatique</h4>
|
<h4 className="text-sm font-medium text-blue-900">Mode rapprochement automatique</h4>
|
||||||
<p className="text-sm text-blue-700 mt-1">
|
<p className="text-sm text-blue-700 mt-1">
|
||||||
Les livraisons de fournisseurs en mode automatique sont validées automatiquement lorsqu'elles ont le statut "delivered" et un numéro de BL.
|
Les livraisons de fournisseurs en mode automatique sont validées automatiquement lorsqu'elles ont le statut "delivered" et un numéro de BL.
|
||||||
{permissions.canEdit('reconciliation') ? (
|
{canModify ? (
|
||||||
" Vous pouvez dévalider ces rapprochements si nécessaire."
|
" Vous pouvez dévalider ces rapprochements si nécessaire."
|
||||||
) : (
|
) : (
|
||||||
" Seuls les directeurs et administrateurs peuvent modifier ces rapprochements."
|
" Seuls les directeurs et administrateurs peuvent modifier ces rapprochements."
|
||||||
@@ -681,46 +331,49 @@ export default function BLReconciliation() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-white border border-gray-200 shadow-lg overflow-hidden rounded-lg">
|
<div className="bg-white border border-gray-200 shadow-lg overflow-hidden rounded-lg">
|
||||||
{/* Pagination du haut */}
|
|
||||||
<RobustPagination
|
|
||||||
currentPage={currentPage}
|
|
||||||
totalPages={totalAutomaticPages}
|
|
||||||
onPageChange={setCurrentPage}
|
|
||||||
totalItems={filteredAutomaticDeliveries.length}
|
|
||||||
itemsPerPage={itemsPerPage}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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 border-b border-gray-200">
|
||||||
<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
|
||||||
<RobustBadge variant="secondary" className="ml-2 text-xs">AUTO</RobustBadge>
|
<Badge variant="secondary" className="ml-2 text-xs">AUTO</Badge>
|
||||||
|
</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
N° BL
|
||||||
|
</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Date Livr.
|
||||||
|
</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Date Valid.
|
||||||
|
</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">
|
||||||
|
Magasin
|
||||||
|
</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Actions
|
||||||
</th>
|
</th>
|
||||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">N° BL</th>
|
|
||||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date Livr.</th>
|
|
||||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date Valid.</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">Statut</th>
|
|
||||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-white divide-y divide-gray-200">
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
{currentAutomaticData.map((delivery: any) => (
|
{filteredAutomaticDeliveries.map((delivery: any) => (
|
||||||
<tr
|
<tr key={delivery.id} className="hover:bg-gray-50 bg-green-50">
|
||||||
key={delivery.id}
|
|
||||||
className={`hover:bg-gray-50 ${
|
|
||||||
isDevalidated(delivery) ? 'bg-red-50' : '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="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>
|
||||||
<div className="w-3 h-3 bg-blue-500 rounded-full" title="Mode automatique" />
|
<Settings className="w-3 h-3 text-blue-500" />
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-sm">
|
<td className="px-3 py-2 text-sm">
|
||||||
@@ -735,8 +388,8 @@ 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.validatedAt ?
|
{delivery.reconciled && delivery.updatedAt ?
|
||||||
safeFormat(delivery.validatedAt, 'dd/MM/yy') :
|
safeFormat(delivery.updatedAt, 'dd/MM/yy') :
|
||||||
'-'
|
'-'
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -755,65 +408,43 @@ export default function BLReconciliation() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-sm">
|
<td className="px-3 py-2 text-sm">
|
||||||
{isDevalidated(delivery) ? (
|
<div className="text-gray-900">
|
||||||
<RobustBadge variant="destructive">
|
{delivery.invoiceAmount ?
|
||||||
Dévalidé
|
`${parseFloat(delivery.invoiceAmount).toFixed(2)}€` :
|
||||||
</RobustBadge>
|
'-'
|
||||||
) : (
|
}
|
||||||
<RobustBadge variant="default" className="bg-green-600 text-white border-green-600">
|
</div>
|
||||||
Auto-validé
|
</td>
|
||||||
</RobustBadge>
|
<td className="px-3 py-2 text-sm">
|
||||||
)}
|
<div className="text-gray-900">
|
||||||
|
{delivery.group?.name}
|
||||||
|
</div>
|
||||||
</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">
|
||||||
{isDevalidated(delivery) && permissions.canEdit('reconciliation') && (
|
{canModify ? (
|
||||||
<>
|
<>
|
||||||
<RobustButton
|
<button
|
||||||
variant="ghost"
|
className="text-gray-600 hover:text-blue-600 transition-colors duration-200 p-1 hover:bg-blue-50"
|
||||||
size="sm"
|
title="Voir les détails"
|
||||||
className="text-gray-600 hover:text-blue-600 p-1"
|
|
||||||
title="Modifier"
|
|
||||||
>
|
>
|
||||||
<Edit className="w-4 h-4" />
|
<Eye className="w-4 h-4" />
|
||||||
</RobustButton>
|
</button>
|
||||||
<RobustButton
|
<button
|
||||||
variant="ghost"
|
onClick={() => handleDevalidateAutoReconciliation(delivery.id)}
|
||||||
size="sm"
|
className="text-gray-600 hover:text-orange-600 transition-colors duration-200 p-1 hover:bg-orange-50"
|
||||||
onClick={() => handleValidateManual(delivery.id)}
|
title="Dévalider le rapprochement automatique"
|
||||||
disabled={validateManualMutation.isPending}
|
|
||||||
className="text-green-600 hover:text-green-700 p-1"
|
|
||||||
title="Valider"
|
|
||||||
>
|
>
|
||||||
<Check className="w-4 h-4" />
|
<X className="w-4 h-4" />
|
||||||
</RobustButton>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
) : (
|
||||||
|
<button
|
||||||
{!isDevalidated(delivery) && permissions.canEdit('reconciliation') && (
|
className="text-gray-600 hover:text-blue-600 transition-colors duration-200 p-1 hover:bg-blue-50"
|
||||||
<RobustButton
|
title="Voir les détails"
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleDevalidate(delivery.id)}
|
|
||||||
disabled={devalidateMutation.isPending}
|
|
||||||
className="text-orange-600 hover:text-orange-700 p-1"
|
|
||||||
title="Dévalider"
|
|
||||||
>
|
>
|
||||||
<Ban className="w-4 h-4" />
|
<Eye className="w-4 h-4" />
|
||||||
</RobustButton>
|
</button>
|
||||||
)}
|
|
||||||
|
|
||||||
{permissions.canDelete('reconciliation') && (
|
|
||||||
<RobustButton
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleDelete(delivery.id)}
|
|
||||||
disabled={deleteMutation.isPending}
|
|
||||||
className="text-red-600 hover:text-red-700 p-1"
|
|
||||||
title="Supprimer"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</RobustButton>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -822,19 +453,10 @@ export default function BLReconciliation() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination du bas */}
|
|
||||||
<RobustPagination
|
|
||||||
currentPage={currentPage}
|
|
||||||
totalPages={totalAutomaticPages}
|
|
||||||
onPageChange={setCurrentPage}
|
|
||||||
totalItems={filteredAutomaticDeliveries.length}
|
|
||||||
itemsPerPage={itemsPerPage}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</TabsContent>
|
||||||
)}
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,840 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { safeFormat } from "@/lib/dateUtils";
|
||||||
|
import { useToast } from "@/hooks/use-toast-robust";
|
||||||
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
|
import { useStore } from "@/components/Layout";
|
||||||
|
import { useAuthUnified } from "@/hooks/useAuthUnified";
|
||||||
|
import { usePermissions } from "@shared/permissions";
|
||||||
|
import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X, Check, Trash2, Ban, ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import type { Supplier } from "@shared/schema";
|
||||||
|
|
||||||
|
// Composants UI robustes sans dépendances shadcn/ui problématiques
|
||||||
|
const RobustButton = ({ children, onClick, disabled = false, variant = "default", size = "default", className = "", title = "" }: {
|
||||||
|
children: React.ReactNode,
|
||||||
|
onClick?: () => void,
|
||||||
|
disabled?: boolean,
|
||||||
|
variant?: "default" | "ghost" | "outline" | "destructive",
|
||||||
|
size?: "default" | "sm" | "lg",
|
||||||
|
className?: string,
|
||||||
|
title?: string
|
||||||
|
}) => {
|
||||||
|
const variantStyles = {
|
||||||
|
default: "bg-blue-600 text-white hover:bg-blue-700 border-blue-600",
|
||||||
|
ghost: "text-gray-600 hover:text-gray-900 hover:bg-gray-100 border-transparent",
|
||||||
|
outline: "border-gray-300 bg-white text-gray-700 hover:bg-gray-50",
|
||||||
|
destructive: "bg-red-600 text-white hover:bg-red-700 border-red-600"
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeStyles = {
|
||||||
|
default: "px-4 py-2 text-sm",
|
||||||
|
sm: "px-3 py-1.5 text-xs",
|
||||||
|
lg: "px-6 py-3 text-base"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
title={title}
|
||||||
|
className={`inline-flex items-center justify-center font-medium rounded-md border transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${variantStyles[variant]} ${sizeStyles[size]} ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RobustInput = ({ placeholder, value, onChange, className = "" }: {
|
||||||
|
placeholder?: string,
|
||||||
|
value: string,
|
||||||
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
|
||||||
|
className?: string
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
className={`w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 ${className}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RobustBadge = ({ children, variant = "default", className = "" }: {
|
||||||
|
children: React.ReactNode,
|
||||||
|
variant?: "default" | "secondary" | "destructive" | "outline",
|
||||||
|
className?: string
|
||||||
|
}) => {
|
||||||
|
const variantStyles = {
|
||||||
|
default: "bg-blue-100 text-blue-800 border-blue-200",
|
||||||
|
secondary: "bg-gray-100 text-gray-800 border-gray-200",
|
||||||
|
destructive: "bg-red-100 text-red-800 border-red-200",
|
||||||
|
outline: "bg-white text-gray-700 border-gray-300"
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${variantStyles[variant]} ${className}`}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RobustTabs = ({ value, onValueChange, children }: {
|
||||||
|
value: string,
|
||||||
|
onValueChange: (value: string) => void,
|
||||||
|
children: React.ReactNode
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div data-tab-value={value} onChange={() => onValueChange}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RobustTabsList = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div className="inline-flex h-10 items-center justify-center rounded-md bg-gray-100 p-1">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RobustTabsTrigger = ({ value, children, isActive, onClick }: {
|
||||||
|
value: string,
|
||||||
|
children: React.ReactNode,
|
||||||
|
isActive: boolean,
|
||||||
|
onClick: () => void
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={`inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all focus:outline-none ${
|
||||||
|
isActive
|
||||||
|
? 'bg-white text-gray-900 shadow-sm'
|
||||||
|
: 'text-gray-600 hover:text-gray-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function BLReconciliation() {
|
||||||
|
const { user } = useAuthUnified();
|
||||||
|
const { selectedStoreId } = useStore();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const permissions = usePermissions(user?.role);
|
||||||
|
|
||||||
|
// Vérification des permissions d'accès au module
|
||||||
|
if (!permissions.canView('reconciliation')) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="bg-orange-50 border-l-4 border-orange-400 p-4">
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<FileText className="h-5 w-5 text-orange-400" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-3">
|
||||||
|
<p className="text-sm text-orange-700">
|
||||||
|
<strong>Accès restreint</strong><br />
|
||||||
|
Seuls les directeurs et administrateurs peuvent accéder au module de rapprochement BL/Factures.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState("manual");
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const itemsPerPage = 20;
|
||||||
|
|
||||||
|
// Récupérer les fournisseurs pour la logique automatique
|
||||||
|
const { data: suppliers = [], isLoading: suppliersLoading } = useQuery<Supplier[]>({
|
||||||
|
queryKey: ['/api/suppliers'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Récupérer les livraisons validées avec BL
|
||||||
|
const { data: deliveriesWithBL = [], isLoading } = useQuery({
|
||||||
|
queryKey: ['/api/deliveries/bl', selectedStoreId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const params = new URLSearchParams({});
|
||||||
|
if (selectedStoreId && user?.role === 'admin') {
|
||||||
|
params.append('storeId', selectedStoreId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`/api/deliveries?${params.toString()}`, {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch deliveries');
|
||||||
|
}
|
||||||
|
|
||||||
|
const deliveries = await response.json();
|
||||||
|
const filtered = Array.isArray(deliveries) ? deliveries.filter((d: any) => d.status === 'delivered') : [];
|
||||||
|
|
||||||
|
return filtered.sort((a: any, b: any) => new Date(b.deliveredDate || b.updatedAt).getTime() - new Date(a.deliveredDate || a.updatedAt).getTime());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Séparer les livraisons selon le mode de rapprochement du fournisseur
|
||||||
|
const manualReconciliationDeliveries = deliveriesWithBL.filter((delivery: any) => {
|
||||||
|
const supplier = suppliers.find((s: any) => s.id === delivery.supplierId);
|
||||||
|
return !supplier?.automaticReconciliation;
|
||||||
|
});
|
||||||
|
|
||||||
|
const automaticReconciliationDeliveries = deliveriesWithBL.filter((delivery: any) => {
|
||||||
|
const supplier = suppliers.find((s: any) => s.id === delivery.supplierId);
|
||||||
|
return supplier?.automaticReconciliation;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mutations pour les actions
|
||||||
|
const validateManualMutation = useMutation({
|
||||||
|
mutationFn: async (deliveryId: number) => {
|
||||||
|
return await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", {
|
||||||
|
reconciled: true,
|
||||||
|
validatedAt: new Date().toISOString()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Succès",
|
||||||
|
description: "Rapprochement validé avec succès",
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] });
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({
|
||||||
|
title: "Erreur",
|
||||||
|
description: "Impossible de valider le rapprochement",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const devalidateMutation = useMutation({
|
||||||
|
mutationFn: async (deliveryId: number) => {
|
||||||
|
return await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", {
|
||||||
|
reconciled: false,
|
||||||
|
validatedAt: null
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Succès",
|
||||||
|
description: "Rapprochement dévalidé avec succès",
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] });
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({
|
||||||
|
title: "Erreur",
|
||||||
|
description: "Impossible de dévalider le rapprochement",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteMutation = useMutation({
|
||||||
|
mutationFn: async (deliveryId: number) => {
|
||||||
|
return await apiRequest(`/api/deliveries/${deliveryId}`, "DELETE");
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Succès",
|
||||||
|
description: "Ligne supprimée avec succès",
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] });
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({
|
||||||
|
title: "Erreur",
|
||||||
|
description: "Impossible de supprimer la ligne",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fonctions de gestion des actions
|
||||||
|
const handleValidateManual = (deliveryId: number) => {
|
||||||
|
if (!permissions.canValidate('reconciliation')) {
|
||||||
|
toast({
|
||||||
|
title: "Accès refusé",
|
||||||
|
description: "Vous n'avez pas les permissions pour valider les rapprochements",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
validateManualMutation.mutate(deliveryId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDevalidate = (deliveryId: number) => {
|
||||||
|
if (!permissions.canEdit('reconciliation')) {
|
||||||
|
toast({
|
||||||
|
title: "Accès refusé",
|
||||||
|
description: "Seuls les administrateurs peuvent dévalider les rapprochements",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (confirm("Êtes-vous sûr de vouloir dévalider ce rapprochement ?")) {
|
||||||
|
devalidateMutation.mutate(deliveryId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (deliveryId: number) => {
|
||||||
|
if (!permissions.canDelete('reconciliation')) {
|
||||||
|
toast({
|
||||||
|
title: "Accès refusé",
|
||||||
|
description: "Seuls les administrateurs peuvent supprimer les lignes",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (confirm("Êtes-vous sûr de vouloir supprimer cette ligne ? Cette action est irréversible.")) {
|
||||||
|
deleteMutation.mutate(deliveryId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filtrage des livraisons par recherche
|
||||||
|
const filterDeliveries = (deliveries: any[]) => {
|
||||||
|
return deliveries.filter((delivery: any) => {
|
||||||
|
const searchLower = searchTerm.toLowerCase();
|
||||||
|
return (
|
||||||
|
delivery.supplier?.name?.toLowerCase().includes(searchLower) ||
|
||||||
|
delivery.blNumber?.toLowerCase().includes(searchLower) ||
|
||||||
|
delivery.invoiceReference?.toLowerCase().includes(searchLower)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredManualDeliveries = filterDeliveries(manualReconciliationDeliveries);
|
||||||
|
const filteredAutomaticDeliveries = filterDeliveries(automaticReconciliationDeliveries);
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
const getCurrentPageData = (data: any[]) => {
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const endIndex = startIndex + itemsPerPage;
|
||||||
|
return data.slice(startIndex, endIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTotalPages = (data: any[]) => {
|
||||||
|
return Math.ceil(data.length / itemsPerPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentManualData = getCurrentPageData(filteredManualDeliveries);
|
||||||
|
const currentAutomaticData = getCurrentPageData(filteredAutomaticDeliveries);
|
||||||
|
|
||||||
|
const totalManualPages = getTotalPages(filteredManualDeliveries);
|
||||||
|
const totalAutomaticPages = getTotalPages(filteredAutomaticDeliveries);
|
||||||
|
|
||||||
|
// Reset page when changing tabs or searching
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, [activeTab, searchTerm]);
|
||||||
|
|
||||||
|
// Vérifier si une ligne est dévalidée (pour la coloration)
|
||||||
|
const isDevalidated = (delivery: any) => {
|
||||||
|
return delivery.reconciled === false && delivery.validatedAt === null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Composant de pagination robuste
|
||||||
|
const RobustPagination = ({ currentPage, totalPages, onPageChange, totalItems, itemsPerPage }: {
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
totalItems: number;
|
||||||
|
itemsPerPage: number;
|
||||||
|
}) => {
|
||||||
|
if (totalPages <= 1) return null;
|
||||||
|
|
||||||
|
const startItem = (currentPage - 1) * itemsPerPage + 1;
|
||||||
|
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between px-4 py-3 bg-white border-t border-gray-200">
|
||||||
|
<div className="flex items-center text-sm text-gray-700">
|
||||||
|
<span>
|
||||||
|
Affichage de {startItem} à {endItem} sur {totalItems} éléments
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RobustButton
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
className="flex items-center space-x-1"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
<span>Précédent</span>
|
||||||
|
</RobustButton>
|
||||||
|
|
||||||
|
<span className="px-3 py-1 text-sm bg-gray-100 rounded">
|
||||||
|
Page {currentPage} sur {totalPages}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<RobustButton
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(currentPage + 1)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
className="flex items-center space-x-1"
|
||||||
|
>
|
||||||
|
<span>Suivant</span>
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</RobustButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading || suppliersLoading) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="animate-pulse space-y-4">
|
||||||
|
<div className="h-8 bg-gray-200 rounded w-1/4"></div>
|
||||||
|
<div className="h-64 bg-gray-200 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* Header avec onglets */}
|
||||||
|
<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>
|
||||||
|
<h2 className="text-2xl font-semibold text-gray-900 flex items-center">
|
||||||
|
<FileText className="w-6 h-6 mr-3 text-blue-600" />
|
||||||
|
Rapprochement BL/Factures
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 mt-1">
|
||||||
|
Gestion des rapprochements manuels et automatiques
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<RobustBadge variant="outline" className="text-sm border border-gray-300">
|
||||||
|
{manualReconciliationDeliveries.length} rapprochements manuels
|
||||||
|
</RobustBadge>
|
||||||
|
<RobustBadge variant="outline" className="text-sm border border-gray-300 bg-blue-50">
|
||||||
|
{automaticReconciliationDeliveries.length} rapprochements automatiques
|
||||||
|
</RobustBadge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Onglets robustes */}
|
||||||
|
<div className="border-b border-gray-200">
|
||||||
|
<nav className="-mb-px flex space-x-8">
|
||||||
|
<RobustTabsTrigger
|
||||||
|
value="manual"
|
||||||
|
isActive={activeTab === "manual"}
|
||||||
|
onClick={() => setActiveTab("manual")}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
<span>Rapprochement Manuel</span>
|
||||||
|
<RobustBadge variant="secondary" className="ml-2">
|
||||||
|
{manualReconciliationDeliveries.length}
|
||||||
|
</RobustBadge>
|
||||||
|
</div>
|
||||||
|
</RobustTabsTrigger>
|
||||||
|
<RobustTabsTrigger
|
||||||
|
value="automatic"
|
||||||
|
isActive={activeTab === "automatic"}
|
||||||
|
onClick={() => setActiveTab("automatic")}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Settings className="w-4 h-4" />
|
||||||
|
<span>Rapprochement Automatique</span>
|
||||||
|
<RobustBadge variant="secondary" className="ml-2">
|
||||||
|
{automaticReconciliationDeliveries.length}
|
||||||
|
</RobustBadge>
|
||||||
|
</div>
|
||||||
|
</RobustTabsTrigger>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filtre de recherche */}
|
||||||
|
<div className="bg-gray-50 border border-gray-200 p-4 rounded-lg">
|
||||||
|
<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" />
|
||||||
|
<RobustInput
|
||||||
|
placeholder="Rechercher par fournisseur, BL ou facture..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contenu des onglets */}
|
||||||
|
{activeTab === "manual" && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{filteredManualDeliveries.length === 0 ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<FileText className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||||
|
Aucun rapprochement manuel trouvé
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Les livraisons de fournisseurs en mode manuel apparaîtront ici.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white border border-gray-200 shadow-lg overflow-hidden rounded-lg">
|
||||||
|
{/* Pagination du haut */}
|
||||||
|
<RobustPagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalManualPages}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
totalItems={filteredManualDeliveries.length}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50 border-b border-gray-200">
|
||||||
|
<tr>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Fournisseur</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">N° BL</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date Livr.</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">Statut</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{currentManualData.map((delivery: any) => (
|
||||||
|
<tr
|
||||||
|
key={delivery.id}
|
||||||
|
className={`hover:bg-gray-50 ${
|
||||||
|
delivery.reconciled ? 'bg-green-50' :
|
||||||
|
isDevalidated(delivery) ? 'bg-red-50' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<td className="px-3 py-2 text-sm">
|
||||||
|
<div className="font-medium text-gray-900 truncate max-w-32">
|
||||||
|
{delivery.supplier?.name}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-sm">
|
||||||
|
<div className="text-gray-900">
|
||||||
|
{delivery.blNumber || (
|
||||||
|
<span className="text-gray-400 italic text-xs">Non renseigné</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-sm">
|
||||||
|
<div className="text-gray-900">
|
||||||
|
{safeFormat(delivery.scheduledDate, 'dd/MM/yy')}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-sm">
|
||||||
|
<div className="font-medium text-gray-900">
|
||||||
|
{delivery.blAmount ?
|
||||||
|
`${parseFloat(delivery.blAmount).toFixed(2)}€` :
|
||||||
|
<span className="text-gray-400 italic text-xs">Non renseigné</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-sm">
|
||||||
|
<div className="text-gray-900">
|
||||||
|
{delivery.invoiceReference || (
|
||||||
|
<span className="text-gray-400 italic text-xs">Non renseigné</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-sm">
|
||||||
|
<div className="text-gray-900">
|
||||||
|
{delivery.invoiceAmount ?
|
||||||
|
`${parseFloat(delivery.invoiceAmount).toFixed(2)}€` :
|
||||||
|
<span className="text-gray-400 italic text-xs">Non renseigné</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-sm">
|
||||||
|
{delivery.reconciled ? (
|
||||||
|
<RobustBadge variant="default" className="bg-green-600 text-white border-green-600">
|
||||||
|
Validé
|
||||||
|
</RobustBadge>
|
||||||
|
) : isDevalidated(delivery) ? (
|
||||||
|
<RobustBadge variant="destructive">
|
||||||
|
Dévalidé
|
||||||
|
</RobustBadge>
|
||||||
|
) : (
|
||||||
|
<RobustBadge variant="outline">
|
||||||
|
En attente
|
||||||
|
</RobustBadge>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-sm">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{permissions.canEdit('reconciliation') && (
|
||||||
|
<RobustButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-gray-600 hover:text-blue-600 p-1"
|
||||||
|
title="Modifier"
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</RobustButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!delivery.reconciled && permissions.canValidate('reconciliation') && (
|
||||||
|
<RobustButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleValidateManual(delivery.id)}
|
||||||
|
disabled={validateManualMutation.isPending}
|
||||||
|
className="text-green-600 hover:text-green-700 p-1"
|
||||||
|
title="Valider"
|
||||||
|
>
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
</RobustButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{delivery.reconciled && permissions.canEdit('reconciliation') && user?.role === 'admin' && (
|
||||||
|
<RobustButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDevalidate(delivery.id)}
|
||||||
|
disabled={devalidateMutation.isPending}
|
||||||
|
className="text-orange-600 hover:text-orange-700 p-1"
|
||||||
|
title="Dévalider"
|
||||||
|
>
|
||||||
|
<Ban className="w-4 h-4" />
|
||||||
|
</RobustButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{permissions.canDelete('reconciliation') && (
|
||||||
|
<RobustButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(delivery.id)}
|
||||||
|
disabled={deleteMutation.isPending}
|
||||||
|
className="text-red-600 hover:text-red-700 p-1"
|
||||||
|
title="Supprimer"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</RobustButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination du bas */}
|
||||||
|
<RobustPagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalManualPages}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
totalItems={filteredManualDeliveries.length}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === "automatic" && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Message d'information */}
|
||||||
|
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<AlertTriangle className="w-5 h-5 text-blue-600 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-blue-900">Mode rapprochement automatique</h4>
|
||||||
|
<p className="text-sm text-blue-700 mt-1">
|
||||||
|
Les livraisons de fournisseurs en mode automatique sont validées automatiquement lorsqu'elles ont le statut "delivered" et un numéro de BL.
|
||||||
|
{permissions.canEdit('reconciliation') ? (
|
||||||
|
" Vous pouvez dévalider ces rapprochements si nécessaire."
|
||||||
|
) : (
|
||||||
|
" Seuls les directeurs et administrateurs peuvent modifier ces rapprochements."
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredAutomaticDeliveries.length === 0 ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<Settings className="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">
|
||||||
|
Aucun rapprochement automatique trouvé
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Les livraisons de fournisseurs en mode automatique apparaîtront ici.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="bg-white border border-gray-200 shadow-lg overflow-hidden rounded-lg">
|
||||||
|
{/* Pagination du haut */}
|
||||||
|
<RobustPagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalAutomaticPages}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
totalItems={filteredAutomaticDeliveries.length}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50 border-b border-gray-200">
|
||||||
|
<tr>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Fournisseur
|
||||||
|
<RobustBadge variant="secondary" className="ml-2 text-xs">AUTO</RobustBadge>
|
||||||
|
</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">N° BL</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date Livr.</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date Valid.</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">Statut</th>
|
||||||
|
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{currentAutomaticData.map((delivery: any) => (
|
||||||
|
<tr
|
||||||
|
key={delivery.id}
|
||||||
|
className={`hover:bg-gray-50 ${
|
||||||
|
isDevalidated(delivery) ? 'bg-red-50' : 'bg-green-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<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">
|
||||||
|
{delivery.supplier?.name}
|
||||||
|
</div>
|
||||||
|
<div className="w-3 h-3 bg-blue-500 rounded-full" title="Mode automatique" />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-sm">
|
||||||
|
<div className="text-gray-900 font-medium">
|
||||||
|
{delivery.blNumber || '-'}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-sm">
|
||||||
|
<div className="text-gray-900">
|
||||||
|
{safeFormat(delivery.scheduledDate, 'dd/MM/yy')}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-sm">
|
||||||
|
<div className="text-gray-900">
|
||||||
|
{delivery.reconciled && delivery.validatedAt ?
|
||||||
|
safeFormat(delivery.validatedAt, 'dd/MM/yy') :
|
||||||
|
'-'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
{isDevalidated(delivery) ? (
|
||||||
|
<RobustBadge variant="destructive">
|
||||||
|
Dévalidé
|
||||||
|
</RobustBadge>
|
||||||
|
) : (
|
||||||
|
<RobustBadge variant="default" className="bg-green-600 text-white border-green-600">
|
||||||
|
Auto-validé
|
||||||
|
</RobustBadge>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-sm">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{isDevalidated(delivery) && permissions.canEdit('reconciliation') && (
|
||||||
|
<>
|
||||||
|
<RobustButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-gray-600 hover:text-blue-600 p-1"
|
||||||
|
title="Modifier"
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
</RobustButton>
|
||||||
|
<RobustButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleValidateManual(delivery.id)}
|
||||||
|
disabled={validateManualMutation.isPending}
|
||||||
|
className="text-green-600 hover:text-green-700 p-1"
|
||||||
|
title="Valider"
|
||||||
|
>
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
</RobustButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isDevalidated(delivery) && permissions.canEdit('reconciliation') && (
|
||||||
|
<RobustButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDevalidate(delivery.id)}
|
||||||
|
disabled={devalidateMutation.isPending}
|
||||||
|
className="text-orange-600 hover:text-orange-700 p-1"
|
||||||
|
title="Dévalider"
|
||||||
|
>
|
||||||
|
<Ban className="w-4 h-4" />
|
||||||
|
</RobustButton>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{permissions.canDelete('reconciliation') && (
|
||||||
|
<RobustButton
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDelete(delivery.id)}
|
||||||
|
disabled={deleteMutation.isPending}
|
||||||
|
className="text-red-600 hover:text-red-700 p-1"
|
||||||
|
title="Supprimer"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</RobustButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination du bas */}
|
||||||
|
<RobustPagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalAutomaticPages}
|
||||||
|
onPageChange={setCurrentPage}
|
||||||
|
totalItems={filteredAutomaticDeliveries.length}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user