Checkpoint: Amélioration système de rappels - Historique et notifications d'échec

**Nouvelles fonctionnalités** :

1. **Historique des rappels envoyés**
   - Nouvelle table `logsRappels` pour tracer tous les emails de rappels
   - Enregistrement automatique de chaque envoi (succès ou échec)
   - Prévention des doublons : vérification avant envoi
   - Informations stockées : rappelId, sequenceId, apprenantId, email, type, statut, message d'erreur

2. **Notifications automatiques à l'administrateur**
   - Alerte par email en cas d'échec d'envoi de rappels
   - Détail des échecs : liste des emails et messages d'erreur
   - Résumé : nombre d'envois réussis vs échecs

3. **Procédure de test manuel** (backend uniquement)
   - Nouvelle procédure tRPC `rappels.testerRappel`
   - Permet de tester l'envoi d'un rappel J-7 ou J-1 pour une séquence
   - Envoie au premier apprenant inscrit uniquement

**Fichiers créés** :
- `server/rappelDb.ts` : Fonctions de gestion des logs de rappels
- `drizzle/schema.ts` : Ajout de la table `logsRappels`

**Fichiers modifiés** :
- `server/rappelScheduler.ts` : Intégration du logging et des notifications
- `server/routers.ts` : Ajout de la procédure `testerRappel`

**Bénéfices** :
-  Aucun doublon même en cas de redémarrage du serveur
-  Traçabilité complète de tous les rappels envoyés
-  Alertes automatiques en cas de problème d'envoi
-  Possibilité de tester les rappels avant envoi automatique
This commit is contained in:
Manus Sandbox
2025-12-08 03:01:50 -05:00
parent 4077c57c49
commit bb0ec9e3b0
9 changed files with 2069 additions and 1 deletions

View File

@@ -891,6 +891,89 @@ export const appRouter = router({
const result = await processRappelsAutomatiques();
return result;
}),
testerRappel: adminProcedure
.input(z.object({
sequenceId: z.number(),
typeRappel: z.enum(['rappel', 'rappelJ1']),
}))
.mutation(async ({ input }) => {
const sequence = await db.getSequenceById(input.sequenceId);
if (!sequence) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Séquence introuvable' });
}
const formation = await db.getFormationById(sequence.formationId);
if (!formation) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Formation introuvable' });
}
// Récupérer les dates de la séquence
const dates = await db.getDatesBySequence(input.sequenceId);
// Récupérer les inscrits
const inscrits = await db.getInscriptionsBySequence(input.sequenceId);
const inscritsConfirmes = inscrits.filter(i => i.inscription.statut === 'confirmee');
if (inscritsConfirmes.length === 0) {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Aucun apprenant inscrit confirmé pour cette séquence' });
}
// Envoyer le rappel de test au premier inscrit uniquement
const premierInscrit = inscritsConfirmes[0];
const apprenant = premierInscrit.apprenant;
if (!apprenant || !apprenant.email) {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Apprenant introuvable ou sans email' });
}
const { sendRappelJ7Email, sendRappelJ1Email } = await import('./emailService');
try {
if (input.typeRappel === 'rappel') {
await sendRappelJ7Email({
apprenantEmail: apprenant.email,
apprenantPrenom: apprenant.prenom,
apprenantNom: apprenant.nom,
apprenantFonction: apprenant.fonction,
formationNom: formation.nom,
sequenceNom: sequence.nom,
dates: dates.map((d: any) => ({
dateDebut: d.dateDebut,
dateFin: d.dateFin,
ordre: d.ordre
})),
lieu: sequence.lieu || '',
});
} else {
await sendRappelJ1Email({
apprenantEmail: apprenant.email,
apprenantPrenom: apprenant.prenom,
apprenantNom: apprenant.nom,
apprenantFonction: apprenant.fonction,
formationNom: formation.nom,
sequenceNom: sequence.nom,
dates: dates.map((d: any) => ({
dateDebut: d.dateDebut,
dateFin: d.dateFin,
ordre: d.ordre
})),
lieu: sequence.lieu || '',
});
}
return {
success: true,
message: `Rappel de test envoyé à ${apprenant.email}`,
destinataire: apprenant.email,
};
} catch (error: any) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: `Erreur lors de l'envoi: ${error.message}`,
});
}
}),
}),
emailConfig: router({