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 }
|
{ 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
|
// Rafraîchissement automatique toutes les 30 secondes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedSequenceId) return;
|
if (!selectedSequenceId) return;
|
||||||
@@ -76,12 +82,13 @@ export default function FormateurEmargement() {
|
|||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
refetchInscrits();
|
refetchInscrits();
|
||||||
refetchPresences();
|
refetchPresences();
|
||||||
|
refetchSignaturesFormateurs();
|
||||||
setLastRefreshTime(new Date());
|
setLastRefreshTime(new Date());
|
||||||
setSecondsSinceRefresh(0);
|
setSecondsSinceRefresh(0);
|
||||||
}, 30000); // 30 secondes
|
}, 30000); // 30 secondes
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [selectedSequenceId, refetchInscrits, refetchPresences]);
|
}, [selectedSequenceId, refetchInscrits, refetchPresences, refetchSignaturesFormateurs]);
|
||||||
|
|
||||||
// Compteur pour "Mis à jour il y a X secondes"
|
// Compteur pour "Mis à jour il y a X secondes"
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -262,6 +269,7 @@ export default function FormateurEmargement() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
refetchInscrits();
|
refetchInscrits();
|
||||||
refetchPresences();
|
refetchPresences();
|
||||||
|
refetchSignaturesFormateurs();
|
||||||
setLastRefreshTime(new Date());
|
setLastRefreshTime(new Date());
|
||||||
setSecondsSinceRefresh(0);
|
setSecondsSinceRefresh(0);
|
||||||
}}
|
}}
|
||||||
@@ -485,6 +493,67 @@ export default function FormateurEmargement() {
|
|||||||
</Card>
|
</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 QR Code Apprenants */}
|
||||||
<Dialog open={showQRDialog} onOpenChange={setShowQRDialog}>
|
<Dialog open={showQRDialog} onOpenChange={setShowQRDialog}>
|
||||||
<DialogContent className="max-w-md">
|
<DialogContent className="max-w-md">
|
||||||
|
|||||||
@@ -108,6 +108,38 @@ export async function getPresencesBySequence(sequenceId: number) {
|
|||||||
return result;
|
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
|
* Récupérer les présences pour une inscription spécifique
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -2796,6 +2796,16 @@ export const appRouter = router({
|
|||||||
return await getPresencesBySequence(input.sequenceId);
|
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
|
// Lister les présences pour une inscription
|
||||||
listByInscription: publicProcedure
|
listByInscription: publicProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
|
|||||||
8
todo.md
8
todo.md
@@ -1527,3 +1527,11 @@
|
|||||||
- [ ] Déployer sur le VPS
|
- [ ] Déployer sur le VPS
|
||||||
- [ ] Tester en production
|
- [ ] Tester en production
|
||||||
- [ ] Valider avec l'utilisateur
|
- [ ] 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