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; 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((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`, }; }