Files
formation-manager-itinova/server/rappelDb.ts
Manus 8f21fdd276 Checkpoint: Checkpoint saved: Correction complète de l'enregistrement des logs de rappels :
**Problèmes corrigés :**
1. Type de rappel erroné : utilisait typeRappel au lieu de type, affichait le mauvais template (J-1 au lieu de J+2)
2. Statut erroné : utilisait "success"/"failed" (anglais) au lieu de "succes"/"echec" (français)
3. Email erroné : utilisait emailDestinataire au lieu de email
4. Champs obsolètes : supprimé dateSequence qui n'existe plus dans le schéma

**Fichiers modifiés :**
- server/rappelDb.ts : Réécriture complète avec les bons noms de colonnes (email, type, statut, typeEnvoi)
- server/rappelScheduler.ts : Correction des appels à logRappelEnvoye avec les bons paramètres
- server/routers.ts : Correction de l'enum du statut dans la procédure historique (succes/echec)

**Résultat :**
- Les logs de rappels enregistrent maintenant correctement le type de rappel (J+2, J-7, etc.)
- Le statut est correctement enregistré en français (succes/echec)
- L'email du destinataire est correctement enregistré
- Les tests de rappel sont distingués des envois automatiques via typeEnvoi

Déployé sur le VPS de production (https://formations.itinova.org)
2026-01-16 06:33:12 -05:00

330 lines
8.6 KiB
TypeScript

import { getDb } from "./db";
import { logsRappels, InsertLogRappel, LogRappel } from "../drizzle/schema";
import { eq, and } from "drizzle-orm";
/**
* Enregistre l'envoi d'un rappel dans les logs
*/
export async function logRappelEnvoye(data: {
rappelId: number;
sequenceId: number;
apprenantId: number;
email: string;
type: string;
typeEnvoi?: 'automatique' | 'test';
statut: "succes" | "echec";
messageErreur?: string;
}): Promise<void> {
const db = await getDb();
if (!db) {
console.warn("[LogsRappels] Base de données non disponible");
return;
}
try {
await db.insert(logsRappels).values({
rappelId: data.rappelId,
sequenceId: data.sequenceId,
apprenantId: data.apprenantId,
email: data.email,
type: data.type as any,
typeEnvoi: data.typeEnvoi || 'automatique',
dateEnvoi: new Date(),
statut: data.statut,
messageErreur: data.messageErreur || null,
});
} catch (error) {
console.error("[LogsRappels] Erreur lors de l'enregistrement du log:", error);
}
}
/**
* Vérifie si un rappel a déjà été envoyé à un apprenant pour une séquence donnée
*/
export async function rappelDejaEnvoye(
rappelId: number,
sequenceId: number,
apprenantId: number
): Promise<boolean> {
const db = await getDb();
if (!db) {
console.warn("[LogsRappels] Base de données non disponible");
return false;
}
try {
const logs = await db
.select()
.from(logsRappels)
.where(
and(
eq(logsRappels.rappelId, rappelId),
eq(logsRappels.sequenceId, sequenceId),
eq(logsRappels.apprenantId, apprenantId),
eq(logsRappels.statut, "succes"),
eq(logsRappels.typeEnvoi, "automatique") // Ne pas compter les tests
)
)
.limit(1);
return logs.length > 0;
} catch (error) {
console.error("[LogsRappels] Erreur lors de la vérification des doublons:", error);
return false;
}
}
/**
* Récupère tous les logs de rappels pour une séquence donnée
*/
export async function getLogsRappelsBySequence(sequenceId: number): Promise<LogRappel[]> {
const db = await getDb();
if (!db) return [];
try {
return await db
.select()
.from(logsRappels)
.where(eq(logsRappels.sequenceId, sequenceId))
.orderBy(logsRappels.dateEnvoi);
} catch (error) {
console.error("[LogsRappels] Erreur lors de la récupération des logs:", error);
return [];
}
}
/**
* Récupère tous les logs de rappels échoués récents (dernières 24h)
*/
export async function getLogsRappelsEchoues(): Promise<LogRappel[]> {
const db = await getDb();
if (!db) return [];
try {
const hier = new Date();
hier.setDate(hier.getDate() - 1);
return await db
.select()
.from(logsRappels)
.where(
and(
eq(logsRappels.statut, "echec"),
// Note: Drizzle ne supporte pas directement les comparaisons de dates
// On récupère tous les échecs et on filtre en JS
)
)
.orderBy(logsRappels.dateEnvoi);
} catch (error) {
console.error("[LogsRappels] Erreur lors de la récupération des échecs:", error);
return [];
}
}
/**
* Compte le nombre de rappels envoyés avec succès pour une séquence
*/
export async function countRappelsEnvoyesSequence(sequenceId: number): Promise<number> {
const db = await getDb();
if (!db) return 0;
try {
const logs = await db
.select()
.from(logsRappels)
.where(
and(
eq(logsRappels.sequenceId, sequenceId),
eq(logsRappels.statut, "succes")
)
);
return logs.length;
} catch (error) {
console.error("[LogsRappels] Erreur lors du comptage:", error);
return 0;
}
}
/**
* Récupère les logs de rappels avec filtres
*/
export async function getLogsRappelsWithFilters(filters: {
dateDebut?: Date;
dateFin?: Date;
sequenceId?: number;
statut?: "succes" | "echec";
email?: string;
limit?: number;
offset?: number;
}): Promise<LogRappel[]> {
const db = await getDb();
if (!db) return [];
try {
let query = db.select().from(logsRappels);
const conditions = [];
if (filters.sequenceId) {
conditions.push(eq(logsRappels.sequenceId, filters.sequenceId));
}
if (filters.statut) {
conditions.push(eq(logsRappels.statut, filters.statut));
}
if (filters.email) {
// Note: Drizzle ne supporte pas LIKE directement, on filtre en JS
}
if (conditions.length > 0) {
query = query.where(and(...conditions)) as any;
}
let logs = await query.orderBy(logsRappels.dateEnvoi).limit(filters.limit || 100).offset(filters.offset || 0);
// Filtrer par email si nécessaire
if (filters.email) {
logs = logs.filter(log => log.email.toLowerCase().includes(filters.email!.toLowerCase()));
}
// Filtrer par date si nécessaire
if (filters.dateDebut) {
logs = logs.filter(log => new Date(log.dateEnvoi) >= filters.dateDebut!);
}
if (filters.dateFin) {
logs = logs.filter(log => new Date(log.dateEnvoi) <= filters.dateFin!);
}
return logs;
} catch (error) {
console.error("[LogsRappels] Erreur lors de la récupération avec filtres:", error);
return [];
}
}
/**
* Calcule les statistiques des rappels
*/
export async function getStatsRappels(): Promise<{
totalEnvoyes: number;
totalSucces: number;
totalEchecs: number;
tauxSucces: number;
emailsProblematiques: Array<{ email: string; nbEchecs: number }>;
}> {
const db = await getDb();
if (!db) {
return {
totalEnvoyes: 0,
totalSucces: 0,
totalEchecs: 0,
tauxSucces: 0,
emailsProblematiques: [],
};
}
try {
const logs = await db.select().from(logsRappels);
const totalEnvoyes = logs.length;
const totalSucces = logs.filter(l => l.statut === "succes").length;
const totalEchecs = logs.filter(l => l.statut === "echec").length;
const tauxSucces = totalEnvoyes > 0 ? (totalSucces / totalEnvoyes) * 100 : 0;
// Compter les échecs par email
const echecsParEmail = new Map<string, number>();
logs.filter(l => l.statut === "echec").forEach(log => {
const count = echecsParEmail.get(log.email) || 0;
echecsParEmail.set(log.email, count + 1);
});
const emailsProblematiques = Array.from(echecsParEmail.entries())
.map(([email, nbEchecs]) => ({ email, nbEchecs }))
.sort((a, b) => b.nbEchecs - a.nbEchecs)
.slice(0, 10); // Top 10
return {
totalEnvoyes,
totalSucces,
totalEchecs,
tauxSucces,
emailsProblematiques,
};
} catch (error) {
console.error("[LogsRappels] Erreur lors du calcul des statistiques:", error);
return {
totalEnvoyes: 0,
totalSucces: 0,
totalEchecs: 0,
tauxSucces: 0,
emailsProblematiques: [],
};
}
}
/**
* Récupère l'évolution des envois par jour
*/
export async function getEvolutionEnvois(): Promise<Array<{
date: string;
nbEnvoyes: number;
nbSucces: number;
nbEchecs: number;
}>> {
const db = await getDb();
if (!db) return [];
try {
const logs = await db.select().from(logsRappels).orderBy(logsRappels.dateEnvoi);
// Grouper par jour
const parJour = new Map<string, { nbEnvoyes: number; nbSucces: number; nbEchecs: number }>();
logs.forEach(log => {
const dateStr = new Date(log.dateEnvoi).toISOString().split('T')[0];
const stats = parJour.get(dateStr) || { nbEnvoyes: 0, nbSucces: 0, nbEchecs: 0 };
stats.nbEnvoyes++;
if (log.statut === "succes") stats.nbSucces++;
else stats.nbEchecs++;
parJour.set(dateStr, stats);
});
return Array.from(parJour.entries())
.map(([date, stats]) => ({ date, ...stats }))
.sort((a, b) => a.date.localeCompare(b.date));
} catch (error) {
console.error("[LogsRappels] Erreur lors de la récupération de l'évolution:", error);
return [];
}
}
/**
* Récupère les logs échoués à réessayer
*/
export async function getLogsAReessayer(): Promise<LogRappel[]> {
const db = await getDb();
if (!db) return [];
try {
const maintenant = new Date();
const logs = await db
.select()
.from(logsRappels)
.where(
and(
eq(logsRappels.statut, "echec"),
// Limiter à 3 tentatives maximum
)
);
// Filtrer en JS pour les conditions complexes
return logs.filter(log => {
if (log.nbTentatives >= 3) return false;
if (!log.prochainEssai) return true; // Premier essai
return new Date(log.prochainEssai) <= maintenant;
});
} catch (error) {
console.error("[LogsRappels] Erreur lors de la récupération des logs à réessayer:", error);
return [];
}
}