From e642f2317009add67861bbc8231a9dfcc167261b Mon Sep 17 00:00:00 2001 From: Manus Date: Thu, 15 Jan 2026 12:22:24 -0500 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Am=C3=A9lioration=20du=20bouton?= =?UTF-8?q?=20"Tester=20le=20rappel"=20pour=20utiliser=20automatiquement?= =?UTF-8?q?=20le=20bon=20template=20configur=C3=A9=20:=20-=20Modification?= =?UTF-8?q?=20de=20la=20proc=C3=A9dure=20testerRappel=20pour=20d=C3=A9tect?= =?UTF-8?q?er=20automatiquement=20le=20premier=20rappel=20actif=20configur?= =?UTF-8?q?=C3=A9=20pour=20la=20s=C3=A9quence=20-=20Suppression=20du=20par?= =?UTF-8?q?am=C3=A8tre=20typeRappel=20(n'est=20plus=20n=C3=A9cessaire)=20-?= =?UTF-8?q?=20Cr=C3=A9ation=20de=20la=20fonction=20sendRappelEmail=20g?= =?UTF-8?q?=C3=A9n=C3=A9rique=20qui=20utilise=20le=20templateType=20du=20r?= =?UTF-8?q?appel=20configur=C3=A9=20-=20Le=20bouton=20frontend=20envoie=20?= =?UTF-8?q?maintenant=20le=20bon=20template=20(rappel=201,=203,=204,=205,?= =?UTF-8?q?=206,=20etc.)=20selon=20la=20configuration=20de=20la=20s=C3=A9q?= =?UTF-8?q?uence=20-=20Gestion=20des=20pi=C3=A8ces=20jointes=20configur?= =?UTF-8?q?=C3=A9es=20dans=20le=20rappel=20-=20Mise=20=C3=A0=20jour=20du?= =?UTF-8?q?=20fichier=20todo.md=20pour=20marquer=20la=20t=C3=A2che=20comme?= =?UTF-8?q?=20termin=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/AdminSequences.tsx | 3 +- server/emailService.ts | 99 +++++++++++++++++++++++++++++ server/routers.ts | 70 ++++++++++---------- todo.md | 1 + 4 files changed, 136 insertions(+), 37 deletions(-) diff --git a/client/src/pages/AdminSequences.tsx b/client/src/pages/AdminSequences.tsx index 7730e27..284ecaf 100644 --- a/client/src/pages/AdminSequences.tsx +++ b/client/src/pages/AdminSequences.tsx @@ -836,8 +836,7 @@ export default function AdminSequences() { onClick={() => { if (sequence.dates && sequence.dates.length > 0) { testerRappelMutation.mutate({ - sequenceId: sequence.id, - typeRappel: 'rappel' as const + sequenceId: sequence.id }); } else { toast.error("Cette séquence n'a pas de dates configurées"); diff --git a/server/emailService.ts b/server/emailService.ts index d63b149..f46c0dd 100644 --- a/server/emailService.ts +++ b/server/emailService.ts @@ -216,6 +216,105 @@ export async function sendTeaserEmail(params: { }); } +/** + * Envoie un email de rappel générique avec le template spécifié + */ +export async function sendRappelEmail(params: { + apprenantEmail: string; + apprenantPrenom: string; + apprenantNom: string; + apprenantFonction: string; + formationNom: string; + sequenceNom: string; + dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>; + lieu: string; + formateur?: string; + templateType: string; + attachmentUrl?: string; + attachmentFilename?: string; + attachmentMimeType?: string; +}): Promise { + const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' : + params.apprenantFonction === 'chef_service' ? 'Chef de service' : ''; + const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom; + + const datesHTML = params.dates.map(date => ` +
+ Date ${date.ordre} :
+ ${date.dateDebut.toLocaleDateString('fr-FR', { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + })} +
+ `).join(''); + + const content = ` +

Rappel : Votre formation

+

Bonjour ${salutation},

+ +

Nous vous rappelons votre formation ${params.formationNom}.

+ +
+

Informations pratiques

+

Séquence : ${params.sequenceNom}

+

Lieu : ${params.lieu}

+ ${params.formateur ? `

Formateur : ${params.formateur}

` : ''} +

Dates :

+ ${datesHTML} +
+ +

Merci de vous présenter à l'heure indiquée.

+ +

Si vous avez des questions, n'hésitez pas à contacter le service RH.

+ +

À très bientôt !

+ `; + + const variables = { + nomApprenant: params.apprenantNom, + prenomApprenant: params.apprenantPrenom, + nomFormation: params.formationNom, + nomSequence: params.sequenceNom, + dateDebut: params.dates[0]?.dateDebut.toLocaleDateString('fr-FR') || '', + dateFin: params.dates[params.dates.length - 1]?.dateFin.toLocaleDateString('fr-FR') || '', + datesHTML: datesHTML, + lieu: params.lieu, + formateur: params.formateur || '', + }; + + // Préparer les pièces jointes si nécessaire + const attachments: Array<{ filename: string; content: string; contentType: string }> = []; + + if (params.attachmentUrl && params.attachmentFilename && params.attachmentMimeType) { + try { + // Télécharger le fichier depuis S3 et le convertir en base64 + const response = await fetch(params.attachmentUrl); + const buffer = await response.arrayBuffer(); + const base64 = Buffer.from(buffer).toString('base64'); + + attachments.push({ + filename: params.attachmentFilename, + content: base64, + contentType: params.attachmentMimeType, + }); + } catch (error) { + console.error('[Email] Erreur lors du téléchargement de la pièce jointe:', error); + // Continuer l'envoi sans la pièce jointe + } + } + + return sendEmail({ + to: params.apprenantEmail, + subject: `Rappel : ${params.formationNom}`, + html: await getEmailTemplate(content, params.templateType, variables), + attachments: attachments.length > 0 ? attachments : undefined, + }); +} + /** * Envoie un email de rappel J-7 */ diff --git a/server/routers.ts b/server/routers.ts index 3d352a1..04d4dea 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -1379,7 +1379,6 @@ export const appRouter = router({ testerRappel: adminProcedure .input(z.object({ sequenceId: z.number(), - typeRappel: z.enum(['rappel', 'rappelJ1']), })) .mutation(async ({ input }) => { const sequence = await db.getSequenceById(input.sequenceId); @@ -1402,6 +1401,21 @@ export const appRouter = router({ // Récupérer les dates de la séquence const dates = await db.getDatesBySequence(input.sequenceId); + // Récupérer les rappels configurés pour la première date de la séquence + if (dates.length === 0) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Cette séquence n\'a pas de dates configurées' }); + } + + const rappelsConfigures = await db.getRappelsByDateFormation(dates[0].id); + const rappelsActifs = rappelsConfigures.filter((r: any) => r.actif); + + if (rappelsActifs.length === 0) { + throw new TRPCError({ code: 'BAD_REQUEST', message: 'Aucun rappel actif configuré pour cette séquence' }); + } + + // Utiliser le premier rappel actif trouvé + const rappelAUtiliser = rappelsActifs[0]; + // Récupérer les inscrits const inscrits = await db.getInscriptionsBySequence(input.sequenceId); const inscritsConfirmes = inscrits.filter(i => i.inscription.statut === 'confirmee'); @@ -1418,42 +1432,28 @@ export const appRouter = router({ throw new TRPCError({ code: 'BAD_REQUEST', message: 'Apprenant introuvable ou sans email' }); } - const { sendRappelJ7Email, sendRappelJ1Email } = await import('./emailService'); + const { sendRappelEmail } = 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 || '', - formateur: formateurNom, - }); - } 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 || '', - formateur: formateurNom, - }); - } + await sendRappelEmail({ + 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 || '', + formateur: formateurNom, + templateType: rappelAUtiliser.templateType, + attachmentUrl: rappelAUtiliser.urlFichier || undefined, + attachmentFilename: rappelAUtiliser.nomFichier || undefined, + attachmentMimeType: rappelAUtiliser.typeFichier || undefined, + }); return { success: true, diff --git a/todo.md b/todo.md index 2950d55..42815ee 100644 --- a/todo.md +++ b/todo.md @@ -819,3 +819,4 @@ - [ ] Corriger les erreurs TypeScript dans les fonctions de gestion des logs - [x] Corriger l'erreur de validation du type de rappel dans la procédure testerRappel (typeRappel invalide) - [x] Corriger l'erreur "Cannot read properties of undefined (reading 'replace')" lors de l'envoi de test de rappel +- [x] Améliorer le bouton "Tester le rappel" pour utiliser automatiquement le bon template configuré pour la séquence (au lieu de toujours envoyer rappel J-7)