Checkpoint: Implémentation du QR code dédié pour l'émargement formateur avec signature tactile

- Création de la page /emargement-formateur/:token avec formulaire simplifié
- Procédure backend presences.validerFormateur dédiée
- Deux boutons QR code dans l'interface formateur (apprenants + formateur)
- Génération automatique des URL distinctes pour chaque type d'utilisateur
- Déploiement réussi sur le VPS de production
This commit is contained in:
Manus
2026-02-02 15:11:18 -05:00
parent f9983adada
commit 04001e7bcd
11 changed files with 1043 additions and 321 deletions

View File

@@ -26,13 +26,14 @@ export default function FormateurEmargement() {
const { user } = useAuth();
const [selectedSequenceId, setSelectedSequenceId] = useState<number | null>(null);
const [showQRDialog, setShowQRDialog] = useState(false);
const [showQRFormateurDialog, setShowQRFormateurDialog] = 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();
// Générer le QR code
// Générer le QR code apprenants
const { data: qrData, mutate: generateQR, isPending: generatingQR } = trpc.presences.generateQRCode.useMutation({
onSuccess: () => {
setShowQRDialog(true);
@@ -44,6 +45,18 @@ export default function FormateurEmargement() {
},
});
// 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 },
@@ -121,6 +134,15 @@ export default function FormateurEmargement() {
generateQR({ sequenceId: selectedSequenceId });
};
const handleGenerateQRFormateur = () => {
if (!selectedSequenceId) {
toast.error("Veuillez sélectionner une séquence");
return;
}
// Générer le QR code formateur
generateQRFormateur({ sequenceId: selectedSequenceId });
};
const handleValiderPresence = (inscriptionId: number, dateFormationId: number, periode: "matin" | "apres_midi") => {
validerPresence({
inscriptionId,
@@ -209,7 +231,7 @@ export default function FormateurEmargement() {
{selectedSequenceId && (
<div className="space-y-3">
<div className="flex gap-2">
<div className="flex gap-2 flex-wrap">
<Button onClick={handleGenerateQR} disabled={generatingQR}>
{generatingQR ? (
<>
@@ -219,7 +241,20 @@ export default function FormateurEmargement() {
) : (
<>
<QrCode className="mr-2 h-4 w-4" />
Afficher le QR code
QR Code Apprenants
</>
)}
</Button>
<Button onClick={handleGenerateQRFormateur} disabled={generatingQRFormateur} variant="secondary">
{generatingQRFormateur ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Génération...
</>
) : (
<>
<QrCode className="mr-2 h-4 w-4" />
QR Code Formateur
</>
)}
</Button>
@@ -451,13 +486,13 @@ export default function FormateurEmargement() {
</Card>
)}
{/* Dialog QR Code */}
{/* Dialog QR Code Apprenants */}
<Dialog open={showQRDialog} onOpenChange={setShowQRDialog}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>QR Code d'émargement</DialogTitle>
<DialogTitle>QR Code d'émargement apprenants</DialogTitle>
<DialogDescription>
Les apprenants et formateurs doivent scanner ce QR code pour valider leur présence avec signature tactile
Les apprenants doivent scanner ce QR code pour valider leur présence avec signature tactile
</DialogDescription>
</DialogHeader>
{qrData && (
@@ -475,6 +510,38 @@ export default function FormateurEmargement() {
</DialogContent>
</Dialog>
{/* Dialog QR Code Formateur */}
<Dialog open={showQRFormateurDialog} onOpenChange={setShowQRFormateurDialog}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>QR Code d'émargement formateur</DialogTitle>
<DialogDescription>
Scannez ce QR code avec votre smartphone pour émarger en tant que formateur avec signature tactile
</DialogDescription>
</DialogHeader>
{qrFormateurData && (
<div className="flex flex-col items-center gap-4 py-4">
<img
src={qrFormateurData.qrCodeDataURL}
alt="QR Code Formateur"
className="w-full max-w-[300px] border rounded-lg"
/>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 w-full">
<p className="text-sm font-semibold text-blue-900 mb-2">👨‍🏫 Mode formateur
</p>
<p className="text-xs text-blue-700">
Ce QR code vous redirigera vers une page dédiée pour l'émargement formateur.
Vous pourrez signer votre présence (matin/après-midi) directement depuis votre smartphone.
</p>
</div>
<p className="text-sm text-muted-foreground text-center">
Affichez ce QR code en plein écran pour faciliter le scan
</p>
</div>
)}
</DialogContent>
</Dialog>
{/* Dialog Signature */}
<Dialog open={showSignatureDialog} onOpenChange={setShowSignatureDialog}>
<DialogContent className="max-w-2xl">