import { useState } 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, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; 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 { Bell, Download, Edit, Plus, Trash2, GraduationCap, Calendar, CalendarDays } from "lucide-react"; import { toast } from "sonner"; import { FileUpload } from "@/components/FileUpload"; import { DateFormationMultiSelect } from "@/components/DateFormationMultiSelect"; // Types de templates disponibles pour les rappels const TEMPLATE_TYPES = [ { value: "rappel1", label: "Rappel 1", joursAvant: 7 }, { value: "rappel2", label: "Rappel 2", joursAvant: 1 }, { value: "rappel3", label: "Rappel 3", joursAvant: 14 }, { value: "rappel4", label: "Rappel 4", joursAvant: 21 }, { value: "rappel5", label: "Rappel 5", joursAvant: 30 }, { value: "rappel6", label: "Rappel 6", joursAvant: 60 }, ]; export default function AdminRappels() { const [isCreateOpen, setIsCreateOpen] = useState(false); const [isEditOpen, setIsEditOpen] = useState(false); const [editingRappel, setEditingRappel] = useState(null); const [filterFormation, setFilterFormation] = useState(""); const [filterSequence, setFilterSequence] = useState(""); const [formData, setFormData] = useState({ nom: "", templateType: "", timing: "pre_formation" as "pre_formation" | "post_formation", joursAvant: 7, heureEnvoi: "09:00", actif: true, dateFormationIds: [] as number[], fichier: null as { nomFichier: string; urlFichier: string; s3Key: string; typeFichier: string; tailleFichier: number; } | null, fichier2: null as { nomFichier: string; urlFichier: string; s3Key: string; typeFichier: string; tailleFichier: number; } | null, }); const { data: rappels, refetch } = trpc.rappels.list.useQuery(); const { data: sequences } = trpc.sequences.list.useQuery(); const createMutation = trpc.rappels.create.useMutation({ onSuccess: () => { toast.success("Rappel créé avec succès"); refetch(); setIsCreateOpen(false); resetForm(); }, onError: (error) => { toast.error(`Erreur: ${error.message}`); }, }); const updateMutation = trpc.rappels.update.useMutation({ onSuccess: () => { toast.success("Rappel modifié avec succès"); refetch(); setIsEditOpen(false); setEditingRappel(null); resetForm(); }, onError: (error) => { toast.error(`Erreur: ${error.message}`); }, }); const deleteMutation = trpc.rappels.delete.useMutation({ onSuccess: () => { toast.success("Rappel supprimé avec succès"); refetch(); }, onError: (error) => { toast.error(`Erreur: ${error.message}`); }, }); const toggleActifMutation = trpc.rappels.toggleActif.useMutation({ onSuccess: () => { toast.success("Statut modifié avec succès"); refetch(); }, onError: (error) => { toast.error(`Erreur: ${error.message}`); }, }); const resetForm = () => { setFormData({ nom: "", templateType: "", fichier: null, fichier2: null, timing: "pre_formation", joursAvant: 7, heureEnvoi: "09:00", actif: true, dateFormationIds: [], }); }; const handleCreate = () => { if (!formData.nom || !formData.templateType) { toast.error("Veuillez remplir tous les champs obligatoires"); return; } createMutation.mutate(formData); }; const handleEdit = (rappel: any) => { setEditingRappel(rappel); setFormData({ nom: rappel.nom, templateType: rappel.templateType, timing: rappel.timing || "pre_formation", joursAvant: rappel.joursAvant, heureEnvoi: rappel.heureEnvoi, actif: rappel.actif, dateFormationIds: rappel.dateFormationIds || [], fichier: rappel.nomFichier ? { nomFichier: rappel.nomFichier, urlFichier: rappel.urlFichier, s3Key: rappel.s3Key, typeFichier: rappel.typeFichier, tailleFichier: rappel.tailleFichier, } : null, fichier2: rappel.nomFichier2 ? { nomFichier: rappel.nomFichier2, urlFichier: rappel.urlFichier2, s3Key: rappel.s3Key2, typeFichier: rappel.typeFichier2, tailleFichier: rappel.tailleFichier2, } : null, }); setIsEditOpen(true); }; const handleUpdate = () => { if (!editingRappel) return; if (!formData.nom || !formData.templateType) { toast.error("Veuillez remplir tous les champs obligatoires"); return; } updateMutation.mutate({ id: editingRappel.id, ...formData, }); }; const handleDelete = (id: number) => { if (confirm("Êtes-vous sûr de vouloir supprimer ce rappel ?")) { deleteMutation.mutate({ id }); } }; const handleToggleActif = (id: number, actif: boolean) => { toggleActifMutation.mutate({ id, actif: !actif }); }; const handleTemplateTypeChange = (value: string) => { const template = TEMPLATE_TYPES.find(t => t.value === value); setFormData({ ...formData, templateType: value, joursAvant: template?.joursAvant || 7, nom: template?.label || "", }); }; const getTemplateName = (type: string) => { return TEMPLATE_TYPES.find(t => t.value === type)?.label || type; }; const exportToCSV = () => { if (!rappels || rappels.length === 0) { toast.error("Aucun rappel à exporter"); return; } // Appliquer les mêmes filtres que le tableau const filteredRappels = rappels.filter((rappel) => { if (filterFormation && filterFormation !== "all") { if (!rappel.datesDetails || rappel.datesDetails.length === 0) { return true; } const hasFormation = rappel.datesDetails.some((d: any) => d.formationNom === filterFormation); if (!hasFormation) return false; } if (filterSequence && filterSequence !== "all") { if (!rappel.datesDetails || rappel.datesDetails.length === 0) { return true; } const hasSequence = rappel.datesDetails.some((d: any) => d.sequenceNom === filterSequence); if (!hasSequence) return false; } return true; }); // Créer le CSV const headers = ["Nom", "Type", "Template", "Délai", "Heure", "Formations", "Séquences", "Dates", "Statut"]; const rows = filteredRappels.map((rappel) => { const type = rappel.datesDetails && rappel.datesDetails.length > 0 ? "Ciblé" : "Global"; const templateName = getTemplateName(rappel.templateType); const delai = rappel.timing === "post_formation" ? `J+${rappel.joursAvant}` : `J-${rappel.joursAvant}`; const statut = rappel.actif ? "Actif" : "Inactif"; let formations = "Toutes"; let sequences = "Toutes"; let dates = "Toutes"; if (rappel.datesDetails && rappel.datesDetails.length > 0) { const formationNames = Array.from(new Set(rappel.datesDetails.map((d: any) => d.formationNom))); const sequenceNames = Array.from(new Set(rappel.datesDetails.map((d: any) => d.sequenceNom))); const dateOrdres = rappel.datesDetails.map((d: any) => `Date ${d.ordre}`).join(", "); formations = formationNames.join("; "); sequences = sequenceNames.join("; "); dates = dateOrdres; } return [rappel.nom, type, templateName, delai, rappel.heureEnvoi, formations, sequences, dates, statut]; }); // Convertir en CSV const csvContent = [ headers.join(","), ...rows.map(row => row.map(cell => `\"${cell}\"`).join(",")) ].join("\n"); // Télécharger le fichier const blob = new Blob(["\ufeff" + csvContent], { type: "text/csv;charset=utf-8;" }); const link = document.createElement("a"); const url = URL.createObjectURL(blob); link.setAttribute("href", url); link.setAttribute("download", `rappels_${new Date().toISOString().split('T')[0]}.csv`); link.style.visibility = "hidden"; document.body.appendChild(link); link.click(); document.body.removeChild(link); toast.success(`Export réussi : ${filteredRappels.length} rappel(s)`); }; const formatDatesInfo = (rappel: any) => { if (!rappel.datesDetails || rappel.datesDetails.length === 0) { return (
Toutes les dates
); } // Grouper par formation et séquence const grouped = rappel.datesDetails.reduce((acc: any, date: any) => { const key = `${date.formationNom}|||${date.sequenceNom}`; if (!acc[key]) { acc[key] = { formationNom: date.formationNom, sequenceNom: date.sequenceNom, dates: [] }; } acc[key].dates.push(date); return acc; }, {}); return (
{Object.values(grouped).map((group: any, groupIdx: number) => (
{group.formationNom} {group.sequenceNom} : {group.dates.map((d: any, idx: number) => ( Date {d.ordre} ))}
))}
); }; const formatDatesInfoOLD = (rappel: any) => { if (!rappel.datesDetails || rappel.datesDetails.length === 0) { return (
Toutes les dates
); } // Grouper par formation et séquence const grouped = rappel.datesDetails.reduce((acc: any, date: any) => { const key = `${date.formationNom}|||${date.sequenceNom}`; if (!acc[key]) { acc[key] = { formationNom: date.formationNom, sequenceNom: date.sequenceNom, dates: [] }; } acc[key].dates.push(date); return acc; }, {}); return (
{Object.values(grouped).map((group: any, groupIdx: number) => (
{group.formationNom} {group.sequenceNom}
{group.dates.map((d: any, idx: number) => ( Date {d.ordre} ))}
))}
); }; return (

Rappels automatiques

Configurez les rappels envoyés automatiquement avant les séquences

Liste des rappels configurés Les rappels actifs seront envoyés automatiquement aux apprenants inscrits {/* Filtres */}
Nom Template Délai Heure Dates concernées Pièce jointe Statut Actions {rappels && rappels.length > 0 ? ( rappels .filter((rappel) => { // Filtrer par formation if (filterFormation && filterFormation !== "all") { if (!rappel.datesDetails || rappel.datesDetails.length === 0) { return true; // Les rappels globaux passent tous les filtres } const hasFormation = rappel.datesDetails.some((d: any) => d.formationNom === filterFormation); if (!hasFormation) return false; } // Filtrer par séquence if (filterSequence && filterSequence !== "all") { if (!rappel.datesDetails || rappel.datesDetails.length === 0) { return true; // Les rappels globaux passent tous les filtres } const hasSequence = rappel.datesDetails.some((d: any) => d.sequenceNom === filterSequence); if (!hasSequence) return false; } return true; }) .map((rappel, index) => (
{rappel.datesDetails && rappel.datesDetails.length > 0 ? ( 🎯 Ciblé ) : ( 🌍 Global )} {rappel.nom}
{getTemplateName(rappel.templateType)} {rappel.timing === "post_formation" ? `J+${rappel.joursAvant}` : `J-${rappel.joursAvant}`} {rappel.heureEnvoi} {formatDatesInfo(rappel)} {rappel.nomFichier && rappel.urlFichier ? ( Oui ) : ( Non )}
)) ) : ( Aucun rappel configuré )}
{/* Dialog de création */} Créer un nouveau rappel Configurez un rappel automatique à envoyer avant les séquences

Le template d'email correspondant sera utilisé pour l'envoi

setFormData({ ...formData, nom: e.target.value })} placeholder="Ex: Rappel automatique J-7" />
setFormData({ ...formData, joursAvant: parseInt(e.target.value) || 1 }) } placeholder="Ex: 7" />

{formData.timing === "pre_formation" ? "Le rappel sera envoyé ce nombre de jours avant le début de la séquence" : "Le rappel sera envoyé ce nombre de jours après la fin de la séquence"}

setFormData({ ...formData, heureEnvoi: e.target.value })} />

L'heure à laquelle le rappel sera envoyé chaque jour

({ id: seq.id, nom: seq.nom, formationNom: seq.formation?.nom || '', dates: seq.dates?.map((d: any) => ({ id: d.id, date: d.dateDebut ? new Date(d.dateDebut).toISOString() : '', heureDebut: d.dateDebut ? new Date(d.dateDebut).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) : '', heureFin: d.dateFin ? new Date(d.dateFin).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) : '', })) || [], })) || []} selectedDateIds={formData.dateFormationIds} onSelectionChange={(dateFormationIds) => setFormData({ ...formData, dateFormationIds })} />
setFormData({ ...formData, fichier })} accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png" maxSize={10} /> setFormData({ ...formData, fichier2 })} accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png" maxSize={10} />
setFormData({ ...formData, actif: checked })} />
{/* Dialog de modification */} Modifier le rappel Modifiez la configuration du rappel automatique
setFormData({ ...formData, nom: e.target.value })} placeholder="Ex: Rappel automatique J-7" />
setFormData({ ...formData, joursAvant: parseInt(e.target.value) || 1 }) } placeholder="Ex: 7" />

{formData.timing === "pre_formation" ? "Le rappel sera envoyé ce nombre de jours avant le début de la séquence" : "Le rappel sera envoyé ce nombre de jours après la fin de la séquence"}

setFormData({ ...formData, heureEnvoi: e.target.value })} />
({ id: seq.id, nom: seq.nom, formationNom: seq.formation?.nom || '', dates: seq.dates?.map(d => ({ id: d.id, date: d.dateDebut ? new Date(d.dateDebut).toISOString() : '', heureDebut: d.dateDebut ? new Date(d.dateDebut).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) : '', heureFin: d.dateFin ? new Date(d.dateFin).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) : '', })) || [], })) || []} selectedDateIds={formData.dateFormationIds} onSelectionChange={(dateFormationIds) => setFormData({ ...formData, dateFormationIds })} />
setFormData({ ...formData, fichier })} accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png" maxSize={10} /> setFormData({ ...formData, fichier2 })} accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png" maxSize={10} />
setFormData({ ...formData, actif: checked })} />
); }