import PDFDocument from "pdfkit"; import fs from "node:fs"; import path from "node:path"; type MaterialReturnRequest = { id: number; titre: string; formData?: string | null; }; type MaterialReturnAssociation = { nomAssociation?: string | null; telephone?: string | null; emailContact?: string | null; ville?: string | null; } | null | undefined; 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("[MaterialReturnPdf] 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 addLabeledValue(doc: PDFKit.PDFDocument, label: string, value: string, x: number, y: number, width: number) { doc.font("Helvetica-Bold").fontSize(9).fillColor("#334155").text(label, x, y, { width }); doc.font("Helvetica").fontSize(10).fillColor("#0f172a").text(value || "........................................................", x, y + 12, { width }); } function addSignatureBlock(doc: PDFKit.PDFDocument, title: string, x: number, y: number, width: number) { doc.roundedRect(x, y, width, 90, 8).stroke("#cbd5e1"); doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f172a").text(title, x + 12, y + 12, { width: width - 24 }); doc.font("Helvetica").fontSize(9).fillColor("#64748b").text("Nom, qualité, signature et date", x + 12, y + 32, { width: width - 24 }); } export async function generateMaterialReturnStatementPdf(input: { request: MaterialReturnRequest; association?: MaterialReturnAssociation; }) { const formData = (() => { try { return input.request.formData ? JSON.parse(input.request.formData) : {}; } catch { return {}; } })(); const manifestationStart = formData.dateDebutManifestation || formData.dateManifestation; const manifestationEnd = formData.dateFinManifestation || formData.dateManifestation; const requestedItems = Object.entries(formData.materielsDemandes || {}) .filter(([, checked]) => Boolean(checked)) .map(([key]) => ({ key, label: key === "tente3x3" ? "Tente 3x3" : key === "chapiteau5x5" ? "Chapiteau 5x5" : key === "podium" ? "Podium" : "Autres", quantity: formData.quantitesDemandees?.[key] || "-", extra: key === "autres" ? formData.autreMaterielPrecisions || "" : "", })); const pdfBuffer = await new Promise((resolve, reject) => { try { const doc = new PDFDocument({ size: "A4", margins: { top: 36, bottom: 40, left: 42, right: 42 }, info: { Title: `Fiche d'état des lieux - Demande ${input.request.id}`, Author: "Communauté de Communes Des Savanes", Subject: "Fiche d'état des lieux du matériel événementiel", }, }); 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.moveTo(doc.page.margins.left, y).lineTo(doc.page.margins.left + pageWidth, y).lineWidth(2).stroke("#efb100"); y += 14; drawLogo(doc, doc.page.margins.left + (pageWidth - 76) / 2, y, 76, 76); y += 82; doc.font("Helvetica").fontSize(9).fillColor("#64748b").text("COMMUNAUTÉ DE COMMUNES DES SAVANES", doc.page.margins.left, y, { width: pageWidth, align: "center", }); y += 16; doc.font("Helvetica-Bold").fontSize(18).fillColor("#0f172a").text("FICHE D'ÉTAT DES LIEUX", doc.page.margins.left, y, { width: pageWidth, align: "center", }); y += 18; doc.font("Helvetica-Bold").fontSize(14).text("DU MATÉRIEL ÉVÉNEMENTIEL DE LA CCDS", doc.page.margins.left, y, { width: pageWidth, align: "center", }); y += 22; doc.roundedRect(doc.page.margins.left, y, pageWidth, 34, 8).fillAndStroke("#eef5ff", "#bfd2ef"); doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f2d63").text("Document de suivi prérempli à compléter contradictoirement", doc.page.margins.left + 14, y + 9, { width: pageWidth - 28, align: "center", }); y += 48; const leftColWidth = (pageWidth - 16) / 2; addLabeledValue(doc, "Commune", formData.commune || input.association?.ville || "", doc.page.margins.left, y, leftColWidth); addLabeledValue(doc, "Demandeur", formData.demandeurNomPrenom || input.association?.nomAssociation || "", doc.page.margins.left + leftColWidth + 16, y, leftColWidth); y += 40; addLabeledValue(doc, "Téléphone", formData.telephoneAssociation || input.association?.telephone || "", doc.page.margins.left, y, leftColWidth); addLabeledValue(doc, "Intitulé de la manifestation", formData.motifDemande || input.request.titre || "", doc.page.margins.left + leftColWidth + 16, y, leftColWidth); y += 40; addLabeledValue( doc, "Date prévue du / au", manifestationStart || manifestationEnd ? `${formatDateFr(manifestationStart)}${manifestationEnd ? ` au ${formatDateFr(manifestationEnd)}` : ""}` : "", doc.page.margins.left, y, leftColWidth ); addLabeledValue(doc, "Date de restitution prévue", formatDateFr(formData.dateRestitution), doc.page.margins.left + leftColWidth + 16, y, leftColWidth); y += 48; doc.font("Helvetica-Bold").fontSize(11).fillColor("#0f172a").text("Matériel concerné", doc.page.margins.left, y); y += 14; const tableX = doc.page.margins.left; const cols = [pageWidth * 0.46, pageWidth * 0.16, pageWidth * 0.19, pageWidth * 0.19]; const headers = ["Équipement", "Qté", "Mise à disposition", "Restitution"]; let cursorX = tableX; headers.forEach((header, index) => { doc.rect(cursorX, y, cols[index], 22).fillAndStroke("#dce8f8", "#bfd2ef"); doc.font("Helvetica-Bold").fontSize(9).fillColor("#0f2d63").text(header, cursorX + 6, y + 7, { width: cols[index] - 12, align: index === 0 ? "left" : "center", }); cursorX += cols[index]; }); y += 22; const items = requestedItems.length > 0 ? requestedItems : [{ key: "none", label: "Aucun matériel renseigné", quantity: "-", extra: "" }]; items.forEach((item) => { const rowHeight = item.extra ? 36 : 28; let rowX = tableX; const values = [ item.extra ? `${item.label} — ${item.extra}` : item.label, item.quantity, "À compléter", "À compléter", ]; values.forEach((value, index) => { doc.rect(rowX, y, cols[index], rowHeight).stroke("#cbd5e1"); doc.font(index < 2 ? "Helvetica" : "Helvetica-Oblique") .fontSize(9) .fillColor(index < 2 ? "#0f172a" : "#64748b") .text(value, rowX + 6, y + 8, { width: cols[index] - 12, align: index === 0 ? "left" : "center", }); rowX += cols[index]; }); y += rowHeight; }); y += 20; doc.roundedRect(doc.page.margins.left, y, pageWidth, 76, 8).stroke("#cbd5e1"); doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f172a").text("Observations contradictoires", doc.page.margins.left + 12, y + 12); doc.font("Helvetica").fontSize(9).fillColor("#64748b").text("Préciser l'état général du matériel, les éventuelles anomalies, manques ou réserves constatées.", doc.page.margins.left + 12, y + 28, { width: pageWidth - 24, }); y += 92; addSignatureBlock(doc, "L'agent CCDS / service récupérateur", doc.page.margins.left, y, (pageWidth - 20) / 2); addSignatureBlock(doc, "L'emprunteur / représentant de l'association", doc.page.margins.left + (pageWidth - 20) / 2 + 20, y, (pageWidth - 20) / 2); y += 104; doc.font("Helvetica").fontSize(8).fillColor("#64748b").text( `Document préparé automatiquement pour la demande #${input.request.id} — à vérifier et compléter sur site lors de la restitution.`, doc.page.margins.left, y, { width: pageWidth, align: "center" } ); doc.end(); } catch (error) { reject(error); } }); return { buffer: pdfBuffer, fileName: `Fiche_Etat_Des_Lieux_${input.request.id}.pdf`, }; } export async function generateCompletedMaterialReturnStatementPdf(input: { request: MaterialReturnRequest; association?: MaterialReturnAssociation; completion: { compliance: "conforme" | "non_conforme"; discrepancyLabels: string[]; discrepancyDetails?: string | null; agentName: string; agentRole: string; borrowerName: string; borrowerRole: string; validatedAt: Date; geoLatitude?: string | null; geoLongitude?: string | null; geoStatus?: string | null; geoFailureReason?: string | null; locationReference?: string | null; agentSignatureBuffer?: Buffer; borrowerSignatureBuffer?: Buffer; }; }) { const formData = (() => { try { return input.request.formData ? JSON.parse(input.request.formData) : {}; } catch { return {}; } })(); const manifestationStart = formData.dateDebutManifestation || formData.dateManifestation; const manifestationEnd = formData.dateFinManifestation || formData.dateManifestation; const requestedItems = Object.entries(formData.materielsDemandes || {}) .filter(([, checked]) => Boolean(checked)) .map(([key]) => ({ key, label: key === "tente3x3" ? "Tente 3x3" : key === "chapiteau5x5" ? "Chapiteau 5x5" : key === "podium" ? "Podium" : "Autres", quantity: formData.quantitesDemandees?.[key] || "-", extra: key === "autres" ? formData.autreMaterielPrecisions || "" : "", })); const pdfBuffer = await new Promise((resolve, reject) => { try { const doc = new PDFDocument({ size: "A4", margins: { top: 36, bottom: 40, left: 42, right: 42 }, info: { Title: `Fiche d'état des lieux finalisée - Demande ${input.request.id}`, Author: "Communauté de Communes Des Savanes", Subject: "Fiche d'état des lieux contradictoire du matériel événementiel", }, }); 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.moveTo(doc.page.margins.left, y).lineTo(doc.page.margins.left + pageWidth, y).lineWidth(2).stroke("#efb100"); y += 14; drawLogo(doc, doc.page.margins.left + (pageWidth - 76) / 2, y, 76, 76); y += 82; doc.font("Helvetica").fontSize(9).fillColor("#64748b").text("COMMUNAUTÉ DE COMMUNES DES SAVANES", doc.page.margins.left, y, { width: pageWidth, align: "center", }); y += 16; doc.font("Helvetica-Bold").fontSize(18).fillColor("#0f172a").text("FICHE D'ÉTAT DES LIEUX FINALISÉE", doc.page.margins.left, y, { width: pageWidth, align: "center", }); y += 18; doc.font("Helvetica-Bold").fontSize(14).text("DU MATÉRIEL ÉVÉNEMENTIEL DE LA CCDS", doc.page.margins.left, y, { width: pageWidth, align: "center", }); y += 22; doc.roundedRect(doc.page.margins.left, y, pageWidth, 44, 8).fillAndStroke( input.completion.compliance === "conforme" ? "#ecfdf3" : "#fff7ed", input.completion.compliance === "conforme" ? "#a7f3d0" : "#fdba74" ); doc.font("Helvetica-Bold").fontSize(11).fillColor(input.completion.compliance === "conforme" ? "#166534" : "#9a3412").text( input.completion.compliance === "conforme" ? "Restitution conforme" : "Restitution non conforme / partielle", doc.page.margins.left + 14, y + 10, { width: pageWidth - 28, align: "center" } ); doc.font("Helvetica").fontSize(9).fillColor("#475569").text( `Validée le ${formatDateFr(input.completion.validatedAt)}`, doc.page.margins.left + 14, y + 25, { width: pageWidth - 28, align: "center" } ); y += 58; const leftColWidth = (pageWidth - 16) / 2; addLabeledValue(doc, "Commune", formData.commune || input.association?.ville || "", doc.page.margins.left, y, leftColWidth); addLabeledValue(doc, "Association / demandeur", input.association?.nomAssociation || formData.demandeurNomPrenom || "", doc.page.margins.left + leftColWidth + 16, y, leftColWidth); y += 40; addLabeledValue(doc, "Téléphone", formData.telephoneAssociation || input.association?.telephone || "", doc.page.margins.left, y, leftColWidth); addLabeledValue(doc, "Manifestation", formData.motifDemande || input.request.titre || "", doc.page.margins.left + leftColWidth + 16, y, leftColWidth); y += 40; addLabeledValue( doc, "Période", manifestationStart || manifestationEnd ? `${formatDateFr(manifestationStart)}${manifestationEnd ? ` au ${formatDateFr(manifestationEnd)}` : ""}` : "", doc.page.margins.left, y, leftColWidth ); addLabeledValue(doc, "Restitution prévue", formatDateFr(formData.dateRestitution), doc.page.margins.left + leftColWidth + 16, y, leftColWidth); y += 48; doc.font("Helvetica-Bold").fontSize(11).fillColor("#0f172a").text("Matériel restitué", doc.page.margins.left, y); y += 14; requestedItems.forEach((item) => { doc.rect(doc.page.margins.left, y, pageWidth, 24).stroke("#cbd5e1"); doc.font("Helvetica").fontSize(9).fillColor("#0f172a").text( `${item.label}${item.extra ? ` — ${item.extra}` : ""}`, doc.page.margins.left + 8, y + 7, { width: pageWidth * 0.65 } ); doc.font("Helvetica-Bold").fontSize(9).text( `Qté : ${item.quantity}`, doc.page.margins.left + pageWidth - 120, y + 7, { width: 110, align: "right" } ); y += 24; }); y += 16; doc.roundedRect(doc.page.margins.left, y, pageWidth, 100, 8).stroke("#cbd5e1"); doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f172a").text("Observations contradictoires", doc.page.margins.left + 12, y + 12); doc.font("Helvetica").fontSize(9).fillColor("#475569").text( input.completion.discrepancyLabels.length > 0 ? input.completion.discrepancyLabels.join(" • ") : "Aucune réserve relevée.", doc.page.margins.left + 12, y + 30, { width: pageWidth - 24 } ); doc.text( input.completion.discrepancyDetails || "Aucune précision complémentaire.", doc.page.margins.left + 12, y + 50, { width: pageWidth - 24 } ); y += 118; const geoLine = input.completion.geoLatitude && input.completion.geoLongitude ? `Géolocalisation : ${input.completion.geoLatitude}, ${input.completion.geoLongitude}` : input.completion.locationReference ? `Référence de lieu : ${input.completion.locationReference}${ input.completion.geoFailureReason ? ` • GPS indisponible (${input.completion.geoFailureReason})` : "" }` : input.completion.geoFailureReason ? `GPS indisponible (${input.completion.geoFailureReason})` : ""; doc.font("Helvetica").fontSize(8).fillColor("#64748b").text( `Horodatage serveur : ${new Date(input.completion.validatedAt).toLocaleString("fr-FR")}${geoLine ? ` • ${geoLine}` : ""}`, doc.page.margins.left, y, { width: pageWidth } ); y += 18; const signatureWidth = (pageWidth - 20) / 2; doc.roundedRect(doc.page.margins.left, y, signatureWidth, 126, 8).stroke("#cbd5e1"); doc.roundedRect(doc.page.margins.left + signatureWidth + 20, y, signatureWidth, 126, 8).stroke("#cbd5e1"); doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f172a").text("Agent CCDS / service récupérateur", doc.page.margins.left + 12, y + 12, { width: signatureWidth - 24, }); doc.font("Helvetica").fontSize(9).fillColor("#475569").text( `${input.completion.agentName}\n${input.completion.agentRole}`, doc.page.margins.left + 12, y + 30, { width: signatureWidth - 24 } ); if (input.completion.agentSignatureBuffer) { doc.image(input.completion.agentSignatureBuffer, doc.page.margins.left + 12, y + 64, { fit: [signatureWidth - 24, 42], align: "center", valign: "center", }); } doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f172a").text("Emprunteur / représentant", doc.page.margins.left + signatureWidth + 32, y + 12, { width: signatureWidth - 24, }); doc.font("Helvetica").fontSize(9).fillColor("#475569").text( `${input.completion.borrowerName}\n${input.completion.borrowerRole}`, doc.page.margins.left + signatureWidth + 32, y + 30, { width: signatureWidth - 24 } ); if (input.completion.borrowerSignatureBuffer) { doc.image(input.completion.borrowerSignatureBuffer, doc.page.margins.left + signatureWidth + 32, y + 64, { fit: [signatureWidth - 24, 42], align: "center", valign: "center", }); } y += 138; doc.font("Helvetica").fontSize(8).fillColor("#64748b").text( `Document finalisé automatiquement pour la demande #${input.request.id}.`, doc.page.margins.left, y, { width: pageWidth, align: "center" } ); doc.end(); } catch (error) { reject(error); } }); return { buffer: pdfBuffer, fileName: `Fiche_Etat_Des_Lieux_Finalisee_${input.request.id}.pdf`, }; }