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:
36
server/db.ts
36
server/db.ts
@@ -30,7 +30,7 @@ import {
|
||||
rappels,
|
||||
InsertRappel,
|
||||
Rappel,
|
||||
rappelSequences
|
||||
rappelDates
|
||||
} from "../drizzle/schema";
|
||||
import { ENV } from './_core/env';
|
||||
|
||||
@@ -972,18 +972,18 @@ export async function getRappels() {
|
||||
|
||||
const allRappels = await db.select().from(rappels).orderBy(rappels.joursAvant);
|
||||
|
||||
// Pour chaque rappel, récupérer les séquences associées
|
||||
const rappelsWithSequences = await Promise.all(
|
||||
// Pour chaque rappel, récupérer les dates de formation associées
|
||||
const rappelsWithDates = await Promise.all(
|
||||
allRappels.map(async (rappel) => {
|
||||
const sequenceIds = await getRappelSequences(rappel.id);
|
||||
const dateFormationIds = await getRappelDates(rappel.id);
|
||||
return {
|
||||
...rappel,
|
||||
sequenceIds,
|
||||
dateFormationIds,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return rappelsWithSequences;
|
||||
return rappelsWithDates;
|
||||
}
|
||||
|
||||
export async function getRappelById(id: number): Promise<Rappel | undefined> {
|
||||
@@ -1037,40 +1037,40 @@ export async function deleteRappel(id: number) {
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
// Supprimer d'abord les associations
|
||||
await db.delete(rappelSequences).where(eq(rappelSequences.rappelId, id));
|
||||
await db.delete(rappelDates).where(eq(rappelDates.rappelId, id));
|
||||
// Puis le rappel
|
||||
await db.delete(rappels).where(eq(rappels.id, id));
|
||||
}
|
||||
|
||||
// ==================== RAPPEL SEQUENCES ====================
|
||||
// ==================== RAPPEL DATES ====================
|
||||
|
||||
export async function createRappelSequences(rappelId: number, sequenceIds: number[]) {
|
||||
export async function createRappelDates(rappelId: number, dateFormationIds: number[]) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
if (sequenceIds.length === 0) return;
|
||||
if (dateFormationIds.length === 0) return;
|
||||
|
||||
const values = sequenceIds.map(sequenceId => ({
|
||||
const values = dateFormationIds.map(dateFormationId => ({
|
||||
rappelId,
|
||||
sequenceId,
|
||||
dateFormationId,
|
||||
}));
|
||||
|
||||
await db.insert(rappelSequences).values(values);
|
||||
await db.insert(rappelDates).values(values);
|
||||
}
|
||||
|
||||
export async function deleteRappelSequences(rappelId: number) {
|
||||
export async function deleteRappelDates(rappelId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
await db.delete(rappelSequences).where(eq(rappelSequences.rappelId, rappelId));
|
||||
await db.delete(rappelDates).where(eq(rappelDates.rappelId, rappelId));
|
||||
}
|
||||
|
||||
export async function getRappelSequences(rappelId: number): Promise<number[]> {
|
||||
export async function getRappelDates(rappelId: number): Promise<number[]> {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
|
||||
const results = await db.select().from(rappelSequences).where(eq(rappelSequences.rappelId, rappelId));
|
||||
return results.map(r => r.sequenceId);
|
||||
const results = await db.select().from(rappelDates).where(eq(rappelDates.rappelId, rappelId));
|
||||
return results.map(r => r.dateFormationId);
|
||||
}
|
||||
|
||||
export async function updateRappelExecution(id: number) {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -1070,7 +1070,7 @@ export const appRouter = router({
|
||||
joursAvant: z.number(),
|
||||
heureEnvoi: z.string().default("09:00"),
|
||||
actif: z.boolean(),
|
||||
sequenceIds: z.array(z.number()).optional(),
|
||||
dateFormationIds: z.array(z.number()).optional(),
|
||||
fichier: z.object({
|
||||
nomFichier: z.string(),
|
||||
urlFichier: z.string(),
|
||||
@@ -1080,14 +1080,14 @@ export const appRouter = router({
|
||||
}).nullable().optional(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
const { sequenceIds, ...rappelData } = input;
|
||||
const { dateFormationIds, ...rappelData } = input;
|
||||
const result: any = await db.createRappel(rappelData);
|
||||
|
||||
// Créer les associations rappel-séquence si des séquences sont spécifiées
|
||||
if (sequenceIds && sequenceIds.length > 0) {
|
||||
// Créer les associations rappel-date si des dates sont spécifiées
|
||||
if (dateFormationIds && dateFormationIds.length > 0) {
|
||||
const rappelId = result.insertId || result[0]?.insertId;
|
||||
if (rappelId) {
|
||||
await db.createRappelSequences(Number(rappelId), sequenceIds);
|
||||
await db.createRappelDates(Number(rappelId), dateFormationIds);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1103,7 +1103,7 @@ export const appRouter = router({
|
||||
joursAvant: z.number().optional(),
|
||||
heureEnvoi: z.string().optional(),
|
||||
actif: z.boolean().optional(),
|
||||
sequenceIds: z.array(z.number()).optional(),
|
||||
dateFormationIds: z.array(z.number()).optional(),
|
||||
fichier: z.object({
|
||||
nomFichier: z.string(),
|
||||
urlFichier: z.string(),
|
||||
@@ -1113,14 +1113,14 @@ export const appRouter = router({
|
||||
}).nullable().optional(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
const { id, sequenceIds, ...data } = input;
|
||||
const { id, dateFormationIds, ...data } = input;
|
||||
await db.updateRappel(id, data);
|
||||
|
||||
// Mettre à jour les associations rappel-séquence si spécifiées
|
||||
if (sequenceIds !== undefined) {
|
||||
await db.deleteRappelSequences(id);
|
||||
if (sequenceIds.length > 0) {
|
||||
await db.createRappelSequences(id, sequenceIds);
|
||||
// Mettre à jour les associations rappel-date si spécifiées
|
||||
if (dateFormationIds !== undefined) {
|
||||
await db.deleteRappelDates(id);
|
||||
if (dateFormationIds.length > 0) {
|
||||
await db.createRappelDates(id, dateFormationIds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user