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)); // 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 (async () => { try { // Logo en haut à droite si disponible if (config?.logoUrl) { try { const logoResponse = await fetch(config.logoUrl); const logoBuffer = Buffer.from(await logoResponse.arrayBuffer()); doc.image(logoBuffer, doc.page.width - 150, 50, { width: 100 }); } catch (error) { console.error("Erreur lors du chargement du logo:", error); } } // 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(3); // Signature si disponible if (config?.signatureUrl) { try { const signatureResponse = await fetch(config.signatureUrl); const signatureBuffer = Buffer.from(await signatureResponse.arrayBuffer()); const signatureX = doc.page.width - 200; const signatureY = doc.y; doc.image(signatureBuffer, signatureX, signatureY, { width: 150, height: 50 }); doc.moveDown(3); } catch (error) { console.error("Erreur lors du chargement de la signature:", error); } } 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 { 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)); } /** * 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); } } /** * Génère une prévisualisation du modèle d'attestation avec des données fictives */ export async function genererPreviewAttestation(): Promise<{ pdfUrl: string }> { const db = await getDb(); if (!db) { throw new Error("Base de données non disponible"); } // Récupérer la configuration de l'attestation const config = await getOrCreateConfigAttestation(); if (!config) { throw new Error("Configuration d'attestation introuvable"); } // Données fictives pour la prévisualisation const donneesFictives = { apprenant: { nom: "Dupont", prenom: "Jean", email: "jean.dupont@example.com", }, formation: { nom: "Formation Manager Itinova", nbJours: 5, }, sequence: { nom: "Groupe A", lieu: "Paris", }, dates: [ { date: new Date("2026-02-10") }, { date: new Date("2026-02-11") }, { date: new Date("2026-02-12") }, { date: new Date("2026-02-13") }, { date: new Date("2026-02-14") }, ], }; // Créer le PDF const doc = new PDFDocument({ size: "A4", margin: 50 }); const chunks: Buffer[] = []; doc.on("data", (chunk) => chunks.push(chunk)); // En-tête avec logo si disponible if (config.logoUrl) { try { const response = await fetch(config.logoUrl); const arrayBuffer = await response.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); doc.image(buffer, 50, 45, { width: 100 }); } catch (error) { console.error("Erreur lors du chargement du logo:", error); } } // Titre doc .fontSize(24) .font("Helvetica-Bold") .text("ATTESTATION DE FORMATION", 0, 150, { align: "center" }); doc.moveDown(2); // Texte de l'attestation avec remplacement des variables let texte = config.texteAttestation || "Nous attestons que {{nomComplet}} a suivi avec assiduité la formation \"{{nomFormation}}\" organisée du {{dateDebut}} au {{dateFin}}."; const dateDebut = format(donneesFictives.dates[0].date, "d MMMM yyyy", { locale: fr }); const dateFin = format(donneesFictives.dates[donneesFictives.dates.length - 1].date, "d MMMM yyyy", { locale: fr }); texte = texte .replace(/\{\{nomComplet\}\}/g, `${donneesFictives.apprenant.prenom} ${donneesFictives.apprenant.nom}`) .replace(/\{\{nomFormation\}\}/g, donneesFictives.formation.nom) .replace(/\{\{dateDebut\}\}/g, dateDebut) .replace(/\{\{dateFin\}\}/g, dateFin) .replace(/\{\{lieu\}\}/g, donneesFictives.sequence.lieu) .replace(/\{\{nbJours\}\}/g, donneesFictives.formation.nbJours.toString()); doc .fontSize(12) .font("Helvetica") .text(texte, { align: "justify", lineGap: 5 }); doc.moveDown(2); // Dates de formation doc.fontSize(10).font("Helvetica-Bold").text("Dates de formation :"); doc.font("Helvetica"); donneesFictives.dates.forEach((d) => { doc.text(`• ${format(d.date, "EEEE d MMMM yyyy", { locale: fr })}`, { indent: 20 }); }); doc.moveDown(2); // Signature doc.fontSize(10).font("Helvetica").text(`Fait à ${donneesFictives.sequence.lieu}, le ${format(new Date(), "d MMMM yyyy", { locale: fr })}`); doc.moveDown(1); if (config.signatureUrl) { try { const response = await fetch(config.signatureUrl); const arrayBuffer = await response.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); doc.image(buffer, doc.x, doc.y, { width: 150 }); doc.moveDown(3); } catch (error) { console.error("Erreur lors du chargement de la signature:", error); } } if (config.nomSignataire) { doc.font("Helvetica-Bold").text(config.nomSignataire); } if (config.fonctionSignataire) { doc.font("Helvetica").text(config.fonctionSignataire); } // Finaliser le PDF doc.end(); await new Promise((resolve) => doc.on("end", resolve)); const pdfBuffer = Buffer.concat(chunks); // Uploader sur S3 const randomSuffix = crypto.randomBytes(8).toString("hex"); const s3Key = `attestations/preview/preview-${randomSuffix}.pdf`; const { url: pdfUrl } = await storagePut(s3Key, pdfBuffer, "application/pdf"); return { pdfUrl }; }