From 727a6678e7151f07bbed360e93e208925de2cd14 Mon Sep 17 00:00:00 2001 From: Manus Date: Thu, 8 Jan 2026 09:21:10 -0500 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Impl=C3=A9mentation=20de=203=20fo?= =?UTF-8?q?nctionnalit=C3=A9s=20avanc=C3=A9es=20pour=20le=20tableau=20des?= =?UTF-8?q?=20rappels=20:=20(1)=20Badges=20visuels=20=F0=9F=8C=8D=20Global?= =?UTF-8?q?=20(bleu)=20et=20=F0=9F=8E=AF=20Cibl=C3=A9=20(orange)=20dans=20?= =?UTF-8?q?la=20colonne=20Nom=20pour=20identifier=20rapidement=20le=20type?= =?UTF-8?q?=20de=20rappel,=20(2)=20Filtres=20par=20formation=20et=20s?= =?UTF-8?q?=C3=A9quence=20permettant=20de=20filtrer=20les=20rappels=20affi?= =?UTF-8?q?ch=C3=A9s=20(les=20rappels=20globaux=20passent=20tous=20les=20f?= =?UTF-8?q?iltres),=20(3)=20Export=20CSV=20avec=20bouton=20d=C3=A9di=C3=A9?= =?UTF-8?q?=20g=C3=A9n=C3=A9rant=20un=20fichier=20contenant=20toutes=20les?= =?UTF-8?q?=20informations=20des=20rappels=20(nom,=20type,=20template,=20d?= =?UTF-8?q?=C3=A9lai,=20heure,=20formations,=20s=C3=A9quences,=20dates,=20?= =?UTF-8?q?statut)=20en=20respectant=20les=20filtres=20actifs.=20Interface?= =?UTF-8?q?=20utilisateur=20am=C3=A9lior=C3=A9e=20avec=20une=20meilleure?= =?UTF-8?q?=20organisation=20visuelle=20et=20des=20outils=20de=20gestion?= =?UTF-8?q?=20plus=20puissants.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/AdminRappels.tsx | 160 ++++++++++++++++++++++++++++-- todo.md | 6 ++ 2 files changed, 159 insertions(+), 7 deletions(-) diff --git a/client/src/pages/AdminRappels.tsx b/client/src/pages/AdminRappels.tsx index 1398e72..3b5761e 100644 --- a/client/src/pages/AdminRappels.tsx +++ b/client/src/pages/AdminRappels.tsx @@ -22,7 +22,7 @@ import { } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { trpc } from "@/lib/trpc"; -import { Bell, Edit, Plus, Trash2 } from "lucide-react"; +import { Bell, Download, Edit, Plus, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { FileUpload } from "@/components/FileUpload"; import { DateFormationMultiSelect } from "@/components/DateFormationMultiSelect"; @@ -41,6 +41,8 @@ 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: "", @@ -183,6 +185,76 @@ export default function AdminRappels() { 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; @@ -227,10 +299,16 @@ export default function AdminRappels() { Configurez les rappels envoyés automatiquement avant les séquences

- +
+ + +
@@ -244,6 +322,41 @@ export default function AdminRappels() { + {/* Filtres */} +
+
+ + +
+
+ + +
+
@@ -258,9 +371,42 @@ export default function AdminRappels() { {rappels && rappels.length > 0 ? ( - rappels.map((rappel) => ( + 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) => ( - {rappel.nom} + +
+ {rappel.datesDetails && rappel.datesDetails.length > 0 ? ( + + 🎯 Ciblé + + ) : ( + + 🌍 Global + + )} + {rappel.nom} +
+
{getTemplateName(rappel.templateType)} {rappel.timing === "post_formation" ? `J+${rappel.joursAvant}` : `J-${rappel.joursAvant}`} diff --git a/todo.md b/todo.md index 0ab7136..424bbb4 100644 --- a/todo.md +++ b/todo.md @@ -566,3 +566,9 @@ - [x] Ajouter une colonne "Dates concernées" avec formations, séquences et dates - [x] Modifier la procédure tRPC pour inclure les informations de dates - [x] Tester l'affichage avec plusieurs rappels + +## Améliorations avancées du tableau des rappels +- [x] Ajouter des badges visuels Global/Ciblé dans la colonne Nom +- [x] Implémenter les filtres par formation et séquence +- [x] Créer la fonction d'export CSV des rappels +- [x] Tester toutes les fonctionnalités