diff --git a/attached_assets/image_1757356440912.png b/attached_assets/image_1757356440912.png new file mode 100644 index 0000000..d7627d3 Binary files /dev/null and b/attached_assets/image_1757356440912.png differ diff --git a/client/src/pages/Avoirs.tsx b/client/src/pages/Avoirs.tsx index c343bd0..a6c376c 100644 --- a/client/src/pages/Avoirs.tsx +++ b/client/src/pages/Avoirs.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { Plus, Search, FileText, CheckCircle, AlertCircle, Clock, Edit, Trash2, UserCheck, Send, Upload, XCircle } from "lucide-react"; import { useStore } from "@/components/Layout"; @@ -362,42 +362,61 @@ export default function Avoirs() { }; // Handle validation/devalidation - const handleValidateAvoir = (avoirId: number) => { - const avoir = avoirs.find(a => a.id === avoirId); - if (avoir) { - editAvoirMutation.mutate({ - id: avoirId, - data: { - supplierId: avoir.supplierId, - groupId: avoir.groupId, - invoiceReference: avoir.invoiceReference || "", - amount: avoir.amount, - comment: avoir.comment || "", - commercialProcessed: avoir.commercialProcessed, - status: avoir.status as "En attente de demande" | "Demandé" | "Reçu", - nocodbVerified: true, - nocodbVerifiedAt: new Date(), + const handleValidateAvoir = async (avoirId: number) => { + try { + // Utiliser la route spécifique pour la vérification NocoDB + await apiRequest(`/api/avoirs/${avoirId}/nocodb-verification`, 'PUT', { + verified: true + }); + + // Marquer le cache comme réconcilié (permanent) + const avoir = avoirs.find(a => a.id === avoirId); + if (avoir?.invoiceReference) { + try { + await apiRequest('/api/cache/mark-reconciled', 'POST', { + invoiceReference: avoir.invoiceReference, + groupId: avoir.groupId + }); + } catch (cacheError) { + console.warn('Cache marking failed but verification succeeded:', cacheError); } + } + + // Recharger les avoirs pour voir les changements + queryClient.invalidateQueries({ queryKey: ['/api/avoirs'] }); + + toast({ + title: "Avoir validé", + description: "L'avoir a été marqué comme validé avec succès", + }); + } catch (error) { + toast({ + title: "Erreur de validation", + description: error instanceof Error ? error.message : "Erreur lors de la validation", + variant: "destructive", }); } }; - const handleDevalidateAvoir = (avoirId: number) => { - const avoir = avoirs.find(a => a.id === avoirId); - if (avoir) { - editAvoirMutation.mutate({ - id: avoirId, - data: { - supplierId: avoir.supplierId, - groupId: avoir.groupId, - invoiceReference: avoir.invoiceReference || "", - amount: avoir.amount, - comment: avoir.comment || "", - commercialProcessed: avoir.commercialProcessed, - status: avoir.status as "En attente de demande" | "Demandé" | "Reçu", - nocodbVerified: false, - nocodbVerifiedAt: null, - } + const handleDevalidateAvoir = async (avoirId: number) => { + try { + // Utiliser la route spécifique pour la vérification NocoDB + await apiRequest(`/api/avoirs/${avoirId}/nocodb-verification`, 'PUT', { + verified: false + }); + + // Recharger les avoirs pour voir les changements + queryClient.invalidateQueries({ queryKey: ['/api/avoirs'] }); + + toast({ + title: "Avoir dévalidé", + description: "L'avoir a été marqué comme non-validé avec succès", + }); + } catch (error) { + toast({ + title: "Erreur de dévalidation", + description: error instanceof Error ? error.message : "Erreur lors de la dévalidation", + variant: "destructive", }); } }; @@ -421,6 +440,26 @@ export default function Avoirs() { ...prev, [variables.avoirId]: result })); + + // Auto-remplissage si facture trouvée et montant disponible + if (result.exists && result.invoiceAmount) { + // Auto-remplir le montant dans l'avoir via API + const avoir = avoirs.find(a => a.id === variables.avoirId); + if (avoir) { + editAvoirMutation.mutate({ + id: variables.avoirId, + data: { + supplierId: avoir.supplierId, + groupId: avoir.groupId, + invoiceReference: avoir.invoiceReference || "", + amount: result.invoiceAmount, // Auto-remplissage du montant + comment: avoir.comment || "", + commercialProcessed: avoir.commercialProcessed, + status: avoir.status as "En attente de demande" | "Demandé" | "Reçu", + } + }); + } + } setVerifyingAvoirs(prev => { const newSet = new Set(prev); @@ -519,6 +558,59 @@ export default function Avoirs() { }); }; + // Charger les résultats de vérification depuis le cache au démarrage + useEffect(() => { + const loadVerificationResults = async () => { + if (!avoirs || avoirs.length === 0) return; + + const avoirsWithReferences = avoirs.filter(avoir => + avoir.invoiceReference?.trim() && + (avoir.group?.nocodbTableName || avoir.group?.nocodbConfigId) + ); + + if (avoirsWithReferences.length === 0) return; + + console.log(`🔍 Chargement des résultats de vérification pour ${avoirsWithReferences.length} avoirs`); + + // Charger les résultats depuis le cache en parallèle + const results = await Promise.allSettled( + avoirsWithReferences.map(async (avoir) => { + try { + const result = await apiRequest(`/api/avoirs/${avoir.id}/verify-invoice`, 'POST', { + invoiceReference: avoir.invoiceReference, + forceRefresh: false // Utiliser le cache si disponible + }); + return { avoirId: avoir.id, result }; + } catch (error) { + console.error(`Erreur chargement cache avoir ${avoir.id}:`, error); + return { + avoirId: avoir.id, + result: { + exists: false, + matchType: 'none', + errorMessage: 'Erreur de chargement' + } + }; + } + }) + ); + + // Appliquer les résultats chargés + const newResults: Record = {}; + results.forEach((result) => { + if (result.status === 'fulfilled') { + const { avoirId, result: verificationResult } = result.value; + newResults[avoirId] = verificationResult; + } + }); + + setAvoirVerificationResults(newResults); + console.log(`✅ Résultats de vérification chargés pour ${Object.keys(newResults).length} avoirs`); + }; + + loadVerificationResults(); + }, [avoirs]); // Se déclenche quand les avoirs sont chargés + // 🔥 FONCTIONS WEBHOOK MODAL (comme rapprochement) const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; diff --git a/server/routes.ts b/server/routes.ts index cc5dfb2..f5c2199 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -2718,6 +2718,24 @@ export async function registerRoutes(app: Express): Promise { } await storage.updateAvoirNocodbVerification(id, verified); + + // Si validé, marquer le cache comme réconcilié (permanent) + if (verified) { + try { + const avoir = await storage.getAvoir(id); + if (avoir?.invoiceReference?.trim()) { + await invoiceVerificationService.updateCacheAsReconciled( + avoir.invoiceReference, + avoir.groupId + ); + console.log('✅ Cache marqué comme réconcilié pour avoir:', id); + } + } catch (cacheError) { + console.error('❌ Erreur marquage cache réconcilié:', cacheError); + // Ne pas faire échouer la validation si le cache échoue + } + } + console.log('✅ Avoir NocoDB verification updated:', id, 'verified:', verified, 'by user:', user.id); res.json({ message: "NocoDB verification status updated successfully" }); } catch (error) { @@ -2726,6 +2744,35 @@ export async function registerRoutes(app: Express): Promise { } }); + // Route pour marquer explicitement le cache comme réconcilié + app.post('/api/cache/mark-reconciled', isAuthenticated, async (req: any, res) => { + try { + const user = await storage.getUserWithGroups(req.user.claims ? req.user.claims.sub : req.user.id); + if (!user) { + return res.status(404).json({ message: "User not found" }); + } + + const { invoiceReference, groupId } = req.body; + + if (!invoiceReference || !groupId) { + return res.status(400).json({ message: "Invoice reference and group ID required" }); + } + + // Seuls admin et directeur peuvent marquer comme réconcilié + if (user.role !== 'admin' && user.role !== 'directeur') { + return res.status(403).json({ message: "Insufficient permissions" }); + } + + await invoiceVerificationService.updateCacheAsReconciled(invoiceReference, groupId); + console.log('✅ Cache marqué comme réconcilié:', { invoiceReference, groupId, user: user.id }); + + res.json({ message: "Cache marked as reconciled successfully" }); + } catch (error) { + console.error("Error marking cache as reconciled:", error); + res.status(500).json({ message: "Failed to mark cache as reconciled" }); + } + }); + // Statistics routes app.get('/api/stats/monthly', isAuthenticated, async (req: any, res) => { try {