- Renommage questionnaire.nom → questionnaire.titre dans QuestionnaireReponse.tsx, AdminQuestionnaireEdit.tsx, questionnaireScheduler.ts, questionnaireExport.ts - Correction des requêtes SQL dans questionnaireDb.ts (suppression de la condition complete inexistante) - Ajout des colonnes valeurMin et valeurMax dans la table questions - Renommage de la colonne nom en titre dans la table questionnaires
352 lines
11 KiB
TypeScript
352 lines
11 KiB
TypeScript
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 };
|
|
}
|