Files
formation-manager-itinova/server/analyticsDb.ts
Manus fc4df8ed36 Checkpoint: Correction des erreurs sur la page rapport-public-cible et harmonisation de la charte graphique des titres :
**Corrections d'erreurs :**

1. **Erreurs SQL DATE_FORMAT corrigées** (analyticsDb.ts) :
   - Remplacement des sous-requêtes corrélées par des JOIN dans getTauxRemplissageByMonth()
   - Utilisation de COUNT(CASE WHEN...) au lieu de SUM(SELECT COUNT(*))
   - Ajout de leftJoin pour éviter les erreurs de sous-requêtes

2. **Erreur React "Cannot read properties of null" corrigée** (AdminRapportPublicCible.tsx) :
   - Ajout de l'opérateur null-safe `?.` pour accéder à `insc.apprenant?.fonction`
   - Vérification explicite si la fonction est null/undefined
   - Gestion du cas "public cible = tous" qui correspond toujours

3. **Tests réussis** :
   - Page rapport-public-cible : affiche correctement les statistiques et recommandations
   - Tableau de bord analytique : graphiques et statistiques fonctionnels
   - Page suivi questionnaires : affiche correctement les données

**Harmonisation charte graphique :**

- Application du style bleu clair (#6B9FE8) avec ombre portée à tous les titres des 32 pages
- Création de la classe CSS `.page-title` réutilisable dans index.css
- Cohérence visuelle sur toute l'application

**Résultat :**
 Toutes les erreurs SQL et React sont corrigées
 Les pages fonctionnent correctement sans erreur
 Charte graphique harmonisée sur toutes les pages
2026-01-20 11:46:56 -05:00

139 lines
5.1 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 = [];
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;
}