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