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 [selectedSequenceId, setSelectedSequenceId] = useState<number | null>(null);
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)
const { data: sequences, isLoading: loadingSequences } = trpc.sequences.list.useQuery();
@@ -61,11 +63,42 @@ export default function FormateurEmargement() {
const interval = setInterval(() => {
refetchInscrits();
refetchPresences();
setLastRefreshTime(new Date());
setSecondsSinceRefresh(0);
}, 30000); // 30 secondes
return () => clearInterval(interval);
}, [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
const { mutate: validerPresence, isPending: validating } = trpc.presences.valider.useMutation({
onSuccess: () => {
@@ -175,30 +208,45 @@ export default function FormateurEmargement() {
</Select>
{selectedSequenceId && (
<div className="flex gap-2">
<Button onClick={handleGenerateQR} disabled={generatingQR}>
{generatingQR ? (
<>
<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
</>
)}
</Button>
<Button
variant="outline"
onClick={() => {
refetchInscrits();
refetchPresences();
}}
>
<RefreshCw className="mr-2 h-4 w-4" />
Actualiser
</Button>
<div className="space-y-3">
<div className="flex gap-2">
<Button onClick={handleGenerateQR} disabled={generatingQR}>
{generatingQR ? (
<>
<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
</>
)}
</Button>
<Button
variant="outline"
onClick={() => {
refetchInscrits();
refetchPresences();
setLastRefreshTime(new Date());
setSecondsSinceRefresh(0);
}}
>
<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>
)}
</CardContent>
@@ -268,6 +316,17 @@ export default function FormateurEmargement() {
</p>
</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 */}
<div className="flex items-center justify-between pl-4">
<div className="flex items-center gap-2 flex-1">
@@ -328,6 +387,17 @@ export default function FormateurEmargement() {
)}
</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 */}
<div className="flex items-center justify-between pl-4">
<div className="flex items-center gap-2 flex-1">