diff --git a/client/src/pages/BLReconciliation.tsx b/client/src/pages/BLReconciliation.tsx index aa2299f..0cafb19 100644 --- a/client/src/pages/BLReconciliation.tsx +++ b/client/src/pages/BLReconciliation.tsx @@ -513,26 +513,37 @@ export default function BLReconciliation() { startProcessingTimer(); try { - const formData = new FormData(); - formData.append('file', selectedFile); - formData.append('supplier', selectedDeliveryForInvoice.supplier?.name || ''); - formData.append('blNumber', selectedDeliveryForInvoice.blNumber || ''); - formData.append('type', 'Facture'); - - // Créer un AbortController pour gérer le timeout - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 secondes - - const response = await fetch(selectedDeliveryForInvoice.group.webhookUrl, { - method: 'POST', - body: formData, - signal: controller.signal, + // Convertir le fichier en base64 pour l'envoyer via notre route backend + const fileBase64 = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const base64 = (reader.result as string).split(',')[1]; // Retirer le préfixe data:... + resolve(base64); + }; + reader.onerror = reject; + reader.readAsDataURL(selectedFile); }); - clearTimeout(timeoutId); + // Préparer les données pour la route backend proxy + const payload = { + webhookUrl: selectedDeliveryForInvoice.group.webhookUrl, + formDataFields: { + file: { + base64: fileBase64, + filename: selectedFile.name, + contentType: selectedFile.type + }, + supplier: selectedDeliveryForInvoice.supplier?.name || '', + blNumber: selectedDeliveryForInvoice.blNumber || '', + type: 'Facture' + } + }; - if (!response.ok) { - throw new Error(`Erreur ${response.status}: ${response.statusText}`); + // Envoyer via la route backend proxy (évite les problèmes CORS) + const response = await apiRequest('/api/reconciliation/send-invoice-webhook', 'POST', payload); + + if (!response || !response.success) { + throw new Error(response?.error || 'Erreur lors de l\'envoi'); } handleCloseWaitingModal(); diff --git a/package-lock.json b/package-lock.json index 0adb9fa..51b2686 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "@radix-ui/react-tooltip": "^1.2.0", "@tanstack/react-query": "^5.60.5", "@types/bcrypt": "^6.0.0", + "@types/form-data": "^2.2.1", "@types/memoizee": "^0.4.12", "@types/node-fetch": "^2.6.13", "@types/pg": "^8.15.4", @@ -58,6 +59,7 @@ "express": "^4.21.2", "express-rate-limit": "^7.5.1", "express-session": "^1.18.1", + "form-data": "^4.0.4", "framer-motion": "^11.13.1", "iconv-lite": "^0.6.3", "input-otp": "^1.4.2", @@ -3750,6 +3752,15 @@ "@types/express": "*" } }, + "node_modules/@types/form-data": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", + "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", diff --git a/package.json b/package.json index 7acdf08..38abed8 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@radix-ui/react-tooltip": "^1.2.0", "@tanstack/react-query": "^5.60.5", "@types/bcrypt": "^6.0.0", + "@types/form-data": "^2.2.1", "@types/memoizee": "^0.4.12", "@types/node-fetch": "^2.6.13", "@types/pg": "^8.15.4", @@ -60,6 +61,7 @@ "express": "^4.21.2", "express-rate-limit": "^7.5.1", "express-session": "^1.18.1", + "form-data": "^4.0.4", "framer-motion": "^11.13.1", "iconv-lite": "^0.6.3", "input-otp": "^1.4.2", diff --git a/server/routes.ts b/server/routes.ts index 78cf794..d710e1b 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -788,6 +788,103 @@ export async function registerRoutes(app: Express): Promise { } }); + // Route proxy pour envoi de factures vers webhook (accessible admin + directeur) + app.post('/api/reconciliation/send-invoice-webhook', isAuthenticated, async (req: any, res) => { + try { + const userId = req.user?.claims?.sub || req.user?.id; + if (!userId) { + return res.status(401).json({ error: 'Utilisateur non authentifié' }); + } + + const user = await storage.getUserWithGroups(userId); + if (!user) { + return res.status(404).json({ error: 'Utilisateur non trouvé' }); + } + + // Autoriser admin ET directeur + if (user.role !== 'admin' && user.role !== 'directeur') { + return res.status(403).json({ error: 'Accès refusé - Admin ou Directeur uniquement' }); + } + + const { webhookUrl, formDataFields } = req.body; + + if (!webhookUrl || !formDataFields) { + return res.status(400).json({ error: 'Données manquantes: webhookUrl ou formDataFields' }); + } + + console.log('📤 INVOICE: Envoi facture via webhook', { + webhookUrl: webhookUrl.substring(0, 50) + '...', + userId: user.id, + userRole: user.role, + fieldsCount: Object.keys(formDataFields).length + }); + + // Préparer le FormData pour l'envoi au webhook externe + const FormData = (await import('form-data')).default; + const formData = new FormData(); + + // Ajouter tous les champs du formData + for (const [key, value] of Object.entries(formDataFields)) { + if (key === 'file' && typeof value === 'object' && (value as any).base64) { + // Reconstituer le fichier depuis base64 + const fileData = value as { base64: string; filename: string; contentType: string }; + const buffer = Buffer.from(fileData.base64, 'base64'); + formData.append('file', buffer, { + filename: fileData.filename, + contentType: fileData.contentType + }); + } else { + formData.append(key, value as string); + } + } + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 60000); // 60 secondes + + const response = await fetch(webhookUrl, { + method: 'POST', + body: formData as any, + headers: formData.getHeaders(), + signal: controller.signal + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + const errorText = await response.text(); + console.error('❌ INVOICE: Erreur webhook', { status: response.status, errorText: errorText.substring(0, 200) }); + throw new Error(`Erreur webhook: ${response.status} ${response.statusText}`); + } + + const result = await response.text(); + console.log('✅ INVOICE: Webhook réussi', { userId: user.id, userRole: user.role }); + + res.json({ + success: true, + message: 'Facture envoyée avec succès', + webhookResponse: result + }); + + } catch (error: any) { + console.error('❌ INVOICE: Erreur envoi facture:', { + name: error.name, + message: error.message + }); + + let errorMessage = 'Erreur lors de l\'envoi de la facture'; + if (error.name === 'AbortError') { + errorMessage = 'Timeout - Le traitement a pris trop de temps'; + } else if (error.message) { + errorMessage = error.message; + } + + res.status(500).json({ + error: errorMessage, + details: error.message + }); + } + }); + // Auth routes handled by authSwitch (local or Replit) // Groups routes