From 7da8853e3da3bc839d66bb56163c37ce97df1985 Mon Sep 17 00:00:00 2001 From: michaelschal <35957947-michaelschal@users.noreply.replit.com> Date: Mon, 8 Sep 2025 17:47:03 +0000 Subject: [PATCH] Add webhook functionality to manage document uploads and processing Adds a modal for uploading PDF documents associated with specific task items, along with a waiting modal that displays a progress timer, mimicking the behavior of the reconciliation system. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 9fcf4b21-eb0c-4e53-a567-e2ce4a6ad869 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/9fcf4b21-eb0c-4e53-a567-e2ce4a6ad869/8RUpoKg --- client/src/pages/Avoirs.tsx | 324 +++++++++++++++++++++++++++++++++--- 1 file changed, 304 insertions(+), 20 deletions(-) diff --git a/client/src/pages/Avoirs.tsx b/client/src/pages/Avoirs.tsx index 046dd2e..c929c11 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 } from "lucide-react"; +import { Plus, Search, FileText, CheckCircle, AlertCircle, Clock, Edit, Trash2, UserCheck, Send, Upload } from "lucide-react"; import { useStore } from "@/components/Layout"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -100,6 +100,17 @@ export default function Avoirs() { const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [selectedAvoir, setSelectedAvoir] = useState(null); + + // État pour le modal d'envoi d'avoir + const [showAvoirModal, setShowAvoirModal] = useState(false); + const [selectedAvoirForUpload, setSelectedAvoirForUpload] = useState(null); + const [selectedFile, setSelectedFile] = useState(null); + const [isUploading, setIsUploading] = useState(false); + + // État pour le modal d'attente du webhook + const [showWaitingModal, setShowWaitingModal] = useState(false); + const [processingSeconds, setProcessingSeconds] = useState(0); + const [processingTimeout, setProcessingTimeout] = useState(null); // Utiliser le contexte global du magasin const { selectedStoreId } = useStore(); const { toast } = useToast(); @@ -317,6 +328,143 @@ export default function Avoirs() { setIsDeleteDialogOpen(true); }; + // 🔥 FONCTIONS WEBHOOK MODAL (comme rapprochement) + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file && file.type === 'application/pdf') { + setSelectedFile(file); + } else { + toast({ + title: "Erreur", + description: "Veuillez sélectionner un fichier PDF", + variant: "destructive", + }); + } + }; + + const handleOpenAvoirModal = (avoir: Avoir) => { + setSelectedAvoirForUpload(avoir); + setSelectedFile(null); + setShowAvoirModal(true); + }; + + const handleCloseAvoirModal = () => { + setShowAvoirModal(false); + setSelectedAvoirForUpload(null); + setSelectedFile(null); + setIsUploading(false); + }; + + const startProcessingTimer = () => { + setProcessingSeconds(0); + const interval = setInterval(() => { + setProcessingSeconds(prev => prev + 1); + }, 1000); + setProcessingTimeout(interval); + }; + + const handleCloseWaitingModal = () => { + setShowWaitingModal(false); + setIsUploading(false); + setProcessingSeconds(0); + if (processingTimeout) { + clearInterval(processingTimeout); + setProcessingTimeout(null); + } + }; + + const handleSendAvoir = async () => { + if (!selectedFile || !selectedAvoirForUpload) { + toast({ + title: "Erreur", + description: "Veuillez sélectionner un fichier PDF", + variant: "destructive", + }); + return; + } + + // Utiliser groupe par défaut (1) pour admin si pas de groupe sélectionné + const groupId = selectedAvoirForUpload.groupId || ((user as any)?.role === 'admin' ? 1 : selectedAvoirForUpload.groupId); + const groups = await queryClient.fetchQuery({ queryKey: ['/api/groups'] }); + const group = (groups as any[]).find(g => g.id === groupId); + + if (!group?.webhookUrl) { + toast({ + title: "Erreur", + description: "Aucun webhook configuré pour ce magasin", + variant: "destructive", + }); + return; + } + + // Fermer le modal de sélection et ouvrir le modal d'attente + setShowAvoirModal(false); + setShowWaitingModal(true); + setIsUploading(true); + startProcessingTimer(); + + try { + const formData = new FormData(); + formData.append('file', selectedFile); + formData.append('supplier', selectedAvoirForUpload.supplier?.name || ''); + formData.append('invoiceReference', selectedAvoirForUpload.invoiceReference || ''); + formData.append('type', 'Avoir'); + + // 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(group.webhookUrl, { + method: 'POST', + body: formData, + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error(`Erreur ${response.status}: ${response.statusText}`); + } + + handleCloseWaitingModal(); + + toast({ + title: "Succès", + description: "Avoir traité avec succès via le webhook", + }); + + // Reset des données + setSelectedAvoirForUpload(null); + setSelectedFile(null); + + // Invalider les caches pour recharger + queryClient.invalidateQueries({ + predicate: (query) => { + return query.queryKey[0]?.toString().includes('/api/avoirs') || false; + } + }); + + } catch (error: any) { + handleCloseWaitingModal(); + + let errorMessage = "Impossible d'envoyer l'avoir"; + if (error.name === 'AbortError') { + errorMessage = "Le traitement a pris trop de temps (timeout de 60 secondes)"; + } else if (error.message) { + errorMessage = error.message; + } + + toast({ + title: "Erreur d'envoi", + description: errorMessage, + variant: "destructive", + }); + + // Rouvrir le modal de sélection en cas d'erreur + setShowAvoirModal(true); + } + }; + // Confirm delete const confirmDelete = () => { if (selectedAvoir) { @@ -679,30 +827,45 @@ export default function Avoirs() { - {canEditDelete ? ( -
+
+ {/* 📋 Icône Upload pour avoirs "Reçu" */} + {avoir.status === 'Reçu' && ( - -
- ) : ( - Lecture seule - )} + )} + {canEditDelete && ( + <> + + + + )} + {!canEditDelete && avoir.status !== 'Reçu' && ( + Lecture seule + )} +
); @@ -893,6 +1056,127 @@ export default function Avoirs() { + + {/* 🌐 Modal d'envoi d'avoir (comme rapprochement) */} + + + + Envoyer Fichier Avoir + +
+ {selectedAvoirForUpload && ( +
+
+
{selectedAvoirForUpload.supplier.name}
+
+ Référence: {selectedAvoirForUpload.invoiceReference || 'Non renseigné'} +
+
+ Magasin: {selectedAvoirForUpload.group.name} +
+
+ Montant: {selectedAvoirForUpload.amount ? `${selectedAvoirForUpload.amount.toFixed(2)} €` : 'Non spécifié'} +
+
+
+ )} + +
+ + + {selectedFile && ( +
+ Fichier sélectionné: {selectedFile.name} +
+ )} +
+ + {selectedAvoirForUpload && ( +
+ Envoi via webhook configuré pour {selectedAvoirForUpload.group.name} +
+ )} +
+ + + + + +
+
+ + {/* ⏱️ Modal d'attente pour le traitement du webhook */} + {}}> + + + + + Traitement en cours + + +
+
+
+
+
+ {processingSeconds}s +
+
+ +
+

+ Traitement de votre avoir en cours... +

+

+ Le workflow peut prendre jusqu'à 1 minute. +
+ Veuillez patienter. +

+
+ +
+
+
+ +
+ {processingSeconds < 60 ? `${60 - processingSeconds}s restantes (max)` : 'Finalisation...'} +
+
+ + {selectedAvoirForUpload && ( +
+
+
+ {selectedAvoirForUpload.supplier.name} +
+
+ Référence: {selectedAvoirForUpload.invoiceReference || 'Non renseigné'} +
+
+ Fichier: {selectedFile?.name} +
+
+
+ )} +
+
+
); } \ No newline at end of file