import { useState } 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 } 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); 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 à uploader pour cet apprenant
{ const file = e.target.files?.[0]; if (!file) return; if (file.type !== 'application/pdf') { toast.error('Seuls les fichiers PDF sont acceptés'); return; } if (file.size > 10 * 1024 * 1024) { toast.error('Le fichier ne doit pas dépasser 10 Mo'); return; } setUploading(true); setUploadProgress(0); try { const formData = new FormData(); formData.append('file', file); // Simuler la progression de l'upload const progressInterval = setInterval(() => { setUploadProgress(prev => { if (prev >= 90) return prev; return prev + 10; }); }, 200); const response = await fetch('/api/upload-file', { method: 'POST', body: formData, }); clearInterval(progressInterval); setUploadProgress(100); if (!response.ok) { throw new Error('Erreur lors de l\'upload'); } const data = await response.json(); setFileData({ nomFichier: file.name, urlFichier: data.url, s3Key: data.key, typeFichier: file.type, tailleFichier: file.size, }); toast.success('Fichier uploadé avec succès'); } catch (error) { toast.error('Erreur lors de l\'upload du fichier'); setUploadProgress(0); } finally { setUploading(false); } }} className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-primary file:text-primary-foreground hover:file:bg-primary/90" /> {uploading && (
Upload en cours... {uploadProgress}%
)} {fileData && !uploading && (

Fichier sélectionné : {fileData.nomFichier}

)}
); } 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 [filtreStatut, setFiltreStatut] = useState<"toutes" | "terminees" | "a_venir">("toutes"); // Récupérer la liste des formations const { data: formations, isLoading: loadingFormations } = trpc.formations.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 } ); // 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, }); }; // Supprimer un document 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 }); } }; // Ouvrir le dialogue d'upload const handleOpenUpload = (inscriptionId: number) => { setSelectedInscriptionId(inscriptionId); setUploadDialogOpen(true); }; 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 {/* Filtre par statut */} {selectedFormationId && ( Filtrer les séquences Afficher uniquement les séquences terminées ou à venir setFiltreStatut(value)}>
)} {/* 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
{modeEnvoi === "manuel" && apprenants && apprenants.length > 0 && ( )}
{loadingApprenants ? (

Chargement...

) : apprenants && apprenants.length > 0 ? (
{/* Regrouper les apprenants par séquence */} {Object.entries( apprenants.reduce((acc: Record, item) => { const sequenceId = item.sequence.id.toString(); if (!acc[sequenceId]) acc[sequenceId] = []; acc[sequenceId].push(item); return acc; }, {}) ) .sort((a, b) => { // Trier par ID de séquence (ordre de création) const idA = parseInt(a[0]); const idB = parseInt(b[0]); return idA - idB; }) .filter(([sequenceId, sequenceApprenants]) => { // Filtrer selon le statut sélectionné if (filtreStatut === "toutes") return true; const premiereDate = sequenceApprenants[0]?.sequence?.premiereDate; if (!premiereDate) return true; // Afficher les séquences sans date const now = new Date(); const sequenceDate = new Date(premiereDate); if (filtreStatut === "terminees") { return sequenceDate < now; } else if (filtreStatut === "a_venir") { return sequenceDate >= now; } return true; }) .map(([sequenceId, sequenceApprenants]) => { const sequenceName = sequenceApprenants[0]?.sequence?.nom || 'Séquence inconnue'; const premiereDate = sequenceApprenants[0]?.sequence?.premiereDate; const dateFormatted = premiereDate ? new Date(premiereDate).toLocaleDateString('fr-FR') : null; return (
{/* En-tête de séquence */}

{sequenceName} {dateFormatted && - {dateFormatted}}

{sequenceApprenants.length} apprenant(s)

{sequenceApprenants.filter(a => a.attestation?.emailEnvoye).length} / {sequenceApprenants.length} envoyé(es)
{/* 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" && ( )} )}
); })}
); })}
) : (

Aucun apprenant inscrit pour cette formation

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