From dde1c8ba9329d1b208981658283a1f796c37b067 Mon Sep 17 00:00:00 2001 From: Biqoz Date: Wed, 5 Nov 2025 15:20:19 +0100 Subject: [PATCH] =?UTF-8?q?am=C3=A9lioration=20recherche=20utilisateurs=20?= =?UTF-8?q?avec=20bouton=20de=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ajout d'un système de recherche avec bouton "Rechercher" - Support de la validation par touche Entrée - Recherche côté serveur avec filtres MongoDB sur nom et email - Réinitialisation automatique de la page lors d'une nouvelle recherche - Suppression du debounce automatique pour un contrôle utilisateur total 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/api/collections/[collection]/route.ts | 8 ++ components/collections/users-table.tsx | 140 ++++++++++++---------- hooks/useCollection.ts | 27 +++-- 3 files changed, 99 insertions(+), 76 deletions(-) diff --git a/app/api/collections/[collection]/route.ts b/app/api/collections/[collection]/route.ts index dca0ecc..797a3c0 100644 --- a/app/api/collections/[collection]/route.ts +++ b/app/api/collections/[collection]/route.ts @@ -55,6 +55,7 @@ export async function GET( if (collection === "users") { const email = searchParams.get("email"); const id = searchParams.get("id"); + const search = searchParams.get("search"); // ✅ AJOUTER cette ligne if (email) { filter.email = email.toLowerCase(); @@ -69,6 +70,13 @@ export async function GET( { status: 400 } ); } + } else if (search) { + // ✅ AJOUTER ce bloc + // Recherche partielle sur nom et email + filter.$or = [ + { name: { $regex: search, $options: "i" } }, + { email: { $regex: search, $options: "i" } }, + ]; } } diff --git a/components/collections/users-table.tsx b/components/collections/users-table.tsx index 3dcd292..18acd55 100644 --- a/components/collections/users-table.tsx +++ b/components/collections/users-table.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useMemo } from "react"; +import { useState, useMemo, useEffect } from "react"; import { useCollection } from "@/hooks/useCollection"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { @@ -20,10 +20,15 @@ import { LibreChatUser, LibreChatBalance } from "@/lib/types"; export function UsersTable() { const [page, setPage] = useState(1); - const [searchTerm, setSearchTerm] = useState(""); + const [searchInput, setSearchInput] = useState(""); // Ce que l'utilisateur tape + const [activeSearch, setActiveSearch] = useState(""); // Ce qui est réellement recherché const limit = 20; - // Charger les utilisateurs + // Réinitialiser la page à 1 quand une nouvelle recherche est lancée + useEffect(() => { + setPage(1); + }, [activeSearch]); + const { data: users = [], total = 0, @@ -31,11 +36,24 @@ export function UsersTable() { } = useCollection("users", { page, limit, + search: activeSearch, }); + // Fonction pour lancer la recherche + const handleSearch = () => { + setActiveSearch(searchInput); + }; + + // Gérer la touche Entrée + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + handleSearch(); + } + }; + // Charger tous les balances pour associer les crédits const { data: balances = [] } = useCollection("balances", { - limit: 1000, // Charger tous les balances + limit: 1000, }); // Créer une map des crédits par utilisateur @@ -47,20 +65,6 @@ export function UsersTable() { return map; }, [balances]); - // Filtrer les utilisateurs selon le terme de recherche - const filteredUsers = useMemo(() => { - if (!searchTerm.trim()) { - return users; - } - - const searchLower = searchTerm.toLowerCase(); - return users.filter( - (user) => - user.name?.toLowerCase().includes(searchLower) || - user.email?.toLowerCase().includes(searchLower) - ); - }, [users, searchTerm]); - const totalPages = Math.ceil(total / limit); const handlePrevPage = () => { @@ -93,16 +97,22 @@ export function UsersTable() {
- Liste des utilisateurs ({searchTerm ? filteredUsers.length : total}) + Liste des utilisateurs ({total}) -
- - setSearchTerm(e.target.value)} - className="pl-10" - /> +
+
+ + setSearchInput(e.target.value)} + onKeyPress={handleKeyPress} + className="pl-10" + /> +
+
@@ -121,12 +131,12 @@ export function UsersTable() { - {filteredUsers.length > 0 ? ( - filteredUsers.map((user) => { + {users.length > 0 ? ( + users.map((user) => { const userCredits = creditsMap.get(user._id) || 0; const isActive = new Date(user.updatedAt || user.createdAt) > - new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // 30 jours en millisecondes + new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); return ( @@ -176,8 +186,8 @@ export function UsersTable() { colSpan={7} className="text-center py-8 text-muted-foreground" > - {searchTerm - ? `Aucun utilisateur trouvé pour "${searchTerm}"` + {activeSearch + ? `Aucun utilisateur trouvé pour "${activeSearch}"` : "Aucun utilisateur trouvé"} @@ -186,42 +196,40 @@ export function UsersTable() {
- {/* Pagination - masquée lors de la recherche */} - {!searchTerm && ( -
-
- Page {page} sur {totalPages} ({total} éléments au total) -
-
- - -
+ {/* Pagination */} +
+
+ {activeSearch ? ( + + {total} résultat(s) pour "{activeSearch}" - Page {page} sur {totalPages} + + ) : ( + + Page {page} sur {totalPages} ({total} utilisateurs au total) + + )}
- )} - - {/* Info de recherche */} - {searchTerm && ( -
- {filteredUsers.length} résultat(s) trouvé(s) pour "{searchTerm} - " +
+ +
- )} +
); diff --git a/hooks/useCollection.ts b/hooks/useCollection.ts index 97b1732..e14a45b 100644 --- a/hooks/useCollection.ts +++ b/hooks/useCollection.ts @@ -1,11 +1,12 @@ "use client"; -import { useState, useEffect, useCallback, useMemo } from 'react'; +import { useState, useEffect, useCallback, useMemo } from "react"; interface UseCollectionOptions { page?: number; limit?: number; filter?: Record; + search?: string; } interface CollectionResponse { @@ -17,7 +18,7 @@ interface CollectionResponse { } export function useCollection>( - collectionName: string, + collectionName: string, options: UseCollectionOptions = {} ) { const [data, setData] = useState([]); @@ -26,7 +27,7 @@ export function useCollection>( const [total, setTotal] = useState(0); const [totalPages, setTotalPages] = useState(0); - const { page = 1, limit = 20, filter = {} } = options; + const { page = 1, limit = 20, filter = {}, search } = options; // Mémoriser la chaîne JSON du filtre pour éviter les re-renders inutiles const filterString = useMemo(() => JSON.stringify(filter), [filter]); @@ -37,22 +38,28 @@ export function useCollection>( const params = new URLSearchParams({ page: page.toString(), limit: limit.toString(), - filter: filterString + filter: filterString, }); - - const response = await fetch(`/api/collections/${collectionName}?${params}`); - if (!response.ok) throw new Error(`Erreur lors du chargement de ${collectionName}`); - + if (search) { + params.append("search", search); + } + + const response = await fetch( + `/api/collections/${collectionName}?${params}` + ); + if (!response.ok) + throw new Error(`Erreur lors du chargement de ${collectionName}`); + const result: CollectionResponse = await response.json(); setData(result.data); setTotal(result.total); setTotalPages(result.totalPages); } catch (err) { - setError(err instanceof Error ? err.message : 'Erreur inconnue'); + setError(err instanceof Error ? err.message : "Erreur inconnue"); } finally { setLoading(false); } - }, [collectionName, page, limit, filterString]); + }, [collectionName, page, limit, filterString, search]); useEffect(() => { fetchData();