Checkpoint: Ajout du système de notifications enrichi comprenant : email de remerciement post-formation automatique, notifications aux formateurs (nouvelle inscription et annulation), alertes de capacité atteinte pour les admins, notifications aux apprenants en liste d'attente quand une place se libère, ajout du champ email pour les formateurs, et scheduler automatique pour les remerciements.

This commit is contained in:
Manus Sandbox
2025-12-15 14:59:59 -05:00
parent 8dcb2e76a0
commit ea57cfc0cb
10 changed files with 2503 additions and 2 deletions

View File

@@ -8,7 +8,7 @@ import * as db from "./db";
import * as analyticsDb from "./analyticsDb";
import { generateAnalyticsExcel, generateAnalyticsPDF } from "./analyticsExportService";
import { TRPCError } from "@trpc/server";
import { sendInscriptionConfirmation, sendGroupEmail } from "./emailService";
import { sendInscriptionConfirmation, sendGroupEmail, sendNotificationFormateurNouvelleInscription, sendNotificationFormateurAnnulation, sendAlerteCapaciteAtteinte, sendNotificationPlaceDisponible, sendRemerciementPostFormation } from "./emailService";
import { generateExcelExport, generatePDFExport, generateFeuillePresence } from "./exportService";
import { parseExcelFile, validateImportData, importToDatabase } from "./importExcel";
@@ -463,6 +463,70 @@ export const appRouter = router({
lieu: inscriptionSequence.lieu,
statut,
});
// Notification au formateur si présent
if (inscriptionSequence.formateurId) {
const formateur = await db.getFormateurById(inscriptionSequence.formateurId);
if (formateur && formateur.email) {
const nbInscritsActuel = await db.countInscriptionsBySequence(input.sequenceId, 'confirmee');
try {
await sendNotificationFormateurNouvelleInscription({
formateurEmail: formateur.email,
formateurNom: formateur.nom,
apprenantNom: inscriptionApprenant.nom,
apprenantPrenom: inscriptionApprenant.prenom,
apprenantFonction: inscriptionApprenant.fonction,
apprenantEtablissement: inscriptionApprenant.codeEtablissement,
formationNom: inscriptionFormation.nom,
sequenceNom: inscriptionSequence.nom,
nbInscrits: nbInscritsActuel,
capaciteMax: inscriptionSequence.capaciteMax,
dates: dates.map(d => ({
dateDebut: new Date(d.dateDebut),
dateFin: new Date(d.dateFin),
ordre: d.ordre,
})),
});
console.log(`[Notification] Email envoyé au formateur ${formateur.email} pour nouvelle inscription`);
} catch (e) {
console.error(`[Notification] Erreur envoi email formateur:`, e);
}
}
}
// Alerte capacité atteinte pour les admins
const nbInscritsApres = await db.countInscriptionsBySequence(input.sequenceId, 'confirmee');
if (nbInscritsApres >= inscriptionSequence.capaciteMax) {
try {
const admins = await db.getAdminUsers();
const adminEmails = admins.filter(a => a.email).map(a => a.email!);
if (adminEmails.length > 0) {
const nbListeAttente = await db.countInscriptionsBySequence(input.sequenceId, 'liste_attente');
let formateurNom: string | undefined;
if (inscriptionSequence.formateurId) {
const formateur = await db.getFormateurById(inscriptionSequence.formateurId);
formateurNom = formateur?.nom;
}
await sendAlerteCapaciteAtteinte({
adminEmails,
formationNom: inscriptionFormation.nom,
sequenceNom: inscriptionSequence.nom,
capaciteMax: inscriptionSequence.capaciteMax,
nbInscrits: nbInscritsApres,
nbListeAttente,
formateurNom,
dates: dates.map(d => ({
dateDebut: new Date(d.dateDebut),
dateFin: new Date(d.dateFin),
ordre: d.ordre,
})),
});
console.log(`[Notification] Alerte capacité atteinte envoyée à ${adminEmails.length} admin(s)`);
}
} catch (e) {
console.error(`[Notification] Erreur envoi alerte capacité:`, e);
}
}
}
return { success: true, statut };
@@ -493,8 +557,63 @@ export const appRouter = router({
throw new TRPCError({ code: 'NOT_FOUND', message: 'Inscription introuvable' });
}
// Récupérer les infos avant la mise à jour pour les notifications
const apprenant = await db.getApprenantById(input.apprenantId);
const formation = await db.getFormationById(sequence.formationId);
await db.updateInscription(inscription.id, { statut: 'annulee' });
// Notifications après désinscription
if (apprenant && formation) {
// Notification au formateur
if (sequence.formateurId) {
const formateur = await db.getFormateurById(sequence.formateurId);
if (formateur && formateur.email) {
const nbInscritsApres = await db.countInscriptionsBySequence(input.sequenceId, 'confirmee');
try {
await sendNotificationFormateurAnnulation({
formateurEmail: formateur.email,
formateurNom: formateur.nom,
apprenantNom: apprenant.nom,
apprenantPrenom: apprenant.prenom,
apprenantFonction: apprenant.fonction,
formationNom: formation.nom,
sequenceNom: sequence.nom,
nbInscrits: nbInscritsApres,
capaciteMax: sequence.capaciteMax,
});
console.log(`[Notification] Email d'annulation envoyé au formateur ${formateur.email}`);
} catch (e) {
console.error(`[Notification] Erreur envoi email formateur:`, e);
}
}
}
// Notifier le premier en liste d'attente qu'une place s'est libérée
const inscriptionsListeAttente = await db.getInscriptionsBySequence(input.sequenceId);
const premierEnAttente = inscriptionsListeAttente
.filter(i => i.inscription.statut === 'liste_attente')
.sort((a, b) => new Date(a.inscription.dateInscription).getTime() - new Date(b.inscription.dateInscription).getTime())[0];
if (premierEnAttente && premierEnAttente.apprenant) {
try {
await sendNotificationPlaceDisponible({
apprenantEmail: premierEnAttente.apprenant.email,
apprenantNom: premierEnAttente.apprenant.nom,
apprenantPrenom: premierEnAttente.apprenant.prenom,
apprenantFonction: premierEnAttente.apprenant.fonction,
formationNom: formation.nom,
sequenceNom: sequence.nom,
positionListeAttente: 1,
delaiReponse: 48,
});
console.log(`[Notification] Email place disponible envoyé à ${premierEnAttente.apprenant.email}`);
} catch (e) {
console.error(`[Notification] Erreur envoi email liste d'attente:`, e);
}
}
}
return { success: true };
}),
@@ -1496,6 +1615,23 @@ export const appRouter = router({
return getEtablissementDetail(input.codeEtablissement);
}),
}),
// ===== NOTIFICATIONS =====
notifications: router({
envoyerRemerciements: adminProcedure
.input(z.object({ sequenceId: z.number() }))
.mutation(async ({ input }) => {
const { envoyerRemerciementsSequence } = await import("./remerciementScheduler");
return envoyerRemerciementsSequence(input.sequenceId);
}),
envoyerRemerciementsAutomatiques: adminProcedure
.mutation(async () => {
const { processRemerciementsAutomatiques } = await import("./remerciementScheduler");
await processRemerciementsAutomatiques();
return { success: true };
}),
}),
});
export type AppRouter = typeof appRouter;