Checkpoint: Ajout d'un indicateur visuel de rafraîchissement (badge "Mis à jour il y a X secondes" avec icône animée) et de statistiques en temps réel du taux de présence par période (matin/après-midi) sur la page d'émargement numérique

This commit is contained in:
Manus
2026-02-02 10:17:14 -05:00
parent 0163b46e5d
commit 68df39a072
2 changed files with 97 additions and 24 deletions

View File

@@ -26,6 +26,8 @@ export default function FormateurEmargement() {
const { user } = useAuth(); const { user } = useAuth();
const [selectedSequenceId, setSelectedSequenceId] = useState<number | null>(null); const [selectedSequenceId, setSelectedSequenceId] = useState<number | null>(null);
const [showQRDialog, setShowQRDialog] = useState(false); const [showQRDialog, setShowQRDialog] = useState(false);
const [lastRefreshTime, setLastRefreshTime] = useState<Date>(new Date());
const [secondsSinceRefresh, setSecondsSinceRefresh] = useState(0);
// Récupérer toutes les séquences (filtrées côté serveur pour les formateurs) // Récupérer toutes les séquences (filtrées côté serveur pour les formateurs)
const { data: sequences, isLoading: loadingSequences } = trpc.sequences.list.useQuery(); const { data: sequences, isLoading: loadingSequences } = trpc.sequences.list.useQuery();
@@ -61,11 +63,42 @@ export default function FormateurEmargement() {
const interval = setInterval(() => { const interval = setInterval(() => {
refetchInscrits(); refetchInscrits();
refetchPresences(); refetchPresences();
setLastRefreshTime(new Date());
setSecondsSinceRefresh(0);
}, 30000); // 30 secondes }, 30000); // 30 secondes
return () => clearInterval(interval); return () => clearInterval(interval);
}, [selectedSequenceId, refetchInscrits, refetchPresences]); }, [selectedSequenceId, refetchInscrits, refetchPresences]);
// Compteur pour "Mis à jour il y a X secondes"
useEffect(() => {
if (!selectedSequenceId) return;
const interval = setInterval(() => {
setSecondsSinceRefresh(Math.floor((new Date().getTime() - lastRefreshTime.getTime()) / 1000));
}, 1000); // Mise à jour chaque seconde
return () => clearInterval(interval);
}, [selectedSequenceId, lastRefreshTime]);
// Calculer les statistiques de présence par date et période
const getPresenceStats = (dateFormationId: number, periode: "matin" | "apres_midi") => {
if (!inscrits || !presences) return { present: 0, total: 0, percentage: 0 };
const total = inscrits.length;
const present = inscrits.filter(inscrit =>
presences.some(p =>
p.inscriptionId === inscrit.inscriptionId &&
p.dateFormationId === dateFormationId &&
p.periode === periode
)
).length;
const percentage = total > 0 ? Math.round((present / total) * 100) : 0;
return { present, total, percentage };
};
// Valider une présence manuellement // Valider une présence manuellement
const { mutate: validerPresence, isPending: validating } = trpc.presences.valider.useMutation({ const { mutate: validerPresence, isPending: validating } = trpc.presences.valider.useMutation({
onSuccess: () => { onSuccess: () => {
@@ -175,30 +208,45 @@ export default function FormateurEmargement() {
</Select> </Select>
{selectedSequenceId && ( {selectedSequenceId && (
<div className="flex gap-2"> <div className="space-y-3">
<Button onClick={handleGenerateQR} disabled={generatingQR}> <div className="flex gap-2">
{generatingQR ? ( <Button onClick={handleGenerateQR} disabled={generatingQR}>
<> {generatingQR ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <>
Génération... <Loader2 className="mr-2 h-4 w-4 animate-spin" />
</> Génération...
) : ( </>
<> ) : (
<QrCode className="mr-2 h-4 w-4" /> <>
Afficher le QR code <QrCode className="mr-2 h-4 w-4" />
</> Afficher le QR code
)} </>
</Button> )}
<Button </Button>
variant="outline" <Button
onClick={() => { variant="outline"
refetchInscrits(); onClick={() => {
refetchPresences(); refetchInscrits();
}} refetchPresences();
> setLastRefreshTime(new Date());
<RefreshCw className="mr-2 h-4 w-4" /> setSecondsSinceRefresh(0);
Actualiser }}
</Button> >
<RefreshCw className="mr-2 h-4 w-4" />
Actualiser
</Button>
</div>
{/* Indicateur de rafraîchissement */}
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<RefreshCw className={`h-3 w-3 ${secondsSinceRefresh < 30 ? 'animate-spin' : ''}`} />
<span>
Mis à jour il y a {secondsSinceRefresh} seconde{secondsSinceRefresh > 1 ? 's' : ''}
</span>
<Badge variant="outline" className="text-xs">
Rafraîchissement auto: 30s
</Badge>
</div>
</div> </div>
)} )}
</CardContent> </CardContent>
@@ -268,6 +316,17 @@ export default function FormateurEmargement() {
</p> </p>
</div> </div>
{/* Statistiques Matin */}
<div className="flex items-center justify-between bg-blue-50 dark:bg-blue-950/20 px-3 py-2 rounded">
<span className="text-xs font-medium">🌅 Matin</span>
<Badge variant="secondary" className="text-xs">
{(() => {
const stats = getPresenceStats(date.id, "matin");
return `${stats.present}/${stats.total} présents (${stats.percentage}%)`;
})()}
</Badge>
</div>
{/* Matin */} {/* Matin */}
<div className="flex items-center justify-between pl-4"> <div className="flex items-center justify-between pl-4">
<div className="flex items-center gap-2 flex-1"> <div className="flex items-center gap-2 flex-1">
@@ -328,6 +387,17 @@ export default function FormateurEmargement() {
)} )}
</div> </div>
{/* Statistiques Après-midi */}
<div className="flex items-center justify-between bg-orange-50 dark:bg-orange-950/20 px-3 py-2 rounded mt-2">
<span className="text-xs font-medium">🌆 Après-midi</span>
<Badge variant="secondary" className="text-xs">
{(() => {
const stats = getPresenceStats(date.id, "apres_midi");
return `${stats.present}/${stats.total} présents (${stats.percentage}%)`;
})()}
</Badge>
</div>
{/* Après-midi */} {/* Après-midi */}
<div className="flex items-center justify-between pl-4"> <div className="flex items-center justify-between pl-4">
<div className="flex items-center gap-2 flex-1"> <div className="flex items-center gap-2 flex-1">

View File

@@ -1493,3 +1493,6 @@
- [x] Créer l'interface de gestion des attestations pour formateurs et super-formateurs (charger, supprimer, modifier, envoyer) - [x] Créer l'interface de gestion des attestations pour formateurs et super-formateurs (charger, supprimer, modifier, envoyer)
- [x] Ajouter un rafraîchissement automatique toutes les 30 secondes sur la page d'émargement numérique - [x] Ajouter un rafraîchissement automatique toutes les 30 secondes sur la page d'émargement numérique
- [x] Ajouter un indicateur visuel de rafraîchissement avec badge "Mis à jour il y a X secondes"
- [x] Ajouter des statistiques en temps réel du taux de présence par période