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:
@@ -12,8 +12,8 @@ interface InscriptionExport {
|
||||
nom: string;
|
||||
prenom: string;
|
||||
email: string;
|
||||
codeEtablissement: string;
|
||||
fonction: string;
|
||||
codeEtablissement: string | null;
|
||||
fonction: string | null;
|
||||
statut: string;
|
||||
dateInscription: Date;
|
||||
}
|
||||
@@ -30,8 +30,8 @@ interface SequenceInfo {
|
||||
interface ApprenantPresence {
|
||||
nom: string;
|
||||
prenom: string;
|
||||
codeEtablissement: string;
|
||||
fonction: string;
|
||||
codeEtablissement: string | null;
|
||||
fonction: string | null;
|
||||
}
|
||||
|
||||
interface FeuillePresenceInfo {
|
||||
@@ -53,7 +53,7 @@ export function generateExcelExport(sequenceInfo: SequenceInfo): Buffer {
|
||||
'Nom': i.nom,
|
||||
'Prénom': i.prenom,
|
||||
'Email': i.email,
|
||||
'Code établissement': i.codeEtablissement,
|
||||
'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'),
|
||||
@@ -136,7 +136,7 @@ export function generatePDFExport(sequenceInfo: SequenceInfo): Buffer {
|
||||
i.nom,
|
||||
i.prenom,
|
||||
i.email,
|
||||
i.codeEtablissement,
|
||||
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',
|
||||
]);
|
||||
@@ -222,7 +222,7 @@ export function generateFeuillePresence(feuilleInfo: FeuillePresenceInfo): Buffe
|
||||
const tableData = feuilleInfo.apprenants.map(i => [
|
||||
i.nom,
|
||||
i.prenom,
|
||||
i.codeEtablissement,
|
||||
i.codeEtablissement || '',
|
||||
i.fonction === 'directeur' ? 'Directeur' : i.fonction === 'chef_service' ? 'Chef de service' : 'Autre',
|
||||
'', // Signature matin
|
||||
'', // Signature après-midi
|
||||
|
||||
@@ -12,6 +12,8 @@ export async function validerPresence(params: {
|
||||
modeValidation: "qrcode" | "manuel";
|
||||
validateurId?: number;
|
||||
commentaire?: string;
|
||||
signatureUrl?: string;
|
||||
signatureS3Key?: string;
|
||||
}) {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Database not available");
|
||||
@@ -41,6 +43,9 @@ export async function validerPresence(params: {
|
||||
modeValidation: params.modeValidation,
|
||||
validateurId: params.validateurId,
|
||||
commentaire: params.commentaire,
|
||||
signatureUrl: params.signatureUrl,
|
||||
signatureS3Key: params.signatureS3Key,
|
||||
dateSigned: params.signatureUrl ? new Date() : undefined,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user