diff --git a/client/src/pages/VentilationFreePro.tsx b/client/src/pages/VentilationFreePro.tsx
index 3cdc6a7..0739581 100644
--- a/client/src/pages/VentilationFreePro.tsx
+++ b/client/src/pages/VentilationFreePro.tsx
@@ -36,8 +36,7 @@ import {
Euro,
Share2,
} from "lucide-react";
-import jsPDF from "jspdf";
-import autoTable from "jspdf-autotable";
+// PDF généré côté serveur via trpc.freepro.generatePdf
// ── Types ──────────────────────────────────────────────────────────────────
@@ -83,138 +82,7 @@ function typeBadgeColor(type: string): string {
return "bg-orange-100 text-orange-800 border-orange-200";
}
-/** Formatage montant pour PDF : sans espace insécable pour éviter les artefacts jsPDF */
-function formatMontantPdf(centimes: number): string {
- const val = centimes / 100;
- const parts = val.toFixed(2).split(".");
- // Séparateur de milliers = espace simple (pas \u202f qui cause le slash)
- const intPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, " ");
- return `${intPart},${parts[1]} EUR`;
-}
-
-// ── Construction du PDF (partagée entre export local et SharePoint) ─────────
-
-function buildPdfDoc(importRecord: ImportRecord, lines: VentilationLine[]): jsPDF {
- // A4 portrait : 210mm × 297mm
- const doc = new jsPDF({ orientation: "portrait", unit: "mm", format: "a4" });
- const pageW = 210;
- const pageH = 297;
- const margin = 14;
- const tableStartY = 31; // Y où commence le tableau
- const tableEndY = pageH - 10; // Y max du tableau
- const usableW = pageW - margin * 2; // 182mm
-
- // ── En-tête ────────────────────────────────────────────────────────────────
- const now = new Date();
- const editDate = `Edite le ${now.toLocaleDateString("fr-FR")} - ${now.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })}`;
- doc.setFontSize(7);
- doc.setFont("helvetica", "normal");
- doc.text(editDate, pageW - margin, 7, { align: "right" });
-
- doc.setFontSize(13);
- doc.setFont("helvetica", "bold");
- doc.text("Ventilation facture FREE PRO", pageW / 2, 13, { align: "center" });
-
- const [moisStr, anneeStr] = importRecord.moisLabel.split("/");
- doc.setFontSize(10);
- doc.text(`01/${moisStr}/${anneeStr}`, pageW / 2, 20, { align: "center" });
-
- doc.setFontSize(8);
- doc.setFont("helvetica", "bold");
- doc.text("ref_piece :", margin, 27);
- doc.setFont("helvetica", "normal");
- doc.text(importRecord.refPiece ?? "", margin + 22, 27);
-
- // ── Calcul des dimensions du tableau ────────────────────────────────────────
- const totalCentimes = lines.reduce((s, l) => s + l.montantCentimes, 0);
- // nbRows = 1 header + N lignes + 1 footer
- const nbRows = lines.length + 2;
- // Hauteur totale disponible pour le tableau
- const availH = tableEndY - tableStartY;
- // Hauteur exacte par ligne
- const rowH = availH / nbRows;
- // Police : 1pt = 0.353mm, on prend 55% de rowH pour le texte
- const fontSize = Math.max(5, Math.min(9, Math.floor(rowH * 0.55 / 0.353)));
-
- // Largeurs colonnes
- const col0W = usableW * 0.45; // Structure
- const col1W = usableW * 0.32; // Type
- const col2W = usableW * 0.23; // Montant
- const col1X = margin + col0W;
- const col2X = col1X + col1W;
-
- // ── Dessin manuel du tableau ─────────────────────────────────────────────────
- doc.setFontSize(fontSize);
-
- const drawRow = (y: number, c0: string, c1: string, c2: string, bold: boolean, bg?: [number, number, number]) => {
- // Fond de ligne
- if (bg) {
- doc.setFillColor(bg[0], bg[1], bg[2]);
- doc.rect(margin, y, usableW, rowH, "F");
- }
- // Texte
- doc.setFont("helvetica", bold ? "bold" : "normal");
- const textY = y + rowH * 0.65; // centrage vertical approximatif
- const pad = 1.5;
- doc.text(c0, margin + pad, textY, { maxWidth: col0W - pad * 2 });
- doc.text(c1, col1X + pad, textY, { maxWidth: col1W - pad * 2 });
- doc.text(c2, col2X + col2W - pad, textY, { align: "right", maxWidth: col2W - pad * 2 });
- };
-
- const drawHLine = (y: number, lw: number, r: number, g: number, b: number) => {
- doc.setDrawColor(r, g, b);
- doc.setLineWidth(lw);
- doc.line(margin, y, margin + usableW, y);
- };
-
- const drawVLines = (y: number, h: number) => {
- doc.setDrawColor(180, 180, 180);
- doc.setLineWidth(0.1);
- doc.line(margin, y, margin, y + h);
- doc.line(col1X, y, col1X, y + h);
- doc.line(col2X, y, col2X, y + h);
- doc.line(margin + usableW, y, margin + usableW, y + h);
- };
-
- // Header
- const headerY = tableStartY;
- drawHLine(headerY, 0.4, 0, 0, 0);
- drawRow(headerY, "Structure", "Type", "Montant TTC", true);
- drawHLine(headerY + rowH, 0.4, 0, 0, 0);
- drawVLines(headerY, rowH);
-
- // Body
- for (let i = 0; i < lines.length; i++) {
- const l = lines[i];
- const y = headerY + rowH * (i + 1);
- const bg: [number, number, number] | undefined = i % 2 === 1 ? [248, 248, 248] : undefined;
- drawRow(y, l.structure ?? "(vide)", l.type, formatMontantPdf(l.montantCentimes), false, bg);
- drawHLine(y + rowH, 0.1, 200, 200, 200);
- drawVLines(y, rowH);
- }
-
- // Footer (Total)
- const footerY = headerY + rowH * (lines.length + 1);
- drawHLine(footerY, 0.4, 0, 0, 0);
- drawRow(footerY, "Total general", "", formatMontantPdf(totalCentimes), true, [240, 240, 240]);
- drawHLine(footerY + rowH, 0.4, 0, 0, 0);
- drawVLines(footerY, rowH);
-
- return doc;
-}
-
-function exportToPdf(importRecord: ImportRecord, lines: VentilationLine[]) {
- const doc = buildPdfDoc(importRecord, lines);
- const [moisStr, anneeStr] = importRecord.moisLabel.split("/");
- const moisPad = moisStr.padStart(2, "0");
- const anneeCourt = anneeStr.slice(2);
- doc.save(`FreePro - ventilation facture ${moisPad}.${anneeCourt}.pdf`);
-}
-
-function buildPdfBase64(importRecord: ImportRecord, lines: VentilationLine[]): string {
- const doc = buildPdfDoc(importRecord, lines);
- return doc.output("datauristring").split(",")[1];
-}
+// PDF généré côté serveur — pas de jsPDF côté client
// ── Composant principal ────────────────────────────────────────────────────
@@ -264,6 +132,29 @@ function VentilationFreeProContent() {
},
});
+ // Mutation génération PDF côté serveur
+ const [isExportingPdf, setIsExportingPdf] = useState(false);
+ const generatePdfMutation = trpc.freepro.generatePdf.useMutation({
+ onSuccess: (data) => {
+ setIsExportingPdf(false);
+ // Télécharger le PDF depuis le base64
+ const byteChars = atob(data.base64);
+ const byteArr = new Uint8Array(byteChars.length);
+ for (let i = 0; i < byteChars.length; i++) byteArr[i] = byteChars.charCodeAt(i);
+ const blob = new Blob([byteArr], { type: 'application/pdf' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = data.fileName;
+ a.click();
+ URL.revokeObjectURL(url);
+ },
+ onError: (err) => {
+ setIsExportingPdf(false);
+ toast.error(`Erreur génération PDF : ${err.message}`);
+ },
+ });
+
const [isExportingSP, setIsExportingSP] = useState(false);
const exportToSharePointMutation = trpc.freepro.exportToSharePoint.useMutation({
onSuccess: (data) => {
@@ -277,13 +168,10 @@ function VentilationFreeProContent() {
});
const handleExportSharePoint = useCallback(() => {
- if (!selectedImportId || !detail) return;
- const imp = detail.import as ImportRecord;
- const lines = (detail.lines ?? []) as VentilationLine[];
- const pdfBase64 = buildPdfBase64(imp, lines);
+ if (!selectedImportId) return;
setIsExportingSP(true);
- exportToSharePointMutation.mutate({ id: selectedImportId, pdfBase64 });
- }, [selectedImportId, detail, exportToSharePointMutation]);
+ exportToSharePointMutation.mutate({ id: selectedImportId });
+ }, [selectedImportId, exportToSharePointMutation]);
// Gestion du fichier
const handleFile = useCallback(
@@ -521,11 +409,16 @@ function VentilationFreeProContent() {
{imp && (