diff --git a/backend/src/services/pdf.ts b/backend/src/services/pdf.ts index 7f7d26b..b4fbf72 100644 --- a/backend/src/services/pdf.ts +++ b/backend/src/services/pdf.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -// @ts-ignore — pdf-lib est complet lors d'un npm install propre sur le VPS -const { PDFDocument, rgb, StandardFonts, PageSizes } = require('pdf-lib'); +// @ts-ignore +const { PDFDocument, rgb, StandardFonts } = require('pdf-lib'); import fs from 'fs/promises'; import path from 'path'; @@ -10,6 +10,12 @@ export interface PdfGuest { company?: string | null; } +/** + * Génère le PDF de la facture. + * - Une page par image (ticket/reçu) + * - Si des invités sont présents, la liste est ajoutée EN BAS de la dernière + * page d'image (même page, sous le ticket), sans page séparée. + */ export async function generateInvoicePdf( imagePaths: string[], guests: PdfGuest[], @@ -17,46 +23,92 @@ export async function generateInvoicePdf( ): Promise { const pdfDoc = await PDFDocument.create(); - for (const imgPath of imagePaths) { + const font = guests.length > 0 ? await pdfDoc.embedFont(StandardFonts.Helvetica) : null; + const fontBold = guests.length > 0 ? await pdfDoc.embedFont(StandardFonts.HelveticaBold) : null; + + for (let idx = 0; idx < imagePaths.length; idx++) { + const imgPath = imagePaths[idx]; const imgBytes = await fs.readFile(imgPath); - const ext = path.extname(imgPath).toLowerCase(); - const image = ext === '.png' + const ext = path.extname(imgPath).toLowerCase(); + const image = ext === '.png' ? await pdfDoc.embedPng(imgBytes) : await pdfDoc.embedJpg(imgBytes); - const dims = image.scale(1); - const page = pdfDoc.addPage([dims.width, dims.height]); - page.drawImage(image, { x: 0, y: 0, width: dims.width, height: dims.height }); - } - if (guests.length > 0) { - const font = await pdfDoc.embedFont(StandardFonts.Helvetica); - const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold); - const [W, H] = PageSizes.A4; - const M = 50; - const LH = 24; - const page = pdfDoc.addPage([W, H]); - let y = H - M; + const imgW = image.width; + const imgH = image.height; - page.drawText('Liste des invites', { x: M, y, size: 18, font: fontBold, color: rgb(0.1, 0.1, 0.1) }); - y -= LH * 1.5; + const isLast = idx === imagePaths.length - 1; + const addList = isLast && guests.length > 0; - page.drawLine({ start: { x: M, y }, end: { x: W - M, y }, thickness: 1, color: rgb(0.2, 0.2, 0.2) }); - y -= LH; + // Hauteur supplémentaire pour la liste d'invités (sous le ticket) + const PADDING = 20; // marge entre ticket et liste + const LINE_H = 22; // hauteur par ligne d'invité + const HEADER_H = 50; // titre + séparateur + const listH = addList + ? HEADER_H + guests.length * LINE_H + PADDING * 2 + : 0; - page.drawText('Nom', { x: M, y, size: 11, font: fontBold, color: rgb(0.3, 0.3, 0.3) }); - page.drawText('Entreprise', { x: M + 230, y, size: 11, font: fontBold, color: rgb(0.3, 0.3, 0.3) }); - y -= 6; + const pageW = imgW; + const pageH = imgH + listH; - page.drawLine({ start: { x: M, y }, end: { x: W - M, y }, thickness: 0.5, color: rgb(0.7, 0.7, 0.7) }); - y -= LH; + const page = pdfDoc.addPage([pageW, pageH]); - for (const guest of guests) { - if (y < M + LH) break; - page.drawText(guest.name, { x: M, y, size: 11, font, color: rgb(0.1, 0.1, 0.1), maxWidth: 220 }); - if (guest.company) { - page.drawText(guest.company, { x: M + 230, y, size: 11, font, color: rgb(0.1, 0.1, 0.1), maxWidth: W - M - 230 - M }); + // ── Image (placée en haut de la page) ────────────────────── + page.drawImage(image, { x: 0, y: listH, width: imgW, height: imgH }); + + // ── Liste des invités (en bas, sous l'image) ─────────────── + if (addList && font && fontBold) { + const M = 30; // marge gauche/droite + let y = listH - PADDING; + + // Ligne de séparation entre ticket et liste + page.drawLine({ + start: { x: M, y: listH - 1 }, + end: { x: pageW - M, y: listH - 1 }, + thickness: 1, + color: rgb(0.8, 0.8, 0.8), + }); + + // Titre + page.drawText('Invités', { + x: M, y, + size: 13, + font: fontBold, + color: rgb(0.15, 0.15, 0.15), + }); + y -= HEADER_H - PADDING - 8; + + // Séparateur sous le titre + page.drawLine({ + start: { x: M, y }, + end: { x: pageW - M, y }, + thickness: 0.5, + color: rgb(0.75, 0.75, 0.75), + }); + y -= 16; + + // Lignes invités + for (const guest of guests) { + if (y < 4) break; + page.drawText(guest.name, { + x: M, y, + size: 11, + font: fontBold, + color: rgb(0.1, 0.1, 0.1), + maxWidth: guest.company ? (pageW - M * 2) / 2 - 10 : pageW - M * 2, + }); + if (guest.company) { + page.drawText(guest.company, { + x: M + (pageW - M * 2) / 2, + y, + size: 11, + font, + color: rgb(0.35, 0.35, 0.35), + maxWidth: (pageW - M * 2) / 2, + }); + } + y -= LINE_H; } - y -= LH; } }