import { useState, useMemo, useEffect } from "react"; import { trpc } from "@/lib/trpc"; import DashboardLayout from "@/components/DashboardLayout"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { toast } from "sonner"; import { Eye, Send, Upload, Trash2, FileText, Calendar, Filter } from "lucide-react"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; // Composant de dialogue d'upload d'attestation function UploadAttestationDialog({ open, onOpenChange, inscriptionId, onSuccess, }: { open: boolean; onOpenChange: (open: boolean) => void; inscriptionId: number | null; onSuccess: () => void; }) { const [uploading, setUploading] = useState(false); const [uploadProgress, setUploadProgress] = useState(0); const [fileData, setFileData] = useState<{ nomFichier: string; urlFichier: string; s3Key: string; typeFichier: string; tailleFichier: number; } | null>(null); // Réinitialiser l'état quand la modal s'ouvre useEffect(() => { if (open) { setUploading(false); setUploadProgress(0); setFileData(null); } }, [open]); const uploadMutation = trpc.gestionAttestations.uploadDocument.useMutation({ onSuccess: () => { onSuccess(); setFileData(null); }, onError: (error) => { toast.error(`Erreur lors de l'upload : ${error.message}`); setUploading(false); }, }); const handleUpload = () => { if (!fileData || !inscriptionId) return; setUploading(true); uploadMutation.mutate({ inscriptionId, documentUrl: fileData.urlFichier, documentS3Key: fileData.s3Key, }); }; return (
Charger une attestation Sélectionnez un fichier PDF pour cet apprenant
); } export default function AdminGestionAttestations() { const [selectedFormationId, setSelectedFormationId] = useState(null); const [modeAttestation, setModeAttestation] = useState<"auto" | "manuel">("auto"); const [modeEnvoi, setModeEnvoi] = useState<"auto" | "manuel">("manuel"); const [uploadDialogOpen, setUploadDialogOpen] = useState(false); const [selectedInscriptionId, setSelectedInscriptionId] = useState(null); const [previewUrl, setPreviewUrl] = useState(null); const [previewDialogOpen, setPreviewDialogOpen] = useState(false); const [filterStatus, setFilterStatus] = useState<"all" | "completed" | "upcoming">("all"); // Récupérer la liste des formations const { data: formations, isLoading: loadingFormations } = trpc.formations.list.useQuery(); // Récupérer toutes les séquences pour obtenir les dates const { data: allSequences } = trpc.sequences.list.useQuery(); // Récupérer la configuration de la formation sélectionnée const { data: formationConfig, refetch: refetchConfig } = trpc.gestionAttestations.getFormationConfig.useQuery( { formationId: selectedFormationId! }, { enabled: !!selectedFormationId } ); // Récupérer les apprenants avec leur statut d'attestation const { data: apprenants, isLoading: loadingApprenants, refetch: refetchApprenants } = trpc.gestionAttestations.getApprenantsWithStatus.useQuery( { formationId: selectedFormationId! }, { enabled: !!selectedFormationId } ); // Les dates sont déjà incluses dans les séquences // Mutation pour mettre à jour la configuration const updateConfigMutation = trpc.gestionAttestations.updateFormationConfig.useMutation({ onSuccess: () => { toast.success("Configuration enregistrée avec succès"); refetchConfig(); }, onError: (error) => { toast.error(`Erreur : ${error.message}`); }, }); // Mutation pour envoyer une attestation const sendAttestationMutation = trpc.gestionAttestations.sendAttestation.useMutation({ onSuccess: () => { toast.success("Attestation envoyée avec succès"); refetchApprenants(); }, onError: (error) => { toast.error(`Erreur lors de l'envoi : ${error.message}`); }, }); // Mutation pour supprimer un document const deleteDocumentMutation = trpc.gestionAttestations.deleteDocument.useMutation({ onSuccess: () => { toast.success("Document supprimé avec succès"); refetchApprenants(); }, onError: (error) => { toast.error(`Erreur lors de la suppression : ${error.message}`); }, }); // Mutation pour envoyer toutes les attestations const sendAllMutation = trpc.gestionAttestations.sendAllAttestations.useMutation({ onSuccess: (result) => { toast.success(`${result.succes} attestation(s) envoyée(s) avec succès${result.echecs > 0 ? `, ${result.echecs} échec(s)` : ''}`); refetchApprenants(); }, onError: (error) => { toast.error(`Erreur lors de l'envoi en masse : ${error.message}`); }, }); // Charger la configuration quand une formation est sélectionnée const handleFormationChange = (formationId: string) => { const id = parseInt(formationId); setSelectedFormationId(id); // Charger la configuration existante const config = formations?.find(f => f.id === id); if (config) { setModeAttestation((config as any).modeAttestation || "auto"); setModeEnvoi((config as any).modeEnvoi || "manuel"); } }; // Enregistrer la configuration const handleSaveConfig = () => { if (!selectedFormationId) return; updateConfigMutation.mutate({ formationId: selectedFormationId, modeAttestation, modeEnvoi, }); }; // Prévisualiser un document const handlePreview = (url: string) => { setPreviewUrl(url); setPreviewDialogOpen(true); }; // Envoyer une attestation const handleSendAttestation = (apprenant: any) => { const pdfUrl = apprenant.attestation?.documentUrl || apprenant.attestation?.urlPdf; if (!pdfUrl) { toast.error("Aucune attestation disponible pour cet apprenant"); return; } sendAttestationMutation.mutate({ attestationId: apprenant.attestation.id, sequenceId: apprenant.sequence.id, apprenantId: apprenant.apprenant.id, apprenantEmail: apprenant.apprenant.email, apprenantNom: apprenant.apprenant.nom, apprenantPrenom: apprenant.apprenant.prenom, formationNom: formations?.find(f => f.id === selectedFormationId)?.nom || "", pdfUrl: pdfUrl, }); }; const handleDeleteDocument = (attestationId: number) => { if (confirm("Êtes-vous sûr de vouloir supprimer ce document ?")) { deleteDocumentMutation.mutate({ attestationId }); } }; // Envoyer toutes les attestations const handleSendAll = () => { if (!selectedFormationId) return; // Compter le nombre d'attestations prêtes à envoyer const attestationsReady = apprenants?.filter(item => { const hasDocument = item.attestation && (item.attestation.urlPdf || item.attestation.documentUrl); return hasDocument; }).length || 0; if (attestationsReady === 0) { toast.error("Aucune attestation prête à envoyer"); return; } if (confirm(`Êtes-vous sûr de vouloir envoyer ${attestationsReady} attestation(s) ?`)) { sendAllMutation.mutate({ formationId: selectedFormationId }); } }; // Envoyer toutes les attestations d'une séquence const handleSendSequence = (sequenceId: number, sequenceApprenants: any[]) => { const attestationsReady = sequenceApprenants.filter(item => { const hasDocument = item.attestation && (item.attestation.documentUrl || item.attestation.urlPdf); return hasDocument && !item.attestation.emailEnvoye; }); if (attestationsReady.length === 0) { toast.error("Aucune attestation prête à envoyer pour cette séquence"); return; } if (confirm(`Êtes-vous sûr de vouloir envoyer ${attestationsReady.length} attestation(s) pour cette séquence ?`)) { // Envoyer chaque attestation de la séquence let successCount = 0; let errorCount = 0; const sendPromises = attestationsReady.map(item => { const pdfUrl = item.attestation.documentUrl || item.attestation.urlPdf; return sendAttestationMutation.mutateAsync({ attestationId: item.attestation.id, sequenceId: item.sequence.id, apprenantId: item.apprenant.id, apprenantEmail: item.apprenant.email, apprenantNom: item.apprenant.nom, apprenantPrenom: item.apprenant.prenom, formationNom: formations?.find(f => f.id === selectedFormationId)?.nom || "", pdfUrl: pdfUrl, }).then(() => { successCount++; }).catch(() => { errorCount++; }); }); Promise.all(sendPromises).finally(() => { if (successCount > 0) { toast.success(`${successCount} attestation(s) envoyée(s) avec succès${errorCount > 0 ? `, ${errorCount} échec(s)` : ''}`); } refetchApprenants(); }); } }; // Ouvrir le dialogue d'upload const handleOpenUpload = (inscriptionId: number) => { setSelectedInscriptionId(inscriptionId); setUploadDialogOpen(true); }; // Calculer les informations de séquence avec dates et tri const sequencesWithDates = useMemo(() => { if (!apprenants || !allSequences) return []; // Regrouper par séquence const grouped = apprenants.reduce((acc: Record, item) => { const sequenceId = item.sequence.id.toString(); if (!acc[sequenceId]) acc[sequenceId] = []; acc[sequenceId].push(item); return acc; }, {}); // Ajouter les dates et calculer le statut const today = new Date(); today.setHours(0, 0, 0, 0); return Object.entries(grouped).map(([sequenceId, sequenceApprenants]) => { const sequenceIdNum = parseInt(sequenceId); const sequence = allSequences.find(s => s.id === sequenceIdNum); const dates = sequence?.dates || []; // Trouver la première et dernière date const sortedDates = dates .map(d => new Date(d.dateDebut)) .sort((a, b) => a.getTime() - b.getTime()); const sortedEndDates = dates .map(d => new Date(d.dateFin)) .sort((a, b) => a.getTime() - b.getTime()); const firstDate = sortedDates.length > 0 ? sortedDates[0] : null; const lastDate = sortedEndDates.length > 0 ? sortedEndDates[sortedEndDates.length - 1] : null; // Déterminer si la séquence est terminée const isCompleted = lastDate ? lastDate < today : false; return { sequenceId: sequenceIdNum, sequenceName: sequenceApprenants[0]?.sequence?.nom || 'Séquence inconnue', apprenants: sequenceApprenants, firstDate, lastDate, isCompleted, }; }) .sort((a, b) => { // Tri chronologique par première date if (!a.firstDate && !b.firstDate) return a.sequenceId - b.sequenceId; if (!a.firstDate) return 1; if (!b.firstDate) return -1; return a.firstDate.getTime() - b.firstDate.getTime(); }); }, [apprenants, allSequences]); // Filtrer les séquences selon le filtre sélectionné const filteredSequences = useMemo(() => { if (filterStatus === "all") return sequencesWithDates; if (filterStatus === "completed") return sequencesWithDates.filter(s => s.isCompleted); if (filterStatus === "upcoming") return sequencesWithDates.filter(s => !s.isCompleted); return sequencesWithDates; }, [sequencesWithDates, filterStatus]); return (

Gestion des attestations de formation

Configurez le mode de génération et d'envoi des attestations pour chaque formation

{/* Sélection de la formation */} Sélectionner une formation Choisissez la formation pour laquelle vous souhaitez gérer les attestations {/* Configuration */} {selectedFormationId && ( Configuration de la formation Définissez comment les attestations seront générées et envoyées {/* Mode de génération */}
setModeAttestation(value)}>
{/* Mode d'envoi */}
setModeEnvoi(value)}>
)} {/* Liste des apprenants */} {selectedFormationId && (
Apprenants inscrits Gérez les attestations pour chaque apprenant inscrit à cette formation
{/* Filtre */} {/* Bouton envoi global */} {modeEnvoi === "manuel" && apprenants && apprenants.length > 0 && ( )}
{loadingApprenants ? (

Chargement...

) : filteredSequences && filteredSequences.length > 0 ? (
{filteredSequences.map(({ sequenceId, sequenceName, apprenants: sequenceApprenants, firstDate, isCompleted }) => { const attestationsEnvoyees = sequenceApprenants.filter(a => a.attestation?.emailEnvoye).length; const attestationsPretesNonEnvoyees = sequenceApprenants.filter(a => { const hasDocument = a.attestation && (a.attestation.documentUrl || a.attestation.urlPdf); return hasDocument && !a.attestation.emailEnvoye; }).length; return (
{/* En-tête de séquence */}

{sequenceName}

{isCompleted && ( Terminée )} {!isCompleted && ( À venir )}

{sequenceApprenants.length} apprenant(s)

{firstDate && (
Début : {firstDate.toLocaleDateString('fr-FR')}
)}
{attestationsEnvoyees} / {sequenceApprenants.length} envoyé(es) {modeEnvoi === "manuel" && attestationsPretesNonEnvoyees > 0 && ( )}
{/* Tableau des apprenants de cette séquence */} Nom Prénom Email Statut attestation Actions {sequenceApprenants.map((item, index) => { const hasAttestation = !!item.attestation; const hasDocument = hasAttestation && (item.attestation.urlPdf || item.attestation.documentUrl); const isSent = hasAttestation && item.attestation.emailEnvoye; const documentUrl = item.attestation?.documentUrl || item.attestation?.urlPdf; return ( {item.apprenant.nom} {item.apprenant.prenom} {item.apprenant.email} {!hasDocument && ( Aucune attestation )} {hasDocument && !isSent && ( Prête à envoyer )} {hasDocument && isSent && ( Envoyée )}
{modeAttestation === "manuel" && ( )} {hasDocument && ( <> {modeEnvoi === "manuel" && ( )} {modeAttestation === "manuel" && ( )} )}
); })}
); })}
) : (

{filterStatus === "all" ? "Aucun apprenant inscrit pour cette formation" : filterStatus === "completed" ? "Aucune séquence terminée" : "Aucune séquence à venir"}

)}
)} {/* Dialog d'upload */} { refetchApprenants(); setUploadDialogOpen(false); toast.success("Attestation chargée avec succès"); }} /> {/* Dialog de prévisualisation */} Prévisualisation de l'attestation
{previewUrl && (