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