Checkpoint: Ajout d'un système complet de filtres et de recherche : filtres par date, lieu, statut et capacité pour les sessions ; recherche par nom/description pour les formations ; recherche multi-critères pour les apprenants. Compteurs de résultats inclus.
This commit is contained in:
@@ -6,14 +6,15 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { Plus, Pencil, Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Plus, Pencil, Trash2, Search, SlidersHorizontal } from "lucide-react";
|
||||
import { useState, useMemo } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export default function AdminApprenants() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [formData, setFormData] = useState({
|
||||
nom: "",
|
||||
prenom: "",
|
||||
@@ -89,6 +90,21 @@ export default function AdminApprenants() {
|
||||
}
|
||||
};
|
||||
|
||||
// Filtrer les apprenants
|
||||
const filteredApprenants = useMemo(() => {
|
||||
if (!apprenants) return [];
|
||||
|
||||
return apprenants.filter(apprenant => {
|
||||
const matchesSearch = searchTerm === "" ||
|
||||
apprenant.nom.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
apprenant.prenom.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
apprenant.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
apprenant.codeEtablissement.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
return matchesSearch;
|
||||
});
|
||||
}, [apprenants, searchTerm]);
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="space-y-6">
|
||||
@@ -177,6 +193,36 @@ export default function AdminApprenants() {
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Filtres et recherche */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<SlidersHorizontal className="w-5 h-5" />
|
||||
Recherche
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="search">Rechercher un apprenant</Label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="search"
|
||||
placeholder="Nom, prénom, email ou code établissement..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Compteur de résultats */}
|
||||
<div className="mt-4 text-sm text-muted-foreground">
|
||||
{filteredApprenants.length} apprenant(s) trouvé(s)
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Liste des apprenants</CardTitle>
|
||||
@@ -191,7 +237,7 @@ export default function AdminApprenants() {
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
) : apprenants && apprenants.length > 0 ? (
|
||||
) : filteredApprenants && filteredApprenants.length > 0 ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -203,7 +249,7 @@ export default function AdminApprenants() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{apprenants.map((apprenant) => (
|
||||
{filteredApprenants.map((apprenant) => (
|
||||
<TableRow key={apprenant.id}>
|
||||
<TableCell className="font-medium">{apprenant.nom}</TableCell>
|
||||
<TableCell>{apprenant.prenom}</TableCell>
|
||||
|
||||
@@ -6,16 +6,19 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
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 { trpc } from "@/lib/trpc";
|
||||
import { Plus, Pencil, Trash2, ExternalLink } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Plus, Pencil, Trash2, ExternalLink, Search, SlidersHorizontal } from "lucide-react";
|
||||
import { useState, useMemo } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export default function AdminFormations() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [filterActif, setFilterActif] = useState<string>("all");
|
||||
const [formData, setFormData] = useState({
|
||||
nom: "",
|
||||
description: "",
|
||||
@@ -94,6 +97,25 @@ export default function AdminFormations() {
|
||||
return `${window.location.origin}/inscription/${lien}`;
|
||||
};
|
||||
|
||||
// Filtrer les formations
|
||||
const filteredFormations = useMemo(() => {
|
||||
if (!formations) return [];
|
||||
|
||||
return formations.filter(formation => {
|
||||
// Filtre par recherche
|
||||
const matchesSearch = searchTerm === "" ||
|
||||
formation.nom.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(formation.description && formation.description.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||
|
||||
// Filtre par statut actif
|
||||
const matchesActif = filterActif === "all" ||
|
||||
(filterActif === "actif" && formation.actif) ||
|
||||
(filterActif === "inactif" && !formation.actif);
|
||||
|
||||
return matchesSearch && matchesActif;
|
||||
});
|
||||
}, [formations, searchTerm, filterActif]);
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="space-y-6">
|
||||
@@ -180,6 +202,54 @@ export default function AdminFormations() {
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Filtres et recherche */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<SlidersHorizontal className="w-5 h-5" />
|
||||
Filtres et recherche
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* Recherche */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="search">Rechercher</Label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="search"
|
||||
placeholder="Nom ou description..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filtre par statut */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="filterActif">Statut</Label>
|
||||
<Select value={filterActif} onValueChange={setFilterActif}>
|
||||
<SelectTrigger id="filterActif">
|
||||
<SelectValue placeholder="Tous les statuts" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Tous les statuts</SelectItem>
|
||||
<SelectItem value="actif">Active</SelectItem>
|
||||
<SelectItem value="inactif">Inactive</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Compteur de résultats */}
|
||||
<div className="mt-4 text-sm text-muted-foreground">
|
||||
{filteredFormations.length} formation(s) trouvée(s)
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Liste des formations</CardTitle>
|
||||
@@ -194,7 +264,7 @@ export default function AdminFormations() {
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
) : formations && formations.length > 0 ? (
|
||||
) : filteredFormations && filteredFormations.length > 0 ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
@@ -206,7 +276,7 @@ export default function AdminFormations() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{formations.map((formation) => (
|
||||
{filteredFormations.map((formation) => (
|
||||
<TableRow key={formation.id}>
|
||||
<TableCell className="font-medium">{formation.nom}</TableCell>
|
||||
<TableCell className="max-w-xs truncate">{formation.description || "-"}</TableCell>
|
||||
|
||||
@@ -7,8 +7,8 @@ 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 { trpc } from "@/lib/trpc";
|
||||
import { Plus, Pencil, Trash2, Users as UsersIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Plus, Pencil, Trash2, Users as UsersIcon, Search, SlidersHorizontal } from "lucide-react";
|
||||
import { useState, useMemo } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { format } from "date-fns";
|
||||
@@ -19,6 +19,10 @@ export default function AdminSessions() {
|
||||
const [, setLocation] = useLocation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [filterLieu, setFilterLieu] = useState<string>("all");
|
||||
const [filterStatut, setFilterStatut] = useState<string>("all");
|
||||
const [sortBy, setSortBy] = useState<"date" | "lieu" | "places">("date");
|
||||
const [formData, setFormData] = useState({
|
||||
formationId: "",
|
||||
nom: "",
|
||||
@@ -127,6 +131,47 @@ export default function AdminSessions() {
|
||||
return formations?.find(f => f.id === formationId)?.nom || "Formation inconnue";
|
||||
};
|
||||
|
||||
// Extraire les lieux uniques pour le filtre
|
||||
const uniqueLieux = useMemo(() => {
|
||||
if (!sessions) return [];
|
||||
const lieux = sessions.map(s => s.lieu);
|
||||
return Array.from(new Set(lieux)).sort();
|
||||
}, [sessions]);
|
||||
|
||||
// Filtrer et trier les sessions
|
||||
const filteredAndSortedSessions = useMemo(() => {
|
||||
if (!sessions) return [];
|
||||
|
||||
let filtered = sessions.filter(session => {
|
||||
// Filtre par recherche (nom de session ou formation)
|
||||
const matchesSearch = searchTerm === "" ||
|
||||
session.nom.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
getFormationName(session.formationId).toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
// Filtre par lieu
|
||||
const matchesLieu = filterLieu === "all" || session.lieu === filterLieu;
|
||||
|
||||
// Filtre par statut
|
||||
const matchesStatut = filterStatut === "all" || session.statut === filterStatut;
|
||||
|
||||
return matchesSearch && matchesLieu && matchesStatut;
|
||||
});
|
||||
|
||||
// Tri
|
||||
filtered.sort((a, b) => {
|
||||
if (sortBy === "date") {
|
||||
return new Date(a.dateDebut).getTime() - new Date(b.dateDebut).getTime();
|
||||
} else if (sortBy === "lieu") {
|
||||
return a.lieu.localeCompare(b.lieu);
|
||||
} else if (sortBy === "places") {
|
||||
return a.capaciteMax - b.capaciteMax;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return filtered;
|
||||
}, [sessions, searchTerm, filterLieu, filterStatut, sortBy, formations]);
|
||||
|
||||
const formatDate = (date: Date | string) => {
|
||||
return format(new Date(date), "dd/MM/yyyy HH:mm", { locale: fr });
|
||||
};
|
||||
@@ -152,13 +197,14 @@ export default function AdminSessions() {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Sessions</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Gérez les sessions de formation et leurs inscriptions
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Sessions</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Gérez les sessions de formation
|
||||
</p>
|
||||
</div>
|
||||
<Dialog open={open} onOpenChange={(isOpen) => {
|
||||
setOpen(isOpen);
|
||||
if (!isOpen) resetForm();
|
||||
@@ -292,6 +338,87 @@ export default function AdminSessions() {
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Filtres et recherche */}
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<SlidersHorizontal className="w-5 h-5" />
|
||||
Filtres et recherche
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{/* Recherche */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="search">Rechercher</Label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="search"
|
||||
placeholder="Nom de session ou formation..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filtre par lieu */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="filterLieu">Lieu</Label>
|
||||
<Select value={filterLieu} onValueChange={setFilterLieu}>
|
||||
<SelectTrigger id="filterLieu">
|
||||
<SelectValue placeholder="Tous les lieux" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Tous les lieux</SelectItem>
|
||||
{uniqueLieux.map(lieu => (
|
||||
<SelectItem key={lieu} value={lieu}>{lieu}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Filtre par statut */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="filterStatut">Statut</Label>
|
||||
<Select value={filterStatut} onValueChange={setFilterStatut}>
|
||||
<SelectTrigger id="filterStatut">
|
||||
<SelectValue placeholder="Tous les statuts" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Tous les statuts</SelectItem>
|
||||
<SelectItem value="ouverte">Ouverte</SelectItem>
|
||||
<SelectItem value="bloquee">Bloquée</SelectItem>
|
||||
<SelectItem value="terminee">Terminée</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Tri */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sortBy">Trier par</Label>
|
||||
<Select value={sortBy} onValueChange={(value: any) => setSortBy(value)}>
|
||||
<SelectTrigger id="sortBy">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="date">Date</SelectItem>
|
||||
<SelectItem value="lieu">Lieu</SelectItem>
|
||||
<SelectItem value="places">Capacité</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Compteur de résultats */}
|
||||
<div className="mt-4 text-sm text-muted-foreground">
|
||||
{filteredAndSortedSessions.length} session(s) trouvée(s)
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
@@ -308,7 +435,7 @@ export default function AdminSessions() {
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
) : sessions && sessions.length > 0 ? (
|
||||
) : filteredAndSortedSessions && filteredAndSortedSessions.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
@@ -323,8 +450,8 @@ export default function AdminSessions() {
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sessions.map((session) => (
|
||||
<TableBody>
|
||||
{filteredAndSortedSessions.map((session) => (
|
||||
<TableRow key={session.id}>
|
||||
<TableCell className="font-medium">{getFormationName(session.formationId)}</TableCell>
|
||||
<TableCell>{session.nom}</TableCell>
|
||||
|
||||
6
todo.md
6
todo.md
@@ -61,3 +61,9 @@
|
||||
|
||||
- [x] Corriger la mise à jour de la liste des apprenants après inscription
|
||||
- [x] Implémenter l'envoi effectif des emails de confirmation
|
||||
|
||||
## Nouvelles fonctionnalités
|
||||
|
||||
- [x] Ajouter des filtres de tri sur la page des sessions (date, lieu, places restantes)
|
||||
- [x] Ajouter des filtres de recherche sur la page des formations
|
||||
- [x] Ajouter des filtres de recherche sur la page des apprenants
|
||||
|
||||
Reference in New Issue
Block a user