diff --git a/client/src/pages/Avoirs.tsx b/client/src/pages/Avoirs.tsx index 1c06e76..62fe7b9 100644 --- a/client/src/pages/Avoirs.tsx +++ b/client/src/pages/Avoirs.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; -import { Plus, Search, FileText, CheckCircle, AlertCircle, Clock, Edit, Trash2, UserCheck, Send, Upload } from "lucide-react"; +import { Plus, Search, FileText, CheckCircle, AlertCircle, Clock, Edit, Trash2, UserCheck, Send, Upload, XCircle } from "lucide-react"; import { useStore } from "@/components/Layout"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -47,6 +47,8 @@ interface Avoir { id: number; name: string; color: string; + nocodbTableName?: string; + nocodbConfigId?: number; }; creator: { id: string; @@ -65,6 +67,8 @@ interface Group { id: number; name: string; color: string; + nocodbTableName?: string; + nocodbConfigId?: number; } // Schema for form validation @@ -111,6 +115,10 @@ export default function Avoirs() { const [showWaitingModal, setShowWaitingModal] = useState(false); const [processingSeconds, setProcessingSeconds] = useState(0); const [processingTimeout, setProcessingTimeout] = useState(null); + + // États pour le système de vérification de facture + const [avoirVerificationResults, setAvoirVerificationResults] = useState>({}); + const [verifyingAvoirs, setVerifyingAvoirs] = useState>(new Set()); // Utiliser le contexte global du magasin const { selectedStoreId } = useStore(); const { toast } = useToast(); @@ -328,6 +336,123 @@ export default function Avoirs() { setIsDeleteDialogOpen(true); }; + // 🔍 FONCTION DE VÉRIFICATION DE FACTURE (comme rapprochement) + const verifyAvoirInvoiceMutation = useMutation({ + mutationFn: async ({ avoirId, invoiceReference, forceRefresh }: { avoirId: number; invoiceReference?: string; forceRefresh?: boolean }) => { + try { + const result = await apiRequest(`/api/avoirs/${avoirId}/verify-invoice`, 'POST', { + invoiceReference, + forceRefresh: forceRefresh || false + }); + return result; + } catch (error: any) { + console.error('Erreur API vérification avoir:', error); + throw new Error(error.message || 'Erreur de vérification'); + } + }, + onSuccess: (result, variables) => { + setAvoirVerificationResults(prev => ({ + ...prev, + [variables.avoirId]: result + })); + + setVerifyingAvoirs(prev => { + const newSet = new Set(prev); + newSet.delete(variables.avoirId); + return newSet; + }); + }, + onError: (error, variables) => { + console.error('Erreur vérification facture avoir:', error); + setAvoirVerificationResults(prev => ({ + ...prev, + [variables.avoirId]: { + exists: false, + matchType: 'none', + errorMessage: error instanceof Error ? error.message : 'Erreur inconnue' + } + })); + + setVerifyingAvoirs(prev => { + const newSet = new Set(prev); + newSet.delete(variables.avoirId); + return newSet; + }); + + toast({ + title: "Erreur de vérification", + description: error instanceof Error ? error.message : 'Erreur inconnue', + variant: "destructive", + }); + } + }); + + // Fonction pour déclencher la vérification d'un avoir + const handleVerifyAvoirInvoice = (avoir: Avoir, forceRefresh: boolean = false) => { + const hasInvoiceRef = avoir.invoiceReference?.trim(); + + if (!hasInvoiceRef) { + toast({ + title: "Référence manquante", + description: "Veuillez saisir une référence de facture avant la vérification", + variant: "destructive", + }); + return; + } + + if (!avoir.group?.nocodbTableName && !avoir.group?.nocodbConfigId) { + toast({ + title: "Vérification non disponible", + description: "Ce magasin n'a pas de configuration NocoDB", + variant: "destructive", + }); + return; + } + + console.log('🔍 Déclenchement vérification avoir:', { + avoirId: avoir.id, + invoiceReference: avoir.invoiceReference, + supplier: avoir.supplier?.name, + group: avoir.group?.name + }); + + setVerifyingAvoirs(prev => new Set(prev).add(avoir.id)); + + verifyAvoirInvoiceMutation.mutate({ + avoirId: avoir.id, + invoiceReference: avoir.invoiceReference, + forceRefresh + }); + }; + + // Fonction pour vérifier tous les avoirs avec une référence facture + const handleVerifyAllAvoirInvoices = () => { + const avoirsToVerify = avoirs.filter(avoir => + avoir.invoiceReference?.trim() && + (avoir.group?.nocodbTableName || avoir.group?.nocodbConfigId) + ); + + if (avoirsToVerify.length === 0) { + toast({ + title: "Aucun avoir à vérifier", + description: "Aucun avoir avec référence de facture trouvé", + }); + return; + } + + avoirsToVerify.forEach((avoir, index) => { + // Délai échelonné pour éviter la surcharge + setTimeout(() => { + handleVerifyAvoirInvoice(avoir, true); // Force refresh pour tous + }, index * 200); // 200ms entre chaque vérification + }); + + toast({ + title: "Vérification lancée", + description: `Vérification de ${avoirsToVerify.length} avoir(s) en cours...`, + }); + }; + // 🔥 FONCTIONS WEBHOOK MODAL (comme rapprochement) const handleFileChange = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; @@ -525,13 +650,30 @@ export default function Avoirs() {

- - - - + )} + + + + + + + + + Créer un nouvel avoir @@ -766,6 +908,9 @@ export default function Avoirs() { Créé le + + Vérification + Actions @@ -826,6 +971,31 @@ export default function Avoirs() { + + {avoir.invoiceReference?.trim() ? ( +
+ {verifyingAvoirs.has(avoir.id) ? ( + + ) : avoirVerificationResults[avoir.id] ? ( + avoirVerificationResults[avoir.id].exists ? ( + + ) : ( + + ) + ) : ( + + )} +
+ ) : ( + Sans réf. + )} +
{/* 📋 Icône Upload pour avoirs "Reçu" */} diff --git a/server/routes.ts b/server/routes.ts index 320b74d..cc5dfb2 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -2600,6 +2600,84 @@ export async function registerRoutes(app: Express): Promise { } }); + // Route de vérification de facture NocoDB pour les avoirs + app.post('/api/avoirs/:id/verify-invoice', 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 avoirId = parseInt(req.params.id); + const avoir = await storage.getAvoir(avoirId); + + if (!avoir) { + return res.status(404).json({ message: "Avoir not found" }); + } + + // Check permissions (using 'deliveries' module for similar logic) + if (!hasPermission(user.role, 'deliveries', 'view')) { + return res.status(403).json({ message: "Insufficient permissions" }); + } + + // Only admin have access to all avoirs, others must be in the same group + if (user.role !== 'admin') { + const userGroupIds = user.userGroups?.map((ug: any) => ug.groupId) || []; + if (!userGroupIds.includes(avoir.groupId)) { + console.log('🚫 Access denied - User groups check:', { + userId: user.id, + userRole: user.role, + userGroupIds, + avoirGroupId: avoir.groupId, + avoirSupplier: avoir.supplier?.name + }); + return res.status(403).json({ message: "Access denied to this group" }); + } + } + + const { invoiceReference, forceRefresh } = req.body; + + if (!avoir.supplier || !avoir.group) { + console.log('❌ Avoir manque informations:', { + avoirId, + hasSupplier: !!avoir.supplier, + hasGroup: !!avoir.group + }); + return res.status(400).json({ message: "Avoir missing supplier or group information" }); + } + + // Vérifier que la référence facture est présente + if (!invoiceReference || !invoiceReference.trim()) { + return res.status(400).json({ message: "Référence de facture requise" }); + } + + console.log('🔍 Vérification facture avoir:', { + avoirId, + invoiceReference, + supplier: avoir.supplier?.name, + group: avoir.group?.name, + groupId: avoir.groupId + }); + + // Vérifier par référence de facture uniquement + const result = await invoiceVerificationService.verifyInvoice( + invoiceReference, + avoir.groupId, + forceRefresh || false, + false // Les avoirs ne sont pas "réconciliés" comme les livraisons + ); + + console.log('✅ Résultat vérification avoir:', result); + res.json(result); + } catch (error) { + console.error("Error verifying avoir invoice:", error); + res.status(500).json({ + message: "Failed to verify avoir invoice", + error: error instanceof Error ? error.message : 'Unknown error' + }); + } + }); + // Avoir status update routes app.put('/api/avoirs/:id/webhook-status', isAuthenticated, async (req: any, res) => { try {