✅ **Corrections apportées** :
**Problèmes résolus** :
- Correction des requêtes SQL Drizzle ORM (remplacement de db.execute() par la syntaxe Drizzle correcte)
- Utilisation de sql template literals pour les fonctions MySQL (DATE_FORMAT, AVG, CAST)
- Correction des noms de colonnes (reponse → reponseNumerique, type → typeQuestion)
- Alignement avec le schéma de base de données existant
**Fonctionnalités opérationnelles** :
- getStatsGlobales() : Statistiques globales (questionnaires, envois, réponses, taux)
- getStatsByFormation() : Taux de réponse par formation avec graphique en barres
- getEvolutionTemporelle() : Évolution mensuelle avec graphique en ligne
- getStatsByFormateur() : Comparaison formateurs avec satisfaction moyenne
**Interface utilisateur** :
- 4 cartes de statistiques globales fonctionnelles
- 4 graphiques interactifs (Recharts) prêts à afficher les données
- Gestion élégante des états vides ("Aucune donnée disponible")
- Tableau détaillé des formateurs avec code couleur
- Lien "Suivi Questionnaires" dans le menu Analyses
Le tableau de bord est maintenant pleinement fonctionnel et affichera automatiquement les statistiques dès que des questionnaires seront envoyés et répondus.
165 lines
5.6 KiB
TypeScript
165 lines
5.6 KiB
TypeScript
import { eq, sql, and, gte, lte } from "drizzle-orm";
|
|
import { getDb } from "./db";
|
|
import {
|
|
questionnaires,
|
|
envoisQuestionnaires,
|
|
reponsesQuestionnaires,
|
|
reponsesQuestions,
|
|
questions,
|
|
sequences,
|
|
formations,
|
|
formateurs
|
|
} from "../drizzle/schema";
|
|
|
|
/**
|
|
* Statistiques de taux de réponse par formation
|
|
*/
|
|
export async function getStatsByFormation() {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
const results = await db
|
|
.select({
|
|
formationId: formations.id,
|
|
formationNom: formations.nom,
|
|
totalEnvois: sql<number>`COUNT(DISTINCT ${envoisQuestionnaires.id})`,
|
|
totalReponses: sql<number>`SUM(CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN 1 ELSE 0 END)`,
|
|
})
|
|
.from(envoisQuestionnaires)
|
|
.innerJoin(sequences, eq(envoisQuestionnaires.sequenceId, sequences.id))
|
|
.innerJoin(formations, eq(sequences.formationId, formations.id))
|
|
.groupBy(formations.id, formations.nom);
|
|
|
|
return results.map(r => ({
|
|
formationId: r.formationId,
|
|
formationNom: r.formationNom,
|
|
totalEnvois: Number(r.totalEnvois),
|
|
totalReponses: Number(r.totalReponses),
|
|
tauxReponse: Number(r.totalEnvois) > 0
|
|
? Math.round((Number(r.totalReponses) / Number(r.totalEnvois)) * 100)
|
|
: 0
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Évolution temporelle des réponses (par mois)
|
|
*/
|
|
export async function getEvolutionTemporelle(startDate?: Date, endDate?: Date) {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
const conditions = [];
|
|
if (startDate) {
|
|
conditions.push(gte(envoisQuestionnaires.dateEnvoi, startDate));
|
|
}
|
|
if (endDate) {
|
|
conditions.push(lte(envoisQuestionnaires.dateEnvoi, endDate));
|
|
}
|
|
|
|
const results = await db
|
|
.select({
|
|
mois: sql<string>`DATE_FORMAT(${envoisQuestionnaires.dateEnvoi}, '%Y-%m')`,
|
|
totalEnvois: sql<number>`COUNT(DISTINCT ${envoisQuestionnaires.id})`,
|
|
totalReponses: sql<number>`SUM(CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN 1 ELSE 0 END)`,
|
|
})
|
|
.from(envoisQuestionnaires)
|
|
.where(conditions.length > 0 ? and(...conditions) : undefined)
|
|
.groupBy(sql`DATE_FORMAT(${envoisQuestionnaires.dateEnvoi}, '%Y-%m')`)
|
|
.orderBy(sql`DATE_FORMAT(${envoisQuestionnaires.dateEnvoi}, '%Y-%m')`);
|
|
|
|
return results.map(r => ({
|
|
mois: r.mois,
|
|
totalEnvois: Number(r.totalEnvois),
|
|
totalReponses: Number(r.totalReponses),
|
|
tauxReponse: Number(r.totalEnvois) > 0
|
|
? Math.round((Number(r.totalReponses) / Number(r.totalEnvois)) * 100)
|
|
: 0
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Comparaison par formateur
|
|
*/
|
|
export async function getStatsByFormateur() {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
// Première requête : statistiques d'envois et réponses par formateur
|
|
const statsEnvois = await db
|
|
.select({
|
|
formateurId: formateurs.id,
|
|
formateurNom: formateurs.nom,
|
|
totalEnvois: sql<number>`COUNT(DISTINCT ${envoisQuestionnaires.id})`,
|
|
totalReponses: sql<number>`SUM(CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN 1 ELSE 0 END)`,
|
|
})
|
|
.from(envoisQuestionnaires)
|
|
.innerJoin(sequences, eq(envoisQuestionnaires.sequenceId, sequences.id))
|
|
.innerJoin(formateurs, eq(sequences.formateurId, formateurs.id))
|
|
.groupBy(formateurs.id, formateurs.nom);
|
|
|
|
// Pour chaque formateur, calculer la moyenne de satisfaction
|
|
const results = await Promise.all(statsEnvois.map(async (stat) => {
|
|
// Calculer la moyenne des réponses de type échelle pour ce formateur
|
|
const satisfactionResult = await db
|
|
.select({
|
|
moyenne: sql<number>`AVG(CAST(${reponsesQuestions.reponseNumerique} AS DECIMAL(10,2)))`,
|
|
})
|
|
.from(reponsesQuestionnaires)
|
|
.innerJoin(sequences, eq(reponsesQuestionnaires.sequenceId, sequences.id))
|
|
.innerJoin(reponsesQuestions, eq(reponsesQuestionnaires.id, reponsesQuestions.reponseQuestionnaireId))
|
|
.innerJoin(questions, eq(reponsesQuestions.questionId, questions.id))
|
|
.where(
|
|
and(
|
|
eq(sequences.formateurId, stat.formateurId),
|
|
eq(reponsesQuestionnaires.complete, true),
|
|
eq(questions.typeQuestion, 'echelle')
|
|
)
|
|
);
|
|
|
|
const moyenneSatisfaction = satisfactionResult[0]?.moyenne
|
|
? Number(satisfactionResult[0].moyenne).toFixed(1)
|
|
: null;
|
|
|
|
return {
|
|
formateurId: stat.formateurId,
|
|
formateurNom: stat.formateurNom || "Non spécifié",
|
|
totalEnvois: Number(stat.totalEnvois),
|
|
totalReponses: Number(stat.totalReponses),
|
|
tauxReponse: Number(stat.totalEnvois) > 0
|
|
? Math.round((Number(stat.totalReponses) / Number(stat.totalEnvois)) * 100)
|
|
: 0,
|
|
moyenneSatisfaction
|
|
};
|
|
}));
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Statistiques globales pour le tableau de bord
|
|
*/
|
|
export async function getStatsGlobales() {
|
|
const db = await getDb();
|
|
if (!db) throw new Error("Database not available");
|
|
|
|
const results = await db
|
|
.select({
|
|
totalQuestionnaires: sql<number>`COUNT(DISTINCT ${questionnaires.id})`,
|
|
totalEnvois: sql<number>`COUNT(DISTINCT ${envoisQuestionnaires.id})`,
|
|
totalReponses: sql<number>`SUM(CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN 1 ELSE 0 END)`,
|
|
})
|
|
.from(questionnaires)
|
|
.leftJoin(envoisQuestionnaires, eq(questionnaires.id, envoisQuestionnaires.questionnaireId));
|
|
|
|
const stats = results[0];
|
|
const totalEnvois = Number(stats.totalEnvois) || 0;
|
|
const totalReponses = Number(stats.totalReponses) || 0;
|
|
|
|
return {
|
|
totalQuestionnaires: Number(stats.totalQuestionnaires) || 0,
|
|
totalEnvois,
|
|
totalReponses,
|
|
tauxReponseGlobal: totalEnvois > 0 ? Number(((totalReponses / totalEnvois) * 100).toFixed(1)) : 0
|
|
};
|
|
}
|