mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-12 01:37:40 +02:00
Add feature to send PDF files to a webhook for processing
Implement a new API endpoint `/api/bap/send-webhook` that accepts PDF uploads and recipient data, processing them via a n8n webhook. This includes client-side state management for file uploads and processing feedback, along with server-side validation and error handling. Dependencies for `form-data`, `multer`, and `node-fetch` were also updated. Replit-Commit-Author: Agent Replit-Commit-Session-Id: cfbfe47c-0c9a-4ebf-8a41-2937407eccff Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/1957c339-2757-4d1f-8e92-e9f71a1ce58e/cfbfe47c-0c9a-4ebf-8a41-2937407eccff/nLNyJ0g
This commit is contained in:
1 parent
13dc0db957
commit
fdbbb14a4f
5 files changed
+630
-13
No files matched your search
@@ -3,6 +3,13 @@ import { useAuthSimple } from "@/hooks/useAuthSimple";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useStore } from "./Layout";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { useState } from "react";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest } from "@/lib/queryClient";
|
||||
import {
|
||||
Store,
|
||||
Calendar,
|
||||
@@ -23,7 +30,9 @@ import {
|
||||
ChevronRight,
|
||||
X,
|
||||
Settings,
|
||||
Wrench
|
||||
Wrench,
|
||||
FileUp,
|
||||
Send
|
||||
} from "lucide-react";
|
||||
|
||||
export default function Sidebar() {
|
||||
@@ -31,6 +40,125 @@ export default function Sidebar() {
|
||||
const [location] = useLocation();
|
||||
const { sidebarCollapsed, setSidebarCollapsed, mobileMenuOpen, setMobileMenuOpen } = useStore();
|
||||
const isMobile = useIsMobile();
|
||||
const [showBapModal, setShowBapModal] = useState(false);
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [selectedRecipient, setSelectedRecipient] = useState<string>('');
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [showWaitingModal, setShowWaitingModal] = useState(false);
|
||||
const [processingSeconds, setProcessingSeconds] = useState(0);
|
||||
const [processingTimeout, setProcessingTimeout] = useState<NodeJS.Timeout | null>(null);
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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 startProcessingTimer = () => {
|
||||
setProcessingSeconds(0);
|
||||
const interval = setInterval(() => {
|
||||
setProcessingSeconds(prev => {
|
||||
if (prev >= 60) {
|
||||
clearInterval(interval);
|
||||
return 60;
|
||||
}
|
||||
return prev + 1;
|
||||
});
|
||||
}, 1000);
|
||||
setProcessingTimeout(interval);
|
||||
};
|
||||
|
||||
const handleCloseWaitingModal = () => {
|
||||
setShowWaitingModal(false);
|
||||
setProcessingSeconds(0);
|
||||
if (processingTimeout) {
|
||||
clearTimeout(processingTimeout);
|
||||
setProcessingTimeout(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendBap = async () => {
|
||||
if (!selectedFile || !selectedRecipient) {
|
||||
toast({
|
||||
title: "Erreur",
|
||||
description: "Veuillez sélectionner un fichier PDF et un destinataire",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Fermer le modal de sélection et ouvrir le modal d'attente
|
||||
setShowBapModal(false);
|
||||
setShowWaitingModal(true);
|
||||
setIsUploading(true);
|
||||
startProcessingTimer();
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('pdf', selectedFile);
|
||||
formData.append('recipient', selectedRecipient);
|
||||
|
||||
// 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('/api/bap/send-webhook', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
signal: controller.signal,
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || `Erreur ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
handleCloseWaitingModal();
|
||||
|
||||
toast({
|
||||
title: "Succès",
|
||||
description: "Fichier BAP envoyé avec succès",
|
||||
});
|
||||
|
||||
// Reset des données
|
||||
setSelectedFile(null);
|
||||
setSelectedRecipient('');
|
||||
|
||||
} catch (error: any) {
|
||||
handleCloseWaitingModal();
|
||||
|
||||
let errorMessage = "Impossible d'envoyer le fichier BAP";
|
||||
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",
|
||||
description: errorMessage,
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
// Réouvrir le modal de sélection en cas d'erreur
|
||||
setShowBapModal(true);
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Debug logging pour production
|
||||
console.log('Sidebar - User:', user);
|
||||
@@ -163,6 +291,13 @@ export default function Sidebar() {
|
||||
icon: Settings,
|
||||
roles: ["admin"]
|
||||
},
|
||||
{
|
||||
path: "#bap",
|
||||
label: "BAP",
|
||||
icon: FileUp,
|
||||
roles: ["admin"],
|
||||
isButton: true
|
||||
},
|
||||
];
|
||||
|
||||
const hasPermission = (roles: string[]) => {
|
||||
@@ -416,6 +551,24 @@ export default function Sidebar() {
|
||||
const Icon = item.icon;
|
||||
const active = isActive(item.path);
|
||||
|
||||
if (item.isButton && item.path === '#bap') {
|
||||
return (
|
||||
<div key={item.path}>
|
||||
<div
|
||||
className={`flex items-center cursor-pointer ${sidebarCollapsed ? 'px-3 py-3 justify-center' : 'px-3 py-2'} text-sm font-medium transition-colors hover:bg-gray-100 text-gray-700`}
|
||||
title={sidebarCollapsed ? item.label : undefined}
|
||||
onClick={() => {
|
||||
setShowBapModal(true);
|
||||
if (isMobile) setMobileMenuOpen(false);
|
||||
}}
|
||||
>
|
||||
<Icon className={`h-4 w-4 ${sidebarCollapsed ? '' : 'mr-3'}`} />
|
||||
{!sidebarCollapsed && item.label}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link key={item.path} href={item.path}>
|
||||
<div
|
||||
@@ -437,6 +590,81 @@ export default function Sidebar() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modal BAP */}
|
||||
<Dialog open={showBapModal} onOpenChange={setShowBapModal}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<FileUp className="h-5 w-5" />
|
||||
Envoi BAP
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="pdf-upload">Fichier PDF</Label>
|
||||
<Input
|
||||
id="pdf-upload"
|
||||
type="file"
|
||||
accept=".pdf"
|
||||
onChange={handleFileChange}
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="recipient">Envoyer à</Label>
|
||||
<Select value={selectedRecipient} onValueChange={setSelectedRecipient}>
|
||||
<SelectTrigger id="recipient" className="mt-1">
|
||||
<SelectValue placeholder="Sélectionner un destinataire" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Prissela">Prissela</SelectItem>
|
||||
<SelectItem value="Célia">Célia</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setShowBapModal(false)}>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSendBap}
|
||||
disabled={!selectedFile || !selectedRecipient || isUploading}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Send className="h-4 w-4" />
|
||||
{isUploading ? 'Envoi...' : 'Envoyer'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Modal d'attente */}
|
||||
<Dialog open={showWaitingModal} onOpenChange={() => {}}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Clock className="h-5 w-5" />
|
||||
Traitement en cours...
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 text-center">
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
<div className="text-sm text-gray-600">
|
||||
Envoi du fichier vers le workflow...
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{processingSeconds < 60
|
||||
? `${processingSeconds}s écoulées`
|
||||
: '60s+ écoulées'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* User Profile & Logout */}
|
||||
<div className="border-t border-gray-200 p-4">
|
||||
{!sidebarCollapsed ? (
|
||||
|
||||
Reference in new issue
Block a user