202 lines
6.9 KiB
TypeScript
202 lines
6.9 KiB
TypeScript
import PDFDocument from "pdfkit";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { getMaterialEventLabel, materialEventItems, sanitizeMaterialEventQuantityMap, type MaterialEventItemKey } from "@shared/materialEvent";
|
|
|
|
const CCDS_LOGO_PATH = path.resolve(process.cwd(), "client/src/assets/ccds.png");
|
|
|
|
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("[MaterialReturnLitigationPdf] 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(amountCents: number | null | undefined) {
|
|
if (!Number.isFinite(amountCents as number)) return "-";
|
|
return new Intl.NumberFormat("fr-FR", {
|
|
style: "currency",
|
|
currency: "EUR",
|
|
}).format((amountCents || 0) / 100);
|
|
}
|
|
|
|
export async function generateMaterialReturnLitigationLetterPdf(input: {
|
|
request: {
|
|
id: number;
|
|
titre: string;
|
|
formData?: string | null;
|
|
};
|
|
association?: {
|
|
nomAssociation?: string | null;
|
|
emailContact?: string | null;
|
|
telephone?: string | null;
|
|
ville?: string | null;
|
|
} | null;
|
|
followup: {
|
|
restitutionDate?: string | Date | null;
|
|
discrepancyCategories?: string[];
|
|
discrepancyDetails?: string | null;
|
|
};
|
|
arbitration: {
|
|
blockedItems: Record<MaterialEventItemKey, number>;
|
|
decision: "partial_retention" | "full_retention" | "dismissed";
|
|
amountCents: number;
|
|
notes?: string | null;
|
|
};
|
|
}) {
|
|
const formData = (() => {
|
|
try {
|
|
return input.request.formData ? JSON.parse(input.request.formData) : {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
})();
|
|
|
|
const blockedItems = sanitizeMaterialEventQuantityMap(input.arbitration.blockedItems);
|
|
const blockedRows = materialEventItems
|
|
.map((item) => ({ ...item, quantity: blockedItems[item.key] || 0 }))
|
|
.filter((item) => item.quantity > 0);
|
|
|
|
const decisionLabel =
|
|
input.arbitration.decision === "full_retention"
|
|
? "Encaissement total de la caution"
|
|
: input.arbitration.decision === "partial_retention"
|
|
? "Retenue partielle sur caution"
|
|
: "Classement sans suite";
|
|
|
|
const pdfBuffer = await new Promise<Buffer>((resolve, reject) => {
|
|
try {
|
|
const doc = new PDFDocument({
|
|
size: "A4",
|
|
margins: { top: 38, bottom: 42, left: 46, right: 46 },
|
|
info: {
|
|
Title: `Courrier de litige - Demande ${input.request.id}`,
|
|
Author: "Communauté de Communes Des Savanes",
|
|
Subject: "Notification de litige sur restitution de matériel",
|
|
},
|
|
});
|
|
|
|
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;
|
|
|
|
drawLogo(doc, doc.page.margins.left, y, 74, 74);
|
|
doc.font("Helvetica-Bold").fontSize(16).fillColor("#0f172a").text(
|
|
"Notification de litige et arbitrage matériel",
|
|
doc.page.margins.left + 92,
|
|
y + 14,
|
|
{ width: pageWidth - 92 }
|
|
);
|
|
doc.font("Helvetica").fontSize(10).fillColor("#475569").text(
|
|
"Communauté de Communes Des Savanes - Direction des Services aux Usagers",
|
|
doc.page.margins.left + 92,
|
|
y + 40,
|
|
{ width: pageWidth - 92 }
|
|
);
|
|
y += 96;
|
|
|
|
const lines = [
|
|
["Association", input.association?.nomAssociation || "-"],
|
|
["Demande", input.request.titre || `Dossier #${input.request.id}`],
|
|
["Commune", formData.commune || input.association?.ville || "-"],
|
|
["Date de restitution", formatDateFr(input.followup.restitutionDate)],
|
|
["Décision d'arbitrage", decisionLabel],
|
|
["Montant retenu", formatCurrency(input.arbitration.amountCents)],
|
|
] as const;
|
|
|
|
lines.forEach(([label, value]) => {
|
|
doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f172a").text(`${label} :`, doc.page.margins.left, y, {
|
|
width: 170,
|
|
});
|
|
doc.font("Helvetica").fontSize(10).fillColor("#0f172a").text(String(value || "-"), doc.page.margins.left + 170, y, {
|
|
width: pageWidth - 170,
|
|
});
|
|
y += 18;
|
|
});
|
|
|
|
y += 8;
|
|
doc.roundedRect(doc.page.margins.left, y, pageWidth, 88, 10).fillAndStroke("#fff7ed", "#fdba74");
|
|
doc.font("Helvetica-Bold").fontSize(11).fillColor("#9a3412").text("Constat de terrain", doc.page.margins.left + 14, y + 12);
|
|
doc.font("Helvetica").fontSize(10).fillColor("#7c2d12").text(
|
|
[
|
|
input.followup.discrepancyCategories?.length
|
|
? `Réserves : ${input.followup.discrepancyCategories.join(", ")}.`
|
|
: null,
|
|
input.followup.discrepancyDetails?.trim() || null,
|
|
].filter(Boolean).join(" "),
|
|
doc.page.margins.left + 14,
|
|
y + 32,
|
|
{ width: pageWidth - 28 }
|
|
);
|
|
y += 108;
|
|
|
|
doc.font("Helvetica-Bold").fontSize(11).fillColor("#0f172a").text("Matériel provisoirement bloqué", doc.page.margins.left, y);
|
|
y += 18;
|
|
|
|
if (blockedRows.length === 0) {
|
|
doc.font("Helvetica").fontSize(10).fillColor("#475569").text(
|
|
"Aucun équipement n'est maintenu en indisponibilité après arbitrage.",
|
|
doc.page.margins.left,
|
|
y,
|
|
{ width: pageWidth }
|
|
);
|
|
y += 22;
|
|
} else {
|
|
blockedRows.forEach((item) => {
|
|
doc.font("Helvetica").fontSize(10).fillColor("#0f172a").text(
|
|
`- ${getMaterialEventLabel(item.key)} : ${item.quantity}`,
|
|
doc.page.margins.left + 8,
|
|
y,
|
|
{ width: pageWidth - 8 }
|
|
);
|
|
y += 16;
|
|
});
|
|
}
|
|
|
|
if (input.arbitration.notes?.trim()) {
|
|
y += 12;
|
|
doc.font("Helvetica-Bold").fontSize(11).fillColor("#0f172a").text("Observations administratives", doc.page.margins.left, y);
|
|
y += 18;
|
|
doc.font("Helvetica").fontSize(10).fillColor("#334155").text(input.arbitration.notes.trim(), doc.page.margins.left, y, {
|
|
width: pageWidth,
|
|
});
|
|
y = doc.y + 10;
|
|
}
|
|
|
|
doc.font("Helvetica").fontSize(9).fillColor("#64748b").text(
|
|
`Document généré automatiquement pour le dossier #${input.request.id}.`,
|
|
doc.page.margins.left,
|
|
Math.max(y + 28, doc.page.height - doc.page.margins.bottom - 18),
|
|
{ width: pageWidth, align: "center" }
|
|
);
|
|
|
|
doc.end();
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
return {
|
|
buffer: pdfBuffer,
|
|
fileName: `Courrier_Litige_${input.request.id}.pdf`,
|
|
};
|
|
}
|