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:
145
server/analyticsExportService.ts
Normal file
145
server/analyticsExportService.ts
Normal 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");
|
||||
}
|
||||
@@ -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`,
|
||||
};
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user