From 5d919f891f2640e8783f7ae896be8036d1d7805e Mon Sep 17 00:00:00 2001 From: Manus Date: Fri, 5 Jun 2026 11:00:37 -0400 Subject: [PATCH] fix: PDF FreePro dessin manuel - 1 page A4 portrait garantie --- client/src/pages/VentilationFreePro.tsx | 159 +++++++++++++----------- 1 file changed, 86 insertions(+), 73 deletions(-) diff --git a/client/src/pages/VentilationFreePro.tsx b/client/src/pages/VentilationFreePro.tsx index 5989bc4..3cdc6a7 100644 --- a/client/src/pages/VentilationFreePro.tsx +++ b/client/src/pages/VentilationFreePro.tsx @@ -100,92 +100,105 @@ function buildPdfDoc(importRecord: ImportRecord, lines: VentilationLine[]): jsPD const pageW = 210; const pageH = 297; const margin = 14; - const headerEndY = 33; // Y après l'en-tête - const footerH = 8; // marge basse + 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 compact - doc.setFontSize(13); - doc.setFont("helvetica", "bold"); - doc.text("Ventilation facture FREE PRO", pageW / 2, 12, { align: "center" }); - - // Date du mois - const [moisStr, anneeStr] = importRecord.moisLabel.split("/"); - const dateLabel = `01/${moisStr}/${anneeStr}`; - doc.setFontSize(10); - doc.text(dateLabel, pageW / 2, 19, { align: "center" }); - - // Date d'édition (haut droite) + // ── 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, 8, { align: "right" }); + 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" }); - // Référence pièce doc.setFontSize(8); doc.setFont("helvetica", "bold"); - doc.text("ref_piece :", margin, 26); + doc.text("ref_piece :", margin, 27); doc.setFont("helvetica", "normal"); - doc.text(importRecord.refPiece ?? "", margin + 22, 26); + doc.text(importRecord.refPiece ?? "", margin + 22, 27); - // Données tableau - const tableData = lines.map((l) => [ - l.structure ?? "(vide)", - l.type, - formatMontantPdf(l.montantCentimes), - ]); + // ── Calcul des dimensions du tableau ──────────────────────────────────────── const totalCentimes = lines.reduce((s, l) => s + l.montantCentimes, 0); - - // Largeurs colonnes A4 portrait (~182mm utiles) - const usableW = pageW - margin * 2; - const colStructure = usableW * 0.45; - const colType = usableW * 0.32; - const colMontant = usableW * 0.23; - - // Calcul précis pour tenir sur 1 page : - // nbRows = lignes données + 1 header + 1 footer + // nbRows = 1 header + N lignes + 1 footer const nbRows = lines.length + 2; - // Hauteur disponible entre fin d'en-tête et bas de page - const availH = pageH - headerEndY - footerH; - // Hauteur max par cellule (en mm) pour tenir en 1 page - const maxCellH = availH / nbRows; - // Padding : 1/4 de la hauteur de cellule, min 0.8, max 2.5 - const cellPad = Math.max(0.8, Math.min(2.5, maxCellH * 0.25)); - // Font size : basé sur hauteur de cellule disponible pour le texte - const textH = maxCellH - cellPad * 2; - // 1pt ≈ 0.353mm ; on vise textH mm de hauteur de texte - const fontSize = Math.max(5, Math.min(9, Math.floor(textH / 0.353))); + // 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))); - autoTable(doc, { - startY: headerEndY, - head: [["Structure", "Type", "Montant TTC"]], - body: tableData, - foot: [["Total general", "", formatMontantPdf(totalCentimes)]], - styles: { fontSize, cellPadding: cellPad, overflow: "ellipsize", minCellHeight: 0 }, - headStyles: { - fillColor: [255, 255, 255], textColor: [0, 0, 0], - fontStyle: "bold", lineWidth: 0.3, lineColor: [0, 0, 0], - minCellHeight: 0, - }, - footStyles: { - fillColor: [255, 255, 255], textColor: [0, 0, 0], - fontStyle: "bold", lineWidth: 0.3, lineColor: [0, 0, 0], - minCellHeight: 0, - }, - bodyStyles: { lineWidth: 0.1, lineColor: [180, 180, 180], minCellHeight: 0 }, - columnStyles: { - 0: { cellWidth: colStructure }, - 1: { cellWidth: colType }, - 2: { cellWidth: colMontant, halign: "right" }, - }, - alternateRowStyles: { fillColor: [248, 248, 248] }, - // Ne pas autoriser de saut de page - pageBreak: "avoid", - rowPageBreak: "avoid", - showFoot: "lastPage", - margin: { left: margin, right: margin, bottom: footerH }, - tableWidth: usableW, - }); + // 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; }