231 lines
8 KiB
TypeScript
231 lines
8 KiB
TypeScript
import PDFDocument from "pdfkit";
|
|
import * as XLSX from "xlsx";
|
|
|
|
type AnalyticsPayload = {
|
|
period: string;
|
|
periodLabel: string;
|
|
comparisonLabel: string;
|
|
startDate: string;
|
|
endDate: string;
|
|
summary: {
|
|
totalReservations: number;
|
|
salleReservations: number;
|
|
materialReservations: number;
|
|
occupancyRate: number;
|
|
activeAssociations: number;
|
|
totalMaterialUnits: number;
|
|
averageReservationsPerAssociation: number;
|
|
};
|
|
associationUsage: Array<{
|
|
associationName: string;
|
|
thematics: string[];
|
|
totalReservations: number;
|
|
salleReservations: number;
|
|
materialReservations: number;
|
|
lastReservationAt: string;
|
|
}>;
|
|
roomOccupancy: Array<{
|
|
roomName: string;
|
|
bookedDays: number;
|
|
occupancyRate: number;
|
|
}>;
|
|
materialUsage: Array<{
|
|
label: string;
|
|
quantity: number;
|
|
requests: number;
|
|
}>;
|
|
history: Array<{
|
|
type: string;
|
|
associationName: string;
|
|
title: string;
|
|
startDate: string;
|
|
endDate: string;
|
|
resources: string;
|
|
status: string;
|
|
createdAt: Date;
|
|
}>;
|
|
};
|
|
|
|
const requestTypeLabels: Record<string, string> = {
|
|
demande_salle: "Demande salle",
|
|
demande_materiel_evenementiel: "Matériel événementiel",
|
|
};
|
|
|
|
const statusLabels: Record<string, string> = {
|
|
brouillon: "Brouillon",
|
|
soumise: "Soumise",
|
|
en_cours_traitement: "En cours",
|
|
information_complementaire: "Info requise",
|
|
validee: "Validée",
|
|
refusee: "Refusée",
|
|
annulee: "Annulée",
|
|
};
|
|
|
|
function formatDateFr(value: string | Date | null | undefined) {
|
|
if (!value) return "-";
|
|
try {
|
|
return new Date(value).toLocaleDateString("fr-FR", {
|
|
day: "2-digit",
|
|
month: "2-digit",
|
|
year: "numeric",
|
|
});
|
|
} catch {
|
|
return String(value);
|
|
}
|
|
}
|
|
|
|
function buildFileBaseName(data: AnalyticsPayload) {
|
|
return `rapport-statistiques-ccds-${data.period}-${data.endDate}`;
|
|
}
|
|
|
|
export async function generateReservationAnalyticsPdf(data: AnalyticsPayload) {
|
|
const doc = new PDFDocument({
|
|
size: "A4",
|
|
margin: 40,
|
|
info: {
|
|
Title: `Rapport statistiques CCDS - ${data.periodLabel}`,
|
|
Author: "Portail Associations CCDS",
|
|
},
|
|
});
|
|
|
|
const chunks: Buffer[] = [];
|
|
doc.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
|
|
const done = new Promise<Buffer>((resolve) => {
|
|
doc.on("end", () => resolve(Buffer.concat(chunks)));
|
|
});
|
|
|
|
doc.fontSize(20).fillColor("#000091").text("Rapport statistiques CCDS");
|
|
doc.moveDown(0.3);
|
|
doc.fontSize(11).fillColor("#475569").text(`${data.periodLabel} - ${formatDateFr(data.startDate)} au ${formatDateFr(data.endDate)}`);
|
|
doc.moveDown(1);
|
|
|
|
doc.fontSize(14).fillColor("#0f172a").text("Synthèse");
|
|
doc.moveDown(0.5);
|
|
[
|
|
["Réservations totales", String(data.summary.totalReservations)],
|
|
["Réservations de salles", String(data.summary.salleReservations)],
|
|
["Réservations de matériel", String(data.summary.materialReservations)],
|
|
["Taux d'occupation des salles", `${data.summary.occupancyRate}%`],
|
|
["Associations utilisatrices", String(data.summary.activeAssociations)],
|
|
["Unités matérielles demandées", String(data.summary.totalMaterialUnits)],
|
|
["Fréquence moyenne par association", String(data.summary.averageReservationsPerAssociation)],
|
|
].forEach(([label, value]) => {
|
|
doc.font("Helvetica-Bold").fontSize(10).fillColor("#0f172a").text(`${label} : `, { continued: true });
|
|
doc.font("Helvetica").text(value);
|
|
});
|
|
|
|
doc.moveDown(1);
|
|
doc.font("Helvetica-Bold").fontSize(14).text("Top associations");
|
|
doc.moveDown(0.5);
|
|
data.associationUsage.slice(0, 10).forEach((entry) => {
|
|
doc.fontSize(10).font("Helvetica-Bold").text(entry.associationName);
|
|
doc.font("Helvetica").text(
|
|
`Catégories: ${entry.thematics.join(" / ") || "Non renseignée"} • Total: ${entry.totalReservations} • Salles: ${entry.salleReservations} • Matériel: ${entry.materialReservations} • Dernière réservation: ${formatDateFr(entry.lastReservationAt)}`
|
|
);
|
|
doc.moveDown(0.4);
|
|
});
|
|
|
|
doc.addPage();
|
|
doc.font("Helvetica-Bold").fontSize(14).text("Occupation des salles");
|
|
doc.moveDown(0.5);
|
|
data.roomOccupancy.forEach((entry) => {
|
|
doc.fontSize(10).font("Helvetica-Bold").text(entry.roomName, { continued: true });
|
|
doc.font("Helvetica").text(` - ${entry.bookedDays} jour(s) réservé(s), ${entry.occupancyRate}% d'occupation`);
|
|
});
|
|
|
|
doc.moveDown(1);
|
|
doc.font("Helvetica-Bold").fontSize(14).text("Matériels les plus demandés");
|
|
doc.moveDown(0.5);
|
|
data.materialUsage.forEach((entry) => {
|
|
doc.fontSize(10).font("Helvetica-Bold").text(entry.label, { continued: true });
|
|
doc.font("Helvetica").text(` - Quantité: ${entry.quantity} • Dossiers: ${entry.requests}`);
|
|
});
|
|
|
|
doc.moveDown(1);
|
|
doc.font("Helvetica-Bold").fontSize(14).text("Historique récent");
|
|
doc.moveDown(0.5);
|
|
data.history.slice(0, 20).forEach((entry) => {
|
|
const period = entry.startDate === entry.endDate
|
|
? formatDateFr(entry.startDate)
|
|
: `${formatDateFr(entry.startDate)} au ${formatDateFr(entry.endDate)}`;
|
|
doc.fontSize(10).font("Helvetica-Bold").text(`${requestTypeLabels[entry.type] || entry.type} - ${entry.associationName}`);
|
|
doc.font("Helvetica").text(`${entry.title} • ${period} • ${entry.resources || "-"} • ${statusLabels[entry.status] || entry.status}`);
|
|
doc.moveDown(0.35);
|
|
});
|
|
|
|
doc.end();
|
|
const buffer = await done;
|
|
return {
|
|
buffer,
|
|
fileName: `${buildFileBaseName(data)}.pdf`,
|
|
contentType: "application/pdf",
|
|
};
|
|
}
|
|
|
|
export function generateReservationAnalyticsExcel(data: AnalyticsPayload) {
|
|
const workbook = XLSX.utils.book_new();
|
|
|
|
const summaryRows = [
|
|
["Rapport statistiques CCDS", ""],
|
|
["Période", `${data.periodLabel} (${data.startDate} au ${data.endDate})`],
|
|
["Comparaison", data.comparisonLabel],
|
|
[],
|
|
["Indicateur", "Valeur"],
|
|
["Réservations totales", data.summary.totalReservations],
|
|
["Réservations de salles", data.summary.salleReservations],
|
|
["Réservations de matériel", data.summary.materialReservations],
|
|
["Taux d'occupation des salles", `${data.summary.occupancyRate}%`],
|
|
["Associations utilisatrices", data.summary.activeAssociations],
|
|
["Unités matérielles demandées", data.summary.totalMaterialUnits],
|
|
["Fréquence moyenne par association", data.summary.averageReservationsPerAssociation],
|
|
];
|
|
|
|
const associationRows = [
|
|
["Association", "Catégories", "Total", "Salles", "Matériel", "Dernière réservation"],
|
|
...data.associationUsage.map((item) => [
|
|
item.associationName,
|
|
item.thematics.join(" / "),
|
|
item.totalReservations,
|
|
item.salleReservations,
|
|
item.materialReservations,
|
|
item.lastReservationAt,
|
|
]),
|
|
];
|
|
|
|
const roomRows = [
|
|
["Salle", "Jours réservés", "Taux d'occupation"],
|
|
...data.roomOccupancy.map((item) => [item.roomName, item.bookedDays, `${item.occupancyRate}%`]),
|
|
];
|
|
|
|
const materialRows = [
|
|
["Matériel", "Quantité", "Dossiers"],
|
|
...data.materialUsage.map((item) => [item.label, item.quantity, item.requests]),
|
|
];
|
|
|
|
const historyRows = [
|
|
["Type", "Association", "Dossier", "Début", "Fin", "Ressources", "Statut"],
|
|
...data.history.map((item) => [
|
|
requestTypeLabels[item.type] || item.type,
|
|
item.associationName,
|
|
item.title,
|
|
item.startDate,
|
|
item.endDate,
|
|
item.resources,
|
|
statusLabels[item.status] || item.status,
|
|
]),
|
|
];
|
|
|
|
XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(summaryRows), "Synthèse");
|
|
XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(associationRows), "Associations");
|
|
XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(roomRows), "Salles");
|
|
XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(materialRows), "Matériel");
|
|
XLSX.utils.book_append_sheet(workbook, XLSX.utils.aoa_to_sheet(historyRows), "Historique");
|
|
|
|
const buffer = XLSX.write(workbook, { type: "buffer", bookType: "xlsx" }) as Buffer;
|
|
return {
|
|
buffer,
|
|
fileName: `${buildFileBaseName(data)}.xlsx`,
|
|
contentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
};
|
|
}
|