From 37924ad4fd2851e8985eddf04e826920324e3fb6 Mon Sep 17 00:00:00 2001 From: Manus Date: Mon, 9 Feb 2026 11:04:09 -0500 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Ajout=20d'un=20encart=20d=C3=A9di?= =?UTF-8?q?=C3=A9=20pour=20afficher=20les=20signatures=20des=20formateurs?= =?UTF-8?q?=20sur=20la=20page=20d'=C3=A9margement=20num=C3=A9rique?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modifications apportées : - Backend : Ajout de la fonction getSignaturesFormateursBySequence dans presenceDb.ts - Backend : Ajout de la procédure tRPC presences.listSignaturesFormateurs dans routers.ts - Frontend : Ajout de l'encart visuel des signatures formateurs dans FormateurEmargement.tsx - L'encart affiche les signatures avec date, période (matin/après-midi), heure et image de signature - Rafraîchissement automatique toutes les 30 secondes - Design distinct avec bordure bleue pour différencier des signatures apprenants - Déployé sur recette (108.143.67.56) et production (98.71.216.234) --- client/src/pages/FormateurEmargement.tsx | 71 +++++++++++++++++++++++- server/presenceDb.ts | 32 +++++++++++ server/routers.ts | 10 ++++ todo.md | 8 +++ 4 files changed, 120 insertions(+), 1 deletion(-) diff --git a/client/src/pages/FormateurEmargement.tsx b/client/src/pages/FormateurEmargement.tsx index b117782..0ec223b 100644 --- a/client/src/pages/FormateurEmargement.tsx +++ b/client/src/pages/FormateurEmargement.tsx @@ -69,6 +69,12 @@ export default function FormateurEmargement() { { enabled: !!selectedSequenceId } ); + // Récupérer les signatures formateurs de la séquence + const { data: signaturesFormateurs, refetch: refetchSignaturesFormateurs } = trpc.presences.listSignaturesFormateurs.useQuery( + { sequenceId: selectedSequenceId || 0 }, + { enabled: !!selectedSequenceId } + ); + // Rafraîchissement automatique toutes les 30 secondes useEffect(() => { if (!selectedSequenceId) return; @@ -76,12 +82,13 @@ export default function FormateurEmargement() { const interval = setInterval(() => { refetchInscrits(); refetchPresences(); + refetchSignaturesFormateurs(); setLastRefreshTime(new Date()); setSecondsSinceRefresh(0); }, 30000); // 30 secondes return () => clearInterval(interval); - }, [selectedSequenceId, refetchInscrits, refetchPresences]); + }, [selectedSequenceId, refetchInscrits, refetchPresences, refetchSignaturesFormateurs]); // Compteur pour "Mis à jour il y a X secondes" useEffect(() => { @@ -262,6 +269,7 @@ export default function FormateurEmargement() { onClick={() => { refetchInscrits(); refetchPresences(); + refetchSignaturesFormateurs(); setLastRefreshTime(new Date()); setSecondsSinceRefresh(0); }} @@ -485,6 +493,67 @@ export default function FormateurEmargement() { )} + {/* Encart des signatures formateurs */} + {selectedSequenceId && signaturesFormateurs && signaturesFormateurs.length > 0 && ( + + + Signatures Formateurs + + Signatures tactiles des formateurs pour cette séquence + + + +
+ {signaturesFormateurs.map((signature: any) => ( +
+
+
+

+ {new Date(signature.dateDebut).toLocaleDateString("fr-FR", { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + })} +

+

+ {signature.periode === "matin" ? "🌅 Matin" : "🌆 Après-midi"} +

+
+ + {new Date(signature.heurePresence).toLocaleTimeString("fr-FR", { + hour: "2-digit", + minute: "2-digit", + })} + +
+ {signature.signatureUrl && ( +
+

Signature tactile :

+
{ + setSelectedSignature(signature.signatureUrl!); + setShowSignatureDialog(true); + }} + > + Signature formateur +
+
+ )} +
+ ))} +
+
+
+ )} + {/* Dialog QR Code Apprenants */} diff --git a/server/presenceDb.ts b/server/presenceDb.ts index e2cfbde..c44fe22 100644 --- a/server/presenceDb.ts +++ b/server/presenceDb.ts @@ -108,6 +108,38 @@ export async function getPresencesBySequence(sequenceId: number) { return result; } +/** + * Récupérer les signatures formateurs pour une séquence + */ +export async function getSignaturesFormateursBySequence(sequenceId: number) { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + const result = await db + .select({ + presenceId: presences.id, + formateurId: presences.formateurId, + dateFormationId: presences.dateFormationId, + periode: presences.periode, + heurePresence: presences.heurePresence, + modeValidation: presences.modeValidation, + signatureUrl: sql`${presences}.signatureUrl`, + signatureS3Key: sql`${presences}.signatureS3Key`, + dateDebut: datesFormation.dateDebut, + dateFin: datesFormation.dateFin, + }) + .from(presences) + .innerJoin(datesFormation, eq(presences.dateFormationId, datesFormation.id)) + .where( + and( + eq(datesFormation.sequenceId, sequenceId), + eq(presences.typeUtilisateur, "formateur") + ) + ); + + return result; +} + /** * Récupérer les présences pour une inscription spécifique */ diff --git a/server/routers.ts b/server/routers.ts index 88920f3..ff198e4 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -2796,6 +2796,16 @@ export const appRouter = router({ return await getPresencesBySequence(input.sequenceId); }), + // Lister les signatures formateurs pour une séquence + listSignaturesFormateurs: protectedProcedure + .input(z.object({ + sequenceId: z.number(), + })) + .query(async ({ input }) => { + const { getSignaturesFormateursBySequence } = await import("./presenceDb"); + return await getSignaturesFormateursBySequence(input.sequenceId); + }), + // Lister les présences pour une inscription listByInscription: publicProcedure .input(z.object({ diff --git a/todo.md b/todo.md index 6d4a5a6..a6072a2 100644 --- a/todo.md +++ b/todo.md @@ -1527,3 +1527,11 @@ - [ ] Déployer sur le VPS - [ ] Tester en production - [ ] Valider avec l'utilisateur + + +## Affichage des signatures formateurs sur la page d'émargement numérique + +- [x] Créer une procédure backend pour récupérer les signatures formateurs d'une séquence +- [x] Ajouter un encart dédié sur la page d'émargement numérique pour afficher les signatures formateurs +- [x] Afficher les signatures formateurs avec date/heure et période (matin/après-midi) +- [x] Déployer sur recette et production