diff --git a/.manus/db/db-query-1768245879881.json b/.manus/db/db-query-1768245879881.json new file mode 100644 index 0000000..5d2a392 --- /dev/null +++ b/.manus/db/db-query-1768245879881.json @@ -0,0 +1,9 @@ +{ + "query": "ALTER TABLE formations \nADD COLUMN modeAttestation ENUM('auto', 'manuel') NOT NULL DEFAULT 'auto',\nADD COLUMN modeEnvoi ENUM('auto', 'manuel') NOT NULL DEFAULT 'manuel';\n\nALTER TABLE attestations\nADD COLUMN documentUrl VARCHAR(500),\nADD COLUMN documentS3Key VARCHAR(500),\nADD COLUMN uploadedBy INT,\nADD COLUMN uploadedAt TIMESTAMP;", + "command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute ALTER TABLE formations \nADD COLUMN modeAttestation ENUM('auto', 'manuel') NOT NULL DEFAULT 'auto',\nADD COLUMN modeEnvoi ENUM('auto', 'manuel') NOT NULL DEFAULT 'manuel';\n\nALTER TABLE attestations\nADD COLUMN documentUrl VARCHAR(500),\nADD COLUMN documentS3Key VARCHAR(500),\nADD COLUMN uploadedBy INT,\nADD COLUMN uploadedAt TIMESTAMP;", + "rows": [], + "messages": [], + "stdout": "", + "stderr": "", + "execution_time_ms": 1919 +} \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index fa1087e..9a22992 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -31,6 +31,7 @@ import AdminFormateurs from "./pages/admin/AdminFormateurs"; import AdminNotifications from "./pages/admin/AdminNotifications"; import AdminAttestations from "./pages/admin/AdminAttestations"; import HistoriqueAttestations from "./pages/HistoriqueAttestations"; +import AdminGestionAttestations from "./pages/AdminGestionAttestations"; import AdminParametres from "./pages/AdminParametres"; import QuestionnaireReponse from "./pages/QuestionnaireReponse"; import Inscription from "./pages/Inscription"; @@ -57,6 +58,7 @@ function Router() { + diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx index 20513e5..10b014b 100644 --- a/client/src/components/DashboardLayout.tsx +++ b/client/src/components/DashboardLayout.tsx @@ -54,6 +54,7 @@ const menuSections = [ { icon: CalendarDays, label: "Calendrier", path: "/admin/calendrier" }, { icon: Users, label: "Apprenants", path: "/admin/apprenants" }, { icon: QrCode, label: "Émargement", path: "/formateur/emargement" }, + { icon: FileText, label: "Gestion des attestations", path: "/admin/gestion-attestations" }, ] }, { diff --git a/client/src/pages/AdminGestionAttestations.tsx b/client/src/pages/AdminGestionAttestations.tsx new file mode 100644 index 0000000..e191542 --- /dev/null +++ b/client/src/pages/AdminGestionAttestations.tsx @@ -0,0 +1,400 @@ +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"; + +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); + + // 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}`); + }, + }); + + // 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) => { + if (!apprenant.attestation?.urlPdf) { + toast.error("Aucune attestation disponible pour cet apprenant"); + return; + } + + sendAttestationMutation.mutate({ + attestationId: apprenant.attestation.id, + apprenantEmail: apprenant.apprenant.email, + apprenantNom: apprenant.apprenant.nom, + apprenantPrenom: apprenant.apprenant.prenom, + formationNom: formations?.find(f => f.id === selectedFormationId)?.nom || "", + pdfUrl: apprenant.attestation.urlPdf, + }); + }; + + // Supprimer un document + const handleDeleteDocument = (attestationId: number) => { + if (confirm("Êtes-vous sûr de vouloir supprimer ce document ?")) { + deleteDocumentMutation.mutate({ attestationId }); + } + }; + + // 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 + + + + + + + + {/* 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 + + + + {loadingApprenants ? ( +

Chargement...

+ ) : apprenants && apprenants.length > 0 ? ( + + + + Nom + Prénom + Email + Séquence + Statut attestation + Actions + + + + {apprenants.map((item) => { + 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} + {item.sequence.nom} + + {!hasDocument && ( + + Aucune attestation + + )} + {hasDocument && !isSent && ( + + Prête à envoyer + + )} + {hasDocument && isSent && ( + + Envoyée + + )} + + +
+ {modeAttestation === "manuel" && ( + + )} + {hasDocument && ( + <> + + {modeEnvoi === "manuel" && !isSent && ( + + )} + {modeAttestation === "manuel" && ( + + )} + + )} +
+
+
+ ); + })} +
+
+ ) : ( +
+ +

Aucun apprenant inscrit pour cette formation

+
+ )} +
+
+ )} + + {/* Dialog d'upload */} + + + + Charger une attestation + + Sélectionnez un fichier PDF à uploader pour cet apprenant + + +
+

+ Fonctionnalité d'upload à implémenter avec un composant FileUpload similaire à celui des rappels +

+ +
+
+
+ + {/* Dialog de prévisualisation */} + + + + Prévisualisation de l'attestation + +
+ {previewUrl && ( +