Files
formation-manager-itinova/server/analyticsDb.ts
Manus 1c15df377d Checkpoint: Correction des erreurs Fail2Ban et SQL DATE_FORMAT :
**1. Correction erreur Fail2Ban (fail2banDb.ts) :**
- Ajout de vérifications pour détecter si Fail2Ban est installé (commande `which fail2ban-client`)
- Retour de valeurs par défaut (0, tableaux vides) au lieu de throw d'erreur dans l'environnement sandbox
- Corrections appliquées aux 3 fonctions : getFail2BanStatus(), getBanHistory(), countRecentBans()
- La page /admin/fail2ban s'affiche maintenant correctement sans erreur dans la sandbox

**2. Correction erreur SQL DATE_FORMAT (analyticsDb.ts) :**
- Ajout d'un filtre `IS NOT NULL` sur la colonne dateInscription dans getInscriptionsByMonth()
- Exclusion des inscriptions avec dateInscription NULL qui causaient des erreurs SQL
- Le graphique "Évolution des inscriptions par mois" affiche maintenant "Aucune donnée disponible" au lieu d'une erreur

**Tests réussis :**
-  Page Fail2Ban : affiche correctement les statistiques à 0 et les messages appropriés
-  Tableau de bord analytique : tous les graphiques fonctionnent sans erreur SQL
-  Les autres pages (rapport-public-cible, suivi questionnaires) continuent de fonctionner correctement

**Résultat :**
 Toutes les erreurs signalées sont corrigées
 L'application fonctionne correctement dans l'environnement sandbox
 Sur le VPS de production, Fail2Ban affichera les vraies valeurs
2026-01-20 11:52:47 -05:00

142 lines
5.3 KiB
TypeScript

/**
* Requêtes analytiques pour le tableau de bord
*/
import { eq, sql, and, gte, lte, desc } from "drizzle-orm";
import { getDb } from "./db";
import { inscriptions, sequences, apprenants, datesFormation, formations } from "../drizzle/schema";
/**
* Récupère les statistiques d'inscriptions par mois
* @param startDate - Date de début de la période
* @param endDate - Date de fin de la période
*/
export async function getInscriptionsByMonth(startDate?: Date, endDate?: Date) {
const db = await getDb();
if (!db) return [];
const conditions = [];
// Exclure les inscriptions avec dateInscription NULL
conditions.push(sql`${inscriptions.dateInscription} IS NOT NULL`);
if (startDate) {
conditions.push(gte(inscriptions.dateInscription, startDate));
}
if (endDate) {
conditions.push(lte(inscriptions.dateInscription, endDate));
}
const result = await db
.select({
mois: sql<string>`DATE_FORMAT(${inscriptions.dateInscription}, '%Y-%m')`,
total: sql<number>`COUNT(*)`,
confirmees: sql<number>`SUM(CASE WHEN ${inscriptions.statut} = 'confirmee' THEN 1 ELSE 0 END)`,
listeAttente: sql<number>`SUM(CASE WHEN ${inscriptions.statut} = 'liste_attente' THEN 1 ELSE 0 END)`,
annulees: sql<number>`SUM(CASE WHEN ${inscriptions.statut} = 'annulee' THEN 1 ELSE 0 END)`,
})
.from(inscriptions)
.where(conditions.length > 0 ? and(...conditions) : undefined)
.groupBy(sql`DATE_FORMAT(${inscriptions.dateInscription}, '%Y-%m')`)
.orderBy(sql`DATE_FORMAT(${inscriptions.dateInscription}, '%Y-%m')`);
return result;
}
/**
* Récupère le taux de remplissage des séquences par mois
*/
export async function getTauxRemplissageByMonth() {
const db = await getDb();
if (!db) return [];
const result = await db
.select({
mois: sql<string>`DATE_FORMAT(${sequences.createdAt}, '%Y-%m')`,
capaciteTotale: sql<number>`SUM(${sequences.capaciteMax})`,
inscritsConfirmes: sql<number>`COUNT(CASE WHEN ${inscriptions.statut} = 'confirmee' THEN ${inscriptions.id} END)`,
tauxRemplissage: sql<number>`ROUND(
(COUNT(CASE WHEN ${inscriptions.statut} = 'confirmee' THEN ${inscriptions.id} END) / SUM(${sequences.capaciteMax})) * 100,
2
)`,
})
.from(sequences)
.leftJoin(inscriptions, eq(sequences.id, inscriptions.sequenceId))
.groupBy(sql`DATE_FORMAT(${sequences.createdAt}, '%Y-%m')`)
.orderBy(sql`DATE_FORMAT(${sequences.createdAt}, '%Y-%m')`);
return result;
}
/**
* Récupère les statistiques de participation par établissement
*/
export async function getParticipationByEtablissement() {
const db = await getDb();
if (!db) return [];
const result = await db
.select({
codeEtablissement: apprenants.codeEtablissement,
totalApprenants: sql<number>`COUNT(DISTINCT ${apprenants.id})`,
totalInscriptions: sql<number>`COUNT(${inscriptions.id})`,
inscriptionsConfirmees: sql<number>`SUM(CASE WHEN ${inscriptions.statut} = 'confirmee' THEN 1 ELSE 0 END)`,
inscriptionsListeAttente: sql<number>`SUM(CASE WHEN ${inscriptions.statut} = 'liste_attente' THEN 1 ELSE 0 END)`,
inscriptionsAnnulees: sql<number>`SUM(CASE WHEN ${inscriptions.statut} = 'annulee' THEN 1 ELSE 0 END)`,
})
.from(apprenants)
.leftJoin(inscriptions, eq(apprenants.id, inscriptions.apprenantId))
.groupBy(apprenants.codeEtablissement)
.orderBy(desc(sql<number>`COUNT(${inscriptions.id})`));
return result;
}
/**
* Récupère les statistiques globales du tableau de bord
*/
export async function getGlobalStats() {
const db = await getDb();
if (!db) return null;
const [stats] = await db
.select({
totalFormations: sql<number>`(SELECT COUNT(*) FROM ${formations} WHERE ${formations.actif} = 1)`,
totalSequences: sql<number>`(SELECT COUNT(*) FROM ${sequences})`,
sequencesOuvertes: sql<number>`(SELECT COUNT(*) FROM ${sequences} WHERE ${sequences.statut} = 'ouverte')`,
totalApprenants: sql<number>`(SELECT COUNT(*) FROM ${apprenants})`,
totalInscriptions: sql<number>`(SELECT COUNT(*) FROM ${inscriptions})`,
inscriptionsConfirmees: sql<number>`(SELECT COUNT(*) FROM ${inscriptions} WHERE ${inscriptions.statut} = 'confirmee')`,
inscriptionsListeAttente: sql<number>`(SELECT COUNT(*) FROM ${inscriptions} WHERE ${inscriptions.statut} = 'liste_attente')`,
tauxRemplissageMoyen: sql<number>`ROUND(
(SELECT COUNT(*) FROM ${inscriptions} WHERE ${inscriptions.statut} = 'confirmee') /
(SELECT SUM(${sequences.capaciteMax}) FROM ${sequences}) * 100,
2
)`,
})
.from(sql`dual`);
return stats;
}
/**
* Récupère les statistiques de participation par fonction
*/
export async function getParticipationByFonction() {
const db = await getDb();
if (!db) return [];
const result = await db
.select({
fonction: apprenants.fonction,
totalApprenants: sql<number>`COUNT(DISTINCT ${apprenants.id})`,
totalInscriptions: sql<number>`COUNT(${inscriptions.id})`,
inscriptionsConfirmees: sql<number>`SUM(CASE WHEN ${inscriptions.statut} = 'confirmee' THEN 1 ELSE 0 END)`,
})
.from(apprenants)
.leftJoin(inscriptions, eq(apprenants.id, inscriptions.apprenantId))
.groupBy(apprenants.fonction)
.orderBy(desc(sql<number>`COUNT(${inscriptions.id})`));
return result;
}