625 lines
23 KiB
TypeScript
625 lines
23 KiB
TypeScript
/**
|
||
* 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';
|
||
import { PNG } from 'pngjs';
|
||
|
||
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;
|
||
presences?: Array<{
|
||
dateFormationId: number;
|
||
periode: 'matin' | 'apres_midi';
|
||
signatureUrl: string | null;
|
||
}>;
|
||
}
|
||
|
||
interface FeuillePresenceInfo {
|
||
formationNom: string;
|
||
sequenceNom: string;
|
||
dates: Array<{ id: number; dateDebut: Date; dateFin: Date; ordre: number }>;
|
||
lieu: string;
|
||
publicCible: string;
|
||
formateur?: string;
|
||
apprenants: ApprenantPresence[];
|
||
signaturesFormateurs?: Array<{
|
||
dateFormationId: number;
|
||
periode: 'matin' | 'apres_midi';
|
||
signatureUrl: string | null;
|
||
}>;
|
||
}
|
||
|
||
/**
|
||
* 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',
|
||
timeZone: 'Europe/Paris'
|
||
})
|
||
});
|
||
});
|
||
|
||
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',
|
||
timeZone: 'Europe/Paris'
|
||
})}`, 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 un PDF de feuille de présence pour UN SEUL jour (sections matin + après-midi + signatures formateur)
|
||
* Retourne un Buffer PDF
|
||
*/
|
||
/**
|
||
* Cache des images compressées pour le PDF.
|
||
* Clé = chemin absolu du fichier, valeur = data URI JPEG compressé ou null.
|
||
*/
|
||
const _imageCache = new Map<string, string | null>();
|
||
|
||
/**
|
||
* Charge et compresse une image (PNG/JPEG) en JPEG 50% max 300x100px.
|
||
* Utilise un cache pour éviter de retraiter plusieurs fois le même fichier.
|
||
* SYNCHRONE : utilise le cache pré-rempli par preloadSignatures().
|
||
*/
|
||
function loadImageForPdf(filePath: string): string | null {
|
||
return _imageCache.get(filePath) ?? null;
|
||
}
|
||
|
||
/**
|
||
* Pré-charge et compresse toutes les signatures en JPEG avant génération PDF.
|
||
* À appeler en async avant generateFeuillePresenceJour().
|
||
*/
|
||
async function preloadSignatures(paths: string[]): Promise<void> {
|
||
await Promise.all(paths.map(async (filePath) => {
|
||
if (_imageCache.has(filePath)) return;
|
||
try {
|
||
if (!fs.existsSync(filePath)) { _imageCache.set(filePath, null); return; }
|
||
const raw = fs.readFileSync(filePath);
|
||
const isJpeg = raw[0] === 0xFF && raw[1] === 0xD8;
|
||
if (isJpeg) {
|
||
// JPEG : pas de redimensionnement, juste encoder en base64
|
||
_imageCache.set(filePath, `data:image/jpeg;base64,${raw.toString('base64')}`);
|
||
return;
|
||
}
|
||
// PNG : lire, redimensionner à max 500x160, ré-encoder
|
||
try {
|
||
const src = PNG.sync.read(raw);
|
||
const maxW = 500, maxH = 160;
|
||
const ratio = Math.min(maxW / src.width, maxH / src.height, 1); // ne pas agrandir
|
||
if (ratio >= 0.95) {
|
||
// Déjà petite, encoder directement
|
||
_imageCache.set(filePath, `data:image/png;base64,${raw.toString('base64')}`);
|
||
return;
|
||
}
|
||
const newW = Math.max(1, Math.round(src.width * ratio));
|
||
const newH = Math.max(1, Math.round(src.height * ratio));
|
||
const dst = new PNG({ width: newW, height: newH });
|
||
for (let y = 0; y < newH; y++) {
|
||
for (let x = 0; x < newW; x++) {
|
||
const srcX = Math.min(src.width - 1, Math.floor(x / ratio));
|
||
const srcY = Math.min(src.height - 1, Math.floor(y / ratio));
|
||
const si = (srcY * src.width + srcX) * 4;
|
||
const di = (y * newW + x) * 4;
|
||
dst.data[di] = src.data[si];
|
||
dst.data[di+1] = src.data[si+1];
|
||
dst.data[di+2] = src.data[si+2];
|
||
dst.data[di+3] = src.data[si+3];
|
||
}
|
||
}
|
||
const out = PNG.sync.write(dst, { colorType: 2, filterType: 4 });
|
||
_imageCache.set(filePath, `data:image/png;base64,${out.toString('base64')}`);
|
||
} catch {
|
||
// Fallback : encoder le PNG brut
|
||
_imageCache.set(filePath, `data:image/png;base64,${raw.toString('base64')}`);
|
||
}
|
||
} catch { _imageCache.set(filePath, null); }
|
||
}));
|
||
}
|
||
|
||
export function generateFeuillePresenceJour(
|
||
feuilleInfo: FeuillePresenceInfo,
|
||
dateInfo: { id: number; dateDebut: Date; dateFin: Date; ordre: number }
|
||
): Buffer {
|
||
// Format A4 portrait : largeur utile ~190mm (210 - 10*2)
|
||
const doc = new jsPDF({ orientation: 'portrait', format: 'a4' });
|
||
|
||
// Charger le logo (avec cache)
|
||
let logoData: string | null = null;
|
||
try {
|
||
const logoPath = path.resolve(process.cwd(), 'client/public/itinova-logo.png');
|
||
logoData = loadImageForPdf(logoPath);
|
||
} catch (error) {
|
||
console.warn('Logo Itinova non trouvé, génération sans logo:', error);
|
||
}
|
||
|
||
const loadSignatureBase64 = (signatureUrl: string | null): string | null => {
|
||
if (!signatureUrl) return null;
|
||
const fp = path.resolve(process.cwd(), signatureUrl.startsWith('/') ? signatureUrl.substring(1) : signatureUrl);
|
||
return loadImageForPdf(fp);
|
||
};
|
||
|
||
const publicCibleLabel =
|
||
feuilleInfo.publicCible === 'directeur' ? 'Directeurs' :
|
||
feuilleInfo.publicCible === 'chef_service' ? 'Chefs de service' :
|
||
feuilleInfo.publicCible === 'tous' ? 'Tous' : 'Autre';
|
||
|
||
const fonctionLabel = (f: string | null) =>
|
||
f === 'directeur' ? 'Directeur' : f === 'chef_service' ? 'Chef de service' : 'Autre';
|
||
|
||
const dateLabel = dateInfo.dateDebut.toLocaleDateString('fr-FR', {
|
||
weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
|
||
});
|
||
|
||
// ── En-tête ──────────────────────────────────────────────────────────────
|
||
if (logoData) doc.addImage(logoData, 'PNG', 160, 8, 40, 15);
|
||
|
||
doc.setFontSize(16);
|
||
doc.setFont('helvetica', 'bold');
|
||
doc.text('Feuille de présence', 10, 18);
|
||
doc.setFont('helvetica', 'normal');
|
||
|
||
doc.setFontSize(9);
|
||
let yPos = 28;
|
||
doc.text(`Formation : ${feuilleInfo.formationNom}`, 10, yPos); yPos += 5;
|
||
doc.text(`Séquence : ${feuilleInfo.sequenceNom}`, 10, yPos); yPos += 5;
|
||
doc.text(`Lieu : ${feuilleInfo.lieu}`, 10, yPos); yPos += 5;
|
||
if (feuilleInfo.formateur) { doc.text(`Formateur : ${feuilleInfo.formateur}`, 10, yPos); yPos += 5; }
|
||
doc.text(`Public cible : ${publicCibleLabel}`, 10, yPos); yPos += 5;
|
||
|
||
doc.setFontSize(11);
|
||
doc.setFont('helvetica', 'bold');
|
||
doc.text(`Jour ${dateInfo.ordre} – ${dateLabel}`, 10, yPos + 3);
|
||
doc.setFont('helvetica', 'normal');
|
||
yPos += 10;
|
||
|
||
// ── Tableau unique avec colonnes Signature Matin + Signature Après-midi ──
|
||
const tableData = feuilleInfo.apprenants.map(apprenant => {
|
||
const presenceMatin = apprenant.presences?.find(
|
||
p => p.dateFormationId === dateInfo.id && p.periode === 'matin'
|
||
);
|
||
const presenceApresMidi = apprenant.presences?.find(
|
||
p => p.dateFormationId === dateInfo.id && p.periode === 'apres_midi'
|
||
);
|
||
return [
|
||
apprenant.nom,
|
||
apprenant.prenom,
|
||
apprenant.codeEtablissement || '',
|
||
fonctionLabel(apprenant.fonction),
|
||
presenceMatin?.signatureUrl || '', // col 4 : sig matin
|
||
presenceApresMidi?.signatureUrl || '', // col 5 : sig après-midi
|
||
];
|
||
});
|
||
|
||
autoTable(doc, {
|
||
startY: yPos,
|
||
head: [['Nom', 'Prénom', 'Code étab.', 'Fonction', 'Signature\nMatin', 'Signature\nAprès-midi']],
|
||
body: tableData,
|
||
margin: { left: 10, right: 10 },
|
||
styles: { fontSize: 8, cellPadding: 2, minCellHeight: 18, overflow: 'linebreak' },
|
||
headStyles: { fillColor: [37, 99, 235], halign: 'center', fontSize: 8 },
|
||
columnStyles: {
|
||
0: { cellWidth: 28 }, // Nom
|
||
1: { cellWidth: 28 }, // Prénom
|
||
2: { cellWidth: 20 }, // Code étab.
|
||
3: { cellWidth: 24 }, // Fonction
|
||
4: { cellWidth: 45 }, // Signature Matin
|
||
5: { cellWidth: 45 }, // Signature Après-midi
|
||
},
|
||
didDrawCell: (data) => {
|
||
if (data.section === 'body' && (data.column.index === 4 || data.column.index === 5)) {
|
||
const sig = loadSignatureBase64(data.cell.raw as string);
|
||
if (sig) {
|
||
doc.addImage(sig, 'PNG', data.cell.x + 2, data.cell.y + 2, data.cell.width - 4, data.cell.height - 4);
|
||
}
|
||
}
|
||
},
|
||
});
|
||
|
||
// ── Signatures formateur (Matin et Après-midi sur la même ligne) ──────────
|
||
const finalY = (doc as any).lastAutoTable.finalY || yPos + 50;
|
||
const sigY = finalY + 10;
|
||
|
||
doc.setFontSize(9);
|
||
doc.setFont('helvetica', 'bold');
|
||
doc.text('Signatures du formateur :', 10, sigY);
|
||
doc.setFont('helvetica', 'normal');
|
||
|
||
// Matin (gauche)
|
||
doc.text('Matin :', 10, sigY + 8);
|
||
const sigMatinFormateur = feuilleInfo.signaturesFormateurs?.find(
|
||
s => s.dateFormationId === dateInfo.id && s.periode === 'matin'
|
||
);
|
||
const sigMatinBase64 = loadSignatureBase64(sigMatinFormateur?.signatureUrl ?? null);
|
||
if (sigMatinBase64) {
|
||
doc.addImage(sigMatinBase64, 'PNG', 30, sigY + 2, 60, 14);
|
||
} else {
|
||
doc.line(30, sigY + 8, 90, sigY + 8);
|
||
}
|
||
|
||
// Après-midi (droite)
|
||
doc.text('Après-midi :', 110, sigY + 8);
|
||
const sigApresMidiFormateur = feuilleInfo.signaturesFormateurs?.find(
|
||
s => s.dateFormationId === dateInfo.id && s.periode === 'apres_midi'
|
||
);
|
||
const sigApresMidiBase64 = loadSignatureBase64(sigApresMidiFormateur?.signatureUrl ?? null);
|
||
if (sigApresMidiBase64) {
|
||
doc.addImage(sigApresMidiBase64, 'PNG', 140, sigY + 2, 60, 14);
|
||
} else {
|
||
doc.line(140, sigY + 8, 200, sigY + 8);
|
||
}
|
||
|
||
return Buffer.from(doc.output('arraybuffer'));
|
||
}
|
||
|
||
/**
|
||
* Génère un tableau de { ordre, filename, buffer } – un PDF par jour
|
||
* Pré-charge et compresse toutes les signatures avant génération.
|
||
*/
|
||
export async function generateFeuillePresenceParJour(
|
||
feuilleInfo: FeuillePresenceInfo
|
||
): Promise<Array<{ ordre: number; filename: string; buffer: Buffer }>> {
|
||
// Collecter tous les chemins d'images à pré-charger
|
||
const imagePaths: string[] = [];
|
||
// Logo
|
||
const logoPath = path.resolve(process.cwd(), 'client/public/itinova-logo.png');
|
||
if (fs.existsSync(logoPath)) imagePaths.push(logoPath);
|
||
// Signatures apprenants
|
||
feuilleInfo.apprenants.forEach(a => {
|
||
a.presences?.forEach(p => {
|
||
if (p.signatureUrl) {
|
||
const fp = path.resolve(process.cwd(), p.signatureUrl.startsWith('/') ? p.signatureUrl.substring(1) : p.signatureUrl);
|
||
if (fs.existsSync(fp)) imagePaths.push(fp);
|
||
}
|
||
});
|
||
});
|
||
// Signatures formateurs
|
||
feuilleInfo.signaturesFormateurs?.forEach(s => {
|
||
if (s.signatureUrl) {
|
||
const fp = path.resolve(process.cwd(), s.signatureUrl.startsWith('/') ? s.signatureUrl.substring(1) : s.signatureUrl);
|
||
if (fs.existsSync(fp)) imagePaths.push(fp);
|
||
}
|
||
});
|
||
// Pré-charger et compresser toutes les images en parallèle
|
||
await preloadSignatures([...new Set(imagePaths)]);
|
||
|
||
const results = feuilleInfo.dates.map(dateInfo => {
|
||
const buffer = generateFeuillePresenceJour(feuilleInfo, dateInfo);
|
||
// Format : feuille_presence_Groupe A_jour3_27-02-2026.pdf
|
||
const dateStr = dateInfo.dateDebut.toLocaleDateString('fr-FR', {
|
||
day: '2-digit', month: '2-digit', year: 'numeric'
|
||
}).replace(/\//g, '-'); // ex: 27-02-2026
|
||
const sequenceNomSafe = feuilleInfo.sequenceNom.replace(/[/\\:*?"<>|]/g, '_');
|
||
const filename = `feuille_presence_${sequenceNomSafe}_jour${dateInfo.ordre}_${dateStr}.pdf`;
|
||
return { ordre: dateInfo.ordre, filename, buffer };
|
||
});
|
||
return results;
|
||
}
|
||
|
||
/**
|
||
* Génère une feuille de présence PDF avec signatures matin/après-midi pour chaque journée
|
||
* @deprecated Utiliser generateFeuillePresenceParJour à la place
|
||
*/
|
||
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',
|
||
timeZone: 'Europe/Paris'
|
||
})}`, 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 => {
|
||
// Trouver les présences pour cette date (utiliser l'ID de la date, pas l'ordre)
|
||
const presenceMatin = i.presences?.find(p => p.dateFormationId === dateInfo.id && p.periode === 'matin');
|
||
const presenceApresMidi = i.presences?.find(p => p.dateFormationId === dateInfo.id && p.periode === 'apres_midi');
|
||
|
||
return [
|
||
i.nom,
|
||
i.prenom,
|
||
i.codeEtablissement || '',
|
||
i.fonction === 'directeur' ? 'Directeur' : i.fonction === 'chef_service' ? 'Chef de service' : 'Autre',
|
||
presenceMatin?.signatureUrl || '', // Signature matin (sera remplacée par l'image)
|
||
presenceApresMidi?.signatureUrl || '', // Signature après-midi (sera remplacée par l'image)
|
||
];
|
||
});
|
||
|
||
autoTable(doc, {
|
||
startY: yPos,
|
||
head: [['Nom', 'Prénom', 'Code étab.', 'Fonction', 'Signature\nMatin', 'Signature\nAprès-midi']],
|
||
body: tableData,
|
||
margin: { left: 10, right: 10 }, // Marges réduites pour maximiser la largeur
|
||
styles: {
|
||
fontSize: 8,
|
||
cellPadding: 2,
|
||
minCellHeight: 15, // Augmenté pour les signatures
|
||
overflow: 'linebreak',
|
||
},
|
||
headStyles: {
|
||
fillColor: [37, 99, 235],
|
||
halign: 'center',
|
||
fontSize: 8,
|
||
},
|
||
columnStyles: {
|
||
0: { cellWidth: 28 }, // Nom
|
||
1: { cellWidth: 28 }, // Prénom
|
||
2: { cellWidth: 20 }, // Code établissement
|
||
3: { cellWidth: 24 }, // Fonction
|
||
4: { cellWidth: 40 }, // Signature matin
|
||
5: { cellWidth: 40 }, // Signature après-midi
|
||
},
|
||
didDrawCell: (data) => {
|
||
// Insérer les images de signatures dans les colonnes 4 et 5
|
||
if (data.section === 'body' && (data.column.index === 4 || data.column.index === 5)) {
|
||
const signatureUrl = data.cell.raw as string;
|
||
|
||
if (signatureUrl && signatureUrl.startsWith('/uploads/signatures/')) {
|
||
try {
|
||
// Construire le chemin absolu vers le fichier de signature
|
||
const signaturePath = path.resolve(process.cwd(), signatureUrl.substring(1)); // Enlever le / du début
|
||
|
||
// Vérifier si le fichier existe
|
||
if (fs.existsSync(signaturePath)) {
|
||
// Charger l'image
|
||
const signatureBuffer = fs.readFileSync(signaturePath);
|
||
const signatureBase64 = `data:image/png;base64,${signatureBuffer.toString('base64')}`;
|
||
|
||
// Insérer l'image dans la cellule
|
||
const cellX = data.cell.x + 2;
|
||
const cellY = data.cell.y + 2;
|
||
const cellWidth = data.cell.width - 4;
|
||
const cellHeight = data.cell.height - 4;
|
||
|
||
doc.addImage(signatureBase64, 'PNG', cellX, cellY, cellWidth, cellHeight);
|
||
}
|
||
} catch (error) {
|
||
console.error('Erreur lors du chargement de la signature:', error);
|
||
}
|
||
}
|
||
}
|
||
},
|
||
});
|
||
|
||
// Ajouter deux champs de signature pour le formateur (Matin et Après-midi)
|
||
const finalY = (doc as any).lastAutoTable.finalY || yPos + 50;
|
||
const signatureY = finalY + 20;
|
||
|
||
doc.setFontSize(10);
|
||
doc.setFont('helvetica', 'bold');
|
||
doc.text('Signatures du formateur :', 14, signatureY);
|
||
doc.setFont('helvetica', 'normal');
|
||
|
||
// Récupérer les signatures formateurs pour cette date
|
||
const signatureMatin = feuilleInfo.signaturesFormateurs?.find(
|
||
s => s.dateFormationId === dateInfo.id && s.periode === 'matin'
|
||
);
|
||
const signatureApresMidi = feuilleInfo.signaturesFormateurs?.find(
|
||
s => s.dateFormationId === dateInfo.id && s.periode === 'apres_midi'
|
||
);
|
||
|
||
// Signature Matin
|
||
doc.setFontSize(9);
|
||
doc.text('Matin :', 14, signatureY + 8);
|
||
|
||
if (signatureMatin?.signatureUrl) {
|
||
try {
|
||
const signaturePath = path.resolve(process.cwd(), signatureMatin.signatureUrl.substring(1));
|
||
if (fs.existsSync(signaturePath)) {
|
||
const signatureBuffer = fs.readFileSync(signaturePath);
|
||
const signatureBase64 = `data:image/png;base64,${signatureBuffer.toString('base64')}`;
|
||
doc.addImage(signatureBase64, 'PNG', 30, signatureY + 2, 60, 15);
|
||
} else {
|
||
doc.line(30, signatureY + 8, 90, signatureY + 8);
|
||
}
|
||
} catch (error) {
|
||
console.error('Erreur lors du chargement de la signature formateur matin:', error);
|
||
doc.line(30, signatureY + 8, 90, signatureY + 8);
|
||
}
|
||
} else {
|
||
doc.line(30, signatureY + 8, 90, signatureY + 8);
|
||
}
|
||
|
||
// Signature Après-midi
|
||
doc.text('Après-midi :', 110, signatureY + 8);
|
||
|
||
if (signatureApresMidi?.signatureUrl) {
|
||
try {
|
||
const signaturePath = path.resolve(process.cwd(), signatureApresMidi.signatureUrl.substring(1));
|
||
if (fs.existsSync(signaturePath)) {
|
||
const signatureBuffer = fs.readFileSync(signaturePath);
|
||
const signatureBase64 = `data:image/png;base64,${signatureBuffer.toString('base64')}`;
|
||
doc.addImage(signatureBase64, 'PNG', 140, signatureY + 2, 60, 15);
|
||
} else {
|
||
doc.line(140, signatureY + 8, 200, signatureY + 8);
|
||
}
|
||
} catch (error) {
|
||
console.error('Erreur lors du chargement de la signature formateur après-midi:', error);
|
||
doc.line(140, signatureY + 8, 200, signatureY + 8);
|
||
}
|
||
} else {
|
||
doc.line(140, signatureY + 8, 200, signatureY + 8);
|
||
}
|
||
};
|
||
|
||
// Générer une page pour chaque date
|
||
feuilleInfo.dates.forEach((date, index) => {
|
||
generatePageForDate(date, index === 0);
|
||
});
|
||
|
||
return Buffer.from(doc.output('arraybuffer'));
|
||
}
|