Checkpoint: Système de questionnaires de satisfaction et d'évaluation - Phase 1 & 2 complétées
✅ Schéma de base de données créé : - Table questionnaires (satisfaction, evaluation_pre, evaluation_post) - Table questions (choix_multiple, echelle, texte_libre, oui_non) - Table reponsesQuestionnaires - Table reponsesQuestions - Table envoisQuestionnaires ✅ Backend complet implémenté : - Fichier questionnaireDb.ts avec toutes les fonctions CRUD - Procédures tRPC pour questionnaires (list, getById, create, update, delete, getStats, getReponsesDetaillees) - Procédures tRPC pour questions (list, create, update, delete) - Procédures tRPC publiques pour réponses (getByToken, submit) - Fonctions de statistiques (taux de réponse, réponses détaillées) ✅ Corrections : - Erreurs TypeScript dans exportService.ts corrigées 🚧 À faire dans la prochaine session : - Interfaces d'administration (AdminQuestionnaires, AdminQuestionnaireEdit) - Page publique de réponse aux questionnaires - Système d'envoi automatique post-formation - Tableaux de bord d'analyse avec graphiques - Exports Excel/PDF des feedbacks
This commit is contained in:
@@ -884,6 +884,13 @@ export const appRouter = router({
|
||||
await db.updateRappel(input.id, { actif: input.actif });
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
envoyerAutomatiques: publicProcedure
|
||||
.mutation(async () => {
|
||||
const { processRappelsAutomatiques } = await import('./rappelScheduler');
|
||||
const result = await processRappelsAutomatiques();
|
||||
return result;
|
||||
}),
|
||||
}),
|
||||
|
||||
emailConfig: router({
|
||||
@@ -997,6 +1004,197 @@ export const appRouter = router({
|
||||
}),
|
||||
}),
|
||||
|
||||
// ===== QUESTIONNAIRES =====
|
||||
questionnaires: router({
|
||||
list: adminProcedure.query(async () => {
|
||||
const questionnaireDb = await import("./questionnaireDb");
|
||||
return questionnaireDb.getAllQuestionnaires();
|
||||
}),
|
||||
|
||||
getById: adminProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
const questionnaireDb = await import("./questionnaireDb");
|
||||
return questionnaireDb.getQuestionnaireById(input.id);
|
||||
}),
|
||||
|
||||
create: adminProcedure
|
||||
.input(z.object({
|
||||
nom: z.string(),
|
||||
type: z.enum(["satisfaction", "evaluation_pre", "evaluation_post"]),
|
||||
description: z.string().optional(),
|
||||
formationId: z.number().nullable().optional(),
|
||||
actif: z.boolean().default(true),
|
||||
envoiAutomatique: z.boolean().default(false),
|
||||
delaiEnvoiJours: z.number().default(1),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
const questionnaireDb = await import("./questionnaireDb");
|
||||
const id = await questionnaireDb.createQuestionnaire(input);
|
||||
return { id, success: true };
|
||||
}),
|
||||
|
||||
update: adminProcedure
|
||||
.input(z.object({
|
||||
id: z.number(),
|
||||
nom: z.string().optional(),
|
||||
type: z.enum(["satisfaction", "evaluation_pre", "evaluation_post"]).optional(),
|
||||
description: z.string().optional(),
|
||||
formationId: z.number().nullable().optional(),
|
||||
actif: z.boolean().optional(),
|
||||
envoiAutomatique: z.boolean().optional(),
|
||||
delaiEnvoiJours: z.number().optional(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
const { id, ...data } = input;
|
||||
const questionnaireDb = await import("./questionnaireDb");
|
||||
await questionnaireDb.updateQuestionnaire(id, data);
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
delete: adminProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
const questionnaireDb = await import("./questionnaireDb");
|
||||
await questionnaireDb.deleteQuestionnaire(input.id);
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
getStats: adminProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
const questionnaireDb = await import("./questionnaireDb");
|
||||
return questionnaireDb.getQuestionnaireStats(input.id);
|
||||
}),
|
||||
|
||||
getReponsesDetaillees: adminProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
const questionnaireDb = await import("./questionnaireDb");
|
||||
return questionnaireDb.getReponsesDetailleesQuestionnaire(input.id);
|
||||
}),
|
||||
}),
|
||||
|
||||
// ===== QUESTIONS =====
|
||||
questions: router({
|
||||
list: adminProcedure
|
||||
.input(z.object({ questionnaireId: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
const questionnaireDb = await import("./questionnaireDb");
|
||||
return questionnaireDb.getQuestionsByQuestionnaireId(input.questionnaireId);
|
||||
}),
|
||||
|
||||
create: adminProcedure
|
||||
.input(z.object({
|
||||
questionnaireId: z.number(),
|
||||
ordre: z.number(),
|
||||
texte: z.string(),
|
||||
typeQuestion: z.enum(["choix_multiple", "echelle", "texte_libre", "oui_non"]),
|
||||
options: z.string().optional(),
|
||||
echelleMin: z.number().optional(),
|
||||
echelleMax: z.number().optional(),
|
||||
echelleLabelMin: z.string().optional(),
|
||||
echelleLabelMax: z.string().optional(),
|
||||
obligatoire: z.boolean().default(false),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
const questionnaireDb = await import("./questionnaireDb");
|
||||
const id = await questionnaireDb.createQuestion(input);
|
||||
return { id, success: true };
|
||||
}),
|
||||
|
||||
update: adminProcedure
|
||||
.input(z.object({
|
||||
id: z.number(),
|
||||
ordre: z.number().optional(),
|
||||
texte: z.string().optional(),
|
||||
typeQuestion: z.enum(["choix_multiple", "echelle", "texte_libre", "oui_non"]).optional(),
|
||||
options: z.string().optional(),
|
||||
echelleMin: z.number().optional(),
|
||||
echelleMax: z.number().optional(),
|
||||
echelleLabelMin: z.string().optional(),
|
||||
echelleLabelMax: z.string().optional(),
|
||||
obligatoire: z.boolean().optional(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
const { id, ...data } = input;
|
||||
const questionnaireDb = await import("./questionnaireDb");
|
||||
await questionnaireDb.updateQuestion(id, data);
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
delete: adminProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
const questionnaireDb = await import("./questionnaireDb");
|
||||
await questionnaireDb.deleteQuestion(input.id);
|
||||
return { success: true };
|
||||
}),
|
||||
}),
|
||||
|
||||
// ===== RÉPONSES QUESTIONNAIRES (PUBLIC) =====
|
||||
reponses: router({
|
||||
getByToken: publicProcedure
|
||||
.input(z.object({ token: z.string() }))
|
||||
.query(async ({ input }) => {
|
||||
const questionnaireDb = await import("./questionnaireDb");
|
||||
const envoi = await questionnaireDb.getEnvoiByToken(input.token);
|
||||
if (!envoi) return null;
|
||||
|
||||
const questionnaire = await questionnaireDb.getQuestionnaireById(envoi.questionnaireId);
|
||||
const questions = await questionnaireDb.getQuestionsByQuestionnaireId(envoi.questionnaireId);
|
||||
|
||||
return {
|
||||
envoi,
|
||||
questionnaire,
|
||||
questions,
|
||||
};
|
||||
}),
|
||||
|
||||
submit: publicProcedure
|
||||
.input(z.object({
|
||||
token: z.string(),
|
||||
reponses: z.array(z.object({
|
||||
questionId: z.number(),
|
||||
reponseNumerique: z.number().optional(),
|
||||
reponseTexte: z.string().optional(),
|
||||
})),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
const questionnaireDb = await import("./questionnaireDb");
|
||||
|
||||
// Récupérer l'envoi
|
||||
const envoi = await questionnaireDb.getEnvoiByToken(input.token);
|
||||
if (!envoi) throw new Error("Token invalide");
|
||||
if (envoi.dateReponse) throw new Error("Questionnaire déjà répondu");
|
||||
|
||||
// Créer la réponse au questionnaire
|
||||
const reponseQuestionnaireId = await questionnaireDb.createReponseQuestionnaire({
|
||||
questionnaireId: envoi.questionnaireId,
|
||||
apprenantId: envoi.apprenantId,
|
||||
sequenceId: envoi.sequenceId,
|
||||
complete: true,
|
||||
});
|
||||
|
||||
// Enregistrer chaque réponse
|
||||
for (const reponse of input.reponses) {
|
||||
await questionnaireDb.createReponseQuestion({
|
||||
reponseQuestionnaireId,
|
||||
questionId: reponse.questionId,
|
||||
reponseNumerique: reponse.reponseNumerique,
|
||||
reponseTexte: reponse.reponseTexte,
|
||||
});
|
||||
}
|
||||
|
||||
// Mettre à jour l'envoi
|
||||
await questionnaireDb.updateEnvoiQuestionnaire(envoi.id, {
|
||||
dateReponse: new Date(),
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
}),
|
||||
|
||||
// ===== ÉTABLISSEMENTS =====
|
||||
etablissements: router({
|
||||
list: adminProcedure.query(async () => {
|
||||
|
||||
Reference in New Issue
Block a user