Checkpoint: Ajout d'un bouton "Tout renvoyer" dans la page de gestion des attestations pour envoyer en masse toutes les attestations d'une formation. Création de la procédure backend sendAllAttestations qui envoie toutes les attestations disponibles et enregistre chaque envoi dans l'historique. Correction du scheduler de rappels pour vérifier l'heure configurée (intervalle 15 min au lieu de 1h).
This commit is contained in:
@@ -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
|
// Charger la configuration quand une formation est sélectionnée
|
||||||
const handleFormationChange = (formationId: string) => {
|
const handleFormationChange = (formationId: string) => {
|
||||||
const id = parseInt(formationId);
|
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
|
// Ouvrir le dialogue d'upload
|
||||||
const handleOpenUpload = (inscriptionId: number) => {
|
const handleOpenUpload = (inscriptionId: number) => {
|
||||||
setSelectedInscriptionId(inscriptionId);
|
setSelectedInscriptionId(inscriptionId);
|
||||||
@@ -373,10 +404,24 @@ export default function AdminGestionAttestations() {
|
|||||||
{selectedFormationId && (
|
{selectedFormationId && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Apprenants inscrits</CardTitle>
|
<div className="flex items-center justify-between">
|
||||||
<CardDescription>
|
<div>
|
||||||
Gérez les attestations pour chaque apprenant inscrit à cette formation
|
<CardTitle>Apprenants inscrits</CardTitle>
|
||||||
</CardDescription>
|
<CardDescription>
|
||||||
|
Gérez les attestations pour chaque apprenant inscrit à cette formation
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
{modeEnvoi === "manuel" && apprenants && apprenants.length > 0 && (
|
||||||
|
<Button
|
||||||
|
onClick={handleSendAll}
|
||||||
|
disabled={sendAllMutation.isPending}
|
||||||
|
variant="default"
|
||||||
|
>
|
||||||
|
<Send className="h-4 w-4 mr-2" />
|
||||||
|
{sendAllMutation.isPending ? "Envoi en cours..." : "Tout renvoyer"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{loadingApprenants ? (
|
{loadingApprenants ? (
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
1
todo.md
1
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 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 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] 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
|
||||||
|
|||||||
Reference in New Issue
Block a user