**Problèmes corrigés :** 1. Type de rappel erroné : utilisait typeRappel au lieu de type, affichait le mauvais template (J-1 au lieu de J+2) 2. Statut erroné : utilisait "success"/"failed" (anglais) au lieu de "succes"/"echec" (français) 3. Email erroné : utilisait emailDestinataire au lieu de email 4. Champs obsolètes : supprimé dateSequence qui n'existe plus dans le schéma **Fichiers modifiés :** - server/rappelDb.ts : Réécriture complète avec les bons noms de colonnes (email, type, statut, typeEnvoi) - server/rappelScheduler.ts : Correction des appels à logRappelEnvoye avec les bons paramètres - server/routers.ts : Correction de l'enum du statut dans la procédure historique (succes/echec) **Résultat :** - Les logs de rappels enregistrent maintenant correctement le type de rappel (J+2, J-7, etc.) - Le statut est correctement enregistré en français (succes/echec) - L'email du destinataire est correctement enregistré - Les tests de rappel sont distingués des envois automatiques via typeEnvoi Déployé sur le VPS de production (https://formations.itinova.org)
295 lines
11 KiB
TypeScript
295 lines
11 KiB
TypeScript
import { getDb } from "./db";
|
|
import { rappels, sequences, inscriptions, apprenants, formations, datesFormation, formateurs } from "../drizzle/schema";
|
|
import { eq, and, gte, lte, sql, inArray } from "drizzle-orm";
|
|
import { sendRappelJ7Email, sendRappelJ1Email } from "./emailService";
|
|
import { logRappelEnvoye, rappelDejaEnvoye } from "./rappelDb";
|
|
import { notifyOwner } from "./_core/notification";
|
|
import { processRemerciementsAutomatiques } from "./remerciementScheduler";
|
|
|
|
/**
|
|
* 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} (${rappel.timing === 'pre_formation' ? 'J-' : 'J+'}${rappel.joursAvant})`);
|
|
|
|
const db = await getDb();
|
|
if (!db) return;
|
|
|
|
try {
|
|
// Calculer la date cible selon le timing (pré ou post-formation)
|
|
const dateCible = new Date();
|
|
if (rappel.timing === 'pre_formation') {
|
|
dateCible.setDate(dateCible.getDate() + rappel.joursAvant);
|
|
} else {
|
|
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 dates de formation le ${dateCible.toLocaleDateString()}`);
|
|
|
|
// Vérifier si ce rappel est associé à des dates spécifiques
|
|
const datesAssociees = rappel.dateFormationIds || [];
|
|
const filtreDates = datesAssociees.length > 0;
|
|
|
|
console.log(`[Rappels] ${filtreDates ? `Filtré sur ${datesAssociees.length} date(s) spécifique(s)` : 'Toutes les dates'}`);
|
|
|
|
// Récupérer les dates de formation concernées
|
|
let datesConcernees;
|
|
if (filtreDates) {
|
|
// Seulement les dates associées à ce rappel
|
|
datesConcernees = await db
|
|
.select({
|
|
dateFormation: datesFormation,
|
|
sequence: sequences,
|
|
formation: formations,
|
|
formateur: formateurs,
|
|
})
|
|
.from(datesFormation)
|
|
.leftJoin(sequences, eq(datesFormation.sequenceId, sequences.id))
|
|
.leftJoin(formations, eq(sequences.formationId, formations.id))
|
|
.leftJoin(formateurs, eq(sequences.formateurId, formateurs.id))
|
|
.where(
|
|
and(
|
|
inArray(datesFormation.id, datesAssociees),
|
|
gte(datesFormation.dateDebut, dateCible),
|
|
lte(datesFormation.dateDebut, dateCibleFin)
|
|
)
|
|
);
|
|
} else {
|
|
// Toutes les dates correspondant à la date cible
|
|
datesConcernees = await db
|
|
.select({
|
|
dateFormation: datesFormation,
|
|
sequence: sequences,
|
|
formation: formations,
|
|
formateur: formateurs,
|
|
})
|
|
.from(datesFormation)
|
|
.leftJoin(sequences, eq(datesFormation.sequenceId, sequences.id))
|
|
.leftJoin(formations, eq(sequences.formationId, formations.id))
|
|
.leftJoin(formateurs, eq(sequences.formateurId, formateurs.id))
|
|
.where(
|
|
and(
|
|
gte(datesFormation.dateDebut, dateCible),
|
|
lte(datesFormation.dateDebut, dateCibleFin)
|
|
)
|
|
);
|
|
}
|
|
|
|
console.log(`[Rappels] ${datesConcernees.length} date(s) de formation trouvée(s)`);
|
|
|
|
// Grouper les dates par séquence pour envoyer un seul email par apprenant
|
|
const sequencesMap = new Map<number, { sequence: any; formation: any; formateur: any; dates: any[] }>();
|
|
|
|
for (const { dateFormation, sequence, formation, formateur } of datesConcernees) {
|
|
if (!dateFormation || !sequence || !formation) continue;
|
|
|
|
if (!sequencesMap.has(sequence.id)) {
|
|
sequencesMap.set(sequence.id, {
|
|
sequence,
|
|
formation,
|
|
formateur,
|
|
dates: [],
|
|
});
|
|
}
|
|
sequencesMap.get(sequence.id)!.dates.push(dateFormation);
|
|
}
|
|
|
|
console.log(`[Rappels] ${sequencesMap.size} séquence(s) concernée(s)`);
|
|
|
|
for (const { sequence, formation, formateur, dates: datesRappel } of Array.from(sequencesMap.values())) {
|
|
if (!sequence || !formation) continue;
|
|
|
|
// Récupérer toutes les dates de cette séquence pour l'email
|
|
const toutesLesDates = 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: toutesLesDates.map(d => ({
|
|
dateDebut: d.dateDebut,
|
|
dateFin: d.dateFin,
|
|
ordre: d.ordre
|
|
})),
|
|
lieu: sequence.lieu || "",
|
|
formateur: formateurNom,
|
|
attachmentUrl: rappel.urlFichier || undefined,
|
|
attachmentFilename: rappel.nomFichier || undefined,
|
|
attachmentMimeType: rappel.typeFichier || undefined,
|
|
});
|
|
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: toutesLesDates.map(d => ({
|
|
dateDebut: d.dateDebut,
|
|
dateFin: d.dateFin,
|
|
ordre: d.ordre
|
|
})),
|
|
lieu: sequence.lieu || "",
|
|
formateur: formateurNom,
|
|
attachmentUrl: rappel.urlFichier || undefined,
|
|
attachmentFilename: rappel.nomFichier || undefined,
|
|
attachmentMimeType: rappel.typeFichier || undefined,
|
|
});
|
|
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,
|
|
email: apprenant.email,
|
|
type: rappel.templateType,
|
|
typeEnvoi: 'automatique',
|
|
statut: "succes",
|
|
});
|
|
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,
|
|
email: apprenant.email,
|
|
type: rappel.templateType,
|
|
typeEnvoi: 'automatique',
|
|
statut: "echec",
|
|
messageErreur: messageErreur,
|
|
});
|
|
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();
|
|
processRemerciementsAutomatiques();
|
|
|
|
// Puis exécuter toutes les heures
|
|
setInterval(() => {
|
|
processRappelsAutomatiques();
|
|
processRemerciementsAutomatiques();
|
|
}, 60 * 60 * 1000); // 1 heure en millisecondes
|
|
|
|
console.log("[Rappels] Scheduler initialisé - vérification toutes les heures (rappels + remerciements)");
|
|
}
|