import { getDb } from "./db"; import { rappels, sequences, inscriptions, apprenants, formations, datesFormation, formateurs } from "../drizzle/schema"; import { eq, and, gte, lte, sql } from "drizzle-orm"; import { sendRappelJ7Email, sendRappelJ1Email } from "./emailService"; import { logRappelEnvoye, rappelDejaEnvoye } from "./rappelDb"; import { notifyOwner } from "./_core/notification"; /** * Job automatique qui vérifie quotidiennement les séquences à venir * et envoie les rappels J-7 et J-1 aux apprenants inscrits */ export async function processRappelsAutomatiques() { console.log("[Rappels] Démarrage du traitement des rappels automatiques..."); const db = await getDb(); if (!db) { console.error("[Rappels] Base de données non disponible"); return; } try { // Récupérer tous les rappels actifs const rappelsActifs = await db .select() .from(rappels) .where(eq(rappels.actif, true)); console.log(`[Rappels] ${rappelsActifs.length} rappel(s) actif(s) trouvé(s)`); for (const rappel of rappelsActifs) { await processRappel(rappel); } console.log("[Rappels] Traitement terminé"); } catch (error) { console.error("[Rappels] Erreur lors du traitement des rappels:", error); } } async function processRappel(rappel: any) { console.log(`[Rappels] Traitement du rappel: ${rappel.nom} (J-${rappel.joursAvant})`); const db = await getDb(); if (!db) return; try { // Calculer la date cible (aujourd'hui + joursAvant) const dateCible = new Date(); dateCible.setDate(dateCible.getDate() + rappel.joursAvant); dateCible.setHours(0, 0, 0, 0); const dateCibleFin = new Date(dateCible); dateCibleFin.setHours(23, 59, 59, 999); console.log(`[Rappels] Recherche des séquences débutant le ${dateCible.toLocaleDateString()}`); // CORRECTION DU BUG: Récupérer d'abord toutes les séquences const toutesSequences = await db .select({ sequence: sequences, formation: formations, formateur: formateurs, }) .from(sequences) .leftJoin(formations, eq(sequences.formationId, formations.id)) .leftJoin(formateurs, eq(sequences.formateurId, formateurs.id)); // Filtrer les séquences dont la première date correspond à la date cible const sequencesConcernees = []; for (const { sequence, formation, formateur } of toutesSequences) { if (!sequence || !formation) continue; // Récupérer la première date de cette séquence const premiereDates = await db .select() .from(datesFormation) .where( and( eq(datesFormation.sequenceId, sequence.id), eq(datesFormation.ordre, 1) ) ) .limit(1); if (premiereDates.length === 0) continue; const premiereDate = premiereDates[0]; const dateDebut = new Date(premiereDate.dateDebut); dateDebut.setHours(0, 0, 0, 0); // Vérifier si cette première date correspond à la date cible if (dateDebut >= dateCible && dateDebut <= dateCibleFin) { sequencesConcernees.push({ sequence, formation, formateur }); } } console.log(`[Rappels] ${sequencesConcernees.length} séquence(s) trouvée(s)`); for (const { sequence, formation, formateur } of sequencesConcernees) { if (!sequence || !formation) continue; // Récupérer toutes les dates de cette séquence const dates = await db .select() .from(datesFormation) .where(eq(datesFormation.sequenceId, sequence.id)) .orderBy(datesFormation.ordre); const formateurNom = formateur?.nom || ""; // Récupérer tous les apprenants inscrits à cette séquence const inscrits = await db .select({ inscription: inscriptions, apprenant: apprenants, }) .from(inscriptions) .leftJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id)) .where( and( eq(inscriptions.sequenceId, sequence.id), eq(inscriptions.statut, "confirmee") ) ); console.log(`[Rappels] ${inscrits.length} apprenant(s) inscrit(s) à la séquence ${sequence.nom}`); // Envoyer le rappel à chaque apprenant let nbEnvoyes = 0; let nbEchecs = 0; const echecs: Array<{ email: string; erreur: string }> = []; for (const { apprenant } of inscrits) { if (!apprenant || !apprenant.email) continue; // Vérifier si le rappel a déjà été envoyé const dejaEnvoye = await rappelDejaEnvoye(rappel.id, sequence.id, apprenant.id); if (dejaEnvoye) { console.log(`[Rappels] Rappel déjà envoyé à ${apprenant.email} pour la séquence ${sequence.nom} - ignoré`); continue; } try { // Choisir la fonction d'envoi selon le type de rappel if (rappel.templateType === "rappel") { // Rappel J-7 await sendRappelJ7Email({ apprenantEmail: apprenant.email, apprenantPrenom: apprenant.prenom, apprenantNom: apprenant.nom, apprenantFonction: apprenant.fonction, formationNom: formation.nom, sequenceNom: sequence.nom, dates: dates.map(d => ({ dateDebut: d.dateDebut, dateFin: d.dateFin, ordre: d.ordre })), lieu: sequence.lieu || "", formateur: formateurNom, }); console.log(`[Rappels] Email J-7 envoyé à ${apprenant.email} pour la séquence ${sequence.nom}`); } else if (rappel.templateType === "rappelJ1") { // Rappel J-1 await sendRappelJ1Email({ apprenantEmail: apprenant.email, apprenantPrenom: apprenant.prenom, apprenantNom: apprenant.nom, apprenantFonction: apprenant.fonction, formationNom: formation.nom, sequenceNom: sequence.nom, dates: dates.map(d => ({ dateDebut: d.dateDebut, dateFin: d.dateFin, ordre: d.ordre })), lieu: sequence.lieu || "", formateur: formateurNom, }); console.log(`[Rappels] Email J-1 envoyé à ${apprenant.email} pour la séquence ${sequence.nom}`); } // Logger le succès await logRappelEnvoye({ rappelId: rappel.id, sequenceId: sequence.id, apprenantId: apprenant.id, emailDestinataire: apprenant.email, typeRappel: rappel.templateType, statut: "success", dateSequence: dates[0].dateDebut, }); nbEnvoyes++; } catch (emailError: any) { const messageErreur = emailError?.message || String(emailError); console.error(`[Rappels] Erreur lors de l'envoi à ${apprenant.email}:`, emailError); // Logger l'échec await logRappelEnvoye({ rappelId: rappel.id, sequenceId: sequence.id, apprenantId: apprenant.id, emailDestinataire: apprenant.email, typeRappel: rappel.templateType, statut: "failed", messageErreur: messageErreur, dateSequence: dates[0].dateDebut, }); nbEchecs++; echecs.push({ email: apprenant.email, erreur: messageErreur }); } } // Notifier l'administrateur en cas d'échecs if (nbEchecs > 0) { const listeEchecs = echecs.map(e => `- ${e.email}: ${e.erreur}`).join("\n"); await notifyOwner({ title: `⚠️ Échecs d'envoi de rappels`, content: `${nbEchecs} rappel(s) n'ont pas pu être envoyés pour la séquence "${sequence.nom}" (${formation.nom}):\n\n${listeEchecs}\n\nRappel: ${rappel.nom} (J-${rappel.joursAvant})\nEnvois réussis: ${nbEnvoyes}`, }); } console.log(`[Rappels] Séquence ${sequence.nom}: ${nbEnvoyes} envoi(s) réussi(s), ${nbEchecs} échec(s)`); } // Mettre à jour la date de dernière exécution await db .update(rappels) .set({ derniereExecution: new Date() }) .where(eq(rappels.id, rappel.id)); } catch (error) { console.error(`[Rappels] Erreur lors du traitement du rappel ${rappel.nom}:`, error); } } /** * Initialise le scheduler pour exécuter les rappels automatiques * Vérifie toutes les heures s'il y a des rappels à envoyer */ export function initRappelScheduler() { console.log("[Rappels] Initialisation du scheduler de rappels automatiques"); // Exécuter immédiatement au démarrage processRappelsAutomatiques(); // Puis exécuter toutes les heures setInterval(() => { processRappelsAutomatiques(); }, 60 * 60 * 1000); // 1 heure en millisecondes console.log("[Rappels] Scheduler initialisé - vérification toutes les heures"); }