/** * bapCartouche.ts * Helper centralisé pour dessiner le cartouche BAP sur un PDF. * Utilise la détection de zone blanche pour positionner le cartouche * sans masquer d'informations importantes. */ import type { PDFDocument, PDFPage } from "pdf-lib"; import { findBestWhiteZone } from "./pdfWhiteZone"; export interface BapCartoucheData { typeAchat: string; destinataire: string; serviceConcerne: string; ventilationComptable: string; validatedAt: Date; signatureImageBytes?: Buffer; signatureMimeType?: "image/png" | "image/jpeg"; signatureName?: string; } /** * Dessine le cartouche BAP sur le PDF. * Détecte automatiquement une zone blanche disponible. * Si aucune zone n'est trouvée, ajoute une nouvelle page annexe. * * @returns Le PDFDocument modifié (même référence) */ export async function drawBapCartouche( pdfDoc: PDFDocument, pdfBuffer: Buffer, data: BapCartoucheData ): Promise { const { rgb, StandardFonts } = await import("pdf-lib"); const pages = pdfDoc.getPages(); const lastPageIndex = pages.length - 1; const lastPage = pages[lastPageIndex]; const { width: pageWidth, height: pageHeight } = lastPage.getSize(); const ZONE_HEIGHT = 160; const ZONE_MARGIN = 28; const ZONE_WIDTH = pageWidth - ZONE_MARGIN * 2; // ── Détection de la zone blanche ───────────────────────────────────────── let targetPage: PDFPage; let zoneX: number; let zoneY: number; let zoneW: number; let zoneH: number; let isNewPage = false; const whiteZone = await findBestWhiteZone(pdfBuffer, pageWidth, pageHeight, lastPageIndex); if (whiteZone && whiteZone.height >= 140) { // Zone blanche trouvée sur la dernière page targetPage = pages[whiteZone.pageIndex]; zoneX = whiteZone.x; zoneY = whiteZone.y; zoneW = whiteZone.width; zoneH = Math.min(ZONE_HEIGHT, whiteZone.height); } else { // Aucune zone suffisante → ajouter une nouvelle page targetPage = pdfDoc.addPage([pageWidth, pageHeight]); zoneX = ZONE_MARGIN; zoneY = pageHeight - ZONE_HEIGHT - 40; zoneW = ZONE_WIDTH; zoneH = ZONE_HEIGHT; isNewPage = true; // Sur la nouvelle page, ajouter un en-tête discret const fontHeader = await pdfDoc.embedFont(StandardFonts.Helvetica); targetPage.drawText("Annexe BAP — Bon à Payer", { x: ZONE_MARGIN, y: pageHeight - 30, size: 10, font: fontHeader, color: rgb(0.5, 0.5, 0.5), }); } // ── Dessin du cartouche ─────────────────────────────────────────────────── const font = await pdfDoc.embedFont(StandardFonts.HelveticaBold); const fontNormal = await pdfDoc.embedFont(StandardFonts.Helvetica); const leftColW = Math.floor(zoneW * 0.62); const rightColX = zoneX + leftColW + 10; const rightColW = zoneW - leftColW - 20; // Fond blanc avec bordure targetPage.drawRectangle({ x: zoneX, y: zoneY, width: zoneW, height: zoneH, color: rgb(1, 1, 1), borderColor: rgb(0.2, 0.2, 0.2), borderWidth: 1, }); // Ligne 1 : CAPEX/OPEX targetPage.drawText(data.typeAchat.toUpperCase(), { x: zoneX + 10, y: zoneY + zoneH - 22, size: 11, font, color: rgb(0.1, 0.1, 0.5), }); // Ligne 2 : BON À PAYER targetPage.drawText("BON À PAYER", { x: zoneX + 10, y: zoneY + zoneH - 42, size: 14, font, color: rgb(0, 0.5, 0), }); // Ligne 3 : Destinataire targetPage.drawText(data.destinataire || "TOUS", { x: zoneX + 10, y: zoneY + zoneH - 62, size: 10, font: fontNormal, color: rgb(0.2, 0.2, 0.2), }); // Séparateur horizontal targetPage.drawLine({ start: { x: zoneX + 10, y: zoneY + zoneH - 72 }, end: { x: zoneX + leftColW - 10, y: zoneY + zoneH - 72 }, thickness: 0.5, color: rgb(0.7, 0.7, 0.7), }); // Ligne 4 : Service + Ventilation targetPage.drawText( `Service : ${data.serviceConcerne || "-"} | Ventilation : ${data.ventilationComptable || "-"}`, { x: zoneX + 10, y: zoneY + zoneH - 88, size: 9, font: fontNormal, color: rgb(0.2, 0.2, 0.2), } ); // Ligne 5 : Date de validation const dateStr = data.validatedAt.toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit", year: "numeric", }); const timeStr = data.validatedAt.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit", }); targetPage.drawText(`Validé le ${dateStr} à ${timeStr}`, { x: zoneX + 10, y: zoneY + zoneH - 104, size: 8, font: fontNormal, color: rgb(0.4, 0.4, 0.4), }); // Note si nouvelle page if (isNewPage) { targetPage.drawText("(cartouche ajouté sur page annexe — facture complète sur page précédente)", { x: zoneX + 10, y: zoneY + zoneH - 120, size: 7, font: fontNormal, color: rgb(0.6, 0.6, 0.6), }); } // Séparateur vertical targetPage.drawLine({ start: { x: zoneX + leftColW, y: zoneY + 10 }, end: { x: zoneX + leftColW, y: zoneY + zoneH - 10 }, thickness: 0.5, color: rgb(0.8, 0.8, 0.8), }); // ── Signature ───────────────────────────────────────────────────────────── if (data.signatureName) { try { const sigWidth = Math.min(rightColW - 10, 110); const sigX = rightColX + (rightColW - sigWidth) / 2; // Afficher l'image si disponible if (data.signatureImageBytes) { const mimeType = data.signatureMimeType || "image/png"; const embeddedSig = mimeType === "image/png" ? await pdfDoc.embedPng(data.signatureImageBytes) : await pdfDoc.embedJpg(data.signatureImageBytes); const sigHeight = Math.round(sigWidth * 0.45); const sigY = zoneY + 35; targetPage.drawImage(embeddedSig, { x: sigX, y: sigY, width: sigWidth, height: sigHeight, }); } // Toujours afficher le nom du signataire const nameW = fontNormal.widthOfTextAtSize(data.signatureName, 9); targetPage.drawText(data.signatureName, { x: rightColX + (rightColW - nameW) / 2, y: zoneY + 22, size: 9, font: fontNormal, color: rgb(0.1, 0.1, 0.4), }); // Ligne de signature si pas d'image if (!data.signatureImageBytes) { targetPage.drawLine({ start: { x: rightColX + 10, y: zoneY + 35 }, end: { x: rightColX + rightColW - 10, y: zoneY + 35 }, thickness: 0.5, color: rgb(0.5, 0.5, 0.5), }); } } catch (_) { /* ignore signature errors */ } } return pdfDoc; }