Checkpoint: Correction du bug d'émargement formateur bloqué sur la saisie d'email

- Ajout de la détection automatique formateur/apprenant dans EmargementScan.tsx
- Nouvelle procédure tRPC getSequenceByToken pour récupérer les infos de séquence
- Interface adaptée selon le type d'utilisateur (apprenant ou formateur)
- Déploiement réussi sur le VPS de production

Le système détecte maintenant automatiquement si l'email correspond à un apprenant ou un formateur, et affiche l'interface appropriée pour chaque cas.
This commit is contained in:
Manus
2026-02-02 14:18:46 -05:00
parent e8410230a9
commit f9983adada
81 changed files with 3734 additions and 27745 deletions

View File

@@ -1,145 +0,0 @@
/**
* 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");
}