Checkpoint: Implémentation de 3 fonctionnalités avancées pour le tableau des rappels : (1) Badges visuels 🌍 Global (bleu) et 🎯 Ciblé (orange) dans la colonne Nom pour identifier rapidement le type de rappel, (2) Filtres par formation et séquence permettant de filtrer les rappels affichés (les rappels globaux passent tous les filtres), (3) Export CSV avec bouton dédié générant un fichier contenant toutes les informations des rappels (nom, type, template, délai, heure, formations, séquences, dates, statut) en respectant les filtres actifs. Interface utilisateur améliorée avec une meilleure organisation visuelle et des outils de gestion plus puissants.

This commit is contained in:
Manus
2026-01-08 09:21:10 -05:00
parent 2b31900255
commit 727a6678e7
2 changed files with 159 additions and 7 deletions

View File

@@ -22,7 +22,7 @@ import {
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { trpc } from "@/lib/trpc"; 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 { toast } from "sonner";
import { FileUpload } from "@/components/FileUpload"; import { FileUpload } from "@/components/FileUpload";
import { DateFormationMultiSelect } from "@/components/DateFormationMultiSelect"; import { DateFormationMultiSelect } from "@/components/DateFormationMultiSelect";
@@ -41,6 +41,8 @@ export default function AdminRappels() {
const [isCreateOpen, setIsCreateOpen] = useState(false); const [isCreateOpen, setIsCreateOpen] = useState(false);
const [isEditOpen, setIsEditOpen] = useState(false); const [isEditOpen, setIsEditOpen] = useState(false);
const [editingRappel, setEditingRappel] = useState<any>(null); const [editingRappel, setEditingRappel] = useState<any>(null);
const [filterFormation, setFilterFormation] = useState<string>("");
const [filterSequence, setFilterSequence] = useState<string>("");
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
nom: "", nom: "",
templateType: "", templateType: "",
@@ -183,6 +185,76 @@ export default function AdminRappels() {
return TEMPLATE_TYPES.find(t => t.value === type)?.label || type; 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) => { const formatDatesInfo = (rappel: any) => {
if (!rappel.datesDetails || rappel.datesDetails.length === 0) { if (!rappel.datesDetails || rappel.datesDetails.length === 0) {
return <span className="text-muted-foreground italic">Toutes les dates</span>; return <span className="text-muted-foreground italic">Toutes les dates</span>;
@@ -227,10 +299,16 @@ export default function AdminRappels() {
Configurez les rappels envoyés automatiquement avant les séquences Configurez les rappels envoyés automatiquement avant les séquences
</p> </p>
</div> </div>
<Button onClick={() => setIsCreateOpen(true)}> <div className="flex gap-2">
<Plus className="mr-2 h-4 w-4" /> <Button variant="outline" onClick={exportToCSV}>
Nouveau rappel <Download className="mr-2 h-4 w-4" />
</Button> Export CSV
</Button>
<Button onClick={() => setIsCreateOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
Nouveau rappel
</Button>
</div>
</div> </div>
<Card> <Card>
@@ -244,6 +322,41 @@ export default function AdminRappels() {
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{/* Filtres */}
<div className="mb-4 flex gap-4">
<div className="flex-1">
<Label htmlFor="filter-formation">Filtrer par formation</Label>
<Select value={filterFormation} onValueChange={setFilterFormation}>
<SelectTrigger id="filter-formation">
<SelectValue placeholder="Toutes les formations" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Toutes les formations</SelectItem>
{sequences && Array.from(new Set(sequences.map(s => s.formation?.nom).filter(Boolean))).map((formationNom) => (
<SelectItem key={formationNom} value={formationNom as string}>
{formationNom}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex-1">
<Label htmlFor="filter-sequence">Filtrer par séquence</Label>
<Select value={filterSequence} onValueChange={setFilterSequence}>
<SelectTrigger id="filter-sequence">
<SelectValue placeholder="Toutes les séquences" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Toutes les séquences</SelectItem>
{sequences && sequences.map((seq) => (
<SelectItem key={seq.id} value={seq.nom}>
{seq.nom}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
@@ -258,9 +371,42 @@ export default function AdminRappels() {
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{rappels && rappels.length > 0 ? ( {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) => (
<TableRow key={rappel.id}> <TableRow key={rappel.id}>
<TableCell className="font-medium">{rappel.nom}</TableCell> <TableCell className="font-medium">
<div className="flex items-center gap-2">
{rappel.datesDetails && rappel.datesDetails.length > 0 ? (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-orange-100 text-orange-800">
🎯 Ciblé
</span>
) : (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800">
🌍 Global
</span>
)}
<span>{rappel.nom}</span>
</div>
</TableCell>
<TableCell>{getTemplateName(rappel.templateType)}</TableCell> <TableCell>{getTemplateName(rappel.templateType)}</TableCell>
<TableCell> <TableCell>
{rappel.timing === "post_formation" ? `J+${rappel.joursAvant}` : `J-${rappel.joursAvant}`} {rappel.timing === "post_formation" ? `J+${rappel.joursAvant}` : `J-${rappel.joursAvant}`}

View File

@@ -566,3 +566,9 @@
- [x] Ajouter une colonne "Dates concernées" avec formations, séquences et dates - [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] Modifier la procédure tRPC pour inclure les informations de dates
- [x] Tester l'affichage avec plusieurs rappels - [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