\n \n
{
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();
});
}
diff --git a/server/rappelScheduler.ts b/server/rappelScheduler.ts
new file mode 100644
index 0000000..7d3e393
--- /dev/null
+++ b/server/rappelScheduler.ts
@@ -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");
+}
diff --git a/todo.md b/todo.md
index 7fba543..9d173fe 100644
--- a/todo.md
+++ b/todo.md
@@ -903,3 +903,17 @@
- [x] Ajouter les types J-7 et J-1 dans l'interface de gestion des rappels
- [ ] Corriger le bug d'insertion SQL (erreur sur derniereExecution)
- [ ] Tester la création de rappels J-7 et J-1
+
+## Finalisation des rappels automatiques
+
+- [x] Corriger le bug d'insertion SQL (champ derniereExecution)
+- [x] Tester la création de rappels J-7 et J-1
+- [x] Créer un job automatique qui vérifie quotidiennement les séquences à venir
+- [x] Implémenter la logique d'envoi automatique des rappels selon les règles configurées
+- [x] Tester l'envoi automatique en conditions réelles (scheduler actif, vérification toutes les heures)
+
+## Personnalisation des templates d'emails
+- [x] Ajouter le logo Itinova dans les templates J-7 et J-1
+- [x] Ajuster les couleurs de l'en-tête selon la charte graphique Itinova (Bleu #283581, Bleu clair #0578BE, Orange #E94E1B, Vert #45ac34)
+- [x] Appliquer un style sobre et professionnelt liens
+- [x] Tester le rendu des templates personnalisés