Checkpoint: Implémentation de la signature manuscrite tactile pour l'émargement via QR code :

- Intégration du composant SignaturePad dans EmargementScan.tsx
- Ajout des champs de signature dans la table presences (signatureUrl, signatureS3Key, dateSigned)
- Modification de la procédure presences.valider pour accepter et uploader la signature en S3
- Stockage automatique de l'URL et de la clé S3 dans la base de données
- Correction des types nullables dans exportService.ts (codeEtablissement, fonction)
- Les apprenants doivent maintenant signer avec le doigt sur l'écran avant de valider leur présence
This commit is contained in:
Manus
2026-01-27 04:29:01 -05:00
parent b3b808145c
commit 1e915acd5b
16 changed files with 4936 additions and 11 deletions

View File

@@ -2386,6 +2386,47 @@ export const appRouter = router({
const { genererPreviewAttestation } = await import("./attestationService");
return genererPreviewAttestation();
}),
// Uploader la signature manuscrite d'un apprenant
uploadSignature: publicProcedure
.input(z.object({
attestationId: z.number(),
signatureDataUrl: z.string(),
}))
.mutation(async ({ input }) => {
const { storagePut } = await import("./storage");
const { getDb } = await import("./db");
const { attestations } = await import("../drizzle/schema");
const { eq } = await import("drizzle-orm");
const db = await getDb();
if (!db) throw new Error("Base de données non disponible");
// Convertir le data URL en buffer
const base64Data = input.signatureDataUrl.replace(/^data:image\/png;base64,/, "");
const buffer = Buffer.from(base64Data, "base64");
// Générer un nom de fichier unique
const fileName = `signatures/attestation-${input.attestationId}-${Date.now()}.png`;
// Uploader vers S3
const { url, key } = await storagePut(fileName, buffer, "image/png");
// Mettre à jour l'attestation dans la base de données
await db
.update(attestations)
.set({
signatureUrl: url,
signatureS3Key: key,
dateSigned: new Date(),
})
.where(eq(attestations.id, input.attestationId));
return {
success: true,
signatureUrl: url,
};
}),
}),
// ===== PRESENCES (ÉMARGEMENT NUMÉRIQUE) =====
@@ -2436,6 +2477,7 @@ export const appRouter = router({
modeValidation: z.enum(["qrcode", "manuel"]),
validateurId: z.number().optional(),
commentaire: z.string().optional(),
signatureDataUrl: z.string().optional(),
}))
.mutation(async ({ input }) => {
const { validerPresence } = await import("./presenceDb");
@@ -2461,6 +2503,20 @@ export const appRouter = router({
}
}
// Uploader la signature en S3 si fournie
let signatureUrl: string | undefined;
let signatureS3Key: string | undefined;
if (input.signatureDataUrl) {
const { storagePut } = await import("./storage");
const base64Data = input.signatureDataUrl.split(",")[1];
const buffer = Buffer.from(base64Data, "base64");
const randomSuffix = Math.random().toString(36).substring(7);
const s3Key = `signatures/presence-${input.inscriptionId}-${input.dateFormationId}-${randomSuffix}.png`;
const { url } = await storagePut(s3Key, buffer, "image/png");
signatureUrl = url;
signatureS3Key = s3Key;
}
const result = await validerPresence({
inscriptionId: input.inscriptionId,
dateFormationId: input.dateFormationId,
@@ -2468,6 +2524,8 @@ export const appRouter = router({
modeValidation: input.modeValidation,
validateurId: input.validateurId,
commentaire: input.commentaire,
signatureUrl,
signatureS3Key,
});
// Vérifier si toutes les présences sont validées