Checkpoint: Ajout de la page de gestion des attestations de formation
Nouvelle fonctionnalité complète pour gérer les attestations : - Schéma de base de données : colonnes modeAttestation et modeEnvoi dans formations, colonnes documentUrl, documentS3Key, uploadedBy, uploadedAt dans attestations - Backend : fichier gestionAttestationsDb.ts, procédures tRPC, fonction sendAttestationEmail - Interface : page AdminGestionAttestations avec sélection de formation, configuration des modes, tableau des apprenants, boutons upload/prévisualisation/envoi/suppression - Navigation : lien dans menu Gestion, route /admin/gestion-attestations
This commit is contained in:
@@ -978,3 +978,46 @@ export async function sendNotificationFormateurPlaceDisponible(params: {
|
||||
html: await getEmailTemplate(content, 'notification_formateur'),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoie une attestation de formation par email
|
||||
*/
|
||||
export async function sendAttestationEmail(params: {
|
||||
to: string;
|
||||
apprenantNom: string;
|
||||
apprenantPrenom: string;
|
||||
formationNom: string;
|
||||
pdfUrl: string;
|
||||
}): Promise<boolean> {
|
||||
const content = `
|
||||
<h2>Votre attestation de formation</h2>
|
||||
|
||||
<p>Bonjour ${params.apprenantPrenom} ${params.apprenantNom},</p>
|
||||
|
||||
<p>Nous avons le plaisir de vous transmettre votre attestation de formation pour :</p>
|
||||
|
||||
<div class="info-box" style="background-color: #dbeafe; border-left-color: #3b82f6;">
|
||||
<h3>${params.formationNom}</h3>
|
||||
</div>
|
||||
|
||||
<p>Vous pouvez télécharger votre attestation en cliquant sur le lien ci-dessous :</p>
|
||||
|
||||
<div style="text-align: center; margin: 30px 0;">
|
||||
<a href="${params.pdfUrl}" style="display: inline-block; padding: 12px 30px; background-color: #3b82f6; color: white; text-decoration: none; border-radius: 5px; font-weight: bold;">
|
||||
📄 Télécharger mon attestation
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p>Cette attestation certifie votre participation à la formation et peut être utilisée pour votre dossier professionnel.</p>
|
||||
|
||||
<p>Nous vous remercions pour votre participation et vous souhaitons une excellente continuation.</p>
|
||||
|
||||
<p>Cordialement,<br/>L'équipe Formation</p>
|
||||
`;
|
||||
|
||||
return sendEmail({
|
||||
to: params.to,
|
||||
subject: `Votre attestation de formation - ${params.formationNom}`,
|
||||
html: await getEmailTemplate(content, 'attestation'),
|
||||
});
|
||||
}
|
||||
|
||||
186
server/gestionAttestationsDb.ts
Normal file
186
server/gestionAttestationsDb.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { eq, and, desc, sql } from "drizzle-orm";
|
||||
import { getDb } from "./db";
|
||||
import { formations, attestations, inscriptions, apprenants, sequences, datesFormation } from "../drizzle/schema";
|
||||
|
||||
/**
|
||||
* Récupérer la configuration d'une formation
|
||||
*/
|
||||
export async function getFormationConfig(formationId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return null;
|
||||
|
||||
const results = await db
|
||||
.select({
|
||||
id: formations.id,
|
||||
nom: formations.nom,
|
||||
modeAttestation: formations.modeAttestation,
|
||||
modeEnvoi: formations.modeEnvoi,
|
||||
})
|
||||
.from(formations)
|
||||
.where(eq(formations.id, formationId))
|
||||
.limit(1);
|
||||
|
||||
return results.length > 0 ? results[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mettre à jour la configuration d'une formation
|
||||
*/
|
||||
export async function updateFormationConfig(
|
||||
formationId: number,
|
||||
modeAttestation: "auto" | "manuel",
|
||||
modeEnvoi: "auto" | "manuel"
|
||||
) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
await db
|
||||
.update(formations)
|
||||
.set({
|
||||
modeAttestation,
|
||||
modeEnvoi,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(formations.id, formationId));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer toutes les attestations d'une formation avec les informations des apprenants
|
||||
*/
|
||||
export async function getAttestationsByFormation(formationId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
|
||||
const results = await db
|
||||
.select({
|
||||
attestation: attestations,
|
||||
apprenant: apprenants,
|
||||
sequence: sequences,
|
||||
})
|
||||
.from(attestations)
|
||||
.innerJoin(inscriptions, eq(attestations.inscriptionId, inscriptions.id))
|
||||
.innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id))
|
||||
.innerJoin(sequences, eq(inscriptions.sequenceId, sequences.id))
|
||||
.where(eq(sequences.formationId, formationId))
|
||||
.orderBy(desc(attestations.createdAt));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploader un document d'attestation pour un apprenant
|
||||
*/
|
||||
export async function uploadAttestationDocument(
|
||||
inscriptionId: number,
|
||||
documentUrl: string,
|
||||
documentS3Key: string,
|
||||
uploadedBy: number
|
||||
) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
// Vérifier si une attestation existe déjà pour cette inscription
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(attestations)
|
||||
.where(eq(attestations.inscriptionId, inscriptionId))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Mettre à jour l'attestation existante
|
||||
await db
|
||||
.update(attestations)
|
||||
.set({
|
||||
documentUrl,
|
||||
documentS3Key,
|
||||
uploadedBy,
|
||||
uploadedAt: new Date(),
|
||||
})
|
||||
.where(eq(attestations.inscriptionId, inscriptionId));
|
||||
|
||||
return existing[0].id;
|
||||
} else {
|
||||
// Créer une nouvelle attestation
|
||||
const result = await db
|
||||
.insert(attestations)
|
||||
.values({
|
||||
inscriptionId,
|
||||
documentUrl,
|
||||
documentS3Key,
|
||||
uploadedBy,
|
||||
uploadedAt: new Date(),
|
||||
})
|
||||
.$returningId();
|
||||
|
||||
return result[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Envoyer une attestation par email
|
||||
*/
|
||||
export async function markAttestationAsSent(attestationId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
await db
|
||||
.update(attestations)
|
||||
.set({
|
||||
emailEnvoye: true,
|
||||
dateEnvoiEmail: new Date(),
|
||||
})
|
||||
.where(eq(attestations.id, attestationId));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer les apprenants d'une formation avec leur statut d'attestation
|
||||
*/
|
||||
export async function getApprenantsWithAttestationStatus(formationId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) return [];
|
||||
|
||||
const results = await db
|
||||
.select({
|
||||
apprenant: apprenants,
|
||||
inscription: inscriptions,
|
||||
sequence: sequences,
|
||||
attestation: attestations,
|
||||
})
|
||||
.from(inscriptions)
|
||||
.innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id))
|
||||
.innerJoin(sequences, eq(inscriptions.sequenceId, sequences.id))
|
||||
.leftJoin(attestations, eq(attestations.inscriptionId, inscriptions.id))
|
||||
.where(
|
||||
and(
|
||||
eq(sequences.formationId, formationId),
|
||||
eq(inscriptions.statut, "confirmee")
|
||||
)
|
||||
)
|
||||
.orderBy(apprenants.nom, apprenants.prenom);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprimer un document d'attestation
|
||||
*/
|
||||
export async function deleteAttestationDocument(attestationId: number) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
|
||||
await db
|
||||
.update(attestations)
|
||||
.set({
|
||||
documentUrl: null,
|
||||
documentS3Key: null,
|
||||
uploadedBy: null,
|
||||
uploadedAt: null,
|
||||
})
|
||||
.where(eq(attestations.id, attestationId));
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -2281,6 +2281,95 @@ export const appRouter = router({
|
||||
return checkAllPresencesValidated(input.inscriptionId);
|
||||
}),
|
||||
}),
|
||||
|
||||
// ===== GESTION DES ATTESTATIONS =====
|
||||
gestionAttestations: router({
|
||||
// Récupérer la configuration d'une formation
|
||||
getFormationConfig: adminProcedure
|
||||
.input(z.object({ formationId: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
const { getFormationConfig } = await import("./gestionAttestationsDb");
|
||||
return getFormationConfig(input.formationId);
|
||||
}),
|
||||
|
||||
// Mettre à jour la configuration d'une formation
|
||||
updateFormationConfig: adminProcedure
|
||||
.input(z.object({
|
||||
formationId: z.number(),
|
||||
modeAttestation: z.enum(["auto", "manuel"]),
|
||||
modeEnvoi: z.enum(["auto", "manuel"]),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
const { updateFormationConfig } = await import("./gestionAttestationsDb");
|
||||
return updateFormationConfig(
|
||||
input.formationId,
|
||||
input.modeAttestation,
|
||||
input.modeEnvoi
|
||||
);
|
||||
}),
|
||||
|
||||
// Récupérer les apprenants avec leur statut d'attestation
|
||||
getApprenantsWithStatus: adminProcedure
|
||||
.input(z.object({ formationId: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
const { getApprenantsWithAttestationStatus } = await import("./gestionAttestationsDb");
|
||||
return getApprenantsWithAttestationStatus(input.formationId);
|
||||
}),
|
||||
|
||||
// Uploader un document d'attestation
|
||||
uploadDocument: adminProcedure
|
||||
.input(z.object({
|
||||
inscriptionId: z.number(),
|
||||
documentUrl: z.string(),
|
||||
documentS3Key: z.string(),
|
||||
}))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { uploadAttestationDocument } = await import("./gestionAttestationsDb");
|
||||
return uploadAttestationDocument(
|
||||
input.inscriptionId,
|
||||
input.documentUrl,
|
||||
input.documentS3Key,
|
||||
ctx.user.id
|
||||
);
|
||||
}),
|
||||
|
||||
// Supprimer un document d'attestation
|
||||
deleteDocument: adminProcedure
|
||||
.input(z.object({ attestationId: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
const { deleteAttestationDocument } = await import("./gestionAttestationsDb");
|
||||
return deleteAttestationDocument(input.attestationId);
|
||||
}),
|
||||
|
||||
// Envoyer une attestation par email
|
||||
sendAttestation: adminProcedure
|
||||
.input(z.object({
|
||||
attestationId: z.number(),
|
||||
apprenantEmail: z.string(),
|
||||
apprenantNom: z.string(),
|
||||
apprenantPrenom: z.string(),
|
||||
formationNom: z.string(),
|
||||
pdfUrl: z.string(),
|
||||
}))
|
||||
.mutation(async ({ input }) => {
|
||||
const { sendAttestationEmail } = await import("./emailService");
|
||||
const { markAttestationAsSent } = await import("./gestionAttestationsDb");
|
||||
|
||||
// Envoyer l'email
|
||||
await sendAttestationEmail({
|
||||
to: input.apprenantEmail,
|
||||
apprenantNom: input.apprenantNom,
|
||||
apprenantPrenom: input.apprenantPrenom,
|
||||
formationNom: input.formationNom,
|
||||
pdfUrl: input.pdfUrl,
|
||||
});
|
||||
|
||||
// Marquer comme envoyé
|
||||
await markAttestationAsSent(input.attestationId);
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
||||
Reference in New Issue
Block a user