/** * Service d'export des données en Excel et PDF */ import * as XLSX from 'xlsx'; import { jsPDF } from 'jspdf'; import autoTable from 'jspdf-autotable'; import * as fs from 'fs'; import * as path from 'path'; interface InscriptionExport { nom: string; prenom: string; email: string; codeEtablissement: string | null; fonction: string | null; statut: string; dateInscription: Date; } interface SequenceInfo { formationNom: string; sequenceNom: string; dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>; lieu: string; publicCible: string; inscriptions: InscriptionExport[]; } interface ApprenantPresence { nom: string; prenom: string; codeEtablissement: string | null; fonction: string | null; } interface FeuillePresenceInfo { formationNom: string; sequenceNom: string; dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>; lieu: string; publicCible: string; formateur?: string; apprenants: ApprenantPresence[]; } /** * Génère un fichier Excel avec la liste des inscrits */ export function generateExcelExport(sequenceInfo: SequenceInfo): Buffer { // Préparer les données const data = sequenceInfo.inscriptions.map(i => ({ 'Nom': i.nom, 'Prénom': i.prenom, 'Email': i.email, 'Code établissement': i.codeEtablissement || '', 'Fonction': i.fonction === 'directeur' ? 'Directeur' : i.fonction === 'chef_service' ? 'Chef de service' : 'Autre', 'Statut': i.statut === 'confirmee' ? 'Confirmée' : i.statut === 'liste_attente' ? 'Liste d\'attente' : 'Annulée', 'Date d\'inscription': i.dateInscription.toLocaleDateString('fr-FR'), })); // Créer le workbook const ws = XLSX.utils.json_to_sheet(data); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Inscrits'); // Ajouter une feuille d'informations const infoData = [ { 'Information': 'Formation', 'Valeur': sequenceInfo.formationNom }, { 'Information': 'Séquence', 'Valeur': sequenceInfo.sequenceNom }, { 'Information': 'Lieu', 'Valeur': sequenceInfo.lieu }, { 'Information': 'Public cible', 'Valeur': sequenceInfo.publicCible === 'directeur' ? 'Directeurs' : sequenceInfo.publicCible === 'chef_service' ? 'Chefs de service' : sequenceInfo.publicCible === 'tous' ? 'Tous' : 'Autre' }, { 'Information': 'Nombre d\'inscrits', 'Valeur': sequenceInfo.inscriptions.length.toString() }, ]; // Ajouter les dates sequenceInfo.dates.forEach(date => { infoData.push({ 'Information': `Date ${date.ordre}`, 'Valeur': date.dateDebut.toLocaleDateString('fr-FR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }) }); }); const wsInfo = XLSX.utils.json_to_sheet(infoData); XLSX.utils.book_append_sheet(wb, wsInfo, 'Informations'); // Générer le buffer return Buffer.from(XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' })); } /** * Génère un PDF avec la liste des inscrits */ export function generatePDFExport(sequenceInfo: SequenceInfo): Buffer { const doc = new jsPDF(); // Titre doc.setFontSize(18); doc.text('Liste des inscrits', 14, 20); // Informations de séquence doc.setFontSize(10); let yPos = 35; doc.text(`Formation : ${sequenceInfo.formationNom}`, 14, yPos); yPos += 6; doc.text(`Séquence : ${sequenceInfo.sequenceNom}`, 14, yPos); yPos += 6; doc.text(`Lieu : ${sequenceInfo.lieu}`, 14, yPos); yPos += 6; doc.text(`Public cible : ${sequenceInfo.publicCible === 'directeur' ? 'Directeurs' : sequenceInfo.publicCible === 'chef_service' ? 'Chefs de service' : sequenceInfo.publicCible === 'tous' ? 'Tous' : 'Autre'}`, 14, yPos); yPos += 8; // Dates de formation doc.setFontSize(9); sequenceInfo.dates.forEach(date => { doc.text(`Date ${date.ordre} : ${date.dateDebut.toLocaleDateString('fr-FR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' })}`, 14, yPos); yPos += 5; }); // Tableau des inscrits const tableData = sequenceInfo.inscriptions.map(i => [ i.nom, i.prenom, i.email, i.codeEtablissement || '', i.fonction === 'directeur' ? 'Directeur' : i.fonction === 'chef_service' ? 'Chef de service' : 'Autre', i.statut === 'confirmee' ? 'Confirmée' : i.statut === 'liste_attente' ? 'Liste d\'attente' : 'Annulée', ]); autoTable(doc, { startY: yPos + 10, head: [['Nom', 'Prénom', 'Email', 'Code étab.', 'Fonction', 'Statut']], body: tableData, styles: { fontSize: 9 }, headStyles: { fillColor: [37, 99, 235] }, }); return Buffer.from(doc.output('arraybuffer')); } /** * Génère une feuille de présence PDF avec signatures matin/après-midi pour chaque journée */ export function generateFeuillePresence(feuilleInfo: FeuillePresenceInfo): Buffer { const doc = new jsPDF(); // Charger le logo let logoData: string | null = null; try { // Utiliser un chemin absolu depuis la racine du projet const logoPath = path.resolve(process.cwd(), 'client/public/itinova-logo.png'); const logoBuffer = fs.readFileSync(logoPath); logoData = `data:image/png;base64,${logoBuffer.toString('base64')}`; } catch (error) { console.warn('Logo Itinova non trouvé, génération sans logo:', error); } // Fonction pour générer une page de feuille de présence pour une journée const generatePageForDate = (dateInfo: { dateDebut: Date; dateFin: Date; ordre: number }, isFirstPage: boolean) => { if (!isFirstPage) { doc.addPage(); } // Ajouter le logo en haut à droite si disponible if (logoData) { doc.addImage(logoData, 'PNG', 160, 10, 40, 15); } // Titre doc.setFontSize(18); doc.text('Feuille de présence', 14, 20); // Informations de séquence doc.setFontSize(10); let yPos = 35; doc.text(`Formation : ${feuilleInfo.formationNom}`, 14, yPos); yPos += 6; doc.text(`Séquence : ${feuilleInfo.sequenceNom}`, 14, yPos); yPos += 6; doc.text(`Lieu : ${feuilleInfo.lieu}`, 14, yPos); yPos += 6; // Ajouter le formateur si disponible if (feuilleInfo.formateur) { doc.text(`Formateur : ${feuilleInfo.formateur}`, 14, yPos); yPos += 6; } doc.text(`Public cible : ${feuilleInfo.publicCible === 'directeur' ? 'Directeurs' : feuilleInfo.publicCible === 'chef_service' ? 'Chefs de service' : feuilleInfo.publicCible === 'tous' ? 'Tous' : 'Autre'}`, 14, yPos); yPos += 8; // Date de la journée doc.setFontSize(12); doc.setFont('helvetica', 'bold'); doc.text(`Date ${dateInfo.ordre} : ${dateInfo.dateDebut.toLocaleDateString('fr-FR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' })}`, 14, yPos); doc.setFont('helvetica', 'normal'); yPos += 10; // Tableau de présence avec signatures matin et après-midi const tableData = feuilleInfo.apprenants.map(i => [ i.nom, i.prenom, i.codeEtablissement || '', i.fonction === 'directeur' ? 'Directeur' : i.fonction === 'chef_service' ? 'Chef de service' : 'Autre', '', // Signature matin '', // Signature après-midi ]); autoTable(doc, { startY: yPos, head: [['Nom', 'Prénom', 'Code étab.', 'Fonction', 'Signature\nMatin', 'Signature\nAprès-midi']], body: tableData, styles: { fontSize: 9, cellPadding: 3, minCellHeight: 10, }, headStyles: { fillColor: [37, 99, 235], halign: 'center', }, columnStyles: { 0: { cellWidth: 30 }, // Nom 1: { cellWidth: 30 }, // Prénom 2: { cellWidth: 25 }, // Code établissement 3: { cellWidth: 30 }, // Fonction 4: { cellWidth: 35 }, // Signature matin 5: { cellWidth: 35 }, // Signature après-midi }, }); // Ajouter un champ de signature pour le formateur en bas de page const finalY = (doc as any).lastAutoTable.finalY || yPos + 50; const signatureY = finalY + 20; doc.setFontSize(10); doc.setFont('helvetica', 'bold'); doc.text('Signature du formateur :', 14, signatureY); doc.setFont('helvetica', 'normal'); // Dessiner une ligne pour la signature doc.line(60, signatureY, 120, signatureY); // Ajouter la date doc.setFontSize(9); doc.text('Date : _______________', 140, signatureY); }; // Générer une page pour chaque date feuilleInfo.dates.forEach((date, index) => { generatePageForDate(date, index === 0); }); return Buffer.from(doc.output('arraybuffer')); }