diff --git a/RECONCILIATION-MODAL-FIX.md b/RECONCILIATION-MODAL-FIX.md new file mode 100644 index 0000000..cf0be6b --- /dev/null +++ b/RECONCILIATION-MODAL-FIX.md @@ -0,0 +1,111 @@ +# Fix Complet - Modal de Rapprochement et Actions + +## ✅ Problèmes Résolus + +### 1. **Validation impossible** ❌ → ✅ +**Avant :** Bouton "Valider" sans vérification des données requises +**Après :** +- Validation conditionnelle : BL number requis +- Message d'erreur explicite : "Le numéro de BL est requis. Cliquez sur 'Voir les détails' pour le renseigner" +- Action rapide si données complètes, sinon redirection vers modal + +### 2. **Pas de modal d'édition** ❌ → ✅ +**Avant :** Bouton "Voir les détails" ne faisait rien +**Après :** +- **Modal ReconciliationModal.tsx** complet avec : + - Édition BL : N° BL (requis) + Montant BL + - Édition Facture : Référence + Montant + - Actions : Enregistrer + Valider directement + - Distinction Manuel/Automatique avec badges + - Validation en temps réel + +### 3. **Dévalidation automatique** 🔄 → À tester +**Fonctionnalité :** +- Bouton dévalider visible pour admin uniquement +- Confirmation avant action +- API call vers `/api/deliveries/${id}` PUT + +## 🎯 Nouvelles Fonctionnalités + +### Modal de Rapprochement +```typescript +// Ouverture via bouton "Voir les détails" +const handleOpenModal = (delivery: any) => { + setSelectedDelivery(delivery); + setIsModalOpen(true); +}; +``` + +### Actions Intelligentes +- **Validation rapide** : Si BL number présent → validation directe +- **Validation guidée** : Si BL number manquant → ouverture modal +- **Édition complète** : Modal avec tous les champs BL/Facture + +### Interface Améliorée +- ✅ Boutons d'action fonctionnels avec tooltips +- ✅ Indicateurs visuels par statut +- ✅ Confirmations de sécurité (window.confirm) +- ✅ Messages d'erreur contextuels + +## 📋 Tests à Effectuer en Production + +### Test 1 : Modal d'Édition +- [ ] Cliquer sur icône "œil" (Eye) → Modal s'ouvre +- [ ] Remplir N° BL obligatoire +- [ ] Remplir montants optionnels +- [ ] Cliquer "Enregistrer" → Données sauvegardées +- [ ] Cliquer "Valider le rapprochement" → Rapprochement validé + +### Test 2 : Validation Rapide +- [ ] Livraison avec BL number → Bouton vert "Valider" fonctionne +- [ ] Livraison sans BL number → Message "Données manquantes" + +### Test 3 : Dévalidation Automatique +- [ ] Se connecter en tant qu'admin +- [ ] Onglet "Rapprochement Automatique" +- [ ] Cliquer bouton orange "Dévalider" → Confirmation puis action + +### Test 4 : Permissions +- **Admin** : Tous boutons visibles (Valider, Supprimer, Dévalider) +- **Directeur** : Valider + Supprimer (pas dévalider) +- **Manager** : Accès refusé au module +- **Employee** : Accès refusé au module + +## 🔧 Debug Dévalidation Automatique + +Si la dévalidation ne fonctionne toujours pas : + +### Vérifier API +```bash +# Test manuel API +curl -X PUT /api/deliveries/[ID] \ + -H "Content-Type: application/json" \ + -d '{"reconciled": false, "validatedAt": null}' +``` + +### Vérifier Permissions +```javascript +// Dans console navigateur +console.log("User role:", user?.role); +console.log("Is admin:", user?.role === 'admin'); +``` + +### Vérifier Console Erreurs +- F12 → Console → Vérifier erreurs JavaScript +- Network → Vérifier requêtes API (200 vs 4xx/5xx) + +## 🚀 Déploiement + +### Fichiers Modifiés +``` +client/src/components/modals/ReconciliationModal.tsx # ✅ Nouveau +client/src/pages/BLReconciliation.tsx # ✅ Modifié +``` + +### Points de Contrôle +- ✅ Modal shadcn/ui Dialog (pas React.forwardRef problématique) +- ✅ Confirmations via window.confirm (évite erreurs React) +- ✅ Gestion d'état locale simple (useState) +- ✅ API calls via apiRequest (standardisé) + +**Status :** ✅ Prêt pour déploiement et test production \ No newline at end of file diff --git a/attached_assets/{E90CACDC-1E90-4AAF-8458-22BC13DF5B30}_1754998300995.png b/attached_assets/{E90CACDC-1E90-4AAF-8458-22BC13DF5B30}_1754998300995.png new file mode 100644 index 0000000..1f5140f Binary files /dev/null and b/attached_assets/{E90CACDC-1E90-4AAF-8458-22BC13DF5B30}_1754998300995.png differ diff --git a/client/src/components/modals/ReconciliationModal.tsx b/client/src/components/modals/ReconciliationModal.tsx new file mode 100644 index 0000000..d402164 --- /dev/null +++ b/client/src/components/modals/ReconciliationModal.tsx @@ -0,0 +1,268 @@ +import React, { useState, useEffect } from "react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { Separator } from "@/components/ui/separator"; +import { useToast } from "@/hooks/use-toast"; +import { apiRequest } from "@/lib/queryClient"; +import { safeFormat } from "@/lib/dateUtils"; +import { FileText, Settings, Check, X } from "lucide-react"; + +interface ReconciliationModalProps { + isOpen: boolean; + onClose: () => void; + delivery: any; + onSave?: () => void; +} + +export default function ReconciliationModal({ + isOpen, + onClose, + delivery, + onSave +}: ReconciliationModalProps) { + const { toast } = useToast(); + const queryClient = useQueryClient(); + + const [formData, setFormData] = useState({ + blNumber: "", + blAmount: "", + invoiceReference: "", + invoiceAmount: "", + reconciled: false, + }); + + // Initialiser le formulaire avec les données de la livraison + useEffect(() => { + if (delivery) { + setFormData({ + blNumber: delivery.blNumber || "", + blAmount: delivery.blAmount || "", + invoiceReference: delivery.invoiceReference || "", + invoiceAmount: delivery.invoiceAmount || "", + reconciled: delivery.reconciled || false, + }); + } + }, [delivery]); + + const updateDeliveryMutation = useMutation({ + mutationFn: async (data: any) => { + await apiRequest(`/api/deliveries/${delivery?.id}`, "PUT", data); + }, + onSuccess: () => { + toast({ + title: "Succès", + description: "Données de rapprochement mises à jour avec succès", + }); + queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); + if (onSave) onSave(); + onClose(); + }, + onError: (error) => { + toast({ + title: "Erreur", + description: "Impossible de mettre à jour les données", + variant: "destructive", + }); + } + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + // Validation basique + if (!formData.blNumber.trim()) { + toast({ + title: "Erreur", + description: "Le numéro de BL est requis", + variant: "destructive", + }); + return; + } + + updateDeliveryMutation.mutate({ + blNumber: formData.blNumber.trim(), + blAmount: formData.blAmount ? parseFloat(formData.blAmount) : null, + invoiceReference: formData.invoiceReference.trim() || null, + invoiceAmount: formData.invoiceAmount ? parseFloat(formData.invoiceAmount) : null, + }); + }; + + const handleValidateReconciliation = () => { + if (!formData.blNumber.trim()) { + toast({ + title: "Erreur", + description: "Veuillez d'abord renseigner le numéro de BL", + variant: "destructive", + }); + return; + } + + updateDeliveryMutation.mutate({ + blNumber: formData.blNumber.trim(), + blAmount: formData.blAmount ? parseFloat(formData.blAmount) : null, + invoiceReference: formData.invoiceReference.trim() || null, + invoiceAmount: formData.invoiceAmount ? parseFloat(formData.invoiceAmount) : null, + reconciled: true, + validatedAt: new Date().toISOString() + }); + }; + + const isAutomaticMode = delivery?.supplier?.automaticReconciliation; + const canValidate = formData.blNumber.trim().length > 0; + + return ( + + + + + {isAutomaticMode ? ( + + ) : ( + + )} + + Rapprochement {isAutomaticMode ? 'Automatique' : 'Manuel'} + + + {isAutomaticMode ? "AUTO" : "MANUEL"} + + + + +
+ {/* Informations de la livraison */} +
+

Informations de la livraison

+
+
+ Fournisseur : +
{delivery?.supplier?.name}
+
+
+ Date prévue : +
+ {safeFormat(delivery?.scheduledDate, 'dd/MM/yyyy')} +
+
+
+ Magasin : +
{delivery?.group?.name}
+
+
+ Statut : + + {delivery?.reconciled ? "Rapproché" : "En attente"} + +
+
+
+ + + + {/* Formulaire de rapprochement */} +
+
+ {/* Données BL */} +
+

Bon de Livraison (BL)

+ +
+ + setFormData(prev => ({ ...prev, blNumber: e.target.value }))} + placeholder="Ex: BL-2025-001" + required + /> +
+ +
+ + setFormData(prev => ({ ...prev, blAmount: e.target.value }))} + placeholder="0.00" + /> +
+
+ + {/* Données Facture */} +
+

Facture

+ +
+ + setFormData(prev => ({ ...prev, invoiceReference: e.target.value }))} + placeholder="Ex: FAC-2025-001" + /> +
+ +
+ + setFormData(prev => ({ ...prev, invoiceAmount: e.target.value }))} + placeholder="0.00" + /> +
+
+
+ + {/* Actions */} +
+
+ + + +
+ + +
+
+ + {/* Note d'aide */} +
+ Note : Le numéro de BL est obligatoire pour valider un rapprochement. + Les montants sont optionnels mais recommandés pour un suivi précis. +
+
+
+
+ ); +} \ No newline at end of file diff --git a/client/src/pages/BLReconciliation.tsx b/client/src/pages/BLReconciliation.tsx index ad78436..a790f36 100644 --- a/client/src/pages/BLReconciliation.tsx +++ b/client/src/pages/BLReconciliation.tsx @@ -11,6 +11,7 @@ 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 } from "lucide-react"; +import ReconciliationModal from "@/components/modals/ReconciliationModal"; export default function BLReconciliation() { const { user } = useAuthUnified(); @@ -42,6 +43,8 @@ export default function BLReconciliation() { const [activeTab, setActiveTab] = useState("manual"); const [searchTerm, setSearchTerm] = useState(""); + const [selectedDelivery, setSelectedDelivery] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); // Récupérer les fournisseurs pour la logique automatique const { data: suppliers = [] } = useQuery({ @@ -83,8 +86,14 @@ export default function BLReconciliation() { return supplier?.automaticReconciliation; }); - // Fonction pour valider un rapprochement (admins et directeurs) - const handleValidateReconciliation = async (deliveryId: number) => { + // Fonction pour ouvrir le modal d'édition + const handleOpenModal = (delivery: any) => { + setSelectedDelivery(delivery); + setIsModalOpen(true); + }; + + // Fonction pour valider un rapprochement rapidement (avec validation des données) + const handleQuickValidate = async (delivery: any) => { if (!permissions.canValidate('reconciliation')) { toast({ title: "Accès refusé", @@ -94,12 +103,22 @@ export default function BLReconciliation() { return; } + // Vérifier que les données minimales sont présentes + if (!delivery.blNumber || delivery.blNumber.trim() === '') { + toast({ + title: "Données manquantes", + description: "Le numéro de BL est requis. Cliquez sur 'Voir les détails' pour le renseigner.", + variant: "destructive", + }); + return; + } + if (!window.confirm("Êtes-vous sûr de vouloir valider ce rapprochement ?")) { return; } try { - await apiRequest(`/api/deliveries/${deliveryId}`, "PUT", { + await apiRequest(`/api/deliveries/${delivery.id}`, "PUT", { reconciled: true, validatedAt: new Date().toISOString() }); @@ -405,7 +424,7 @@ export default function BLReconciliation() { <> {permissions.canValidate('reconciliation') && ( @@ -591,8 +611,9 @@ export default function BLReconciliation() {
@@ -617,6 +638,17 @@ export default function BLReconciliation() { )} + + {/* Modal de rapprochement */} + setIsModalOpen(false)} + delivery={selectedDelivery} + onSave={() => { + // Rafraîchir les données après sauvegarde + queryClient.invalidateQueries({ queryKey: ['/api/deliveries/bl'] }); + }} + />
); } \ No newline at end of file