From 1255124c3d590169f61fb9561ae32b41d571ae14 Mon Sep 17 00:00:00 2001 From: Manus Sandbox Date: Mon, 1 Dec 2025 03:34:08 -0500 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Rappels=20automatiques=20J-7=20et?= =?UTF-8?q?=20J-1=20finalis=C3=A9s=20avec=20templates=20personnalis=C3=A9s?= =?UTF-8?q?=20Itinova?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .manus/db/db-query-1764576831815.json | 9 ++ .manus/db/db-query-1764576855484.json | 9 ++ .manus/db/db-query-1764577975507.json | 19 +++ server/_core/index.ts | 4 + server/rappelScheduler.ts | 180 ++++++++++++++++++++++++++ todo.md | 14 ++ 6 files changed, 235 insertions(+) create mode 100644 .manus/db/db-query-1764576831815.json create mode 100644 .manus/db/db-query-1764576855484.json create mode 100644 .manus/db/db-query-1764577975507.json create mode 100644 server/rappelScheduler.ts diff --git a/.manus/db/db-query-1764576831815.json b/.manus/db/db-query-1764576831815.json new file mode 100644 index 0000000..79b2f37 --- /dev/null +++ b/.manus/db/db-query-1764576831815.json @@ -0,0 +1,9 @@ +{ + "query": "DROP TABLE IF EXISTS rappels;", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute DROP TABLE IF EXISTS rappels;", + "rows": [], + "messages": [], + "stdout": "", + "stderr": "", + "execution_time_ms": 1191 +} \ No newline at end of file diff --git a/.manus/db/db-query-1764576855484.json b/.manus/db/db-query-1764576855484.json new file mode 100644 index 0000000..43d5580 --- /dev/null +++ b/.manus/db/db-query-1764576855484.json @@ -0,0 +1,9 @@ +{ + "query": "CREATE TABLE IF NOT EXISTS `rappels` (\n `id` int AUTO_INCREMENT NOT NULL,\n `nom` varchar(255) NOT NULL,\n `templateType` varchar(50) NOT NULL,\n `joursAvant` int NOT NULL,\n `heureEnvoi` varchar(5) NOT NULL DEFAULT '09:00',\n `actif` boolean NOT NULL DEFAULT true,\n `derniereExecution` timestamp NULL,\n `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updatedAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n CONSTRAINT `rappels_id` PRIMARY KEY(`id`)\n);", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute CREATE TABLE IF NOT EXISTS `rappels` (\n `id` int AUTO_INCREMENT NOT NULL,\n `nom` varchar(255) NOT NULL,\n `templateType` varchar(50) NOT NULL,\n `joursAvant` int NOT NULL,\n `heureEnvoi` varchar(5) NOT NULL DEFAULT '09:00',\n `actif` boolean NOT NULL DEFAULT true,\n `derniereExecution` timestamp NULL,\n `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updatedAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n CONSTRAINT `rappels_id` PRIMARY KEY(`id`)\n);", + "rows": [], + "messages": [], + "stdout": "", + "stderr": "", + "execution_time_ms": 188 +} \ No newline at end of file diff --git a/.manus/db/db-query-1764577975507.json b/.manus/db/db-query-1764577975507.json new file mode 100644 index 0000000..6214fc0 --- /dev/null +++ b/.manus/db/db-query-1764577975507.json @@ -0,0 +1,19 @@ +{ + "query": "SELECT type, name, LEFT(bodyContent, 200) as preview FROM emailTemplates WHERE type IN ('rappel', 'rappelJ1')", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute SELECT type, name, LEFT(bodyContent, 200) as preview FROM emailTemplates WHERE type IN ('rappel', 'rappelJ1')", + "rows": [ + { + "type": "rappel", + "name": "Rappel J-7", + "preview": "" + } + ], + "messages": [ + "
", + " ", + "
\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