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:
@@ -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">
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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({
|
||||
|
||||
8
todo.md
8
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
|
||||
|
||||
Reference in New Issue
Block a user