Checkpoint: Ajout de 3 améliorations pour le système de notifications :

1. Interface de gestion des emails formateurs (Configuration > Formateurs) - permet de gérer les adresses email des formateurs pour activer/désactiver les notifications
2. Tableau de bord des notifications (Configuration > Notifications) - historique complet des emails envoyés avec filtres et statistiques
3. Lien vers questionnaire de satisfaction automatiquement inclus dans l'email de remerciement post-formation
This commit is contained in:
Manus Sandbox
2025-12-15 15:27:30 -05:00
parent ea57cfc0cb
commit 5fcc98135c
12 changed files with 3281 additions and 7 deletions

View File

@@ -1,7 +1,82 @@
import { getDb } from "./db";
import { sequences, inscriptions, apprenants, formations, datesFormation, formateurs } from "../drizzle/schema";
import { sequences, inscriptions, apprenants, formations, datesFormation, formateurs, questionnaires, envoisQuestionnaires } from "../drizzle/schema";
import { eq, and, lte, sql } from "drizzle-orm";
import { sendRemerciementPostFormation } from "./emailService";
import { logNotification } from "./notificationLogsDb";
import crypto from "crypto";
/**
* Génère un token unique pour accéder au questionnaire
*/
function generateToken(): string {
return crypto.randomBytes(32).toString("hex");
}
/**
* Récupère ou crée un lien questionnaire pour un apprenant
*/
async function getOrCreateQuestionnaireLink(
apprenantId: number,
sequenceId: number
): Promise<string | null> {
const db = await getDb();
if (!db) return null;
try {
// Chercher un questionnaire de satisfaction actif
const [questionnaire] = await db
.select()
.from(questionnaires)
.where(
and(
eq(questionnaires.type, "satisfaction"),
eq(questionnaires.actif, true)
)
)
.limit(1);
if (!questionnaire) {
console.log("[Remerciements] Aucun questionnaire de satisfaction actif trouvé");
return null;
}
// Vérifier si un envoi existe déjà
const [envoiExistant] = await db
.select()
.from(envoisQuestionnaires)
.where(
and(
eq(envoisQuestionnaires.questionnaireId, questionnaire.id),
eq(envoisQuestionnaires.apprenantId, apprenantId),
eq(envoisQuestionnaires.sequenceId, sequenceId)
)
)
.limit(1);
if (envoiExistant) {
// Retourner le lien existant
const baseUrl = process.env.VITE_FRONTEND_URL || "http://localhost:3000";
return `${baseUrl}/questionnaire/${envoiExistant.token}`;
}
// Créer un nouvel envoi
const token = generateToken();
await db.insert(envoisQuestionnaires).values({
questionnaireId: questionnaire.id,
apprenantId,
sequenceId,
token,
dateEnvoi: new Date(),
dateReponse: null,
});
const baseUrl = process.env.VITE_FRONTEND_URL || "http://localhost:3000";
return `${baseUrl}/questionnaire/${token}`;
} catch (error) {
console.error("[Remerciements] Erreur lors de la création du lien questionnaire:", error);
return null;
}
}
/**
* Table pour suivre les remerciements déjà envoyés
@@ -111,6 +186,9 @@ async function processRemerciementSequence(
}
try {
// Générer le lien questionnaire
const lienQuestionnaire = await getOrCreateQuestionnaireLink(apprenant.id, sequence.id);
await sendRemerciementPostFormation({
apprenantEmail: apprenant.email,
apprenantNom: apprenant.nom,
@@ -119,12 +197,35 @@ async function processRemerciementSequence(
formationNom: formation.nom,
sequenceNom: sequence.nom,
formateurNom: formateur?.nom,
lienQuestionnaire: lienQuestionnaire || undefined,
});
// Logger la notification
await logNotification({
type: "remerciement",
sequenceId: sequence.id,
apprenantId: apprenant.id,
emailDestinataire: apprenant.email,
sujet: `Merci pour votre participation - ${formation.nom}`,
statut: "success",
metadata: { lienQuestionnaire },
});
marquerRemerciementEnvoye(inscription.id, sequence.id);
envoyesCount++;
console.log(`[Remerciements] Email envoyé à ${apprenant.email}`);
} catch (error) {
} catch (error: any) {
// Logger l'échec
await logNotification({
type: "remerciement",
sequenceId: sequence.id,
apprenantId: apprenant.id,
emailDestinataire: apprenant.email,
sujet: `Merci pour votre participation - ${formation.nom}`,
statut: "failed",
messageErreur: error.message,
});
errorsCount++;
console.error(`[Remerciements] Erreur envoi à ${apprenant.email}:`, error);
}
@@ -189,6 +290,9 @@ export async function envoyerRemerciementsSequence(sequenceId: number): Promise<
for (const { inscription, apprenant } of inscriptionsConfirmees) {
try {
// Générer le lien questionnaire
const lienQuestionnaire = await getOrCreateQuestionnaireLink(apprenant.id, sequenceId);
await sendRemerciementPostFormation({
apprenantEmail: apprenant.email,
apprenantNom: apprenant.nom,
@@ -197,9 +301,33 @@ export async function envoyerRemerciementsSequence(sequenceId: number): Promise<
formationNom: formation.nom,
sequenceNom: sequence.nom,
formateurNom: formateur?.nom,
lienQuestionnaire: lienQuestionnaire || undefined,
});
// Logger la notification
await logNotification({
type: "remerciement",
sequenceId,
apprenantId: apprenant.id,
emailDestinataire: apprenant.email,
sujet: `Merci pour votre participation - ${formation.nom}`,
statut: "success",
metadata: { lienQuestionnaire },
});
sent++;
} catch (error) {
} catch (error: any) {
// Logger l'échec
await logNotification({
type: "remerciement",
sequenceId,
apprenantId: apprenant.id,
emailDestinataire: apprenant.email,
sujet: `Merci pour votre participation - ${formation.nom}`,
statut: "failed",
messageErreur: error.message,
});
console.error(`[Remerciements] Erreur envoi à ${apprenant.email}:`, error);
failed++;
}