diff --git a/ROLLBACK-SOLUTION-REACT-ERROR-310.md b/ROLLBACK-SOLUTION-REACT-ERROR-310.md new file mode 100644 index 0000000..d9ca374 --- /dev/null +++ b/ROLLBACK-SOLUTION-REACT-ERROR-310.md @@ -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** \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index 9558d80..653212b 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,6 +1,6 @@ import { queryClient } from "./lib/queryClient"; import { QueryClientProvider } from "@tanstack/react-query"; -import { ToasterRobust } from "@/components/ToasterRobust"; +import { Toaster } from "@/components/ui/toaster"; import { TooltipProvider } from "@/components/ui/tooltip"; import RouterProduction from "@/components/RouterProduction"; import ErrorBoundary from "@/components/ErrorBoundary"; @@ -10,7 +10,7 @@ function App() { - + diff --git a/client/src/pages/BLReconciliation.tsx b/client/src/pages/BLReconciliation.tsx index c2cba4e..8566a08 100644 --- a/client/src/pages/BLReconciliation.tsx +++ b/client/src/pages/BLReconciliation.tsx @@ -1,134 +1,24 @@ -import { useState, useEffect } from "react"; +import React, { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; 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 { 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 ( - - ); -}; - -const RobustInput = ({ placeholder, value, onChange, className = "" }: { - placeholder?: string, - value: string, - onChange: (e: React.ChangeEvent) => void, - className?: string -}) => { - return ( - - ); -}; - -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 ( - - {children} - - ); -}; - -const RobustTabs = ({ value, onValueChange, children }: { - value: string, - onValueChange: (value: string) => void, - children: React.ReactNode -}) => { - return ( -
onValueChange}> - {children} -
- ); -}; - -const RobustTabsList = ({ children }: { children: React.ReactNode }) => { - return ( -
- {children} -
- ); -}; - -const RobustTabsTrigger = ({ value, children, isActive, onClick }: { - value: string, - children: React.ReactNode, - isActive: boolean, - onClick: () => void -}) => { - return ( - - ); -}; +import { Search, Edit, FileText, Settings, Eye, AlertTriangle, X } from "lucide-react"; 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')) { + // Redirection pour les employés + if (user?.role === 'employee') { return (
@@ -139,7 +29,7 @@ export default function BLReconciliation() {

Accès restreint
- 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.

@@ -150,11 +40,9 @@ export default function BLReconciliation() { 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({ + const { data: suppliers = [] } = useQuery({ queryKey: ['/api/suppliers'], }); @@ -193,112 +81,36 @@ export default function BLReconciliation() { 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: () => { + // Fonction pour dévalider un rapprochement automatique (directeurs et admins uniquement) + const handleDevalidateAutoReconciliation = async (deliveryId: number) => { + if (user?.role !== 'directeur' && user?.role !== 'admin') { 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", + title: "Accès refusé", + description: "Seuls les directeurs et administrateurs peuvent dévalider les rapprochements automatiques", variant: "destructive", }); + return; } - }); - const devalidateMutation = useMutation({ - mutationFn: async (deliveryId: number) => { - return await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", { + try { + await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", { reconciled: false, validatedAt: null }); - }, - onSuccess: () => { + toast({ title: "Succès", - description: "Rapprochement dévalidé avec succès", + description: "Rapprochement automatique dévalidé avec succès", }); + queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); - }, - onError: () => { + } catch (error) { 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 @@ -316,94 +128,7 @@ export default function BLReconciliation() { 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 ( -
-
- - Affichage de {startItem} à {endItem} sur {totalItems} éléments - -
-
- onPageChange(currentPage - 1)} - disabled={currentPage === 1} - className="flex items-center space-x-1" - > - - Précédent - - - - Page {currentPage} sur {totalPages} - - - onPageChange(currentPage + 1)} - disabled={currentPage === totalPages} - className="flex items-center space-x-1" - > - Suivant - - -
-
- ); - }; - - if (isLoading || suppliersLoading) { - return ( -
-
-
-
-
-
- ); - } + const canModify = user?.role === 'directeur' || user?.role === 'admin'; return (
@@ -420,64 +145,52 @@ export default function BLReconciliation() {

- + {manualReconciliationDeliveries.length} rapprochements manuels - - + + {automaticReconciliationDeliveries.length} rapprochements automatiques - +
- {/* Onglets robustes */} -
- -
+ {/* Onglets */} + + + + + Rapprochement Manuel + + {manualReconciliationDeliveries.length} + + + + + Rapprochement Automatique + + {automaticReconciliationDeliveries.length} + + + + {/* Filtre de recherche */}
- setSearchTerm(e.target.value)} - className="pl-10" + className="pl-10 border border-gray-300 shadow-sm" />
{/* Contenu des onglets */} - {activeTab === "manual" && ( -
+ + {filteredManualDeliveries.length === 0 ? (
@@ -490,38 +203,39 @@ export default function BLReconciliation() {
) : (
- {/* Pagination du haut */} - -
- - - - - - - - + + + + + + + + - {currentManualData.map((delivery: any) => ( - + {filteredManualDeliveries.map((delivery: any) => ( + @@ -635,22 +296,11 @@ export default function BLReconciliation() {
FournisseurN° BLDate Livr.Montant BLRef. FactureMontant Fact.StatutActions + Fournisseur + + N° BL + + Date Livr. + + Montant BL + + Ref. Facture + + Montant Fact. + + Magasin + + Actions +
{delivery.supplier?.name} @@ -563,71 +277,18 @@ export default function BLReconciliation() {
- {delivery.reconciled ? ( - - Validé - - ) : isDevalidated(delivery) ? ( - - Dévalidé - - ) : ( - - En attente - - )} +
+ {delivery.group?.name} +
- {permissions.canEdit('reconciliation') && ( - - - - )} - - {!delivery.reconciled && permissions.canValidate('reconciliation') && ( - handleValidateManual(delivery.id)} - disabled={validateManualMutation.isPending} - className="text-green-600 hover:text-green-700 p-1" - title="Valider" - > - - - )} - - {delivery.reconciled && permissions.canEdit('reconciliation') && user?.role === 'admin' && ( - handleDevalidate(delivery.id)} - disabled={devalidateMutation.isPending} - className="text-orange-600 hover:text-orange-700 p-1" - title="Dévalider" - > - - - )} - - {permissions.canDelete('reconciliation') && ( - handleDelete(delivery.id)} - disabled={deleteMutation.isPending} - className="text-red-600 hover:text-red-700 p-1" - title="Supprimer" - > - - - )} +
- - {/* Pagination du bas */} -
)} -
- )} - - {activeTab === "automatic" && ( -
+ + + {/* Message d'information */}
@@ -659,7 +309,7 @@ export default function BLReconciliation() {

Mode rapprochement automatique

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." ) : ( " Seuls les directeurs et administrateurs peuvent modifier ces rapprochements." @@ -681,46 +331,49 @@ export default function BLReconciliation() {

) : (
- {/* Pagination du haut */} - -
+ + + + + + + + - - - - - - - - {currentAutomaticData.map((delivery: any) => ( - + {filteredAutomaticDeliveries.map((delivery: any) => ( + + @@ -822,19 +453,10 @@ export default function BLReconciliation() {
Fournisseur - AUTO + AUTO + + N° BL + + Date Livr. + + Date Valid. + + Montant BL + + Ref. Facture + + Montant Fact. + + Magasin + + Actions N° BLDate Livr.Date Valid.Montant BLRef. FactureStatutActions
{delivery.supplier?.name}
-
+
@@ -735,8 +388,8 @@ export default function BLReconciliation() {
- {delivery.reconciled && delivery.validatedAt ? - safeFormat(delivery.validatedAt, 'dd/MM/yy') : + {delivery.reconciled && delivery.updatedAt ? + safeFormat(delivery.updatedAt, 'dd/MM/yy') : '-' }
@@ -755,65 +408,43 @@ export default function BLReconciliation() {
- {isDevalidated(delivery) ? ( - - Dévalidé - - ) : ( - - Auto-validé - - )} +
+ {delivery.invoiceAmount ? + `${parseFloat(delivery.invoiceAmount).toFixed(2)}€` : + '-' + } +
+
+
+ {delivery.group?.name} +
- {isDevalidated(delivery) && permissions.canEdit('reconciliation') && ( + {canModify ? ( <> - - - - handleValidateManual(delivery.id)} - disabled={validateManualMutation.isPending} - className="text-green-600 hover:text-green-700 p-1" - title="Valider" + + + - )} - - {!isDevalidated(delivery) && permissions.canEdit('reconciliation') && ( - handleDevalidate(delivery.id)} - disabled={devalidateMutation.isPending} - className="text-orange-600 hover:text-orange-700 p-1" - title="Dévalider" + ) : ( + )}
- - {/* Pagination du bas */} -
)} -
- )} +
+
); } \ No newline at end of file diff --git a/client/src/pages/BLReconciliation_recent_broken.tsx b/client/src/pages/BLReconciliation_recent_broken.tsx new file mode 100644 index 0000000..c2cba4e --- /dev/null +++ b/client/src/pages/BLReconciliation_recent_broken.tsx @@ -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 ( + + ); +}; + +const RobustInput = ({ placeholder, value, onChange, className = "" }: { + placeholder?: string, + value: string, + onChange: (e: React.ChangeEvent) => void, + className?: string +}) => { + return ( + + ); +}; + +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 ( + + {children} + + ); +}; + +const RobustTabs = ({ value, onValueChange, children }: { + value: string, + onValueChange: (value: string) => void, + children: React.ReactNode +}) => { + return ( +
onValueChange}> + {children} +
+ ); +}; + +const RobustTabsList = ({ children }: { children: React.ReactNode }) => { + return ( +
+ {children} +
+ ); +}; + +const RobustTabsTrigger = ({ value, children, isActive, onClick }: { + value: string, + children: React.ReactNode, + isActive: boolean, + onClick: () => void +}) => { + return ( + + ); +}; + +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 ( +
+
+
+
+ +
+
+

+ Accès restreint
+ Seuls les directeurs et administrateurs peuvent accéder au module de rapprochement BL/Factures. +

+
+
+
+
+ ); + } + + 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({ + 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 ( +
+
+ + Affichage de {startItem} à {endItem} sur {totalItems} éléments + +
+
+ onPageChange(currentPage - 1)} + disabled={currentPage === 1} + className="flex items-center space-x-1" + > + + Précédent + + + + Page {currentPage} sur {totalPages} + + + onPageChange(currentPage + 1)} + disabled={currentPage === totalPages} + className="flex items-center space-x-1" + > + Suivant + + +
+
+ ); + }; + + if (isLoading || suppliersLoading) { + return ( +
+
+
+
+
+
+ ); + } + + return ( +
+ {/* Header avec onglets */} +
+
+
+

+ + Rapprochement BL/Factures +

+

+ Gestion des rapprochements manuels et automatiques +

+
+
+ + {manualReconciliationDeliveries.length} rapprochements manuels + + + {automaticReconciliationDeliveries.length} rapprochements automatiques + +
+
+ + {/* Onglets robustes */} +
+ +
+
+ + {/* Filtre de recherche */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+
+ + {/* Contenu des onglets */} + {activeTab === "manual" && ( +
+ {filteredManualDeliveries.length === 0 ? ( +
+ +

+ Aucun rapprochement manuel trouvé +

+

+ Les livraisons de fournisseurs en mode manuel apparaîtront ici. +

+
+ ) : ( +
+ {/* Pagination du haut */} + + +
+ + + + + + + + + + + + + + + {currentManualData.map((delivery: any) => ( + + + + + + + + + + + ))} + +
FournisseurN° BLDate Livr.Montant BLRef. FactureMontant Fact.StatutActions
+
+ {delivery.supplier?.name} +
+
+
+ {delivery.blNumber || ( + Non renseigné + )} +
+
+
+ {safeFormat(delivery.scheduledDate, 'dd/MM/yy')} +
+
+
+ {delivery.blAmount ? + `${parseFloat(delivery.blAmount).toFixed(2)}€` : + Non renseigné + } +
+
+
+ {delivery.invoiceReference || ( + Non renseigné + )} +
+
+
+ {delivery.invoiceAmount ? + `${parseFloat(delivery.invoiceAmount).toFixed(2)}€` : + Non renseigné + } +
+
+ {delivery.reconciled ? ( + + Validé + + ) : isDevalidated(delivery) ? ( + + Dévalidé + + ) : ( + + En attente + + )} + +
+ {permissions.canEdit('reconciliation') && ( + + + + )} + + {!delivery.reconciled && permissions.canValidate('reconciliation') && ( + handleValidateManual(delivery.id)} + disabled={validateManualMutation.isPending} + className="text-green-600 hover:text-green-700 p-1" + title="Valider" + > + + + )} + + {delivery.reconciled && permissions.canEdit('reconciliation') && user?.role === 'admin' && ( + handleDevalidate(delivery.id)} + disabled={devalidateMutation.isPending} + className="text-orange-600 hover:text-orange-700 p-1" + title="Dévalider" + > + + + )} + + {permissions.canDelete('reconciliation') && ( + handleDelete(delivery.id)} + disabled={deleteMutation.isPending} + className="text-red-600 hover:text-red-700 p-1" + title="Supprimer" + > + + + )} +
+
+
+ + {/* Pagination du bas */} + +
+ )} +
+ )} + + {activeTab === "automatic" && ( +
+ {/* Message d'information */} +
+
+ +
+

Mode rapprochement automatique

+

+ 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." + )} +

+
+
+
+ + {filteredAutomaticDeliveries.length === 0 ? ( +
+ +

+ Aucun rapprochement automatique trouvé +

+

+ Les livraisons de fournisseurs en mode automatique apparaîtront ici. +

+
+ ) : ( +
+ {/* Pagination du haut */} + + +
+ + + + + + + + + + + + + + + {currentAutomaticData.map((delivery: any) => ( + + + + + + + + + + + ))} + +
+ Fournisseur + AUTO + N° BLDate Livr.Date Valid.Montant BLRef. FactureStatutActions
+
+
+ {delivery.supplier?.name} +
+
+
+
+
+ {delivery.blNumber || '-'} +
+
+
+ {safeFormat(delivery.scheduledDate, 'dd/MM/yy')} +
+
+
+ {delivery.reconciled && delivery.validatedAt ? + safeFormat(delivery.validatedAt, 'dd/MM/yy') : + '-' + } +
+
+
+ {delivery.blAmount ? + `${parseFloat(delivery.blAmount).toFixed(2)}€` : + '-' + } +
+
+
+ {delivery.invoiceReference || '-'} +
+
+ {isDevalidated(delivery) ? ( + + Dévalidé + + ) : ( + + Auto-validé + + )} + +
+ {isDevalidated(delivery) && permissions.canEdit('reconciliation') && ( + <> + + + + handleValidateManual(delivery.id)} + disabled={validateManualMutation.isPending} + className="text-green-600 hover:text-green-700 p-1" + title="Valider" + > + + + + )} + + {!isDevalidated(delivery) && permissions.canEdit('reconciliation') && ( + handleDevalidate(delivery.id)} + disabled={devalidateMutation.isPending} + className="text-orange-600 hover:text-orange-700 p-1" + title="Dévalider" + > + + + )} + + {permissions.canDelete('reconciliation') && ( + handleDelete(delivery.id)} + disabled={deleteMutation.isPending} + className="text-red-600 hover:text-red-700 p-1" + title="Supprimer" + > + + + )} +
+
+
+ + {/* Pagination du bas */} + +
+ )} +
+ )} +
+ ); +} \ No newline at end of file