From 2c0f379c52ed106ea73db2c89d3bac4069a0fb98 Mon Sep 17 00:00:00 2001 From: Manus Date: Sun, 18 Jan 2026 08:39:13 -0500 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Ajout=20d'un=20bouton=20"Tout=20r?= =?UTF-8?q?envoyer"=20dans=20la=20page=20de=20gestion=20des=20attestations?= =?UTF-8?q?=20pour=20envoyer=20en=20masse=20toutes=20les=20attestations=20?= =?UTF-8?q?d'une=20formation.=20Cr=C3=A9ation=20de=20la=20proc=C3=A9dure?= =?UTF-8?q?=20backend=20sendAllAttestations=20qui=20envoie=20toutes=20les?= =?UTF-8?q?=20attestations=20disponibles=20et=20enregistre=20chaque=20envo?= =?UTF-8?q?i=20dans=20l'historique.=20Correction=20du=20scheduler=20de=20r?= =?UTF-8?q?appels=20pour=20v=C3=A9rifier=20l'heure=20configur=C3=A9e=20(in?= =?UTF-8?q?tervalle=2015=20min=20au=20lieu=20de=201h).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/AdminGestionAttestations.tsx | 53 ++++++++++- server/routers.ts | 93 +++++++++++++++++++ todo.md | 1 + 3 files changed, 143 insertions(+), 4 deletions(-) diff --git a/client/src/pages/AdminGestionAttestations.tsx b/client/src/pages/AdminGestionAttestations.tsx index c2d8fce..3d54399 100644 --- a/client/src/pages/AdminGestionAttestations.tsx +++ b/client/src/pages/AdminGestionAttestations.tsx @@ -193,6 +193,17 @@ export default function AdminGestionAttestations() { }, }); + // Mutation pour envoyer toutes les attestations + const sendAllMutation = trpc.gestionAttestations.sendAllAttestations.useMutation({ + onSuccess: (result) => { + toast.success(`${result.succes} attestation(s) envoyée(s) avec succès${result.echecs > 0 ? `, ${result.echecs} échec(s)` : ''}`); + refetchApprenants(); + }, + onError: (error) => { + toast.error(`Erreur lors de l'envoi en masse : ${error.message}`); + }, + }); + // Charger la configuration quand une formation est sélectionnée const handleFormationChange = (formationId: string) => { const id = parseInt(formationId); @@ -251,6 +262,26 @@ export default function AdminGestionAttestations() { } }; + // Envoyer toutes les attestations + const handleSendAll = () => { + if (!selectedFormationId) return; + + // Compter le nombre d'attestations prêtes à envoyer + const attestationsReady = apprenants?.filter(item => { + const hasDocument = item.attestation && (item.attestation.urlPdf || item.attestation.documentUrl); + return hasDocument; + }).length || 0; + + if (attestationsReady === 0) { + toast.error("Aucune attestation prête à envoyer"); + return; + } + + if (confirm(`Êtes-vous sûr de vouloir envoyer ${attestationsReady} attestation(s) ?`)) { + sendAllMutation.mutate({ formationId: selectedFormationId }); + } + }; + // Ouvrir le dialogue d'upload const handleOpenUpload = (inscriptionId: number) => { setSelectedInscriptionId(inscriptionId); @@ -373,10 +404,24 @@ export default function AdminGestionAttestations() { {selectedFormationId && ( - Apprenants inscrits - - Gérez les attestations pour chaque apprenant inscrit à cette formation - +
+
+ Apprenants inscrits + + Gérez les attestations pour chaque apprenant inscrit à cette formation + +
+ {modeEnvoi === "manuel" && apprenants && apprenants.length > 0 && ( + + )} +
{loadingApprenants ? ( diff --git a/server/routers.ts b/server/routers.ts index 61e53cf..9be9b77 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -2574,6 +2574,99 @@ export const appRouter = router({ } }), + // Envoyer toutes les attestations d'une formation en masse + sendAllAttestations: adminProcedure + .input(z.object({ formationId: z.number() })) + .mutation(async ({ input }) => { + const { getApprenantsWithAttestationStatus } = await import("./gestionAttestationsDb"); + const { sendAttestationEmail } = await import("./emailService"); + const { markAttestationAsSent } = await import("./gestionAttestationsDb"); + const { enregistrerEnvoiAttestation } = await import("./attestationsDb"); + const { getDb } = await import("./db"); + const { formations } = await import("../drizzle/schema"); + const { eq } = await import("drizzle-orm"); + + try { + // Récupérer tous les apprenants avec attestations + const apprenants = await getApprenantsWithAttestationStatus(input.formationId); + + // Récupérer le nom de la formation + const db = await getDb(); + if (!db) throw new Error("Database not available"); + const formation = await db.select().from(formations).where(eq(formations.id, input.formationId)).limit(1); + const formationNom = formation[0]?.nom || ""; + + let succes = 0; + let echecs = 0; + const erreurs: string[] = []; + + // Filtrer les apprenants qui ont une attestation prête + const apprenantsAvecAttestation = apprenants.filter(item => { + const hasDocument = item.attestation && item.attestation.documentUrl; + return hasDocument; + }); + + console.log(`[sendAllAttestations] Envoi de ${apprenantsAvecAttestation.length} attestation(s) pour la formation ${formationNom}`); + + // Envoyer chaque attestation + for (const item of apprenantsAvecAttestation) { + if (!item.attestation || !item.attestation.documentUrl) continue; + const pdfUrl = item.attestation.documentUrl; + + try { + // Envoyer l'email + await sendAttestationEmail({ + to: item.apprenant.email, + apprenantNom: item.apprenant.nom, + apprenantPrenom: item.apprenant.prenom, + formationNom: formationNom, + pdfUrl: pdfUrl, + }); + + // Marquer comme envoyé + await markAttestationAsSent(item.attestation.id); + + // Enregistrer dans l'historique + await enregistrerEnvoiAttestation({ + sequenceId: item.sequence.id, + apprenantId: item.apprenant.id, + statut: "envoye", + urlAttestation: pdfUrl, + }); + + succes++; + console.log(`[sendAllAttestations] Envoi réussi pour ${item.apprenant.email}`); + } catch (error: any) { + echecs++; + const messageErreur = error?.message || String(error); + erreurs.push(`${item.apprenant.email}: ${messageErreur}`); + console.error(`[sendAllAttestations] Erreur pour ${item.apprenant.email}:`, error); + + // Enregistrer l'échec dans l'historique + await enregistrerEnvoiAttestation({ + sequenceId: item.sequence.id, + apprenantId: item.apprenant.id, + statut: "erreur", + messageErreur: messageErreur, + urlAttestation: pdfUrl, + }); + } + } + + console.log(`[sendAllAttestations] Terminé: ${succes} succès, ${echecs} échecs`); + + return { + succes, + echecs, + erreurs, + total: apprenantsAvecAttestation.length, + }; + } catch (error: any) { + console.error("[sendAllAttestations] Erreur globale:", error); + throw error; + } + }), + }), }); diff --git a/todo.md b/todo.md index 0c67ff1..81a22a4 100644 --- a/todo.md +++ b/todo.md @@ -865,3 +865,4 @@ - [x] Corriger l'erreur "Failed to parse URL" lors de l'envoi d'attestation avec un chemin local (gérer les chemins locaux et URLs complètes) - [x] Corriger le bouton Renvoyer inactif dans la gestion des attestations (correction selectedSequenceId -> apprenant.sequence.id) - [x] Corriger le problème d'envoi automatique des rappels qui ne se déclenchent pas à l'heure programmée (ajout vérification heure + intervalle 15 min au lieu de 1h) +- [x] Ajouter un bouton "Tout renvoyer" pour envoyer en masse toutes les attestations chargées manuellement sur une séquence