import { useState, useMemo } from "react"; import DashboardLayout from "@/components/DashboardLayout"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { trpc } from "@/lib/trpc"; import { Calendar, Edit, Eye, Plus, Trash2, X, Bell, Maximize2, Minimize2, Send } from "lucide-react"; import FormateurCombobox from "@/components/FormateurCombobox"; import CapacityProgressBar from "@/components/CapacityProgressBar"; import { toast } from "sonner"; import { useLocation } from "wouter"; import { format } from "date-fns"; interface DateFormation { dateDebut: string; dateFin: string; ordre: number; } export default function AdminSequences() { const [, setLocation] = useLocation(); const [isCreateOpen, setIsCreateOpen] = useState(false); const [isEditOpen, setIsEditOpen] = useState(false); const [editingSequence, setEditingSequence] = useState(null); const [searchTerm, setSearchTerm] = useState(""); const [filterLieu, setFilterLieu] = useState("all"); const [filterStatut, setFilterStatut] = useState("all"); const [filterPublicCible, setFilterPublicCible] = useState("all"); const [sortBy, setSortBy] = useState<"date" | "lieu" | "capacite">("date"); const [isCompactView, setIsCompactView] = useState(true); const [formData, setFormData] = useState({ formationId: "", nom: "", lieu: "", publicCible: "autre" as "directeur" | "chef_service" | "autre" | "tous", capaciteMax: "12", dateBlocage: "", statut: "ouverte" as "ouverte" | "bloquee" | "terminee", formateur: null as number | null, dates: [ { dateDebut: "", dateFin: "", ordre: 1 } ] as DateFormation[], }); const utils = trpc.useUtils(); const { data: formations } = trpc.formations.list.useQuery(); const { data: sequences, isLoading } = trpc.sequences.list.useQuery(); const createMutation = trpc.sequences.create.useMutation({ onSuccess: () => { toast.success("Séquence créée avec succès"); setIsCreateOpen(false); resetForm(); utils.sequences.list.invalidate(); }, onError: (error) => { toast.error(`Erreur : ${error.message}`); }, }); const updateMutation = trpc.sequences.update.useMutation({ onSuccess: async () => { toast.success("Séquence modifiée avec succès"); setIsEditOpen(false); setEditingSequence(null); // Forcer un refetch immédiat au lieu d'une simple invalidation await utils.sequences.list.refetch(); }, onError: (error) => { toast.error(`Erreur : ${error.message}`); }, }); const deleteMutation = trpc.sequences.delete.useMutation({ onSuccess: () => { toast.success("Séquence supprimée avec succès"); utils.sequences.list.invalidate(); }, onError: (error) => { toast.error(`Erreur : ${error.message}`); }, }); const testerRappelMutation = trpc.rappels.testerRappel.useMutation({ onSuccess: () => { toast.success("Email de test envoyé avec succès"); }, onError: (error) => { toast.error(`Erreur lors de l'envoi du test : ${error.message}`); }, }); const resetForm = () => { setFormData({ formationId: "", nom: "", lieu: "", publicCible: "autre", capaciteMax: "12", dateBlocage: "", statut: "ouverte", formateur: null, dates: [{ dateDebut: "", dateFin: "", ordre: 1 }], }); }; const handleEdit = (sequence: any) => { setEditingSequence(sequence); // Fonction helper pour formater une date en toute sécurité const safeFormatDate = (dateValue: any): string => { if (!dateValue) return ""; try { // Si c'est un objet Date JavaScript if (dateValue instanceof Date) { // MySQL stocke les dates en heure locale (pas UTC) // Donc on utilise les méthodes locales pour extraire les valeurs const year = dateValue.getFullYear(); const month = String(dateValue.getMonth() + 1).padStart(2, '0'); const day = String(dateValue.getDate()).padStart(2, '0'); const hours = String(dateValue.getHours()).padStart(2, '0'); const minutes = String(dateValue.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day}T${hours}:${minutes}`; } // Si c'est déjà une chaîne au bon format, la retourner directement if (typeof dateValue === 'string' && dateValue.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/)) { return dateValue.substring(0, 16); // Garder seulement YYYY-MM-DDTHH:mm } // Parser la date MySQL (format: "YYYY-MM-DD HH:mm:ss") 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 ""; } const [, year, month, day, hours, minutes] = match; return `${year}-${month}-${day}T${hours}:${minutes}`; } catch (error) { console.error('Erreur de formatage de date:', error, dateValue); return ""; } }; setFormData({ formationId: sequence.formationId.toString(), nom: sequence.nom || "", lieu: sequence.lieu || "", publicCible: sequence.publicCible || "autre", capaciteMax: sequence.capaciteMax?.toString() || "12", dateBlocage: safeFormatDate(sequence.dateBlocage), statut: sequence.statut || "ouverte", formateur: sequence.formateurId?.toString() || "", dates: (sequence.dates || []).map((d: any) => ({ dateDebut: safeFormatDate(d.dateDebut), dateFin: safeFormatDate(d.dateFin), ordre: d.ordre || 1, })), }); setIsEditOpen(true); }; // Convertir une date locale (datetime-local) en ISO UTC const convertLocalToUTC = (localDateStr: string): string => { if (!localDateStr || localDateStr.trim() === '') return ''; // datetime-local retourne "YYYY-MM-DDTHH:mm" // On doit le traiter comme heure locale et le convertir en UTC const localDate = new Date(localDateStr); return localDate.toISOString(); }; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!formData.dates.length) { toast.error("Veuillez ajouter au moins une date de formation"); return; } const data = { formationId: parseInt(formData.formationId), nom: formData.nom, lieu: formData.lieu, publicCible: formData.publicCible, capaciteMax: parseInt(formData.capaciteMax), dateBlocage: convertLocalToUTC(formData.dateBlocage), statut: formData.statut, formateurId: formData.formateur ? parseInt(formData.formateur) : null, dates: formData.dates.map(d => ({ ...d, dateDebut: convertLocalToUTC(d.dateDebut), dateFin: convertLocalToUTC(d.dateFin), })), }; if (editingSequence) { console.log('[CLIENT] Envoi de la mise à jour:', { id: editingSequence.id, ...data }); updateMutation.mutate({ id: editingSequence.id, ...data }); } else { createMutation.mutate(data); } }; const handleDelete = (id: number) => { if (confirm("Êtes-vous sûr de vouloir supprimer cette séquence ?")) { deleteMutation.mutate({ id }); } }; const addDate = () => { if (formData.dates.length >= 4) { toast.error("Maximum 4 dates par séquence"); return; } setFormData({ ...formData, dates: [...formData.dates, { dateDebut: "", dateFin: "", ordre: formData.dates.length + 1 }], }); }; const removeDate = (index: number) => { if (formData.dates.length <= 1) { toast.error("Au moins une date est requise"); return; } const newDates = formData.dates.filter((_, i) => i !== index); // Réordonner les dates newDates.forEach((d, i) => d.ordre = i + 1); setFormData({ ...formData, dates: newDates }); }; const updateDate = (index: number, field: "dateDebut" | "dateFin", value: string) => { const newDates = [...formData.dates]; newDates[index][field] = value; setFormData({ ...formData, dates: newDates }); }; // Filtrage et tri const filteredAndSortedSequences = useMemo(() => { if (!sequences) return []; let filtered = sequences.filter((seq) => { const matchSearch = seq.nom.toLowerCase().includes(searchTerm.toLowerCase()); const matchLieu = filterLieu === "all" || seq.lieu === filterLieu; const matchStatut = filterStatut === "all" || seq.statut === filterStatut; const matchPublicCible = filterPublicCible === "all" || seq.publicCible === filterPublicCible; return matchSearch && matchLieu && matchStatut && matchPublicCible; }); filtered.sort((a, b) => { if (sortBy === "date") { const dateA = a.dates[0] ? new Date(a.dates[0].dateDebut).getTime() : 0; const dateB = b.dates[0] ? new Date(b.dates[0].dateDebut).getTime() : 0; return dateA - dateB; } else if (sortBy === "lieu") { return a.lieu.localeCompare(b.lieu); } else { return b.capaciteMax - a.capaciteMax; } }); return filtered; }, [sequences, searchTerm, filterLieu, filterStatut, filterPublicCible, sortBy]); const uniqueLieux = useMemo(() => { if (!sequences) return []; return Array.from(new Set(sequences.map((s) => s.lieu))); }, [sequences]); const getFormationName = (formationId: number) => { return formations?.find((f) => f.id === formationId)?.nom || "N/A"; }; // Générer une couleur unique par formation (en évitant vert, orange, rouge) const formationColorMap = useMemo(() => { if (!formations) return new Map(); const colors = [ "text-blue-600", "text-purple-600", "text-pink-600", "text-indigo-600", "text-cyan-600", "text-teal-600", "text-violet-600", "text-fuchsia-600", "text-sky-600", "text-amber-600", ]; const map = new Map(); formations.forEach((formation, index) => { map.set(formation.id, colors[index % colors.length]); }); return map; }, [formations]); const getFormationColor = (formationId: number) => { return formationColorMap.get(formationId) || "text-gray-600"; }; 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 pour affichage:', 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 pour affichage:', error, dateValue); return "N/A"; } }; const getStatutBadge = (statut: string) => { const colors = { ouverte: "bg-green-100 text-green-800", bloquee: "bg-orange-100 text-orange-800", terminee: "bg-gray-100 text-gray-800", }; return colors[statut as keyof typeof colors] || colors.ouverte; }; const getPublicCibleBadge = (publicCible: string) => { const colors = { directeur: "bg-blue-100 text-blue-800", chef_service: "bg-green-100 text-green-800", tous: "bg-purple-100 text-purple-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", tous: "Tous", autre: "Autre", }; return labels[publicCible as keyof typeof labels] || "Autre"; }; return (

Gestion des Séquences

Créez et gérez les séquences de formation avec leurs dates

Créer une nouvelle séquence Remplissez les informations de la séquence et ajoutez jusqu'à 4 dates de formation
setFormData({ ...formData, nom: e.target.value })} placeholder="Ex: Séquence 1 - Février 2026" required />
setFormData({ ...formData, lieu: e.target.value })} placeholder="Ex: Salle de formation A" required />
setFormData({ ...formData, formateur: value })} />
setFormData({ ...formData, capaciteMax: e.target.value })} required />
setFormData({ ...formData, dateBlocage: e.target.value })} required />
{formData.dates.map((date, index) => (
Date {date.ordre} {formData.dates.length > 1 && ( )}
updateDate(index, "dateDebut", e.target.value)} required />
updateDate(index, "dateFin", e.target.value)} required />
))}
{/* Filtres */} Filtres et recherche
setSearchTerm(e.target.value)} />

{filteredAndSortedSequences.length} séquence(s) trouvée(s)

{/* Liste des séquences */} Liste des séquences {sequences?.length || 0} séquence(s) au total {isLoading ? (
Chargement...
) : filteredAndSortedSequences.length === 0 ? (
Aucune séquence trouvée
) : (
Formation Nom Dates Rappels pré-formation Rappels post-formation {!isCompactView && ( <> Lieu Formateur Public cible )} Capacité Statut Actions {filteredAndSortedSequences.map((sequence, index) => ( {getFormationName(sequence.formationId)} {sequence.nom}
{sequence.dates.map((date: any) => (
Date {date.ordre}:{" "} {formatDate(date.dateDebut)}
))}
{sequence.dates.map((date: any) => { const rappelsPre = date.rappels?.filter((r: any) => r.timing === 'pre_formation') || []; return (
{rappelsPre.length > 0 ? (
{rappelsPre.map((rappel: any) => ( {rappel.nom} ))}
) : ( - )}
); })}
{sequence.dates.map((date: any) => { const rappelsPost = date.rappels?.filter((r: any) => r.timing === 'post_formation') || []; return (
{rappelsPost.length > 0 ? (
{rappelsPost.map((rappel: any) => ( {rappel.nom} ))}
) : ( - )}
); })}
{!isCompactView && ( <> {sequence.lieu} {sequence.formateur?.nom || "-"} {getPublicCibleLabel(sequence.publicCible)} )} {sequence.statut}
))}
)}
{/* Dialog d'édition */} Modifier la séquence Modifiez les informations de la séquence et ses dates
setFormData({ ...formData, nom: e.target.value })} required />
setFormData({ ...formData, lieu: e.target.value })} required />
setFormData({ ...formData, capaciteMax: e.target.value })} required />
setFormData({ ...formData, formateur: value })} placeholder="Sélectionner un formateur" />
setFormData({ ...formData, dateBlocage: e.target.value })} required />
{formData.dates.map((date, index) => (
Date {date.ordre} {formData.dates.length > 1 && ( )}
updateDate(index, "dateDebut", e.target.value)} required />
updateDate(index, "dateFin", e.target.value)} required />
))}
); }