Checkpoint: Correction du bug d'émargement formateur bloqué sur la saisie d'email

- Ajout de la détection automatique formateur/apprenant dans EmargementScan.tsx
- Nouvelle procédure tRPC getSequenceByToken pour récupérer les infos de séquence
- Interface adaptée selon le type d'utilisateur (apprenant ou formateur)
- Déploiement réussi sur le VPS de production

Le système détecte maintenant automatiquement si l'email correspond à un apprenant ou un formateur, et affiche l'interface appropriée pour chaque cas.
This commit is contained in:
Manus
2026-02-02 14:18:46 -05:00
parent e8410230a9
commit f9983adada
81 changed files with 3734 additions and 27745 deletions

View File

@@ -1,351 +0,0 @@
import { getDb } from "./db";
import { sequences, inscriptions, apprenants, formations, datesFormation, formateurs, questionnaires, envoisQuestionnaires, logsNotifications } from "../drizzle/schema";
import { eq, and, lte, sql } from "drizzle-orm";
import { sendRemerciementPostFormation } from "./emailService";
import { logNotification } from "./notificationLogsDb";
import crypto from "crypto";
/**
* Génère un token unique pour accéder au questionnaire
*/
function generateToken(): string {
return crypto.randomBytes(32).toString("hex");
}
/**
* Récupère ou crée un lien questionnaire pour un apprenant
*/
async function getOrCreateQuestionnaireLink(
apprenantId: number,
sequenceId: number
): Promise<string | null> {
const db = await getDb();
if (!db) return null;
try {
// Chercher un questionnaire de satisfaction actif
const [questionnaire] = await db
.select()
.from(questionnaires)
.where(
and(
eq(questionnaires.type, "satisfaction"),
eq(questionnaires.actif, true)
)
)
.limit(1);
if (!questionnaire) {
console.log("[Remerciements] Aucun questionnaire de satisfaction actif trouvé");
return null;
}
// Vérifier si un envoi existe déjà
const [envoiExistant] = await db
.select()
.from(envoisQuestionnaires)
.where(
and(
eq(envoisQuestionnaires.questionnaireId, questionnaire.id),
eq(envoisQuestionnaires.apprenantId, apprenantId),
eq(envoisQuestionnaires.sequenceId, sequenceId)
)
)
.limit(1);
if (envoiExistant) {
// Retourner le lien existant
const baseUrl = process.env.VITE_FRONTEND_URL || "http://localhost:3000";
return `${baseUrl}/questionnaire/${envoiExistant.token}`;
}
// Créer un nouvel envoi
const token = generateToken();
await db.insert(envoisQuestionnaires).values({
questionnaireId: questionnaire.id,
apprenantId,
sequenceId,
token,
dateEnvoi: new Date(),
dateReponse: null,
});
const baseUrl = process.env.VITE_FRONTEND_URL || "http://localhost:3000";
return `${baseUrl}/questionnaire/${token}`;
} catch (error) {
console.error("[Remerciements] Erreur lors de la création du lien questionnaire:", error);
return null;
}
}
/**
* Vérifie si un remerciement a déjà été envoyé pour une inscription en vérifiant dans la base de données
*/
async function remerciementDejaEnvoye(apprenantId: number, sequenceId: number): Promise<boolean> {
const db = await getDb();
if (!db) return false;
try {
const [existing] = await db
.select({ id: logsNotifications.id })
.from(logsNotifications)
.where(
and(
eq(logsNotifications.type, "remerciement"),
eq(logsNotifications.apprenantId, apprenantId),
eq(logsNotifications.sequenceId, sequenceId),
eq(logsNotifications.statut, "success")
)
)
.limit(1);
return !!existing;
} catch (error) {
console.error("[Remerciements] Erreur lors de la vérification du log:", error);
return false;
}
}
/**
* Job automatique qui vérifie quotidiennement les séquences terminées
* et envoie les emails de remerciement aux apprenants
*/
export async function processRemerciementsAutomatiques() {
console.log("[Remerciements] Démarrage du traitement des remerciements post-formation...");
const db = await getDb();
if (!db) {
console.error("[Remerciements] Base de données non disponible");
return;
}
try {
const now = new Date();
const hier = new Date();
hier.setDate(hier.getDate() - 1);
hier.setHours(23, 59, 59, 999);
// Récupérer les séquences dont la dernière date est passée (terminées hier ou avant)
const sequencesTerminees = await db
.select({
sequence: sequences,
formation: formations,
formateur: formateurs,
derniereDate: sql<Date>`MAX(${datesFormation.dateFin})`.as('derniereDate'),
})
.from(sequences)
.innerJoin(formations, eq(sequences.formationId, formations.id))
.leftJoin(formateurs, eq(sequences.formateurId, formateurs.id))
.innerJoin(datesFormation, eq(sequences.id, datesFormation.sequenceId))
.where(eq(sequences.statut, 'ouverte')) // Séquences encore ouvertes (pas encore marquées terminées)
.groupBy(sequences.id, formations.id, formateurs.id)
.having(lte(sql`MAX(${datesFormation.dateFin})`, hier));
console.log(`[Remerciements] ${sequencesTerminees.length} séquence(s) terminée(s) trouvée(s)`);
for (const { sequence, formation, formateur, derniereDate } of sequencesTerminees) {
await processRemerciementSequence(sequence, formation, formateur, derniereDate);
}
console.log("[Remerciements] Traitement terminé");
} catch (error) {
console.error("[Remerciements] Erreur lors du traitement des remerciements:", error);
}
}
async function processRemerciementSequence(
sequence: any,
formation: any,
formateur: any | null,
derniereDate: Date
) {
console.log(`[Remerciements] Traitement de la séquence: ${sequence.nom} (terminée le ${derniereDate})`);
const db = await getDb();
if (!db) return;
try {
// Récupérer les inscriptions confirmées pour cette séquence
const inscriptionsConfirmees = await db
.select({
inscription: inscriptions,
apprenant: apprenants,
})
.from(inscriptions)
.innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id))
.where(
and(
eq(inscriptions.sequenceId, sequence.id),
eq(inscriptions.statut, 'confirmee')
)
);
console.log(`[Remerciements] ${inscriptionsConfirmees.length} inscription(s) confirmée(s) pour ${sequence.nom}`);
let envoyesCount = 0;
let errorsCount = 0;
for (const { inscription, apprenant } of inscriptionsConfirmees) {
// Vérifier si le remerciement a déjà été envoyé (vérification en base de données)
const dejaEnvoye = await remerciementDejaEnvoye(apprenant.id, sequence.id);
if (dejaEnvoye) {
console.log(`[Remerciements] Remerciement déjà envoyé pour ${apprenant.email}`);
continue;
}
try {
// Générer le lien questionnaire
const lienQuestionnaire = await getOrCreateQuestionnaireLink(apprenant.id, sequence.id);
await sendRemerciementPostFormation({
apprenantEmail: apprenant.email,
apprenantNom: apprenant.nom,
apprenantPrenom: apprenant.prenom,
apprenantFonction: apprenant.fonction,
formationNom: formation.nom,
sequenceNom: sequence.nom,
formateurNom: formateur?.nom,
lienQuestionnaire: lienQuestionnaire || undefined,
});
// Logger la notification (sert aussi de marqueur pour éviter les doublons)
await logNotification({
type: "remerciement",
sequenceId: sequence.id,
apprenantId: apprenant.id,
emailDestinataire: apprenant.email,
sujet: `Merci pour votre participation - ${formation.nom}`,
statut: "success",
metadata: { lienQuestionnaire },
});
envoyesCount++;
console.log(`[Remerciements] Email envoyé à ${apprenant.email}`);
} catch (error: any) {
// Logger l'échec
await logNotification({
type: "remerciement",
sequenceId: sequence.id,
apprenantId: apprenant.id,
emailDestinataire: apprenant.email,
sujet: `Merci pour votre participation - ${formation.nom}`,
statut: "failed",
messageErreur: error.message,
});
errorsCount++;
console.error(`[Remerciements] Erreur envoi à ${apprenant.email}:`, error);
}
}
console.log(`[Remerciements] Séquence ${sequence.nom}: ${envoyesCount} envoyé(s), ${errorsCount} erreur(s)`);
// Optionnel: Marquer la séquence comme terminée
// await db.update(sequences).set({ statut: 'terminee' }).where(eq(sequences.id, sequence.id));
} catch (error) {
console.error(`[Remerciements] Erreur pour la séquence ${sequence.nom}:`, error);
}
}
/**
* Envoie manuellement un email de remerciement pour une séquence spécifique
* Utilisé par l'interface admin pour envoyer les remerciements à la demande
*/
export async function envoyerRemerciementsSequence(sequenceId: number): Promise<{ sent: number; failed: number }> {
const db = await getDb();
if (!db) {
throw new Error("Base de données non disponible");
}
// Récupérer la séquence avec formation et formateur
const sequenceData = await db
.select({
sequence: sequences,
formation: formations,
formateur: formateurs,
})
.from(sequences)
.innerJoin(formations, eq(sequences.formationId, formations.id))
.leftJoin(formateurs, eq(sequences.formateurId, formateurs.id))
.where(eq(sequences.id, sequenceId))
.limit(1);
if (sequenceData.length === 0) {
throw new Error("Séquence introuvable");
}
const { sequence, formation, formateur } = sequenceData[0];
// Récupérer les inscriptions confirmées
const inscriptionsConfirmees = await db
.select({
inscription: inscriptions,
apprenant: apprenants,
})
.from(inscriptions)
.innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id))
.where(
and(
eq(inscriptions.sequenceId, sequenceId),
eq(inscriptions.statut, 'confirmee')
)
);
let sent = 0;
let failed = 0;
for (const { inscription, apprenant } of inscriptionsConfirmees) {
// Vérifier si le remerciement a déjà été envoyé
const dejaEnvoye = await remerciementDejaEnvoye(apprenant.id, sequenceId);
if (dejaEnvoye) {
console.log(`[Remerciements] Remerciement déjà envoyé pour ${apprenant.email}, ignoré`);
continue;
}
try {
// Générer le lien questionnaire
const lienQuestionnaire = await getOrCreateQuestionnaireLink(apprenant.id, sequenceId);
await sendRemerciementPostFormation({
apprenantEmail: apprenant.email,
apprenantNom: apprenant.nom,
apprenantPrenom: apprenant.prenom,
apprenantFonction: apprenant.fonction,
formationNom: formation.nom,
sequenceNom: sequence.nom,
formateurNom: formateur?.nom,
lienQuestionnaire: lienQuestionnaire || undefined,
});
// Logger la notification
await logNotification({
type: "remerciement",
sequenceId,
apprenantId: apprenant.id,
emailDestinataire: apprenant.email,
sujet: `Merci pour votre participation - ${formation.nom}`,
statut: "success",
metadata: { lienQuestionnaire },
});
sent++;
} catch (error: any) {
// Logger l'échec
await logNotification({
type: "remerciement",
sequenceId,
apprenantId: apprenant.id,
emailDestinataire: apprenant.email,
sujet: `Merci pour votre participation - ${formation.nom}`,
statut: "failed",
messageErreur: error.message,
});
console.error(`[Remerciements] Erreur envoi à ${apprenant.email}:`, error);
failed++;
}
}
return { sent, failed };
}