diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..a4a68ce --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,15 @@ +{ + "permissions": { + "allow": [ + "Bash(cat:*)", + "Bash(awk:*)", + "Bash(useCollection.tmp)", + "Bash(npm run build:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git push)" + ], + "deny": [], + "ask": [] + } +} diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f2d8493 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,13 @@ +## CRITICAL: File Editing on Windows + +### ⚠️ MANDATORY: Always Use Backslashes on Windows for File Paths + +When using Edit or MultiEdit tools on Windows, you MUST use backslashes (\) in file paths, NOT forward slashes (/). + +#### ❌ WRONG - Will cause errors: + +Edit(file_path: "D:/repos/project/file.tsx", ...) + +#### ✅ CORRECT - Always works: + +Edit(file_path: "D:\repos\project\file.tsx", ...) diff --git a/components/collections/users-table.old.tsx b/components/collections/users-table.old.tsx new file mode 100644 index 0000000..dea6a67 --- /dev/null +++ b/components/collections/users-table.old.tsx @@ -0,0 +1,215 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { useCollection } from "@/hooks/useCollection"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Input } from "@/components/ui/input"; +import { ChevronLeft, ChevronRight, Search } from "lucide-react"; +import { formatDate } from "@/lib/utils"; +import { LibreChatUser, LibreChatBalance } from "@/lib/types"; + +export function UsersTable() { + const [page, setPage] = useState(1); + const [searchTerm, setSearchTerm] = useState(""); + const limit = 20; + + const { + data: users = [], + total = 0, + loading: usersLoading, + } = useCollection("users", { + page, + limit, + // ✅ AJOUTER le searchTerm ici + search: searchTerm, + }); + + // Charger tous les balances pour associer les crédits + const { data: balances = [] } = useCollection("balances", { + limit: 1000, // Charger tous les balances + }); + + // Créer une map des crédits par utilisateur + const creditsMap = useMemo(() => { + const map = new Map(); + balances.forEach((balance) => { + map.set(balance.user, balance.tokenCredits || 0); + }); + return map; + }, [balances]); + + const totalPages = Math.ceil(total / limit); + + const handlePrevPage = () => { + setPage((prev) => Math.max(1, prev - 1)); + }; + + const handleNextPage = () => { + setPage((prev) => Math.min(totalPages, prev + 1)); + }; + + if (usersLoading) { + return ( + + + Liste des utilisateurs + + +
+ {Array.from({ length: 5 }).map((_, i) => ( +
+ ))} +
+ + + ); + } + + return ( + + +
+ + Liste des utilisateurs ({searchTerm ? users.length : total}) + +
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+
+
+ +
+ + + + ID + Nom + Email + Rôle + Crédits + Statut + Créé le + + + + {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 + + return ( + + + + {user._id.slice(-8)} + + + + {user.name} + + + {user.email} + + + + {user.role} + + + + + {userCredits.toLocaleString()} crédits + + + + + + {isActive ? "Actif" : "Inactif"} + + + + + + {formatDate(user.createdAt)} + + + + ); + }) + ) : ( + + + {searchTerm + ? `Aucun utilisateur trouvé pour "${searchTerm}"` + : "Aucun utilisateur trouvé"} + + + )} + +
+
+ + {/* Pagination - masquée lors de la recherche */} + {!searchTerm && ( +
+
+ Page {page} sur {totalPages} ({total} éléments au total) +
+
+ + +
+
+ )} + + {/* Info de recherche */} + {searchTerm && ( +
+ {users.length} résultat(s) trouvé(s) pour "{searchTerm} + " +
+ )} +
+
+ ); +} diff --git a/hooks/useCollection.old.ts b/hooks/useCollection.old.ts new file mode 100644 index 0000000..1bbc3f4 --- /dev/null +++ b/hooks/useCollection.old.ts @@ -0,0 +1,73 @@ +"use client"; + +import { useState, useEffect, useCallback, useMemo } from "react"; + +interface UseCollectionOptions { + page?: number; + limit?: number; + filter?: Record; + search?: string; +} + +interface CollectionResponse { + data: T[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + +export function useCollection>( + collectionName: string, + options: UseCollectionOptions = {} +) { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [total, setTotal] = useState(0); + const [totalPages, setTotalPages] = useState(0); + + 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]); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + const params = new URLSearchParams({ + page: page.toString(), + limit: limit.toString(), + filter: filterString, + }); + 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"); + } finally { + setLoading(false); + } + }, [collectionName, page, limit, filterString]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const refetch = useCallback(() => { + return fetchData(); + }, [fetchData]); + + return { data, loading, error, total, totalPages, refetch }; +} diff --git a/hooks/useCollection.ts.bak b/hooks/useCollection.ts.bak new file mode 100644 index 0000000..1bbc3f4 --- /dev/null +++ b/hooks/useCollection.ts.bak @@ -0,0 +1,73 @@ +"use client"; + +import { useState, useEffect, useCallback, useMemo } from "react"; + +interface UseCollectionOptions { + page?: number; + limit?: number; + filter?: Record; + search?: string; +} + +interface CollectionResponse { + data: T[]; + total: number; + page: number; + limit: number; + totalPages: number; +} + +export function useCollection>( + collectionName: string, + options: UseCollectionOptions = {} +) { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [total, setTotal] = useState(0); + const [totalPages, setTotalPages] = useState(0); + + 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]); + + const fetchData = useCallback(async () => { + try { + setLoading(true); + const params = new URLSearchParams({ + page: page.toString(), + limit: limit.toString(), + filter: filterString, + }); + 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"); + } finally { + setLoading(false); + } + }, [collectionName, page, limit, filterString]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const refetch = useCallback(() => { + return fetchData(); + }, [fetchData]); + + return { data, loading, error, total, totalPages, refetch }; +}