Checkpoint: Phase 1 : Système d'attestations de formation automatiques

 Infrastructure backend complète :
- Tables attestations et configAttestation en base de données
- Service de génération PDF (attestationService.ts) avec PDFKit
- Procédures tRPC pour générer, récupérer et configurer les attestations
- Stockage S3 des PDF générés

 Interface d'administration :
- Page de configuration des attestations (/admin/attestations)
- Personnalisation du texte avec variables dynamiques
- Champs pour signataire (nom et fonction)
- Placeholders pour logo et signature (à implémenter en phase 2)

📋 À faire en Phase 2 :
- Upload des images (logo/signature) vers S3
- Génération automatique après validation de présence
- Envoi automatique par email aux apprenants
- Espace apprenant pour télécharger les attestations
This commit is contained in:
Manus
2026-01-07 06:35:12 -05:00
parent 29b35fceaa
commit 2eb352206f
14 changed files with 3098 additions and 0 deletions

View File

@@ -1736,6 +1736,105 @@ export const appRouter = router({
});
}),
}),
// ===== ATTESTATIONS =====
attestations: router({
// Générer une attestation pour une inscription
generer: adminProcedure
.input(z.object({ inscriptionId: z.number() }))
.mutation(async ({ input }) => {
const attestationService = await import("./attestationService");
const { getDb } = await import("./db");
const { inscriptions, apprenants, sequences } = await import("../drizzle/schema");
const { eq } = await import("drizzle-orm");
const db = await getDb();
if (!db) throw new Error("Base de données non disponible");
// Récupérer les infos de l'inscription
const [inscriptionData] = await db
.select({
inscription: inscriptions,
apprenant: apprenants,
sequence: sequences,
})
.from(inscriptions)
.innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id))
.innerJoin(sequences, eq(inscriptions.sequenceId, sequences.id))
.where(eq(inscriptions.id, input.inscriptionId))
.limit(1);
if (!inscriptionData) {
throw new Error("Inscription introuvable");
}
// Vérifier si une attestation existe déjà
const existingAttestation = await attestationService.getAttestation(
inscriptionData.apprenant.id,
inscriptionData.sequence.id
);
if (existingAttestation) {
return existingAttestation;
}
// Générer le PDF
const { s3Key, pdfUrl } = await attestationService.genererAttestationPDF(input.inscriptionId);
// Enregistrer dans la base
await attestationService.enregistrerAttestation(
input.inscriptionId,
inscriptionData.apprenant.id,
inscriptionData.sequence.id,
s3Key,
pdfUrl
);
return { s3Key, pdfUrl };
}),
// Récupérer l'attestation d'un apprenant pour une séquence
get: publicProcedure
.input(z.object({
apprenantId: z.number(),
sequenceId: z.number(),
}))
.query(async ({ input }) => {
const attestationService = await import("./attestationService");
return attestationService.getAttestation(input.apprenantId, input.sequenceId);
}),
// Récupérer toutes les attestations d'un apprenant
listByApprenant: publicProcedure
.input(z.object({ apprenantId: z.number() }))
.query(async ({ input }) => {
const attestationService = await import("./attestationService");
return attestationService.getAttestationsApprenant(input.apprenantId);
}),
// Configuration des attestations
getConfig: adminProcedure
.query(async () => {
const attestationService = await import("./attestationService");
return attestationService.getOrCreateConfigAttestation();
}),
updateConfig: adminProcedure
.input(z.object({
logoS3Key: z.string().optional(),
logoUrl: z.string().optional(),
signatureS3Key: z.string().optional(),
signatureUrl: z.string().optional(),
nomSignataire: z.string().optional(),
fonctionSignataire: z.string().optional(),
texteAttestation: z.string().optional(),
}))
.mutation(async ({ input }) => {
const attestationService = await import("./attestationService");
await attestationService.updateConfigAttestation(input);
return { success: true };
}),
}),
});
export type AppRouter = typeof appRouter;