Files
formation-manager-itinova/server/questionnaireSuiviDb.ts
Manus Sandbox 46b3f377d1 Checkpoint: Tableau de bord de suivi des questionnaires
 **Nouvelles fonctionnalités implémentées** :

**Backend complet** :
- Fichier questionnaireSuiviDb.ts avec 4 fonctions de statistiques
- getStatsByFormation() : Taux de réponse par formation
- getEvolutionTemporelle() : Évolution mensuelle des envois/réponses
- getStatsByFormateur() : Comparaison par formateur avec satisfaction moyenne
- getStatsGlobales() : Statistiques globales (questionnaires, envois, réponses, taux)
- Procédures tRPC complètes dans routers.ts

**Interface utilisateur** :
- Page AdminQuestionnaireSuivi avec graphiques interactifs (Recharts)
- 4 cartes de statistiques globales (questionnaires, envois, réponses, taux)
- Graphique en barres : Taux de réponse par formation
- Graphique en ligne : Évolution temporelle mensuelle
- Graphique camembert : Répartition des réponses par formation
- Graphique double axe : Comparaison formateurs (taux + satisfaction)
- Tableau détaillé des formateurs avec code couleur (vert >70%, orange >50%, rouge <50%)
- Lien "Suivi Questionnaires" ajouté dans le menu Analyses

Le tableau de bord offre une vue complète des performances des questionnaires pour optimiser le suivi et l'amélioration continue.
2025-12-07 16:03:24 -05:00

155 lines
5.5 KiB
TypeScript

import { eq, sql, and, gte, lte } from "drizzle-orm";
import { getDb } from "./db";
import {
questionnaires,
envoisQuestionnaires,
reponsesQuestionnaires,
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>`COUNT(DISTINCT CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN ${envoisQuestionnaires.id} 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");
let query = db
.select({
mois: sql<string>`DATE_FORMAT(${envoisQuestionnaires.dateEnvoi}, '%Y-%m')`,
totalEnvois: sql<number>`COUNT(DISTINCT ${envoisQuestionnaires.id})`,
totalReponses: sql<number>`COUNT(DISTINCT CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN ${envoisQuestionnaires.id} END)`,
})
.from(envoisQuestionnaires);
// Filtrer par dates si spécifiées
const conditions = [];
if (startDate) {
conditions.push(gte(envoisQuestionnaires.dateEnvoi, startDate));
}
if (endDate) {
conditions.push(lte(envoisQuestionnaires.dateEnvoi, endDate));
}
if (conditions.length > 0) {
query = query.where(and(...conditions)) as any;
}
const results = await query.groupBy(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");
const results = await db
.select({
formateurId: formateurs.id,
formateurNom: formateurs.nom,
totalEnvois: sql<number>`COUNT(DISTINCT ${envoisQuestionnaires.id})`,
totalReponses: sql<number>`COUNT(DISTINCT CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN ${envoisQuestionnaires.id} END)`,
moyenneSatisfaction: sql<number>`AVG(CASE
WHEN ${reponsesQuestionnaires.complete} = 1
THEN (SELECT AVG(CAST(rq.reponse AS DECIMAL(10,2)))
FROM reponsesQuestions rq
INNER JOIN questions q ON rq.questionId = q.id
WHERE rq.reponseQuestionnaireId = ${reponsesQuestionnaires.id}
AND q.type = 'echelle'
AND rq.reponse REGEXP '^[0-9]+$')
END)`,
})
.from(envoisQuestionnaires)
.innerJoin(sequences, eq(envoisQuestionnaires.sequenceId, sequences.id))
.leftJoin(formateurs, eq(sequences.formateurId, formateurs.id))
.leftJoin(reponsesQuestionnaires, and(
eq(envoisQuestionnaires.apprenantId, reponsesQuestionnaires.apprenantId),
eq(envoisQuestionnaires.sequenceId, reponsesQuestionnaires.sequenceId)
))
.where(sql`${formateurs.id} IS NOT NULL`)
.groupBy(formateurs.id, formateurs.nom);
return results.map(r => ({
formateurId: r.formateurId,
formateurNom: r.formateurNom || "Non spécifié",
totalEnvois: Number(r.totalEnvois),
totalReponses: Number(r.totalReponses),
tauxReponse: Number(r.totalEnvois) > 0
? Math.round((Number(r.totalReponses) / Number(r.totalEnvois)) * 100)
: 0,
moyenneSatisfaction: r.moyenneSatisfaction ? Number(r.moyenneSatisfaction).toFixed(1) : null
}));
}
/**
* Statistiques globales pour le tableau de bord
*/
export async function getStatsGlobales() {
const db = await getDb();
if (!db) throw new Error("Database not available");
const [stats] = await db
.select({
totalQuestionnaires: sql<number>`COUNT(DISTINCT ${questionnaires.id})`,
totalEnvois: sql<number>`COUNT(DISTINCT ${envoisQuestionnaires.id})`,
totalReponses: sql<number>`COUNT(DISTINCT CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN ${envoisQuestionnaires.id} END)`,
tauxReponseGlobal: sql<number>`ROUND(
COUNT(DISTINCT CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN ${envoisQuestionnaires.id} END) * 100.0 /
NULLIF(COUNT(DISTINCT ${envoisQuestionnaires.id}), 0),
1
)`,
})
.from(questionnaires)
.leftJoin(envoisQuestionnaires, eq(questionnaires.id, envoisQuestionnaires.questionnaireId));
return {
totalQuestionnaires: Number(stats.totalQuestionnaires),
totalEnvois: Number(stats.totalEnvois),
totalReponses: Number(stats.totalReponses),
tauxReponseGlobal: Number(stats.tauxReponseGlobal) || 0
};
}