Initial local backup snapshot
This commit is contained in:
commit
acd9e14ba5
367 changed files with 118038 additions and 0 deletions
254
server/salleConventionPdf.ts
Normal file
254
server/salleConventionPdf.ts
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
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<Buffer>((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`,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue