import { useState, useCallback, useRef, useEffect } from "react"; import DashboardLayout from "@/components/DashboardLayout"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { trpc } from "@/lib/trpc"; import { Upload as UploadIcon, FileText, Loader2, CheckCircle, AlertCircle, FolderOpen, File, X, ChevronRight, } from "lucide-react"; import { toast } from "sonner"; import { useLocation } from "wouter"; interface UploadedFile { name: string; sourceFileId: number; status: "pending" | "processing" | "completed" | "error"; progress?: string; invoicesDetected?: number; } export default function Upload() { const [, setLocation] = useLocation(); const [isDragging, setIsDragging] = useState(false); const [uploadMode, setUploadMode] = useState<"file" | "folder">("file"); const [uploadedFiles, setUploadedFiles] = useState([]); const [isUploading, setIsUploading] = useState(false); const fileInputRef = useRef(null); const folderInputRef = useRef(null); const uploadMutation = trpc.invoices.upload.useMutation(); // Poll pour tous les fichiers en cours const pendingIds = uploadedFiles .filter((f) => f.status === "processing" || f.status === "pending") .map((f) => f.sourceFileId); const { data: sourceFiles } = trpc.sourceFiles.getByIds.useQuery( { ids: pendingIds }, { enabled: pendingIds.length > 0, refetchInterval: (query) => { const data = query.state.data; if (!data || data.length === 0) return false; const allDone = data.every( (f) => !f || f.processingStatus === "completed" || f.processingStatus === "error" ); return allDone ? false : 2000; }, } ); // Mettre à jour les statuts depuis le polling useEffect(() => { if (!sourceFiles || sourceFiles.length === 0) return; sourceFiles.forEach((sf) => { if (!sf) return; setUploadedFiles((prev) => prev.map((f) => { if (f.sourceFileId !== sf.id) return f; if (f.status === sf.processingStatus) return f; return { ...f, status: (sf.processingStatus ?? "pending") as UploadedFile["status"], progress: sf.processingProgress ?? undefined, invoicesDetected: sf.totalInvoicesDetected ?? undefined, }; }) ); }); }, [sourceFiles]); const uploadFile = async (file: File): Promise => { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = async () => { const base64 = (reader.result as string).split(",")[1]; try { const data = await uploadMutation.mutateAsync({ fileName: file.name, fileData: base64!, }); resolve(data.sourceFileId); } catch (err: any) { toast.error(`Erreur pour ${file.name} : ${err.message || "Erreur inconnue"}`); resolve(null); } }; reader.readAsDataURL(file); }); }; const handleFiles = async (files: File[]) => { const pdfFiles = files.filter((f) => f.type === "application/pdf"); if (pdfFiles.length === 0) { toast.error("Aucun fichier PDF trouvé"); return; } if (pdfFiles.length < files.length) { toast.warning(`${files.length - pdfFiles.length} fichier(s) ignoré(s) (non PDF)`); } setIsUploading(true); toast.info(`Envoi de ${pdfFiles.length} fichier(s)...`); for (const file of pdfFiles) { const sourceFileId = await uploadFile(file); if (sourceFileId !== null) { setUploadedFiles((prev) => [ ...prev, { name: file.name, sourceFileId, status: "processing" }, ]); } } setIsUploading(false); toast.success(`${pdfFiles.length} fichier(s) envoyé(s) — traitement en cours`); }; const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); }, []); const handleDrop = useCallback( (e: React.DragEvent) => { e.preventDefault(); setIsDragging(false); const files = Array.from(e.dataTransfer.files); handleFiles(files); }, [uploadMode] ); const handleFileInputChange = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); if (files.length > 0) handleFiles(files); // Reset input e.target.value = ""; }; const completedCount = uploadedFiles.filter((f) => f.status === "completed").length; const errorCount = uploadedFiles.filter((f) => f.status === "error").length; const processingCount = uploadedFiles.filter( (f) => f.status === "processing" || f.status === "pending" ).length; return (
{/* Header */}

Importer des factures

Importez un fichier PDF unique ou tout un dossier de factures

{/* Mode selector */}
{/* Drop zone */} {uploadMode === "file" ? "Upload de fichier PDF" : "Import d'un dossier"} {uploadMode === "file" ? "Glissez-déposez un fichier PDF ou cliquez pour sélectionner" : "Sélectionnez un dossier — tous les fichiers PDF qu'il contient seront importés"}
{ if (uploadMode === "file") fileInputRef.current?.click(); else folderInputRef.current?.click(); }} > {/* Hidden inputs */} {uploadMode === "file" ? ( ) : ( )}

{isUploading ? "Envoi en cours..." : uploadMode === "file" ? "Glissez-déposez un fichier PDF ici" : "Cliquez pour sélectionner un dossier"}

{uploadMode === "file" ? "ou cliquez pour sélectionner un ou plusieurs fichiers PDF" : "Tous les fichiers PDF du dossier sélectionné seront importés"}

{/* Liste des fichiers uploadés */} {uploadedFiles.length > 0 && (
Fichiers importés {completedCount > 0 && ( {completedCount} terminé(s) )} {processingCount > 0 && ( {processingCount} en cours )} {errorCount > 0 && ( {errorCount} erreur(s) )}
{completedCount > 0 && ( )}
{uploadedFiles.map((f, i) => (
{f.status === "processing" || f.status === "pending" ? ( ) : f.status === "completed" ? ( ) : ( )}

{f.name}

{f.status === "processing" && (

{f.progress || "Traitement en cours..."}

)} {f.status === "completed" && f.invoicesDetected !== undefined && (

{f.invoicesDetected} facture(s) détectée(s)

)} {f.status === "error" && (

{f.progress || "Erreur de traitement"}

)}
{f.status === "processing" || f.status === "pending" ? "En cours" : f.status === "completed" ? "Terminé" : "Erreur"}
))}
)}
); }