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:
@@ -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">
|
||||||
|
|||||||
3
todo.md
3
todo.md
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user