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