Checkpoint: Refonte majeure du système de rappels : sélection granulaire au niveau des dates de formation individuelles (dateFormationId) au lieu des séquences. Permet de choisir précisément quelles dates d'une séquence recevront un rappel. Interface hiérarchique avec séquences dépliables et checkboxes par date. Scheduler adapté pour traiter date par date.

This commit is contained in:
Manus
2026-01-08 08:53:41 -05:00
parent 6ee87f9b22
commit f331f9bb5f
8 changed files with 363 additions and 113 deletions

View File

@@ -39,91 +39,101 @@ export async function processRappelsAutomatiques() {
}
async function processRappel(rappel: any) {
console.log(`[Rappels] Traitement du rappel: ${rappel.nom} (J-${rappel.joursAvant})`);
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 (aujourd'hui + joursAvant)
// Calculer la date cible selon le timing (pré ou post-formation)
const dateCible = new Date();
dateCible.setDate(dateCible.getDate() + rappel.joursAvant);
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 séquences débutant le ${dateCible.toLocaleDateString()}`);
console.log(`[Rappels] Recherche des dates de formation le ${dateCible.toLocaleDateString()}`);
// Vérifier si ce rappel est associé à des séquences spécifiques
const sequencesAssociees = rappel.sequenceIds || [];
const filtreSequences = sequencesAssociees.length > 0;
// Vérifier si ce rappel est associé à des dates spécifiques
const datesAssociees = rappel.dateFormationIds || [];
const filtreDates = datesAssociees.length > 0;
console.log(`[Rappels] ${filtreSequences ? `Filtré sur ${sequencesAssociees.length} séquence(s) spécifique(s)` : 'Toutes les séquences'}`);
console.log(`[Rappels] ${filtreDates ? `Filtré sur ${datesAssociees.length} date(s) spécifique(s)` : 'Toutes les dates'}`);
// Récupérer toutes les séquences (ou uniquement celles associées)
let toutesSequences;
if (filtreSequences) {
// Seulement les séquences associées à ce rappel
toutesSequences = await db
// 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(sequences)
.from(datesFormation)
.leftJoin(sequences, eq(datesFormation.sequenceId, sequences.id))
.leftJoin(formations, eq(sequences.formationId, formations.id))
.leftJoin(formateurs, eq(sequences.formateurId, formateurs.id))
.where(inArray(sequences.id, sequencesAssociees));
.where(
and(
inArray(datesFormation.id, datesAssociees),
gte(datesFormation.dateDebut, dateCible),
lte(datesFormation.dateDebut, dateCibleFin)
)
);
} else {
// Toutes les séquences
toutesSequences = await db
// Toutes les dates correspondant à la date cible
datesConcernees = await db
.select({
dateFormation: datesFormation,
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)
.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.sequenceId, sequence.id),
eq(datesFormation.ordre, 1)
gte(datesFormation.dateDebut, dateCible),
lte(datesFormation.dateDebut, dateCibleFin)
)
)
.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)`);
console.log(`[Rappels] ${datesConcernees.length} date(s) de formation trouvée(s)`);
for (const { sequence, formation, formateur } of sequencesConcernees) {
// 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
const dates = await db
// 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))
@@ -174,7 +184,7 @@ async function processRappel(rappel: any) {
apprenantFonction: apprenant.fonction,
formationNom: formation.nom,
sequenceNom: sequence.nom,
dates: dates.map(d => ({
dates: toutesLesDates.map(d => ({
dateDebut: d.dateDebut,
dateFin: d.dateFin,
ordre: d.ordre
@@ -195,7 +205,7 @@ async function processRappel(rappel: any) {
apprenantFonction: apprenant.fonction,
formationNom: formation.nom,
sequenceNom: sequence.nom,
dates: dates.map(d => ({
dates: toutesLesDates.map(d => ({
dateDebut: d.dateDebut,
dateFin: d.dateFin,
ordre: d.ordre
@@ -217,7 +227,7 @@ async function processRappel(rappel: any) {
emailDestinataire: apprenant.email,
typeRappel: rappel.templateType,
statut: "success",
dateSequence: dates[0].dateDebut,
dateSequence: toutesLesDates[0].dateDebut,
});
nbEnvoyes++;
} catch (emailError: any) {
@@ -233,7 +243,7 @@ async function processRappel(rappel: any) {
typeRappel: rappel.templateType,
statut: "failed",
messageErreur: messageErreur,
dateSequence: dates[0].dateDebut,
dateSequence: toutesLesDates[0].dateDebut,
});
nbEchecs++;
echecs.push({ email: apprenant.email, erreur: messageErreur });