Checkpoint: Rappels automatiques J-7 et J-1 finalisés avec templates personnalisés Itinova

- Correction du bug d'insertion SQL des rappels (champ derniereExecution)
- Création de la table rappels et des fonctions CRUD
- Implémentation du scheduler automatique (vérification toutes les heures)
- Personnalisation des templates J-7 et J-1 avec logo et couleurs Itinova (Bleu #283581, Bleu clair #0578BE, Orange #E94E1B, Vert #45ac34)
- Style sobre et professionnel appliqué
- Variable {{datesHTML}} pour afficher toutes les dates de chaque séquence
This commit is contained in:
Manus Sandbox
2025-12-01 03:34:08 -05:00
parent 548dbc12b9
commit 1255124c3d
6 changed files with 235 additions and 0 deletions

View File

@@ -8,6 +8,7 @@ import { appRouter } from "../routers";
import { createContext } from "./context";
import localAuthRouter from "./localAuth";
import { serveStatic, setupVite } from "./vite";
import { initRappelScheduler } from "../rappelScheduler";
function isPortAvailable(port: number): Promise<boolean> {
return new Promise(resolve => {
@@ -62,6 +63,9 @@ async function startServer() {
server.listen(port, () => {
console.log(`Server running on http://localhost:${port}/`);
// Initialiser le scheduler de rappels automatiques
initRappelScheduler();
});
}

180
server/rappelScheduler.ts Normal file
View File

@@ -0,0 +1,180 @@
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";
/**
* 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()}`);
// Trouver toutes les séquences dont la première date correspond à la date cible
const sequencesConcernees = await db
.select({
sequence: sequences,
formation: formations,
dateFormation: datesFormation,
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(
eq(datesFormation.ordre, 1), // Première date seulement
gte(datesFormation.dateDebut, dateCible),
lte(datesFormation.dateDebut, dateCibleFin)
)
);
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
for (const { apprenant } of inscrits) {
if (!apprenant || !apprenant.email) 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 || "",
});
console.log(`[Rappels] Email J-7 envoyé à ${apprenant.email}`);
} 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 || "",
});
console.log(`[Rappels] Email J-1 envoyé à ${apprenant.email}`);
}
} catch (emailError) {
console.error(`[Rappels] Erreur lors de l'envoi à ${apprenant.email}:`, emailError);
}
}
}
// 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");
}