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

@@ -0,0 +1,9 @@
{
"query": "ALTER TABLE formateurs ADD COLUMN email varchar(320) DEFAULT NULL;",
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute ALTER TABLE formateurs ADD COLUMN email varchar(320) DEFAULT NULL;",
"rows": [],
"messages": [],
"stdout": "",
"stderr": "",
"execution_time_ms": 593
}

File diff suppressed because it is too large Load Diff

View File

@@ -155,6 +155,13 @@
"when": 1765183092631,
"tag": "0021_equal_the_fallen",
"breakpoints": true
},
{
"idx": 22,
"version": "5",
"when": 1765828479906,
"tag": "0022_closed_lucky_pierre",
"breakpoints": true
}
]
}

View File

@@ -226,6 +226,8 @@ export const formateurs = mysqlTable("formateurs", {
id: int("id").autoincrement().primaryKey(),
/** Nom complet du formateur */
nom: varchar("nom", { length: 255 }).notNull(),
/** Email du formateur pour les notifications */
email: varchar("email", { length: 320 }),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});

View File

@@ -893,3 +893,16 @@ export async function updateRappelExecution(id: number) {
derniereExecution: new Date(),
}).where(eq(rappels.id, id));
}
// ==================== UTILISATEURS ADMIN ====================
/**
* Récupère tous les utilisateurs avec le rôle admin
*/
export async function getAdminUsers() {
const db = await getDb();
if (!db) return [];
return await db.select().from(users).where(eq(users.role, 'admin'));
}

View File

@@ -474,3 +474,370 @@ export async function sendPasswordResetEmail(params: {
html: await getEmailTemplate(content, 'reset_password'),
});
}
/**
* ========================================
* SYSTÈME DE NOTIFICATIONS ENRICHI
* ========================================
*/
/**
* Envoie un email de remerciement post-formation à un apprenant
*/
export async function sendRemerciementPostFormation(params: {
apprenantEmail: string;
apprenantNom: string;
apprenantPrenom: string;
apprenantFonction: string;
formationNom: string;
sequenceNom: string;
formateurNom?: string;
lienQuestionnaire?: string;
}): Promise<boolean> {
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
params.apprenantFonction === 'chef_service' ? 'Chef de service' : '';
const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom;
const content = `
<h2>Merci pour votre participation !</h2>
<p>Bonjour ${salutation},</p>
<p>Nous tenons à vous remercier pour votre participation à la formation <strong>${params.formationNom}</strong> (${params.sequenceNom}).</p>
${params.formateurNom ? `<p>Nous espérons que les sessions animées par <strong>${params.formateurNom}</strong> ont répondu à vos attentes.</p>` : ''}
<div class="info-box">
<h3>Votre avis compte !</h3>
<p>Afin d'améliorer continuellement nos formations, nous vous invitons à partager votre retour d'expérience.</p>
${params.lienQuestionnaire ? `
<div style="text-align: center; margin: 20px 0;">
<a href="${params.lienQuestionnaire}" class="button">Donner mon avis</a>
</div>
` : '<p>Un questionnaire de satisfaction vous sera envoyé prochainement.</p>'}
</div>
<p>Nous vous souhaitons une excellente continuation dans vos fonctions et espérons vous revoir lors de prochaines formations.</p>
<p>Cordialement,<br/>L'équipe Formation</p>
`;
const variables = {
nomApprenant: params.apprenantNom,
prenomApprenant: params.apprenantPrenom,
nomFormation: params.formationNom,
nomSequence: params.sequenceNom,
formateur: params.formateurNom || '',
};
return sendEmail({
to: params.apprenantEmail,
subject: `Merci pour votre participation - ${params.formationNom}`,
html: await getEmailTemplate(content, 'remerciement', variables),
});
}
/**
* Envoie une notification au formateur pour une nouvelle inscription
*/
export async function sendNotificationFormateurNouvelleInscription(params: {
formateurEmail: string;
formateurNom: string;
apprenantNom: string;
apprenantPrenom: string;
apprenantFonction: string;
apprenantEtablissement?: string;
formationNom: string;
sequenceNom: string;
nbInscrits: number;
capaciteMax: number;
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
}): Promise<boolean> {
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
params.apprenantFonction === 'chef_service' ? 'Chef de service' : 'Autre';
const datesHTML = params.dates.map(date => `
<li>Date ${date.ordre} : ${date.dateDebut.toLocaleDateString('fr-FR', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric'
})}</li>
`).join('');
const content = `
<h2>🎓 Nouvelle inscription à votre formation</h2>
<p>Bonjour ${params.formateurNom},</p>
<p>Un nouvel apprenant vient de s'inscrire à votre formation.</p>
<div class="info-box">
<h3>Détails de l'inscription</h3>
<p><strong>Apprenant :</strong> ${params.apprenantPrenom} ${params.apprenantNom}</p>
<p><strong>Fonction :</strong> ${fonctionLabel}</p>
${params.apprenantEtablissement ? `<p><strong>Établissement :</strong> ${params.apprenantEtablissement}</p>` : ''}
<p><strong>Formation :</strong> ${params.formationNom}</p>
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
</div>
<div class="info-box" style="background-color: #dbeafe; border-left-color: #3b82f6;">
<h3>📊 État des inscriptions</h3>
<p><strong>${params.nbInscrits} / ${params.capaciteMax}</strong> places occupées</p>
<div style="background-color: #e5e7eb; border-radius: 4px; height: 20px; margin: 10px 0;">
<div style="background-color: #3b82f6; border-radius: 4px; height: 20px; width: ${Math.min(100, (params.nbInscrits / params.capaciteMax) * 100)}%;"></div>
</div>
</div>
<h4>Dates de la formation :</h4>
<ul>${datesHTML}</ul>
<p>Cordialement,<br/>L'équipe Formation</p>
`;
return sendEmail({
to: params.formateurEmail,
subject: `Nouvelle inscription - ${params.formationNom} (${params.sequenceNom})`,
html: await getEmailTemplate(content, 'notification_formateur'),
});
}
/**
* Envoie une notification au formateur pour une annulation d'inscription
*/
export async function sendNotificationFormateurAnnulation(params: {
formateurEmail: string;
formateurNom: string;
apprenantNom: string;
apprenantPrenom: string;
apprenantFonction: string;
formationNom: string;
sequenceNom: string;
nbInscrits: number;
capaciteMax: number;
raisonAnnulation?: string;
}): Promise<boolean> {
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
params.apprenantFonction === 'chef_service' ? 'Chef de service' : 'Autre';
const content = `
<h2>⚠️ Annulation d'inscription</h2>
<p>Bonjour ${params.formateurNom},</p>
<p>Un apprenant a annulé son inscription à votre formation.</p>
<div class="info-box" style="background-color: #fef3c7; border-left-color: #f59e0b;">
<h3>Détails de l'annulation</h3>
<p><strong>Apprenant :</strong> ${params.apprenantPrenom} ${params.apprenantNom}</p>
<p><strong>Fonction :</strong> ${fonctionLabel}</p>
<p><strong>Formation :</strong> ${params.formationNom}</p>
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
${params.raisonAnnulation ? `<p><strong>Raison :</strong> ${params.raisonAnnulation}</p>` : ''}
</div>
<div class="info-box" style="background-color: #dbeafe; border-left-color: #3b82f6;">
<h3>📊 État des inscriptions après annulation</h3>
<p><strong>${params.nbInscrits} / ${params.capaciteMax}</strong> places occupées</p>
<p><strong>${params.capaciteMax - params.nbInscrits}</strong> place(s) disponible(s)</p>
</div>
<p>Cordialement,<br/>L'équipe Formation</p>
`;
return sendEmail({
to: params.formateurEmail,
subject: `Annulation d'inscription - ${params.formationNom} (${params.sequenceNom})`,
html: await getEmailTemplate(content, 'notification_formateur'),
});
}
/**
* Envoie une alerte aux admins quand la capacité maximale est atteinte
*/
export async function sendAlerteCapaciteAtteinte(params: {
adminEmails: string[];
formationNom: string;
sequenceNom: string;
capaciteMax: number;
nbInscrits: number;
nbListeAttente: number;
formateurNom?: string;
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
}): Promise<{ sent: number; failed: number }> {
const datesHTML = params.dates.map(date => `
<li>${date.dateDebut.toLocaleDateString('fr-FR', {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric'
})}</li>
`).join('');
const content = `
<h2>🔔 Alerte : Capacité maximale atteinte</h2>
<div class="info-box" style="background-color: #fee2e2; border-left-color: #ef4444;">
<h3>⚠️ La séquence est complète</h3>
<p>La capacité maximale de la séquence <strong>${params.sequenceNom}</strong> a été atteinte.</p>
</div>
<div class="info-box">
<h3>Détails de la séquence</h3>
<p><strong>Formation :</strong> ${params.formationNom}</p>
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
${params.formateurNom ? `<p><strong>Formateur :</strong> ${params.formateurNom}</p>` : ''}
<p><strong>Capacité :</strong> ${params.nbInscrits} / ${params.capaciteMax} (100%)</p>
${params.nbListeAttente > 0 ? `<p><strong>Liste d'attente :</strong> ${params.nbListeAttente} personne(s)</p>` : ''}
</div>
<h4>Dates prévues :</h4>
<ul>${datesHTML}</ul>
<div class="info-box" style="background-color: #dbeafe; border-left-color: #3b82f6;">
<h3>Actions possibles</h3>
<ul>
<li>Augmenter la capacité maximale de la séquence</li>
<li>Créer une nouvelle séquence pour cette formation</li>
<li>Contacter les personnes en liste d'attente pour les informer</li>
</ul>
</div>
<p>Cordialement,<br/>Le système de gestion des formations</p>
`;
let sent = 0;
let failed = 0;
for (const adminEmail of params.adminEmails) {
try {
await sendEmail({
to: adminEmail,
subject: `🔔 Capacité atteinte - ${params.formationNom} (${params.sequenceNom})`,
html: await getEmailTemplate(content, 'alerte_admin'),
});
sent++;
} catch (error) {
console.error(`Erreur envoi alerte à ${adminEmail}:`, error);
failed++;
}
}
return { sent, failed };
}
/**
* Envoie une notification aux apprenants en liste d'attente quand une place se libère
*/
export async function sendNotificationPlaceDisponible(params: {
apprenantEmail: string;
apprenantNom: string;
apprenantPrenom: string;
apprenantFonction: string;
formationNom: string;
sequenceNom: string;
positionListeAttente: number;
lienConfirmation?: string;
delaiReponse?: number; // en heures
}): Promise<boolean> {
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
params.apprenantFonction === 'chef_service' ? 'Chef de service' : '';
const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom;
const delai = params.delaiReponse || 48;
const content = `
<h2>🎉 Une place s'est libérée !</h2>
<p>Bonjour ${salutation},</p>
<p>Bonne nouvelle ! Une place vient de se libérer pour la formation à laquelle vous êtes inscrit(e) en liste d'attente.</p>
<div class="info-box" style="background-color: #d1fae5; border-left-color: #10b981;">
<h3>✅ Vous pouvez maintenant confirmer votre inscription</h3>
<p><strong>Formation :</strong> ${params.formationNom}</p>
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
<p><strong>Votre position :</strong> ${params.positionListeAttente}${params.positionListeAttente === 1 ? 'er' : 'ème'} sur la liste d'attente</p>
</div>
${params.lienConfirmation ? `
<div style="text-align: center; margin: 30px 0;">
<a href="${params.lienConfirmation}" class="button">Confirmer mon inscription</a>
</div>
` : '<p>Veuillez contacter le service RH pour confirmer votre inscription.</p>'}
<div class="info-box" style="background-color: #fef3c7; border-left-color: #f59e0b;">
<h3>⏰ Important</h3>
<p>Vous avez <strong>${delai} heures</strong> pour confirmer votre inscription.</p>
<p>Passé ce délai, la place sera proposée à la personne suivante sur la liste d'attente.</p>
</div>
<p>Cordialement,<br/>L'équipe Formation</p>
`;
const variables = {
nomApprenant: params.apprenantNom,
prenomApprenant: params.apprenantPrenom,
nomFormation: params.formationNom,
nomSequence: params.sequenceNom,
};
return sendEmail({
to: params.apprenantEmail,
subject: `🎉 Place disponible - ${params.formationNom}`,
html: await getEmailTemplate(content, 'notification_liste_attente', variables),
});
}
/**
* Envoie une notification à un apprenant en liste d'attente pour l'informer d'une annulation de séquence
*/
export async function sendNotificationAnnulationListeAttente(params: {
apprenantEmail: string;
apprenantNom: string;
apprenantPrenom: string;
apprenantFonction: string;
formationNom: string;
sequenceNom: string;
raisonAnnulation?: string;
alternativesDisponibles?: Array<{ sequenceNom: string; nbPlaces: number }>;
}): Promise<boolean> {
const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' :
params.apprenantFonction === 'chef_service' ? 'Chef de service' : '';
const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom;
const alternativesHTML = params.alternativesDisponibles && params.alternativesDisponibles.length > 0
? `
<div class="info-box" style="background-color: #dbeafe; border-left-color: #3b82f6;">
<h3>📋 Séquences alternatives disponibles</h3>
<ul>
${params.alternativesDisponibles.map(alt => `
<li><strong>${alt.sequenceNom}</strong> - ${alt.nbPlaces} place(s) disponible(s)</li>
`).join('')}
</ul>
<p>Contactez le service RH pour vous inscrire à une autre séquence.</p>
</div>
` : '';
const content = `
<h2>Information : Annulation de séquence</h2>
<p>Bonjour ${salutation},</p>
<p>Nous vous informons que la séquence de formation pour laquelle vous étiez en liste d'attente a été annulée.</p>
<div class="info-box" style="background-color: #fee2e2; border-left-color: #ef4444;">
<h3>Séquence annulée</h3>
<p><strong>Formation :</strong> ${params.formationNom}</p>
<p><strong>Séquence :</strong> ${params.sequenceNom}</p>
${params.raisonAnnulation ? `<p><strong>Raison :</strong> ${params.raisonAnnulation}</p>` : ''}
</div>
${alternativesHTML}
<p>Nous nous excusons pour ce désagrément et restons à votre disposition pour toute question.</p>
<p>Cordialement,<br/>L'équipe Formation</p>
`;
return sendEmail({
to: params.apprenantEmail,
subject: `Information : Annulation de séquence - ${params.formationNom}`,
html: await getEmailTemplate(content, 'notification_annulation'),
});
}

View File

@@ -4,6 +4,7 @@ import { eq, and, gte, lte, sql } from "drizzle-orm";
import { sendRappelJ7Email, sendRappelJ1Email } from "./emailService";
import { logRappelEnvoye, rappelDejaEnvoye } from "./rappelDb";
import { notifyOwner } from "./_core/notification";
import { processRemerciementsAutomatiques } from "./remerciementScheduler";
/**
* Job automatique qui vérifie quotidiennement les séquences à venir
@@ -243,11 +244,13 @@ export function initRappelScheduler() {
// Exécuter immédiatement au démarrage
processRappelsAutomatiques();
processRemerciementsAutomatiques();
// Puis exécuter toutes les heures
setInterval(() => {
processRappelsAutomatiques();
processRemerciementsAutomatiques();
}, 60 * 60 * 1000); // 1 heure en millisecondes
console.log("[Rappels] Scheduler initialisé - vérification toutes les heures");
console.log("[Rappels] Scheduler initialisé - vérification toutes les heures (rappels + remerciements)");
}

View File

@@ -0,0 +1,209 @@
import { getDb } from "./db";
import { sequences, inscriptions, apprenants, formations, datesFormation, formateurs } from "../drizzle/schema";
import { eq, and, lte, sql } from "drizzle-orm";
import { sendRemerciementPostFormation } from "./emailService";
/**
* Table pour suivre les remerciements déjà envoyés
*/
const remerciementsEnvoyes = new Map<string, Date>();
/**
* Vérifie si un remerciement a déjà été envoyé pour une inscription
*/
function remerciementDejaEnvoye(inscriptionId: number, sequenceId: number): boolean {
const key = `${inscriptionId}-${sequenceId}`;
return remerciementsEnvoyes.has(key);
}
/**
* Marque un remerciement comme envoyé
*/
function marquerRemerciementEnvoye(inscriptionId: number, sequenceId: number): void {
const key = `${inscriptionId}-${sequenceId}`;
remerciementsEnvoyes.set(key, new Date());
}
/**
* 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é
if (remerciementDejaEnvoye(inscription.id, sequence.id)) {
console.log(`[Remerciements] Remerciement déjà envoyé pour ${apprenant.email}`);
continue;
}
try {
await sendRemerciementPostFormation({
apprenantEmail: apprenant.email,
apprenantNom: apprenant.nom,
apprenantPrenom: apprenant.prenom,
apprenantFonction: apprenant.fonction,
formationNom: formation.nom,
sequenceNom: sequence.nom,
formateurNom: formateur?.nom,
});
marquerRemerciementEnvoye(inscription.id, sequence.id);
envoyesCount++;
console.log(`[Remerciements] Email envoyé à ${apprenant.email}`);
} catch (error) {
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) {
try {
await sendRemerciementPostFormation({
apprenantEmail: apprenant.email,
apprenantNom: apprenant.nom,
apprenantPrenom: apprenant.prenom,
apprenantFonction: apprenant.fonction,
formationNom: formation.nom,
sequenceNom: sequence.nom,
formateurNom: formateur?.nom,
});
sent++;
} catch (error) {
console.error(`[Remerciements] Erreur envoi à ${apprenant.email}:`, error);
failed++;
}
}
return { sent, failed };
}

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;

10
todo.md
View File

@@ -427,3 +427,13 @@
- [x] Bug persistant : J-1 fonctionne mais J-7 ne fonctionne toujours pas
- [x] Analyser la différence entre le template J-7 et J-1
- [x] Corriger le template J-7 définitivement
## 🔔 Système de notifications enrichi
- [x] Email de remerciement post-formation
- [x] Notification aux formateurs (nouvelle inscription)
- [x] Notification aux formateurs (annulation)
- [x] Alerte capacité atteinte (pour les admins)
- [x] Notification d'annulation aux apprenants en liste d'attente
- [x] Ajout du champ email pour les formateurs
- [x] Scheduler automatique pour les remerciements post-formation
- [x] Procédure tRPC pour envoi manuel des remerciements