Checkpoint: Ajout de filtres de période (dernier mois, trimestre, année) pour affiner l'analyse des tendances. Implémentation de l'export Excel des rapports analytiques avec 5 feuilles (statistiques globales, inscriptions par mois, taux de remplissage, participation par établissement et par fonction). Création du schéma de base de données pour les alertes automatiques (fonctionnalité future). Les utilisateurs peuvent maintenant filtrer les données analytiques par période et télécharger des rapports Excel complets.

This commit is contained in:
Manus Sandbox
2025-11-23 16:36:26 -05:00
parent 64db3f1aa6
commit 03efb59cbf
11 changed files with 1914 additions and 7 deletions

View File

@@ -0,0 +1,145 @@
/**
* Service d'export des rapports analytiques en PDF et Excel
*/
import ExcelJS from 'exceljs';
import { getGlobalStats, getInscriptionsByMonth, getTauxRemplissageByMonth, getParticipationByEtablissement, getParticipationByFonction } from './analyticsDb';
/**
* Génère un rapport Excel des statistiques analytiques
*/
export async function generateAnalyticsExcel(startDate?: Date, endDate?: Date): Promise<Buffer> {
const workbook = new ExcelJS.Workbook();
// Récupérer toutes les données
const globalStats = await getGlobalStats();
const inscriptionsByMonth = await getInscriptionsByMonth(startDate, endDate);
const tauxRemplissage = await getTauxRemplissageByMonth();
const participationEtablissement = await getParticipationByEtablissement();
const participationFonction = await getParticipationByFonction();
// Feuille 1: Statistiques globales
const statsSheet = workbook.addWorksheet('Statistiques globales');
statsSheet.columns = [
{ header: 'Indicateur', key: 'indicateur', width: 30 },
{ header: 'Valeur', key: 'valeur', width: 15 },
];
if (globalStats) {
statsSheet.addRows([
{ indicateur: 'Total Formations actives', valeur: globalStats.totalFormations },
{ indicateur: 'Total Séquences', valeur: globalStats.totalSequences },
{ indicateur: 'Séquences ouvertes', valeur: globalStats.sequencesOuvertes },
{ indicateur: 'Total Apprenants', valeur: globalStats.totalApprenants },
{ indicateur: 'Total Inscriptions', valeur: globalStats.totalInscriptions },
{ indicateur: 'Inscriptions confirmées', valeur: globalStats.inscriptionsConfirmees },
{ indicateur: 'Inscriptions en liste d\'attente', valeur: globalStats.inscriptionsListeAttente },
{ indicateur: 'Taux de remplissage moyen (%)', valeur: globalStats.tauxRemplissageMoyen },
]);
}
// Feuille 2: Inscriptions par mois
const inscriptionsSheet = workbook.addWorksheet('Inscriptions par mois');
inscriptionsSheet.columns = [
{ header: 'Mois', key: 'mois', width: 15 },
{ header: 'Total', key: 'total', width: 12 },
{ header: 'Confirmées', key: 'confirmees', width: 12 },
{ header: 'Liste d\'attente', key: 'listeAttente', width: 15 },
{ header: 'Annulées', key: 'annulees', width: 12 },
];
inscriptionsByMonth.forEach(item => {
inscriptionsSheet.addRow({
mois: item.mois,
total: Number(item.total),
confirmees: Number(item.confirmees),
listeAttente: Number(item.listeAttente),
annulees: Number(item.annulees),
});
});
// Feuille 3: Taux de remplissage
const tauxSheet = workbook.addWorksheet('Taux de remplissage');
tauxSheet.columns = [
{ header: 'Mois', key: 'mois', width: 15 },
{ header: 'Capacité totale', key: 'capaciteTotale', width: 15 },
{ header: 'Inscrits confirmés', key: 'inscritsConfirmes', width: 18 },
{ header: 'Taux de remplissage (%)', key: 'tauxRemplissage', width: 20 },
];
tauxRemplissage.forEach(item => {
tauxSheet.addRow({
mois: item.mois,
capaciteTotale: Number(item.capaciteTotale),
inscritsConfirmes: Number(item.inscritsConfirmes),
tauxRemplissage: Number(item.tauxRemplissage),
});
});
// Feuille 4: Participation par établissement
const etablissementSheet = workbook.addWorksheet('Participation par établissement');
etablissementSheet.columns = [
{ header: 'Code établissement', key: 'codeEtablissement', width: 20 },
{ header: 'Total apprenants', key: 'totalApprenants', width: 15 },
{ header: 'Total inscriptions', key: 'totalInscriptions', width: 18 },
{ header: 'Confirmées', key: 'confirmees', width: 12 },
{ header: 'Liste d\'attente', key: 'listeAttente', width: 15 },
{ header: 'Annulées', key: 'annulees', width: 12 },
];
participationEtablissement.forEach(item => {
etablissementSheet.addRow({
codeEtablissement: item.codeEtablissement,
totalApprenants: Number(item.totalApprenants),
totalInscriptions: Number(item.totalInscriptions),
confirmees: Number(item.inscriptionsConfirmees),
listeAttente: Number(item.inscriptionsListeAttente),
annulees: Number(item.inscriptionsAnnulees),
});
});
// Feuille 5: Participation par fonction
const fonctionSheet = workbook.addWorksheet('Participation par fonction');
fonctionSheet.columns = [
{ header: 'Fonction', key: 'fonction', width: 20 },
{ header: 'Total apprenants', key: 'totalApprenants', width: 15 },
{ header: 'Total inscriptions', key: 'totalInscriptions', width: 18 },
{ header: 'Confirmées', key: 'confirmees', width: 12 },
];
participationFonction.forEach(item => {
fonctionSheet.addRow({
fonction: item.fonction === 'directeur' ? 'Directeurs' :
item.fonction === 'chef_service' ? 'Chefs de service' : 'Autres',
totalApprenants: Number(item.totalApprenants),
totalInscriptions: Number(item.totalInscriptions),
confirmees: Number(item.inscriptionsConfirmees),
});
});
// Styliser les en-têtes
[statsSheet, inscriptionsSheet, tauxSheet, etablissementSheet, fonctionSheet].forEach(sheet => {
sheet.getRow(1).font = { bold: true };
sheet.getRow(1).fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FF3B82F6' },
};
sheet.getRow(1).font = { bold: true, color: { argb: 'FFFFFFFF' } };
});
// Générer le buffer
const buffer = await workbook.xlsx.writeBuffer();
return Buffer.from(buffer);
}
/**
* Génère un rapport PDF des statistiques analytiques
* Note: Cette fonction retourne actuellement un placeholder
* Une implémentation complète nécessiterait une bibliothèque comme puppeteer ou pdfkit
*/
export async function generateAnalyticsPDF(startDate?: Date, endDate?: Date): Promise<Buffer> {
// Pour l'instant, retourner un message indiquant que la fonctionnalité est en développement
// Une implémentation complète nécessiterait de générer un HTML et de le convertir en PDF
throw new Error("L'export PDF sera implémenté dans une prochaine version");
}

View File

@@ -6,6 +6,7 @@ import { parseLocalDateTime } from "./dateUtils";
import { z } from "zod";
import * as db from "./db";
import * as analyticsDb from "./analyticsDb";
import { generateAnalyticsExcel, generateAnalyticsPDF } from "./analyticsExportService";
import { TRPCError } from "@trpc/server";
import { sendInscriptionConfirmation, sendGroupEmail } from "./emailService";
import { generateExcelExport, generatePDFExport, generateFeuillePresence } from "./exportService";
@@ -858,6 +859,36 @@ export const appRouter = router({
participationByFonction: adminProcedure.query(async () => {
return analyticsDb.getParticipationByFonction();
}),
exportExcel: adminProcedure
.input(z.object({
startDate: z.string().optional(),
endDate: z.string().optional(),
}))
.mutation(async ({ input }) => {
const startDate = input.startDate ? new Date(input.startDate) : undefined;
const endDate = input.endDate ? new Date(input.endDate) : undefined;
const buffer = await generateAnalyticsExcel(startDate, endDate);
return {
data: buffer.toString('base64'),
filename: `rapport-analytique-${new Date().toISOString().split('T')[0]}.xlsx`,
};
}),
exportPDF: adminProcedure
.input(z.object({
startDate: z.string().optional(),
endDate: z.string().optional(),
}))
.mutation(async ({ input }) => {
const startDate = input.startDate ? new Date(input.startDate) : undefined;
const endDate = input.endDate ? new Date(input.endDate) : undefined;
const buffer = await generateAnalyticsPDF(startDate, endDate);
return {
data: buffer.toString('base64'),
filename: `rapport-analytique-${new Date().toISOString().split('T')[0]}.pdf`,
};
}),
}),
});