diff --git a/client/src/pages/Invoices.tsx b/client/src/pages/Invoices.tsx index 8dad30c..bf125f4 100644 --- a/client/src/pages/Invoices.tsx +++ b/client/src/pages/Invoices.tsx @@ -87,15 +87,10 @@ export default function Invoices() { const exportMutation = trpc.sftp.exportToPdf.useMutation({ onSuccess: (data) => { - toast.success(`${data.invoices.length} facture(s) exportée(s) avec succès`); - - // Open PDFs in new tabs - data.invoices.forEach((inv, index) => { - // Add a small delay between each window to avoid popup blocking - setTimeout(() => { - window.open(inv.fileUrl, '_blank'); - }, index * 100); - }); + toast.success( + `${data.copiedCount} facture(s) exportée(s) avec succès vers:\n${data.exportFolder}`, + { duration: 5000 } + ); setSelectedIds([]); utils.invoices.list.invalidate(); diff --git a/client/src/pages/InvoicesBAP.tsx b/client/src/pages/InvoicesBAP.tsx index ef68320..ff40ac2 100644 --- a/client/src/pages/InvoicesBAP.tsx +++ b/client/src/pages/InvoicesBAP.tsx @@ -32,7 +32,7 @@ import { TableRow, } from "@/components/ui/table"; import { trpc } from "@/lib/trpc"; -import { Search, FileText, Download, FileSpreadsheet, Trash2, Edit, Trash } from "lucide-react"; +import { Search, FileText, Download, FileSpreadsheet, Trash2, Edit, Trash, CheckCircle } from "lucide-react"; import * as XLSX from 'xlsx'; import { toast } from "sonner"; import { useLocation } from "wouter"; @@ -106,15 +106,10 @@ export default function InvoicesBAP() { const exportMutation = trpc.sftp.exportToPdf.useMutation({ onSuccess: (data) => { - toast.success(`${data.invoices.length} facture(s) exportée(s) avec succès`); - - // Open PDFs in new tabs - data.invoices.forEach((inv, index) => { - // Add a small delay between each window to avoid popup blocking - setTimeout(() => { - window.open(inv.fileUrl, '_blank'); - }, index * 100); - }); + toast.success( + `${data.copiedCount} facture(s) exportée(s) avec succès vers:\n${data.exportFolder}`, + { duration: 5000 } + ); setSelectedIds([]); utils.invoices.list.invalidate(); @@ -576,7 +571,16 @@ export default function InvoicesBAP() { - {getQualityBadge(invoice.qualityScore)} + +
+ {getQualityBadge(invoice.qualityScore)} + {isEligibleForExport(invoice) && ( +
+ +
+ )} +
+
{getExportStatusBadge(invoice.exportStatus || "not_exported")}
diff --git a/server/routers.ts b/server/routers.ts index 00a4c3e..d1b7532 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -525,6 +525,20 @@ export const appRouter = router({ exportToPdf: protectedProcedure .input(z.object({ invoiceIds: z.array(z.number()) })) .mutation(async ({ input, ctx }) => { + const fs = await import('fs/promises'); + const path = await import('path'); + + // Get export folder from settings + const settings = await getImportSettingsByUser(ctx.user.id); + const exportFolder = settings?.exportFolder; + + if (!exportFolder) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Le dossier d'export n'est pas configuré. Veuillez le définir dans les paramètres de réception." + }); + } + // Validate that all invoices have quality score of 100 const invoices = await Promise.all( input.invoiceIds.map(id => getInvoiceById(id)) @@ -537,27 +551,72 @@ export const appRouter = router({ if (invalidInvoices.length > 0) { throw new TRPCError({ code: "BAD_REQUEST", - message: "Toutes les factures doivent avoir un score de qualit\u00e9 de 100% pour \u00eatre export\u00e9es" + message: "Toutes les factures doivent avoir un score de qualité de 100% pour être exportées" }); } - // Update export status + // Create export folder if it doesn't exist + try { + await fs.mkdir(exportFolder, { recursive: true }); + } catch (error) { + console.error('Error creating export folder:', error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Impossible de créer le dossier d'export: ${exportFolder}` + }); + } + + // Copy PDFs to export folder + const copiedFiles: string[] = []; + const errors: string[] = []; + const STORAGE_BASE_PATH = process.env.STORAGE_BASE_PATH || path.join(process.cwd(), "storage"); + for (const invoice of invoices) { - if (invoice) { - await updateInvoice(invoice.id, { - exportStatus: "exported", - exportedAt: new Date(), - exportMode: "manual", - }); + if (invoice && invoice.fileKey) { + try { + // Build source path from fileKey + const sourcePath = path.join(STORAGE_BASE_PATH, invoice.fileKey); + + // Extract filename from fileKey + const filename = path.basename(invoice.fileKey); + const destPath = path.join(exportFolder, filename); + + // Copy file + await fs.copyFile(sourcePath, destPath); + copiedFiles.push(destPath); + + // Update export status + await updateInvoice(invoice.id, { + exportStatus: "exported", + exportedAt: new Date(), + exportMode: "manual", + }); + } catch (error: any) { + console.error(`Error copying file for invoice ${invoice.id}:`, error); + errors.push(`${invoice.supplierName || 'Inconnu'} (${invoice.invoiceNumber || 'N/A'}): ${error.message}`); + + // Update export status to error + await updateInvoice(invoice.id, { + exportStatus: "export_error", + exportMode: "manual", + }); + } } } - // Return the file URLs for PDF generation on client side + if (errors.length > 0) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Erreurs lors de l'export:\n${errors.join('\n')}` + }); + } + return { success: true, + exportFolder, + copiedCount: copiedFiles.length, invoices: invoices.filter(Boolean).map(inv => ({ id: inv!.id, - fileUrl: inv!.fileUrl, supplierName: inv!.supplierName, invoiceNumber: inv!.invoiceNumber, invoiceDate: inv!.invoiceDate, diff --git a/todo.md b/todo.md index ee69dee..2f3ca74 100644 --- a/todo.md +++ b/todo.md @@ -486,4 +486,15 @@ - [x] Ajouter bouton "Exporter" qui s'active si au moins une facture est cochée - [x] Utiliser la route tRPC existante pour exporter les factures sélectionnées - [x] Implémenter la génération de fichiers d'export (PDF) -- [ ] Tester la sélection et l'export des factures BAP +- [x] Tester la sélection et l'export des factures BAP + +## Améliorations export factures BAP +- [x] Ajouter icône verte ✓ dans la colonne Score pour les factures exportables +- [x] Modifier la fonction getQualityBadge pour afficher l'indicateur d'exportabilité +- [x] Modifier la route tRPC exportToPdf pour copier les PDFs vers exportFolder +- [x] Ajouter la logique de copie de fichiers avec fs.copyFile +- [x] Récupérer le paramètre exportFolder depuis importSettings +- [x] Créer le dossier exportFolder s'il n'existe pas +- [x] Gérer les erreurs de copie de fichiers +- [x] Afficher une notification de succès avec le chemin du dossier d'export +- [ ] Tester l'export vers le dossier configuré