import PDFDocument from "pdfkit"; import fs from "node:fs"; import path from "node:path"; import type { MaterialContractFinancialMode, MaterialContractStatus } from "./materialConventionPdf"; const CCDS_LOGO_PATH = path.resolve(process.cwd(), "client/src/assets/ccds.png"); type ContractRequestLike = { id: number; titre: string; status: string; formData?: string | null; }; type ContractAssociationLike = { nomAssociation?: string | null; adresse?: string | null; codePostal?: string | null; ville?: string | null; telephone?: string | null; emailContact?: string | null; nomRepresentant?: string | null; } | null | undefined; type ContractDecision = { financialMode: MaterialContractFinancialMode; depositRequired: boolean; depositAmountCents: number; rentalAmountCents: number; pricingNotes?: string | null; contractStatus: MaterialContractStatus; contractGeneratedAt?: string | Date | null; contractValidatedByUserId?: number | null; }; 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("[SalleConventionPdf] Impossible de charger le logo CCDS:", error); } } 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 formatCurrency(cents: number | null | undefined) { return new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" }).format((cents || 0) / 100); } function getFinancialModeLabel(mode: MaterialContractFinancialMode) { switch (mode) { case "gratuite": return "Mise à disposition gratuite"; case "gratuite_avec_caution": return "Mise à disposition gratuite avec caution"; case "location_payante": return "Location payante"; default: return mode; } } function getScheduleSummary(formData: any) { const dailySlots = Array.isArray(formData?.horairesParJour) ? formData.horairesParJour.filter((slot: any) => slot?.date) : []; if (Boolean(formData?.useDetailedSchedule) && dailySlots.length > 0) { return dailySlots .map((slot: any) => `${formatDateFr(slot.date)} : ${slot.heureDebut || "?"} - ${slot.heureFin || "?"}`) .join(" | "); } if (formData?.heureDebut || formData?.heureFin) { return `${formData.heureDebut || "?"} - ${formData.heureFin || "?"}`; } return "-"; } export async function generateSalleConventionPdf(input: { request: ContractRequestLike; association?: ContractAssociationLike; decision: ContractDecision; }) { const formData = (() => { try { return input.request.formData ? JSON.parse(input.request.formData) : {}; } catch { return {}; } })(); const sallesSelectionnees = Array.isArray(formData.sallesSelectionnees) && formData.sallesSelectionnees.length > 0 ? formData.sallesSelectionnees.join(", ") : "-"; const reservationStart = formData.dateReservation; const reservationEnd = formData.dateFinReservation || formData.dateReservation; const scheduleSummary = getScheduleSummary(formData); const pdfBuffer = await new Promise((resolve, reject) => { try { const doc = new PDFDocument({ size: "A4", margins: { top: 38, bottom: 42, left: 42, right: 42 }, info: { Title: `Convention salle CCDS - dossier ${input.request.id}`, Author: "Communauté de Communes Des Savanes", Subject: "Convention de mise à disposition / location d'un local CCDS", }, }); const chunks: Buffer[] = []; doc.on("data", (chunk: Buffer) => chunks.push(chunk)); doc.on("end", () => resolve(Buffer.concat(chunks))); doc.on("error", reject); const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right; let y = doc.page.margins.top; doc.lineWidth(2).strokeColor("#efb100").moveTo(doc.page.margins.left, y).lineTo(doc.page.margins.left + pageWidth, y).stroke(); y += 16; drawLogo(doc, doc.page.margins.left + (pageWidth - 84) / 2, y, 84, 84); y += 92; doc.font("Helvetica").fontSize(9).fillColor("#64748b").text("COMMUNAUTÉ DE COMMUNES DES SAVANES", doc.page.margins.left, y, { width: pageWidth, align: "center", }); y += 18; doc.font("Helvetica-Bold").fontSize(17).fillColor("#0f172a").text( "CONVENTION DE MISE À DISPOSITION / LOCATION", doc.page.margins.left, y, { width: pageWidth, align: "center" } ); y += 20; doc.font("Helvetica-Bold").fontSize(14).text("D'UN LOCAL - MAISON DE LA JEUNESSE DES SAVANES", doc.page.margins.left, y, { width: pageWidth, align: "center", }); y += 28; doc.roundedRect(doc.page.margins.left, y, pageWidth, 42, 8).fillAndStroke("#eef5ff", "#bfd2ef"); doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f2d63").text( "Document généré à partir de la décision administrative CCDS", doc.page.margins.left + 14, y + 9, { width: pageWidth - 28, align: "center" } ); doc.font("Helvetica").fontSize(9).fillColor("#334155").text( `Dossier #${input.request.id} — statut contrat : ${input.decision.contractStatus}`, doc.page.margins.left + 14, y + 23, { width: pageWidth - 28, align: "center" } ); y += 56; const infoRows: Array<[string, string]> = [ ["Association", input.association?.nomAssociation || formData.nomAssociation || "-"], ["Représentant", input.association?.nomRepresentant || formData.representantLegal || "-"], ["Adresse", [input.association?.adresse, input.association?.codePostal, input.association?.ville].filter(Boolean).join(" ") || "-"], ["Téléphone", input.association?.telephone || formData.telephoneAssociation || "-"], ["Email", input.association?.emailContact || formData.emailAssociation || "-"], ["Local réservé", sallesSelectionnees], [ "Période d'utilisation", reservationStart || reservationEnd ? `${formatDateFr(reservationStart)}${reservationEnd ? ` au ${formatDateFr(reservationEnd)}` : ""}` : "-", ], ["Créneau / horaires", scheduleSummary], ["Objet", formData.motifReservation || input.request.titre || "-"], ]; for (const [label, value] of infoRows) { doc.font("Helvetica-Bold").fontSize(9).fillColor("#334155").text(`${label} :`, doc.page.margins.left, y, { width: 140 }); doc.font("Helvetica").fontSize(10).fillColor("#0f172a").text(value, doc.page.margins.left + 145, y - 1, { width: pageWidth - 145, }); y += 18; } y += 10; doc.roundedRect(doc.page.margins.left, y, pageWidth, 108, 8).fillAndStroke("#f8fafc", "#cbd5e1"); doc.font("Helvetica-Bold").fontSize(11).fillColor("#0f172a").text("Conditions financières", doc.page.margins.left + 14, y + 12); doc.font("Helvetica").fontSize(10).fillColor("#334155") .text(`Régime retenu : ${getFinancialModeLabel(input.decision.financialMode)}`, doc.page.margins.left + 14, y + 34, { width: pageWidth - 28, }) .text(`Montant de location : ${formatCurrency(input.decision.rentalAmountCents)}`, doc.page.margins.left + 14, y + 52, { width: pageWidth - 28, }) .text( `Caution : ${input.decision.depositRequired ? formatCurrency(input.decision.depositAmountCents) : "Aucune caution exigée"}`, doc.page.margins.left + 14, y + 70, { width: pageWidth - 28 } ); y += 122; if (input.decision.pricingNotes?.trim()) { doc.font("Helvetica-Bold").fontSize(11).fillColor("#0f172a").text("Clauses / réserves spécifiques", doc.page.margins.left, y); y += 16; doc.roundedRect(doc.page.margins.left, y, pageWidth, 70, 8).stroke("#cbd5e1"); doc.font("Helvetica").fontSize(10).fillColor("#334155").text(input.decision.pricingNotes.trim(), doc.page.margins.left + 12, y + 12, { width: pageWidth - 24, }); y += 84; } y += 8; doc.font("Helvetica").fontSize(9).fillColor("#475569").text( "Cette convention formalise les conditions administratives retenues par la CCDS pour la mise à disposition ou la location du local sollicité.", doc.page.margins.left, y, { width: pageWidth } ); y += 36; const signatureWidth = (pageWidth - 16) / 2; doc.roundedRect(doc.page.margins.left, y, signatureWidth, 84, 8).stroke("#cbd5e1"); doc.roundedRect(doc.page.margins.left + signatureWidth + 16, y, signatureWidth, 84, 8).stroke("#cbd5e1"); doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f172a") .text("Association emprunteuse", doc.page.margins.left + 12, y + 12, { width: signatureWidth - 24, align: "center" }) .text("CCDS / DSU", doc.page.margins.left + signatureWidth + 28, y + 12, { width: signatureWidth - 24, align: "center" }); doc.font("Helvetica").fontSize(9).fillColor("#64748b") .text("Nom, qualité et signature", doc.page.margins.left + 12, y + 56, { width: signatureWidth - 24, align: "center" }) .text("Visa administratif", doc.page.margins.left + signatureWidth + 28, y + 56, { width: signatureWidth - 24, align: "center" }); doc.end(); } catch (error) { reject(error); } }); return { buffer: pdfBuffer, fileName: `convention-salle-ccds-${input.request.id}.pdf`, }; }