portail-associations/server/salleInvoicePdf.ts

150 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import PDFDocument from "pdfkit";
import fs from "node:fs";
import path from "node:path";
import type { SallePricingSummary } from "@shared/sallePricing";
const CCDS_LOGO_PATH = path.resolve(process.cwd(), "client/src/assets/ccds.png");
function formatCurrency(cents: number) {
return new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" }).format(cents / 100);
}
function formatDateFr(date: string | Date | null | undefined) {
if (!date) return "-";
try {
return new Date(date).toLocaleDateString("fr-FR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
} catch {
return String(date);
}
}
function drawLogo(doc: PDFKit.PDFDocument, x: number, y: number, width: number, height: number) {
if (!fs.existsSync(CCDS_LOGO_PATH)) return;
try {
doc.image(CCDS_LOGO_PATH, x, y, { fit: [width, height], align: "center", valign: "center" });
} catch (error) {
console.warn("[SalleInvoicePdf] Impossible de charger le logo CCDS:", error);
}
}
export async function generateSalleInvoicePdf(input: {
requestId: number;
requestTitle: string;
associationName: string;
datesLabel: string;
pricing: SallePricingSummary;
}) {
const pdfBuffer = await new Promise<Buffer>((resolve, reject) => {
try {
const doc = new PDFDocument({
size: "A4",
margins: { top: 36, bottom: 42, left: 34, right: 34 },
});
const chunks: Buffer[] = [];
doc.on("data", (chunk: Buffer) => chunks.push(chunk));
doc.on("end", () => resolve(Buffer.concat(chunks)));
doc.on("error", reject);
const width = doc.page.width - doc.page.margins.left - doc.page.margins.right;
let y = doc.page.margins.top;
drawLogo(doc, doc.page.margins.left, y, 180, 70);
doc.font("Helvetica-Bold").fontSize(18).fillColor("#244f7a").text(
`Facture N° ${String(input.requestId).padStart(3, "0")}/${new Date().getFullYear()}`,
doc.page.margins.left,
y + 34,
{ width, align: "right" }
);
y += 82;
const infoRows: Array<[string, string]> = [
["Date", formatDateFr(new Date())],
["Référence dossier", `Réservation salle #${input.requestId}`],
["Émis par", "Maison de la Jeunesse des Savanes - CCDS"],
["Association", input.associationName],
["Période", input.datesLabel],
];
const labelWidth = 160;
infoRows.forEach(([label, value]) => {
doc.rect(doc.page.margins.left, y, labelWidth, 36).stroke("#d9d9d9");
doc.rect(doc.page.margins.left + labelWidth, y, width - labelWidth, 36).stroke("#d9d9d9");
doc.font("Helvetica-Bold").fontSize(10).fillColor("#404040").text(label, doc.page.margins.left + 10, y + 12, { width: labelWidth - 20 });
doc.font("Helvetica").fontSize(10).fillColor("#1f2937").text(value, doc.page.margins.left + labelWidth + 10, y + 12, {
width: width - labelWidth - 20,
});
y += 36;
});
y += 26;
const col1 = width * 0.52;
const col2 = width * 0.13;
const col3 = width * 0.17;
const col4 = width - col1 - col2 - col3;
doc.rect(doc.page.margins.left, y, width, 40).fillAndStroke("#244f7a", "#244f7a");
doc.font("Helvetica-Bold").fontSize(10).fillColor("#ffffff")
.text("DESCRIPTION", doc.page.margins.left + 14, y + 14, { width: col1 - 20 })
.text("QTÉ", doc.page.margins.left + col1 + 10, y + 14, { width: col2 - 20, align: "center" })
.text("PRIX UNITAIRE", doc.page.margins.left + col1 + col2 + 10, y + 14, { width: col3 - 20, align: "center" })
.text("TOTAL", doc.page.margins.left + col1 + col2 + col3 + 10, y + 14, { width: col4 - 20, align: "center" });
y += 40;
input.pricing.lineItems.forEach((line) => {
const rowHeight = 88;
doc.rect(doc.page.margins.left, y, col1, rowHeight).stroke("#d9d9d9");
doc.rect(doc.page.margins.left + col1, y, col2, rowHeight).stroke("#d9d9d9");
doc.rect(doc.page.margins.left + col1 + col2, y, col3, rowHeight).stroke("#d9d9d9");
doc.rect(doc.page.margins.left + col1 + col2 + col3, y, col4, rowHeight).stroke("#d9d9d9");
doc.font("Helvetica-Bold").fontSize(11).fillColor("#1f2937").text(
`Location ${line.salleNom}`,
doc.page.margins.left + 14,
y + 12,
{ width: col1 - 24 }
);
doc.font("Helvetica").fontSize(10).fillColor("#334155").text(
`${line.categoryLabel}\n${input.datesLabel}\n${input.requestTitle}`,
doc.page.margins.left + 14,
y + 32,
{ width: col1 - 24 }
);
doc.font("Helvetica-Bold").fontSize(12).fillColor("#1f2937")
.text(String(line.quantity), doc.page.margins.left + col1, y + 34, { width: col2, align: "center" })
.text(formatCurrency(line.unitAmountCents), doc.page.margins.left + col1 + col2, y + 34, { width: col3, align: "center" })
.text(formatCurrency(line.totalAmountCents), doc.page.margins.left + col1 + col2 + col3, y + 34, { width: col4, align: "center" });
y += rowHeight;
});
doc.rect(doc.page.margins.left, y, col1 + col2 + col3, 44).stroke("#244f7a");
doc.rect(doc.page.margins.left + col1 + col2 + col3, y, col4, 44).stroke("#244f7a");
doc.font("Helvetica-Bold").fontSize(12).fillColor("#244f7a")
.text("TOTAL À RÉGLER", doc.page.margins.left, y + 14, { width: col1 + col2 + col3 - 16, align: "right" })
.fillColor("#c2410c")
.text(formatCurrency(input.pricing.totalAmountCents), doc.page.margins.left + col1 + col2 + col3, y + 14, { width: col4, align: "center" });
y += 60;
doc.roundedRect(doc.page.margins.left, y, width, 110, 8).fillAndStroke("#f8fafc", "#cbd5e1");
doc.font("Helvetica-Bold").fontSize(11).fillColor("#1f2937").text("Récapitulatif pour lenvoi final", doc.page.margins.left + 16, y + 14);
doc.font("Helvetica").fontSize(10).fillColor("#475569")
.text("Cette facture accompagne la validation finale de la réservation. Le règlement est attendu selon les modalités précisées par la Maison de la Jeunesse des Savanes.", doc.page.margins.left + 16, y + 36, {
width: width - 32,
})
.text("RIB MJS : à joindre ou communiquer depuis laccueil si nécessaire.", doc.page.margins.left + 16, y + 72, {
width: width - 32,
});
doc.end();
} catch (error) {
reject(error);
}
});
return {
buffer: pdfBuffer,
fileName: `facture-salle-ccds-${input.requestId}.pdf`,
};
}