import { useAuth } from "@/_core/hooks/useAuth"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { trpc } from "@/lib/trpc"; import { CheckCircle2, Circle, Loader2, QrCode, RefreshCw } from "lucide-react"; import { useState, useEffect } from "react"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import DashboardLayout from "@/components/DashboardLayout"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; export default function FormateurEmargement() { const { user } = useAuth(); const [selectedSequenceId, setSelectedSequenceId] = useState(null); const [showQRDialog, setShowQRDialog] = useState(false); const [showQRFormateurDialog, setShowQRFormateurDialog] = useState(false); const [lastRefreshTime, setLastRefreshTime] = useState(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(); // Générer le QR code apprenants const { data: qrData, mutate: generateQR, isPending: generatingQR } = trpc.presences.generateQRCode.useMutation({ onSuccess: () => { setShowQRDialog(true); }, onError: (error) => { toast.error("Erreur lors de la génération du QR code", { description: error.message, }); }, }); // Générer le QR code formateur const { data: qrFormateurData, mutate: generateQRFormateur, isPending: generatingQRFormateur } = trpc.presences.generateQRCodeFormateur.useMutation({ onSuccess: () => { setShowQRFormateurDialog(true); }, onError: (error) => { toast.error("Erreur lors de la génération du QR code formateur", { description: error.message, }); }, }); // Récupérer les inscrits avec les dates de formation const { data: inscrits, isLoading: loadingInscrits, refetch: refetchInscrits } = trpc.inscriptions.listWithDates.useQuery( { sequenceId: selectedSequenceId || 0 }, { enabled: !!selectedSequenceId } ); // Récupérer les présences de la séquence const { data: presences, refetch: refetchPresences } = trpc.presences.listBySequence.useQuery( { sequenceId: selectedSequenceId || 0 }, { 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; const interval = setInterval(() => { refetchInscrits(); refetchPresences(); refetchSignaturesFormateurs(); setLastRefreshTime(new Date()); setSecondsSinceRefresh(0); }, 30000); // 30 secondes return () => clearInterval(interval); }, [selectedSequenceId, refetchInscrits, refetchPresences, refetchSignaturesFormateurs]); // 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: () => { toast.success("Présence validée avec succès"); refetchInscrits(); refetchPresences(); }, onError: (error) => { toast.error("Erreur lors de la validation", { description: error.message, }); }, }); const handleGenerateQR = () => { if (!selectedSequenceId) { toast.error("Veuillez sélectionner une séquence"); return; } generateQR({ sequenceId: selectedSequenceId }); }; const handleGenerateQRFormateur = () => { if (!selectedSequenceId) { toast.error("Veuillez sélectionner une séquence"); return; } generateQRFormateur({ sequenceId: selectedSequenceId }); }; const handleValiderPresence = (inscriptionId: number, dateFormationId: number, periode: "matin" | "apres_midi") => { validerPresence({ inscriptionId, dateFormationId, periode, modeValidation: "manuel", validateurId: user?.id, }); }; // Vérifier si un apprenant a validé sa présence pour une date et une période const isPresent = (inscriptionId: number, dateFormationId: number, periode: "matin" | "apres_midi") => { return presences?.some( (p) => p.inscriptionId === inscriptionId && p.dateFormationId === dateFormationId && p.periode === periode ); }; // Récupérer les informations de présence pour une date et une période const getPresenceInfo = (inscriptionId: number, dateFormationId: number, periode: "matin" | "apres_midi") => { return presences?.find( (p) => p.inscriptionId === inscriptionId && p.dateFormationId === dateFormationId && p.periode === periode ); }; // État pour la modale de signature const [selectedSignature, setSelectedSignature] = useState(null); const [showSignatureDialog, setShowSignatureDialog] = useState(false); if (loadingSequences) { return (
); } if (!sequences || sequences.length === 0) { return (
Aucune séquence Vous n'avez aucune séquence de formation assignée pour le moment.
); } return (

Émargement numérique

Gérez les présences de vos apprenants avec le QR code

{/* Sélection de la séquence */} Sélectionner une séquence Choisissez la séquence pour laquelle vous souhaitez gérer les présences {selectedSequenceId && (
{/* Indicateur de rafraîchissement */}
Mis à jour il y a {secondsSinceRefresh} seconde{secondsSinceRefresh > 1 ? 's' : ''} Rafraîchissement auto: 30s
)}
{/* Liste des inscrits */} {selectedSequenceId && ( Liste des inscrits Validez manuellement les présences si nécessaire {loadingInscrits ? (
) : !inscrits || inscrits.length === 0 ? (

Aucun inscrit pour cette séquence

) : (
{inscrits.map((inscrit: any) => (

{inscrit.apprenant?.prenom} {inscrit.apprenant?.nom}

{inscrit.apprenant?.email}

{inscrit.inscription.statut}
{/* Dates de formation */}
{inscrit.dates && Array.isArray(inscrit.dates) && inscrit.dates.length > 0 ? inscrit.dates.map((date: any) => { if (!date || !date.id) return null; const presentMatin = isPresent(inscrit.inscription.id, date.id, "matin"); const presentApresMidi = isPresent(inscrit.inscription.id, date.id, "apres_midi"); return (

Date {date.ordre}

{new Date(date.dateDebut).toLocaleDateString("fr-FR", { weekday: "long", year: "numeric", month: "long", day: "numeric", })}

{/* Matin */}
{presentMatin ? ( ) : ( )} 🌅 Matin {(() => { const presenceInfo = getPresenceInfo(inscrit.inscription.id, date.id, "matin"); if (presenceInfo) { return (
• {inscrit.apprenant?.prenom} {inscrit.apprenant?.nom} • {new Date(presenceInfo.heurePresence).toLocaleString("fr-FR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" })} {presenceInfo.signatureUrl && ( )}
); } return null; })()}
{!presentMatin && ( )}
{/* Après-midi */}
{presentApresMidi ? ( ) : ( )} 🌆 Après-midi {(() => { const presenceInfo = getPresenceInfo(inscrit.inscription.id, date.id, "apres_midi"); if (presenceInfo) { return (
• {inscrit.apprenant?.prenom} {inscrit.apprenant?.nom} • {new Date(presenceInfo.heurePresence).toLocaleString("fr-FR", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit" })} {presenceInfo.signatureUrl && ( )}
); } return null; })()}
{!presentApresMidi && ( )}
); }) : (

Aucune date de formation disponible

)}
))}
)}
)} {/* Encart des signatures formateurs */} {selectedSequenceId && signaturesFormateurs && signaturesFormateurs.length > 0 && ( Signatures Formateurs Signatures tactiles des formateurs pour cette séquence
{(() => { // Regrouper les signatures par date const signaturesByDate = signaturesFormateurs.reduce((acc: any, signature: any) => { const dateKey = signature.dateFormationId; if (!acc[dateKey]) { acc[dateKey] = { dateDebut: signature.dateDebut, dateFin: signature.dateFin, matin: null, apres_midi: null, }; } if (signature.periode === "matin") { acc[dateKey].matin = signature; } else { acc[dateKey].apres_midi = signature; } return acc; }, {}); return Object.values(signaturesByDate).map((dateGroup: any) => (
{/* En-tête de la date */}

{new Date(dateGroup.dateDebut).toLocaleDateString("fr-FR", { weekday: "long", year: "numeric", month: "long", day: "numeric", })}

{/* Signature Matin */}

🌅 Matin

{dateGroup.matin && ( {new Date(dateGroup.matin.heurePresence).toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit", })} )}
{dateGroup.matin?.signatureUrl ? (
{ setSelectedSignature(dateGroup.matin.signatureUrl!); setShowSignatureDialog(true); }} > Signature formateur matin
) : (

Aucune signature

)}
{/* Signature Après-midi */}

🌆 Après-midi

{dateGroup.apres_midi && ( {new Date(dateGroup.apres_midi.heurePresence).toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit", })} )}
{dateGroup.apres_midi?.signatureUrl ? (
{ setSelectedSignature(dateGroup.apres_midi.signatureUrl!); setShowSignatureDialog(true); }} > Signature formateur après-midi
) : (

Aucune signature

)}
)); })()}
)} {/* Dialog QR Code Apprenants */} QR Code d'émargement apprenants Les apprenants doivent scanner ce QR code pour valider leur présence {qrData && (
QR Code

Affichez ce QR code en plein écran pour faciliter le scan

)}
{/* Dialog QR Code Formateur */} QR Code d'émargement formateur Scannez ce QR code pour valider votre présence en tant que formateur {qrFormateurData && (
QR Code Formateur

Mode formateur : redirige vers la page d'émargement dédiée

)}
{/* Dialog Signature */} Signature de l'apprenant Signature manuscrite capturée lors de la validation de présence {selectedSignature && (
Signature
)}
); }