Initial local backup snapshot

This commit is contained in:
Selecta Keke 2026-06-24 00:02:55 -03:00
commit acd9e14ba5
367 changed files with 118038 additions and 0 deletions

150
server/salleInvoicePdf.ts Normal file
View file

@ -0,0 +1,150 @@
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`,
};
}