This commit is contained in:
Manus
2026-01-07 09:23:52 -05:00
parent 2eb352206f
commit cc3badffa3
14 changed files with 0 additions and 3098 deletions

View File

@@ -1,283 +0,0 @@
import { getDb } from "./db";
import { attestations, configAttestation, inscriptions, apprenants, sequences, formations, datesFormation } from "../drizzle/schema";
import { eq, and } from "drizzle-orm";
import { storagePut } from "./storage";
import PDFDocument from "pdfkit";
import { format } from "date-fns";
import { fr } from "date-fns/locale";
import crypto from "crypto";
/**
* Génère une attestation de formation en PDF
*/
export async function genererAttestationPDF(inscriptionId: number): Promise<{ s3Key: string; pdfUrl: string }> {
const db = await getDb();
if (!db) {
throw new Error("Base de données non disponible");
}
// Récupérer les données de l'inscription
const [inscriptionData] = await db
.select({
inscription: inscriptions,
apprenant: apprenants,
sequence: sequences,
formation: formations,
})
.from(inscriptions)
.innerJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id))
.innerJoin(sequences, eq(inscriptions.sequenceId, sequences.id))
.innerJoin(formations, eq(sequences.formationId, formations.id))
.where(eq(inscriptions.id, inscriptionId))
.limit(1);
if (!inscriptionData) {
throw new Error("Inscription introuvable");
}
const { inscription, apprenant, sequence, formation } = inscriptionData;
// Récupérer les dates de la séquence
const dates = await db
.select()
.from(datesFormation)
.where(eq(datesFormation.sequenceId, sequence.id))
.orderBy(datesFormation.dateDebut);
// Récupérer la configuration de l'attestation
const [config] = await db
.select()
.from(configAttestation)
.limit(1);
// Créer le PDF
const doc = new PDFDocument({
size: "A4",
margins: { top: 50, bottom: 50, left: 50, right: 50 },
});
const chunks: Buffer[] = [];
doc.on("data", (chunk: Buffer) => chunks.push(chunk));
return new Promise((resolve, reject) => {
doc.on("end", async () => {
try {
const pdfBuffer = Buffer.concat(chunks);
// Générer une clé S3 unique
const randomSuffix = crypto.randomBytes(8).toString("hex");
const s3Key = `attestations/${apprenant.id}-${sequence.id}-${randomSuffix}.pdf`;
// Upload vers S3
const { url } = await storagePut(s3Key, pdfBuffer, "application/pdf");
resolve({ s3Key, pdfUrl: url });
} catch (error) {
reject(error);
}
});
doc.on("error", reject);
// Construction du PDF
try {
// Logo (si configuré)
if (config?.logoUrl) {
// TODO: télécharger et insérer le logo
// doc.image(logoPath, 50, 45, { width: 100 });
}
// Titre
doc.fontSize(24).font("Helvetica-Bold").text("ATTESTATION DE FORMATION", { align: "center" });
doc.moveDown(2);
// Texte personnalisable ou texte par défaut
const texteAttestation = config?.texteAttestation ||
"Nous attestons que {{nomComplet}} a suivi avec assiduité la formation \"{{nomFormation}}\" organisée du {{dateDebut}} au {{dateFin}}.";
// Remplacer les variables
const nomComplet = `${apprenant.prenom} ${apprenant.nom}`;
const dateDebut = dates.length > 0 ? format(new Date(dates[0].dateDebut), "dd MMMM yyyy", { locale: fr }) : "";
const dateFin = dates.length > 0 ? format(new Date(dates[dates.length - 1].dateFin), "dd MMMM yyyy", { locale: fr }) : "";
const texteRempli = texteAttestation
.replace(/\{\{nomComplet\}\}/g, nomComplet)
.replace(/\{\{nomFormation\}\}/g, formation.nom)
.replace(/\{\{dateDebut\}\}/g, dateDebut)
.replace(/\{\{dateFin\}\}/g, dateFin);
doc.fontSize(12).font("Helvetica").text(texteRempli, { align: "justify" });
doc.moveDown(2);
// Détails de la formation
doc.fontSize(10).font("Helvetica-Bold").text("Détails de la formation :", { underline: true });
doc.moveDown(0.5);
doc.font("Helvetica");
doc.text(`Formation : ${formation.nom}`);
doc.text(`Séquence : ${sequence.nom}`);
doc.text(`Lieu : ${sequence.lieu}`);
doc.moveDown(0.5);
doc.text("Dates :");
dates.forEach((date) => {
const dateStr = format(new Date(date.dateDebut), "dd/MM/yyyy", { locale: fr });
const heureDebut = format(new Date(date.dateDebut), "HH:mm");
const heureFin = format(new Date(date.dateFin), "HH:mm");
doc.text(`${dateStr} de ${heureDebut} à ${heureFin}`);
});
doc.moveDown(3);
// Signature
doc.fontSize(10);
doc.text(`Fait le ${format(new Date(), "dd MMMM yyyy", { locale: fr })}`, { align: "right" });
doc.moveDown(1);
if (config?.signatureUrl) {
// TODO: télécharger et insérer la signature
// doc.image(signaturePath, 400, doc.y, { width: 100 });
doc.moveDown(3);
}
if (config?.nomSignataire) {
doc.text(config.nomSignataire, { align: "right" });
}
if (config?.fonctionSignataire) {
doc.text(config.fonctionSignataire, { align: "right" });
}
doc.end();
} catch (error) {
doc.end();
reject(error);
}
});
}
/**
* Enregistre une attestation dans la base de données
*/
export async function enregistrerAttestation(
inscriptionId: number,
apprenantId: number,
sequenceId: number,
s3Key: string,
pdfUrl: string
): Promise<number> {
const db = await getDb();
if (!db) {
throw new Error("Base de données non disponible");
}
const [result] = await db.insert(attestations).values({
inscriptionId,
apprenantId,
sequenceId,
s3Key,
pdfUrl,
emailEnvoye: false,
});
return result.insertId;
}
/**
* Récupère l'attestation d'un apprenant pour une séquence
*/
export async function getAttestation(apprenantId: number, sequenceId: number) {
const db = await getDb();
if (!db) return null;
const [attestation] = await db
.select()
.from(attestations)
.where(
and(
eq(attestations.apprenantId, apprenantId),
eq(attestations.sequenceId, sequenceId)
)
)
.limit(1);
return attestation || null;
}
/**
* Récupère toutes les attestations d'un apprenant
*/
export async function getAttestationsApprenant(apprenantId: number) {
const db = await getDb();
if (!db) return [];
return db
.select({
attestation: attestations,
sequence: sequences,
formation: formations,
})
.from(attestations)
.innerJoin(sequences, eq(attestations.sequenceId, sequences.id))
.innerJoin(formations, eq(sequences.formationId, formations.id))
.where(eq(attestations.apprenantId, apprenantId))
.orderBy(attestations.dateGeneration);
}
/**
* Marque une attestation comme envoyée par email
*/
export async function marquerAttestationEnvoyee(attestationId: number) {
const db = await getDb();
if (!db) return;
await db
.update(attestations)
.set({
emailEnvoye: true,
dateEnvoiEmail: new Date(),
})
.where(eq(attestations.id, attestationId));
}
/**
* Récupère ou crée la configuration des attestations
*/
export async function getOrCreateConfigAttestation() {
const db = await getDb();
if (!db) return null;
let [config] = await db.select().from(configAttestation).limit(1);
if (!config) {
// Créer une configuration par défaut
await db.insert(configAttestation).values({
texteAttestation: "Nous attestons que {{nomComplet}} a suivi avec assiduité la formation \"{{nomFormation}}\" organisée du {{dateDebut}} au {{dateFin}}.",
});
[config] = await db.select().from(configAttestation).limit(1);
}
return config;
}
/**
* Met à jour la configuration des attestations
*/
export async function updateConfigAttestation(data: {
logoS3Key?: string;
logoUrl?: string;
signatureS3Key?: string;
signatureUrl?: string;
nomSignataire?: string;
fonctionSignataire?: string;
texteAttestation?: string;
}) {
const db = await getDb();
if (!db) return;
const [existing] = await db.select().from(configAttestation).limit(1);
if (existing) {
await db.update(configAttestation).set(data).where(eq(configAttestation.id, existing.id));
} else {
await db.insert(configAttestation).values(data);
}
}

View File

@@ -1736,105 +1736,6 @@ 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;