Checkpoint: Implémentation de l'émargement formateur par QR code avec signature tactile

Modifications principales :
- Ajout des champs typeUtilisateur et formateurId dans la table presences pour distinguer formateurs et apprenants
- Détection automatique du rôle (formateur/apprenant) via l'email lors du scan du QR code
- Interface de signature tactile réutilisée pour les formateurs
- Intégration des signatures tactiles formateur dans la génération PDF de la feuille de présence
- QR code dans l'espace formateur accessible pour l'émargement
- Tests unitaires validés avec succès

Le système permet maintenant aux formateurs d'émarger (matin et après-midi) via le même QR code que les apprenants, avec détection automatique du rôle et intégration des signatures dans le PDF.
This commit is contained in:
Manus
2026-02-02 13:24:08 -05:00
parent eebcb64848
commit e8410230a9
11 changed files with 2765 additions and 75 deletions

View File

@@ -36,9 +36,16 @@ interface ApprenantPresence {
dateFormationId: number;
periode: 'matin' | 'apres_midi';
signatureUrl: string | null;
typeUtilisateur?: 'apprenant' | 'formateur';
}>;
}
interface FormateurPresence {
dateFormationId: number;
periode: 'matin' | 'apres_midi';
signatureUrl: string | null;
}
interface FeuillePresenceInfo {
formationNom: string;
sequenceNom: string;
@@ -47,6 +54,7 @@ interface FeuillePresenceInfo {
publicCible: string;
formateur?: string;
apprenants: ApprenantPresence[];
presencesFormateur?: FormateurPresence[];
}
/**
@@ -301,14 +309,56 @@ export function generateFeuillePresence(feuilleInfo: FeuillePresenceInfo): Buffe
doc.text('Signatures du formateur :', 14, signatureY);
doc.setFont('helvetica', 'normal');
// Trouver les signatures du formateur pour cette date
const presenceFormateurMatin = feuilleInfo.presencesFormateur?.find(
p => p.dateFormationId === dateInfo.id && p.periode === 'matin'
);
const presenceFormateurApresMidi = feuilleInfo.presencesFormateur?.find(
p => p.dateFormationId === dateInfo.id && p.periode === 'apres_midi'
);
// Signature Matin
doc.setFontSize(9);
doc.text('Matin :', 14, signatureY + 8);
doc.line(30, signatureY + 8, 90, signatureY + 8);
if (presenceFormateurMatin?.signatureUrl) {
try {
const signaturePath = path.resolve(process.cwd(), presenceFormateurMatin.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, 55, 10);
} 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);
doc.line(140, signatureY + 8, 200, signatureY + 8);
if (presenceFormateurApresMidi?.signatureUrl) {
try {
const signaturePath = path.resolve(process.cwd(), presenceFormateurApresMidi.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, 55, 10);
} 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