Checkpoint: Tableau de bord personnalisé pour formateurs avec statistiques (total séquences, séquences à venir, total apprenants) et liste des prochaines interventions avec détails (dates, lieu, capacité). Redirection automatique des formateurs vers /formateur au lieu de /admin. Notifications automatiques par email pour les formateurs lors de nouvelles inscriptions, annulations, capacité maximale atteinte et places disponibles. Menus Configuration, Traçabilité et Analyses masqués pour les formateurs.

This commit is contained in:
Manus
2026-01-09 11:05:07 -05:00
parent 5f1df3f9cc
commit 55f0f2795e
8 changed files with 560 additions and 6 deletions

View File

@@ -235,6 +235,25 @@ export const appRouter = router({
return db.getFormateurs();
}),
// Tableau de bord formateur
dashboardStats: protectedProcedure.query(async ({ ctx }) => {
if (ctx.user.role !== 'formateur' || !ctx.user.formateurId) {
throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès réservé aux formateurs' });
}
const formateurDb = await import('./formateurDb');
return formateurDb.getFormateurDashboardStats(ctx.user.formateurId);
}),
prochainesSequences: protectedProcedure
.input(z.object({ limit: z.number().optional().default(10) }))
.query(async ({ ctx, input }) => {
if (ctx.user.role !== 'formateur' || !ctx.user.formateurId) {
throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès réservé aux formateurs' });
}
const formateurDb = await import('./formateurDb');
return formateurDb.getFormateurProchainesSequences(ctx.user.formateurId, input.limit);
}),
getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => {
return db.getFormateurById(input.id);
}),
@@ -550,7 +569,7 @@ export const appRouter = router({
}
}
// Alerte capacité atteinte pour les admins
// Alerte capacité atteinte pour les admins ET le formateur
const nbInscritsApres = await db.countInscriptionsBySequence(input.sequenceId, 'confirmee');
if (nbInscritsApres >= inscriptionSequence.capaciteMax) {
try {
@@ -579,6 +598,28 @@ export const appRouter = router({
});
console.log(`[Notification] Alerte capacité atteinte envoyée à ${adminEmails.length} admin(s)`);
}
// Notification au formateur pour capacité atteinte
if (inscriptionSequence.formateurId) {
const formateur = await db.getFormateurById(inscriptionSequence.formateurId);
if (formateur && formateur.email) {
const { sendNotificationFormateurCapaciteAtteinte } = await import('./emailService');
await sendNotificationFormateurCapaciteAtteinte({
formateurEmail: formateur.email,
formateurNom: formateur.nom,
formationNom: inscriptionFormation.nom,
sequenceNom: inscriptionSequence.nom,
capaciteMax: inscriptionSequence.capaciteMax,
nbInscrits: nbInscritsApres,
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 au formateur ${formateur.email}`);
}
}
} catch (e) {
console.error(`[Notification] Erreur envoi alerte capacité:`, e);
}
@@ -717,7 +758,59 @@ export const appRouter = router({
id: z.number(),
statut: z.enum(['confirmee', 'liste_attente', 'annulee']),
})).mutation(async ({ input }) => {
// Récupérer l'inscription avant modification
const inscriptionAvant = await db.getInscriptionById(input.id);
const ancienStatut = inscriptionAvant?.statut;
await db.updateInscription(input.id, { statut: input.statut });
// Si passage à annulée, notifier le formateur
if (input.statut === 'annulee' && ancienStatut !== 'annulee' && inscriptionAvant) {
const sequence = await db.getSequenceById(inscriptionAvant.sequenceId);
const apprenant = await db.getApprenantById(inscriptionAvant.apprenantId);
if (sequence && apprenant && sequence.formateurId) {
const formateur = await db.getFormateurById(sequence.formateurId);
const formation = await db.getFormationById(sequence.formationId);
if (formateur && formateur.email && formation) {
const nbInscritsApres = await db.countInscriptionsBySequence(sequence.id, '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] Annulation envoyée au formateur ${formateur.email}`);
// Si une place se libère (passage de capacité max à disponible)
if (nbInscritsApres < sequence.capaciteMax && (nbInscritsApres + 1) >= sequence.capaciteMax) {
const { sendNotificationFormateurPlaceDisponible } = await import('./emailService');
await sendNotificationFormateurPlaceDisponible({
formateurEmail: formateur.email,
formateurNom: formateur.nom,
formationNom: formation.nom,
sequenceNom: sequence.nom,
capaciteMax: sequence.capaciteMax,
nbInscrits: nbInscritsApres,
placesDisponibles: sequence.capaciteMax - nbInscritsApres,
});
console.log(`[Notification] Place disponible envoyée au formateur ${formateur.email}`);
}
} catch (e) {
console.error(`[Notification] Erreur envoi notification formateur:`, e);
}
}
}
}
return { success: true };
}),