Files
2026-02-02 17:32:58 -05:00

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
};
}