import { getDb } from "./db"; 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 { 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 */ const remerciementsEnvoyes = new Map(); /** * Vérifie si un remerciement a déjà été envoyé pour une inscription */ function remerciementDejaEnvoye(inscriptionId: number, sequenceId: number): boolean { const key = `${inscriptionId}-${sequenceId}`; return remerciementsEnvoyes.has(key); } /** * Marque un remerciement comme envoyé */ function marquerRemerciementEnvoye(inscriptionId: number, sequenceId: number): void { const key = `${inscriptionId}-${sequenceId}`; remerciementsEnvoyes.set(key, new Date()); } /** * Job automatique qui vérifie quotidiennement les séquences terminées * et envoie les emails de remerciement aux apprenants */ export async function processRemerciementsAutomatiques() { console.log("[Remerciements] Démarrage du traitement des remerciements post-formation..."); const db = await getDb(); if (!db) { console.error("[Remerciements] Base de données non disponible"); return; } try { const now = new Date(); const hier = new Date(); hier.setDate(hier.getDate() - 1); hier.setHours(23, 59, 59, 999); // Récupérer les séquences dont la dernière date est passée (terminées hier ou avant) const sequencesTerminees = await db .select({ sequence: sequences, formation: formations, formateur: formateurs, derniereDate: sql`MAX(${datesFormation.dateFin})`.as('derniereDate'), }) .from(sequences) .innerJoin(formations, eq(sequences.formationId, formations.id)) .leftJoin(formateurs, eq(sequences.formateurId, formateurs.id)) .innerJoin(datesFormation, eq(sequences.id, datesFormation.sequenceId)) .where(eq(sequences.statut, 'ouverte')) // Séquences encore ouvertes (pas encore marquées terminées) .groupBy(sequences.id, formations.id, formateurs.id) .having(lte(sql`MAX(${datesFormation.dateFin})`, hier)); console.log(`[Remerciements] ${sequencesTerminees.length} séquence(s) terminée(s) trouvée(s)`); for (const { sequence, formation, formateur, derniereDate } of sequencesTerminees) { await processRemerciementSequence(sequence, formation, formateur, derniereDate); } console.log("[Remerciements] Traitement terminé"); } catch (error) { console.error("[Remerciements] Erreur lors du traitement des remerciements:", error); } } async function processRemerciementSequence( sequence: any, formation: any, formateur: any | null, derniereDate: Date ) { console.log(`[Remerciements] Traitement de la séquence: ${sequence.nom} (terminée le ${derniereDate})`); const db = await getDb(); if (!db) return; try { // Récupérer les inscriptions confirmées pour cette séquence const inscriptionsConfirmees = await db .select({ inscription: inscriptions, apprenant: apprenants, }) .from(inscriptions) .innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)) .where( and( eq(inscriptions.sequenceId, sequence.id), eq(inscriptions.statut, 'confirmee') ) ); console.log(`[Remerciements] ${inscriptionsConfirmees.length} inscription(s) confirmée(s) pour ${sequence.nom}`); let envoyesCount = 0; let errorsCount = 0; for (const { inscription, apprenant } of inscriptionsConfirmees) { // Vérifier si le remerciement a déjà été envoyé if (remerciementDejaEnvoye(inscription.id, sequence.id)) { console.log(`[Remerciements] Remerciement déjà envoyé pour ${apprenant.email}`); continue; } try { // Générer le lien questionnaire const lienQuestionnaire = await getOrCreateQuestionnaireLink(apprenant.id, sequence.id); await sendRemerciementPostFormation({ apprenantEmail: apprenant.email, apprenantNom: apprenant.nom, apprenantPrenom: apprenant.prenom, apprenantFonction: apprenant.fonction, 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: 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); } } console.log(`[Remerciements] Séquence ${sequence.nom}: ${envoyesCount} envoyé(s), ${errorsCount} erreur(s)`); // Optionnel: Marquer la séquence comme terminée // await db.update(sequences).set({ statut: 'terminee' }).where(eq(sequences.id, sequence.id)); } catch (error) { console.error(`[Remerciements] Erreur pour la séquence ${sequence.nom}:`, error); } } /** * Envoie manuellement un email de remerciement pour une séquence spécifique * Utilisé par l'interface admin pour envoyer les remerciements à la demande */ export async function envoyerRemerciementsSequence(sequenceId: number): Promise<{ sent: number; failed: number }> { const db = await getDb(); if (!db) { throw new Error("Base de données non disponible"); } // Récupérer la séquence avec formation et formateur const sequenceData = await db .select({ sequence: sequences, formation: formations, formateur: formateurs, }) .from(sequences) .innerJoin(formations, eq(sequences.formationId, formations.id)) .leftJoin(formateurs, eq(sequences.formateurId, formateurs.id)) .where(eq(sequences.id, sequenceId)) .limit(1); if (sequenceData.length === 0) { throw new Error("Séquence introuvable"); } const { sequence, formation, formateur } = sequenceData[0]; // Récupérer les inscriptions confirmées const inscriptionsConfirmees = await db .select({ inscription: inscriptions, apprenant: apprenants, }) .from(inscriptions) .innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)) .where( and( eq(inscriptions.sequenceId, sequenceId), eq(inscriptions.statut, 'confirmee') ) ); let sent = 0; let failed = 0; 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, apprenantPrenom: apprenant.prenom, apprenantFonction: apprenant.fonction, 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: 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++; } } return { sent, failed }; }