Checkpoint: Ajout de 3 améliorations pour le système de notifications :

1. Interface de gestion des emails formateurs (Configuration > Formateurs) - permet de gérer les adresses email des formateurs pour activer/désactiver les notifications
2. Tableau de bord des notifications (Configuration > Notifications) - historique complet des emails envoyés avec filtres et statistiques
3. Lien vers questionnaire de satisfaction automatiquement inclus dans l'email de remerciement post-formation
This commit is contained in:
Manus Sandbox
2025-12-15 15:27:30 -05:00
parent ea57cfc0cb
commit 5fcc98135c
12 changed files with 3281 additions and 7 deletions

View File

@@ -9,6 +9,7 @@ import * as analyticsDb from "./analyticsDb";
import { generateAnalyticsExcel, generateAnalyticsPDF } from "./analyticsExportService";
import { TRPCError } from "@trpc/server";
import { sendInscriptionConfirmation, sendGroupEmail, sendNotificationFormateurNouvelleInscription, sendNotificationFormateurAnnulation, sendAlerteCapaciteAtteinte, sendNotificationPlaceDisponible, sendRemerciementPostFormation } from "./emailService";
import { logNotification } from "./notificationLogsDb";
import { generateExcelExport, generatePDFExport, generateFeuillePresence } from "./exportService";
import { parseExcelFile, validateImportData, importToDatabase } from "./importExcel";
@@ -226,6 +227,7 @@ export const appRouter = router({
create: adminProcedure.input(z.object({
nom: z.string().min(1),
email: z.string().email().optional(),
})).mutation(async ({ input }) => {
await db.createFormateur(input);
return { success: true };
@@ -234,6 +236,7 @@ export const appRouter = router({
update: adminProcedure.input(z.object({
id: z.number(),
nom: z.string().min(1).optional(),
email: z.string().email().nullable().optional(),
})).mutation(async ({ input }) => {
const { id, ...data } = input;
await db.updateFormateur(id, data);
@@ -487,8 +490,29 @@ export const appRouter = router({
ordre: d.ordre,
})),
});
// Logger la notification
await logNotification({
type: "notification_formateur_inscription",
sequenceId: input.sequenceId,
apprenantId: inscriptionApprenant.id,
formateurId: formateur.id,
emailDestinataire: formateur.email,
sujet: `Nouvelle inscription - ${inscriptionFormation.nom}`,
statut: "success",
});
console.log(`[Notification] Email envoyé au formateur ${formateur.email} pour nouvelle inscription`);
} catch (e) {
} catch (e: any) {
// Logger l'échec
await logNotification({
type: "notification_formateur_inscription",
sequenceId: input.sequenceId,
apprenantId: inscriptionApprenant.id,
formateurId: formateur.id,
emailDestinataire: formateur.email,
sujet: `Nouvelle inscription - ${inscriptionFormation.nom}`,
statut: "failed",
messageErreur: e.message,
});
console.error(`[Notification] Erreur envoi email formateur:`, e);
}
}
@@ -582,8 +606,29 @@ export const appRouter = router({
nbInscrits: nbInscritsApres,
capaciteMax: sequence.capaciteMax,
});
// Logger la notification
await logNotification({
type: "notification_formateur_annulation",
sequenceId: input.sequenceId,
apprenantId: apprenant.id,
formateurId: formateur.id,
emailDestinataire: formateur.email,
sujet: `Annulation d'inscription - ${formation.nom}`,
statut: "success",
});
console.log(`[Notification] Email d'annulation envoyé au formateur ${formateur.email}`);
} catch (e) {
} catch (e: any) {
// Logger l'échec
await logNotification({
type: "notification_formateur_annulation",
sequenceId: input.sequenceId,
apprenantId: apprenant.id,
formateurId: formateur.id,
emailDestinataire: formateur.email,
sujet: `Annulation d'inscription - ${formation.nom}`,
statut: "failed",
messageErreur: e.message,
});
console.error(`[Notification] Erreur envoi email formateur:`, e);
}
}
@@ -607,8 +652,27 @@ export const appRouter = router({
positionListeAttente: 1,
delaiReponse: 48,
});
// Logger la notification
await logNotification({
type: "notification_liste_attente",
sequenceId: input.sequenceId,
apprenantId: premierEnAttente.apprenant.id,
emailDestinataire: premierEnAttente.apprenant.email,
sujet: `Place disponible - ${formation.nom}`,
statut: "success",
});
console.log(`[Notification] Email place disponible envoyé à ${premierEnAttente.apprenant.email}`);
} catch (e) {
} catch (e: any) {
// Logger l'échec
await logNotification({
type: "notification_liste_attente",
sequenceId: input.sequenceId,
apprenantId: premierEnAttente.apprenant.id,
emailDestinataire: premierEnAttente.apprenant.email,
sujet: `Place disponible - ${formation.nom}`,
statut: "failed",
messageErreur: e.message,
});
console.error(`[Notification] Erreur envoi email liste d'attente:`, e);
}
}
@@ -1631,6 +1695,46 @@ export const appRouter = router({
await processRemerciementsAutomatiques();
return { success: true };
}),
// Historique des notifications
historique: adminProcedure
.input(z.object({
type: z.enum(["remerciement", "notification_formateur_inscription", "notification_formateur_annulation", "alerte_capacite", "notification_liste_attente"]).optional(),
dateDebut: z.string().optional(),
dateFin: z.string().optional(),
statut: z.enum(["success", "failed"]).optional(),
email: z.string().optional(),
sequenceId: z.number().optional(),
limit: z.number().default(50),
offset: z.number().default(0),
}))
.query(async ({ input }) => {
const { getNotificationLogs } = await import("./notificationLogsDb");
return getNotificationLogs({
type: input.type,
dateDebut: input.dateDebut ? new Date(input.dateDebut) : undefined,
dateFin: input.dateFin ? new Date(input.dateFin) : undefined,
statut: input.statut,
email: input.email,
sequenceId: input.sequenceId,
limit: input.limit,
offset: input.offset,
});
}),
// Statistiques des notifications
statistiques: adminProcedure
.input(z.object({
dateDebut: z.string().optional(),
dateFin: z.string().optional(),
}))
.query(async ({ input }) => {
const { getNotificationStats } = await import("./notificationLogsDb");
return getNotificationStats({
dateDebut: input.dateDebut ? new Date(input.dateDebut) : undefined,
dateFin: input.dateFin ? new Date(input.dateFin) : undefined,
});
}),
}),
});