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