diff --git a/.manus/db/db-query-1765142806487.json b/.manus/db/db-query-1765142806487.json new file mode 100644 index 0000000..af62684 --- /dev/null +++ b/.manus/db/db-query-1765142806487.json @@ -0,0 +1,14 @@ +{ + "query": "SELECT YEAR(NOW()) as annee, MONTH(NOW()) as mois", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute SELECT YEAR(NOW()) as annee, MONTH(NOW()) as mois", + "rows": [ + { + "annee": "2025", + "mois": "12" + } + ], + "messages": [], + "stdout": "annee\tmois\n2025\t12\n", + "stderr": "", + "execution_time_ms": 149 +} \ No newline at end of file diff --git a/.manus/db/db-query-1765142811401.json b/.manus/db/db-query-1765142811401.json new file mode 100644 index 0000000..59d21f1 --- /dev/null +++ b/.manus/db/db-query-1765142811401.json @@ -0,0 +1,13 @@ +{ + "query": "SELECT COUNT(*) as total FROM envoisQuestionnaires", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute SELECT COUNT(*) as total FROM envoisQuestionnaires", + "rows": [ + { + "total": "0" + } + ], + "messages": [], + "stdout": "total\n0\n", + "stderr": "", + "execution_time_ms": 62 +} \ No newline at end of file diff --git a/.manus/db/db-query-1765142816098.json b/.manus/db/db-query-1765142816098.json new file mode 100644 index 0000000..5eea9f2 --- /dev/null +++ b/.manus/db/db-query-1765142816098.json @@ -0,0 +1,9 @@ +{ + "query": "SELECT YEAR(dateEnvoi) as annee, MONTH(dateEnvoi) as mois, COUNT(DISTINCT id) as total FROM envoisQuestionnaires GROUP BY YEAR(dateEnvoi), MONTH(dateEnvoi) LIMIT 5", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute SELECT YEAR(dateEnvoi) as annee, MONTH(dateEnvoi) as mois, COUNT(DISTINCT id) as total FROM envoisQuestionnaires GROUP BY YEAR(dateEnvoi), MONTH(dateEnvoi) LIMIT 5", + "rows": [], + "messages": [], + "stdout": "", + "stderr": "", + "execution_time_ms": 51 +} \ No newline at end of file diff --git a/server/questionnaireSuiviDb.ts b/server/questionnaireSuiviDb.ts index 65eba72..8dcd1ba 100644 --- a/server/questionnaireSuiviDb.ts +++ b/server/questionnaireSuiviDb.ts @@ -4,6 +4,8 @@ import { questionnaires, envoisQuestionnaires, reponsesQuestionnaires, + reponsesQuestions, + questions, sequences, formations, formateurs @@ -21,7 +23,7 @@ export async function getStatsByFormation() { formationId: formations.id, formationNom: formations.nom, totalEnvois: sql`COUNT(DISTINCT ${envoisQuestionnaires.id})`, - totalReponses: sql`COUNT(DISTINCT CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN ${envoisQuestionnaires.id} END)`, + totalReponses: sql`SUM(CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN 1 ELSE 0 END)`, }) .from(envoisQuestionnaires) .innerJoin(sequences, eq(envoisQuestionnaires.sequenceId, sequences.id)) @@ -46,15 +48,6 @@ 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`DATE_FORMAT(${envoisQuestionnaires.dateEnvoi}, '%Y-%m')`, - totalEnvois: sql`COUNT(DISTINCT ${envoisQuestionnaires.id})`, - totalReponses: sql`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)); @@ -63,11 +56,16 @@ export async function getEvolutionTemporelle(startDate?: Date, endDate?: Date) { 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')`); + const results = await db + .select({ + mois: sql`DATE_FORMAT(${envoisQuestionnaires.dateEnvoi}, '%Y-%m')`, + totalEnvois: sql`COUNT(DISTINCT ${envoisQuestionnaires.id})`, + totalReponses: sql`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, @@ -86,42 +84,55 @@ export async function getStatsByFormateur() { const db = await getDb(); if (!db) throw new Error("Database not available"); - const results = await db + // 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`COUNT(DISTINCT ${envoisQuestionnaires.id})`, - totalReponses: sql`COUNT(DISTINCT CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN ${envoisQuestionnaires.id} END)`, - moyenneSatisfaction: sql`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)`, + totalReponses: sql`SUM(CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN 1 ELSE 0 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`) + .innerJoin(formateurs, eq(sequences.formateurId, formateurs.id)) .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 + // 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`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; } /** @@ -131,24 +142,23 @@ export async function getStatsGlobales() { const db = await getDb(); if (!db) throw new Error("Database not available"); - const [stats] = await db + const results = await db .select({ totalQuestionnaires: sql`COUNT(DISTINCT ${questionnaires.id})`, totalEnvois: sql`COUNT(DISTINCT ${envoisQuestionnaires.id})`, - totalReponses: sql`COUNT(DISTINCT CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN ${envoisQuestionnaires.id} END)`, - tauxReponseGlobal: sql`ROUND( - COUNT(DISTINCT CASE WHEN ${envoisQuestionnaires.dateReponse} IS NOT NULL THEN ${envoisQuestionnaires.id} END) * 100.0 / - NULLIF(COUNT(DISTINCT ${envoisQuestionnaires.id}), 0), - 1 - )`, + totalReponses: sql`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), - totalEnvois: Number(stats.totalEnvois), - totalReponses: Number(stats.totalReponses), - tauxReponseGlobal: Number(stats.tauxReponseGlobal) || 0 + totalQuestionnaires: Number(stats.totalQuestionnaires) || 0, + totalEnvois, + totalReponses, + tauxReponseGlobal: totalEnvois > 0 ? Number(((totalReponses / totalEnvois) * 100).toFixed(1)) : 0 }; } diff --git a/test-query.mjs b/test-query.mjs new file mode 100644 index 0000000..94de88f --- /dev/null +++ b/test-query.mjs @@ -0,0 +1,14 @@ +import { drizzle } from "drizzle-orm/mysql2"; +import mysql from "mysql2/promise"; + +const connection = await mysql.createConnection(process.env.DATABASE_URL); +const db = drizzle(connection); + +try { + const result = await connection.query("SELECT YEAR(NOW()) as annee, MONTH(NOW()) as mois"); + console.log("Test YEAR/MONTH réussi:", result[0]); +} catch (error) { + console.error("Erreur:", error.message); +} + +await connection.end(); diff --git a/test-trpc.mjs b/test-trpc.mjs new file mode 100644 index 0000000..097cf59 --- /dev/null +++ b/test-trpc.mjs @@ -0,0 +1,10 @@ +import { getStatsGlobales } from './server/questionnaireSuiviDb.ts'; + +try { + console.log("Test des statistiques globales..."); + const stats = await getStatsGlobales(); + console.log("Résultat:", stats); +} catch (error) { + console.error("Erreur:", error.message); + console.error("Stack:", error.stack); +} diff --git a/todo.md b/todo.md index b03b95f..705b26f 100644 --- a/todo.md +++ b/todo.md @@ -36,3 +36,10 @@ - [x] Créer la page AdminQuestionnaireSuivi avec graphiques - [x] Ajouter le lien dans le menu de navigation - [x] Tester toutes les fonctionnalités + +## Correction des erreurs SQL du tableau de bord de suivi + +- [x] Corriger la requête getEvolutionTemporelle (DATE_FORMAT) +- [x] Corriger la requête getStatsByFormateur (sous-requête AVG) +- [x] Simplifier les requêtes pour compatibilité MySQL +- [x] Tester toutes les requêtes corrigées