Checkpoint: Intégration des signatures tactiles dans les feuilles de présence PDF :

 Modifications implémentées :
- Interface ApprenantPresence étendue pour inclure les présences avec signatures
- Fonction generateFeuillePresence modifiée pour charger et afficher les images de signatures
- Hook didDrawCell ajouté pour insérer les images dans les cellules du tableau PDF
- Procédure exportFeuillePresence modifiée pour récupérer toutes les présences avec signatures
- Signatures chargées depuis le système de fichiers local (uploads/signatures/)
- Gestion automatique des cas où la signature n'existe pas (cellule vide)
- Hauteur des cellules augmentée (minCellHeight: 15) pour accommoder les signatures

📋 Fonctionnement :
- Lors de l'export PDF, le système récupère toutes les présences de la séquence
- Pour chaque apprenant, les signatures sont associées à leurs dates respectives
- Les images PNG sont chargées depuis le disque et converties en base64
- Les signatures sont insérées dans les colonnes Matin/Après-midi correspondantes
- Si aucune signature n'existe, la cellule reste vide pour signature manuscrite

🔧 Technique :
- Utilisation de jsPDF avec jspdf-autotable
- Chargement des images avec fs.readFileSync
- Conversion en data URL base64 pour insertion dans le PDF
- Gestion des erreurs avec console.warn si fichier manquant
This commit is contained in:
Manus
2026-01-28 05:21:57 -05:00
parent a0141e3f71
commit 8f808980dc
3 changed files with 79 additions and 15 deletions

View File

@@ -32,6 +32,10 @@ interface ApprenantPresence {
prenom: string;
codeEtablissement: string | null;
fonction: string | null;
presences?: Array<{
dateFormationId: number;
signatureUrl: string | null;
}>;
}
interface FeuillePresenceInfo {
@@ -219,14 +223,19 @@ export function generateFeuillePresence(feuilleInfo: FeuillePresenceInfo): Buffe
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
]);
const tableData = feuilleInfo.apprenants.map(i => {
// Trouver les présences pour cette date
const presenceMatin = i.presences?.find(p => p.dateFormationId === dateInfo.ordre);
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)
'', // Signature après-midi (pour l'instant vide, sera implémenté si nécessaire)
];
});
autoTable(doc, {
startY: yPos,
@@ -235,7 +244,7 @@ export function generateFeuillePresence(feuilleInfo: FeuillePresenceInfo): Buffe
styles: {
fontSize: 9,
cellPadding: 3,
minCellHeight: 10,
minCellHeight: 15, // Augmenté pour les signatures
},
headStyles: {
fillColor: [37, 99, 235],
@@ -249,6 +258,35 @@ export function generateFeuillePresence(feuilleInfo: FeuillePresenceInfo): Buffe
4: { cellWidth: 35 }, // Signature matin
5: { cellWidth: 35 }, // 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.warn('Erreur lors du chargement de la signature:', error);
}
}
}
},
});
// Ajouter un champ de signature pour le formateur en bas de page

View File

@@ -1147,14 +1147,29 @@ export const appRouter = router({
}
}
// Récupérer toutes les présences avec signatures pour cette séquence
const { getPresencesBySequence } = await import("./presenceDb");
const presences = await getPresencesBySequence(input.sequenceId);
const data = inscriptions
.filter(i => i.inscription.statut === 'confirmee' && i.apprenant)
.map(i => ({
nom: i.apprenant!.nom,
prenom: i.apprenant!.prenom,
codeEtablissement: i.apprenant!.codeEtablissement,
fonction: i.apprenant!.fonction,
}));
.map(i => {
// Récupérer les présences de cet apprenant
const apprenantPresences = presences
.filter(p => p.inscriptionId === i.inscription.id)
.map(p => ({
dateFormationId: p.dateFormationId,
signatureUrl: p.signatureUrl,
}));
return {
nom: i.apprenant!.nom,
prenom: i.apprenant!.prenom,
codeEtablissement: i.apprenant!.codeEtablissement,
fonction: i.apprenant!.fonction,
presences: apprenantPresences,
};
});
const buffer = await generateFeuillePresence({
formationNom: formation.nom,

11
todo.md
View File

@@ -1343,3 +1343,14 @@
- [x] Tester localement avant déploiement (fonctionne en local)
- [x] Déployer sur VPS (rebuild complet effectué)
- [ ] Tester sur VPS après rebuild
## Intégration des signatures dans les feuilles de présence PDF
- [x] Analyser le code actuel de génération des feuilles de présence PDF
- [x] Modifier exportService.ts pour charger les images de signatures
- [x] Intégrer les signatures dans le layout du PDF (colonnes Matin/Après-midi)
- [x] Gérer les cas où la signature n'existe pas (affichage vide)
- [x] Modifier routers.ts pour récupérer les présences avec signatures
- [ ] Tester la génération avec et sans signatures
- [ ] Vérifier la qualité et la taille des signatures dans le PDF
- [ ] Créer un checkpoint