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 (
);
}
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