import DashboardLayout from "@/components/DashboardLayout"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { trpc } from "@/lib/trpc"; import { ArrowLeft, Download, Mail, Search, Eye } from "lucide-react"; import { useLocation, useRoute } from "wouter"; import { useState, useMemo } from "react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Skeleton } from "@/components/ui/skeleton"; import { toast } from "sonner"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; export default function AdminSequenceInscrits() { const [, setLocation] = useLocation(); const [, params] = useRoute("/admin/sequences/:id/inscrits"); const sequenceId = params?.id ? parseInt(params.id) : 0; const getPublicCibleBadge = (publicCible: string) => { const colors = { directeur: "bg-blue-100 text-blue-800", chef_service: "bg-green-100 text-green-800", autre: "bg-gray-100 text-gray-800", }; return colors[publicCible as keyof typeof colors] || colors.autre; }; const getPublicCibleLabel = (publicCible: string) => { const labels = { directeur: "Directeur", chef_service: "Chef de service", autre: "Autre", }; return labels[publicCible as keyof typeof labels] || "Autre"; }; // États pour les filtres const [searchTerm, setSearchTerm] = useState(""); const [filterStatut, setFilterStatut] = useState("all"); const [filterEtablissement, setFilterEtablissement] = useState("all"); const [filterFonction, setFilterFonction] = useState("all"); const [sortBy, setSortBy] = useState<"date" | "nom">("date"); const [showPreviewModal, setShowPreviewModal] = useState(false); const [previewType, setPreviewType] = useState<"teaser" | "rappel">("teaser"); const utils = trpc.useUtils(); const { data: sequence, isLoading: loadingSequence } = trpc.sequences.getById.useQuery({ id: sequenceId }); const { data: inscriptions, isLoading: loadingInscriptions } = trpc.inscriptions.listBySequence.useQuery({ sequenceId }); const { data: formations } = trpc.formations.list.useQuery(); const { data: emailPreview, isLoading: loadingPreview } = trpc.inscriptions.emailPreview.useQuery( { sequenceId, type: previewType }, { enabled: showPreviewModal } ); const updateStatutMutation = trpc.inscriptions.updateStatut.useMutation({ onSuccess: () => { utils.inscriptions.listBySequence.invalidate({ sequenceId }); toast.success("Statut mis à jour avec succès"); }, onError: (error) => { toast.error("Erreur lors de la mise à jour : " + error.message); }, }); const sendEmailsMutation = trpc.inscriptions.sendGroupEmail.useMutation({ onSuccess: (result) => { if (result.sent > 0) { toast.success(result.message || `${result.sent} email(s) envoyé(s) avec succès`); } else { toast.warning("Aucun email envoyé. Vérifiez qu'il y a des apprenants confirmés."); } }, onError: (error) => { toast.error("Erreur lors de l'envoi : " + error.message); }, }); const handleStatutChange = (inscriptionId: number, newStatut: string) => { updateStatutMutation.mutate({ id: inscriptionId, statut: newStatut as "confirmee" | "liste_attente" | "annulee", }); }; const exportExcelMutation = trpc.inscriptions.exportExcel.useMutation({ onSuccess: (result) => { const blob = new Blob([Uint8Array.from(atob(result.buffer), c => c.charCodeAt(0))], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = result.filename; a.click(); window.URL.revokeObjectURL(url); toast.success("Export Excel téléchargé"); }, onError: (error) => { toast.error("Erreur lors de l'export : " + error.message); }, }); const exportPDFMutation = trpc.inscriptions.exportPDF.useMutation({ onSuccess: (result) => { const blob = new Blob([Uint8Array.from(atob(result.buffer), c => c.charCodeAt(0))], { type: 'application/pdf' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = result.filename; a.click(); window.URL.revokeObjectURL(url); toast.success("Export PDF téléchargé"); }, onError: (error) => { toast.error("Erreur lors de l'export : " + error.message); }, }); const exportFeuilleMutation = trpc.inscriptions.exportFeuillePresence.useMutation({ onSuccess: (result) => { const blob = new Blob([Uint8Array.from(atob(result.buffer), c => c.charCodeAt(0))], { type: 'application/pdf' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = result.filename; a.click(); window.URL.revokeObjectURL(url); toast.success("Feuille de présence téléchargée"); }, onError: (error) => { toast.error("Erreur lors de l'export : " + error.message); }, }); const handleExportExcel = () => { exportExcelMutation.mutate({ sequenceId }); }; const handleExportPDF = () => { exportPDFMutation.mutate({ sequenceId }); }; const handleExportFeuille = () => { exportFeuilleMutation.mutate({ sequenceId }); }; const handleSendEmails = (type: "teaser" | "rappel" | "rappel_j1") => { const typeLabel = type === "teaser" ? "teaser" : type === "rappel" ? "de rappel J-7" : "de rappel J-1"; if (confirm(`Êtes-vous sûr de vouloir envoyer les emails ${typeLabel} à tous les inscrits confirmés ?`)) { sendEmailsMutation.mutate({ sequenceId, type }); } }; // Filtrage et tri const filteredAndSortedInscriptions = useMemo(() => { if (!inscriptions) return []; let filtered = inscriptions.filter((item) => { if (!item.apprenant) return false; const matchSearch = item.apprenant.nom.toLowerCase().includes(searchTerm.toLowerCase()) || item.apprenant.prenom.toLowerCase().includes(searchTerm.toLowerCase()) || item.apprenant.email.toLowerCase().includes(searchTerm.toLowerCase()); const matchStatut = filterStatut === "all" || item.inscription.statut === filterStatut; const matchEtablissement = filterEtablissement === "all" || item.apprenant.codeEtablissement === filterEtablissement; const matchFonction = filterFonction === "all" || item.apprenant.fonction === filterFonction; return matchSearch && matchStatut && matchEtablissement && matchFonction; }); filtered.sort((a, b) => { if (sortBy === "date") { return new Date(a.inscription.dateInscription).getTime() - new Date(b.inscription.dateInscription).getTime(); } else { return (a.apprenant?.nom || "").localeCompare(b.apprenant?.nom || ""); } }); return filtered; }, [inscriptions, searchTerm, filterStatut, filterEtablissement, filterFonction, sortBy]); const uniqueEtablissements = useMemo(() => { if (!inscriptions) return []; return Array.from(new Set(inscriptions.map((i) => i.apprenant?.codeEtablissement).filter(Boolean))); }, [inscriptions]); const getFormationName = () => { if (!sequence || !formations) return "N/A"; return formations.find((f) => f.id === sequence.formationId)?.nom || "N/A"; }; const formatDate = (dateValue: any) => { if (!dateValue) return "N/A"; try { // Si c'est déjà un objet Date JavaScript if (dateValue instanceof Date) { const day = String(dateValue.getDate()).padStart(2, '0'); const month = String(dateValue.getMonth() + 1).padStart(2, '0'); const year = dateValue.getFullYear(); const hours = String(dateValue.getHours()).padStart(2, '0'); const minutes = String(dateValue.getMinutes()).padStart(2, '0'); return `${day}/${month}/${year} ${hours}:${minutes}`; } // Parser la date MySQL (chaîne) const dateStr = String(dateValue); const match = dateStr.match(/(\d{4})-(\d{2})-(\d{2})[T ]?(\d{2}):(\d{2})/); if (!match) { console.warn('Format de date non reconnu:', dateValue); return "N/A"; } const [, year, month, day, hours, minutes] = match; return `${day}/${month}/${year} ${hours}:${minutes}`; } catch (error) { console.error('Erreur de formatage de date:', error, dateValue); return "N/A"; } }; const getStatutBadge = (statut: string) => { const colors = { confirmee: "bg-green-100 text-green-800", liste_attente: "bg-orange-100 text-orange-800", annulee: "bg-gray-100 text-gray-800", }; const labels = { confirmee: "Confirmée", liste_attente: "Liste d'attente", annulee: "Annulée", }; return { color: colors[statut as keyof typeof colors] || colors.confirmee, label: labels[statut as keyof typeof labels] || statut, }; }; const getFonctionLabel = (fonction: string) => { const labels = { directeur: "Directeur", chef_service: "Chef de service", autre: "Autre", }; return labels[fonction as keyof typeof labels] || fonction; }; if (loadingSequence || loadingInscriptions) { return (
); } if (!sequence) { return (
Séquence introuvable La séquence demandée n'existe pas.
); } const nbConfirmes = inscriptions?.filter((i) => i.inscription.statut === "confirmee").length || 0; const nbListeAttente = inscriptions?.filter((i) => i.inscription.statut === "liste_attente").length || 0; return (

Inscrits à la séquence

{getFormationName()} - {sequence.nom}

{/* Informations de la séquence */} Informations de la séquence

Formation

{getFormationName()}

Nom de la séquence

{sequence.nom}

Lieu

{sequence.lieu}

Capacité

{nbConfirmes} / {sequence.capaciteMax} inscrits confirmés

Public cible

{getPublicCibleLabel(sequence.publicCible)}

Dates de formation

{sequence.dates.map((date: any) => (
Date {date.ordre}: Du {formatDate(date.dateDebut)} au {formatDate(date.dateFin)}
))}
{/* Actions */} Actions Envoi d'emails et exports
{/* Filtres */} Filtres et recherche
setSearchTerm(e.target.value)} />

{filteredAndSortedInscriptions.length} inscription(s) trouvée(s) sur {inscriptions?.length || 0} au total

{/* Liste des inscrits */} Liste des inscrits {nbConfirmes} confirmé(s), {nbListeAttente} en liste d'attente {filteredAndSortedInscriptions.length === 0 ? (
Aucune inscription trouvée
) : (
Nom Prénom Email Code étab. Fonction Date inscription Statut Actions {filteredAndSortedInscriptions.map((item) => { if (!item.apprenant) return null; const statutInfo = getStatutBadge(item.inscription.statut); return ( {item.apprenant.nom} {item.apprenant.prenom} {item.apprenant.email} {item.apprenant.codeEtablissement} {getFonctionLabel(item.apprenant.fonction)} {formatDate(item.inscription.dateInscription)} {statutInfo.label} ); })}
)}
{/* Modal d'aperçu d'email */} Aperçu de l'email {previewType === "teaser" ? "teaser" : previewType === "rappel" ? "de rappel J-7" : "de rappel J-1"} {loadingPreview ? ( "Chargement de l'aperçu..." ) : emailPreview ? ( `Destinataire : ${emailPreview.recipient.prenom} ${emailPreview.recipient.nom} (${emailPreview.recipient.email})` ) : ( "Erreur lors du chargement de l'aperçu" )} {loadingPreview ? (
) : emailPreview ? (

Sujet :

{emailPreview.subject}