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 = { demande_salle: "Demande salle", demande_materiel_evenementiel: "Matériel événementiel", }; const statusLabels: Record = { 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((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", }; }