Checkpoint: Ajout d'un encart dédié pour afficher les signatures des formateurs sur la page d'émargement numérique

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)
This commit is contained in:
Manus
2026-02-09 11:04:09 -05:00
parent a40e0bf6cd
commit 37924ad4fd
4 changed files with 120 additions and 1 deletions

View File

@@ -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() {
</Card>
)}
{/* Encart des signatures formateurs */}
{selectedSequenceId && signaturesFormateurs && signaturesFormateurs.length > 0 && (
<Card className="border-2 border-blue-200 bg-blue-50/30">
<CardHeader>
<CardTitle className="text-blue-700">Signatures Formateurs</CardTitle>
<CardDescription>
Signatures tactiles des formateurs pour cette séquence
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{signaturesFormateurs.map((signature: any) => (
<div
key={signature.presenceId}
className="border border-blue-200 rounded-lg p-4 bg-white space-y-3"
>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-blue-900">
{new Date(signature.dateDebut).toLocaleDateString("fr-FR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})}
</p>
<p className="text-sm text-muted-foreground">
{signature.periode === "matin" ? "🌅 Matin" : "🌆 Après-midi"}
</p>
</div>
<Badge variant="outline" className="bg-blue-100 text-blue-700 border-blue-300">
{new Date(signature.heurePresence).toLocaleTimeString("fr-FR", {
hour: "2-digit",
minute: "2-digit",
})}
</Badge>
</div>
{signature.signatureUrl && (
<div className="flex flex-col gap-2">
<p className="text-xs text-muted-foreground">Signature tactile :</p>
<div className="border-2 border-blue-200 rounded-lg p-2 bg-white cursor-pointer hover:border-blue-400 transition-colors"
onClick={() => {
setSelectedSignature(signature.signatureUrl!);
setShowSignatureDialog(true);
}}
>
<img
src={signature.signatureUrl}
alt="Signature formateur"
className="w-full h-24 object-contain"
/>
</div>
</div>
)}
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Dialog QR Code Apprenants */}
<Dialog open={showQRDialog} onOpenChange={setShowQRDialog}>
<DialogContent className="max-w-md">

View File

@@ -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<string | null>`${presences}.signatureUrl`,
signatureS3Key: sql<string | null>`${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
*/

View File

@@ -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({

View File

@@ -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