import { useCallback, useEffect, useState } from "react"; import { get, put, post, del } from "./api"; export interface User { id: string; username: string; classId: string | null; isActive: boolean; balance: string; xp: string; level: number; dailyStreak: number; settings: Record; createdAt: string; updatedAt: string; class?: Class; } export interface Class { id: string; name: string; description: string; icon: string; } export interface Item { id: number; name: string; description: string; rarity: string; sellPrice: string; buyPrice: string; } export interface InventoryEntry { userId: string; itemId: number; quantity: string; item?: Item; } export interface UserFilters { search: string; classId: string | null; isActive: boolean | null; sortBy: "username" | "level" | "balance" | "xp"; sortOrder: "asc" | "desc"; } export function useUsers() { // User list state const [users, setUsers] = useState([]); const [total, setTotal] = useState(0); const [currentPage, setCurrentPage] = useState(1); const [limit, setLimit] = useState(50); // Filters const [filters, setFiltersState] = useState({ search: "", classId: null, isActive: null, sortBy: "balance", sortOrder: "desc", }); // Detail panel state const [selectedUser, setSelectedUser] = useState(null); const [userDraft, setUserDraft] = useState | null>(null); const [inventoryDraft, setInventoryDraft] = useState([]); // Reference data const [classes, setClasses] = useState([]); const [items, setItems] = useState([]); // UI state const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); // Fetch users with filters and pagination const fetchUsers = useCallback(async () => { try { setLoading(true); setError(null); const params = new URLSearchParams(); if (filters.search) params.set("search", filters.search); if (filters.classId) params.set("classId", filters.classId); if (filters.isActive !== null) params.set("isActive", String(filters.isActive)); params.set("sortBy", filters.sortBy); params.set("sortOrder", filters.sortOrder); params.set("limit", String(limit)); params.set("offset", String((currentPage - 1) * limit)); const data = await get<{ users: User[]; total: number }>( `/api/users?${params.toString()}` ); setUsers(data.users); setTotal(data.total); } catch (e) { setError(e instanceof Error ? e.message : "Failed to load users"); } finally { setLoading(false); } }, [filters, currentPage, limit]); // Fetch single user by ID const fetchUserById = useCallback(async (id: string) => { try { const user = await get(`/api/users/${id}`); return user; } catch (e) { setError(e instanceof Error ? e.message : "Failed to load user"); return null; } }, []); // Fetch classes for filter dropdown const fetchClasses = useCallback(async () => { try { const data = await get<{ classes: Class[] }>("/api/classes"); setClasses(data.classes || []); } catch (e) { console.error("Failed to load classes:", e); setClasses([]); } }, []); // Fetch items for inventory management const fetchItems = useCallback(async () => { try { const data = await get<{ items: Item[]; total: number }>("/api/items"); setItems(data.items || []); } catch (e) { console.error("Failed to load items:", e); setItems([]); } }, []); // Fetch user inventory const fetchInventory = useCallback(async (userId: string) => { try { const data = await get<{ inventory: InventoryEntry[] }>( `/api/users/${userId}/inventory` ); setInventoryDraft(data.inventory); return data.inventory; } catch (e) { setError(e instanceof Error ? e.message : "Failed to load inventory"); return []; } }, []); // Update user const updateUser = useCallback(async (id: string, data: Partial) => { try { setSaving(true); setError(null); const result = await put<{ success: boolean; user: User }>( `/api/users/${id}`, data ); return result.user; } catch (e) { setError(e instanceof Error ? e.message : "Failed to update user"); return null; } finally { setSaving(false); } }, []); // Add item to inventory const addInventoryItem = useCallback( async (userId: string, itemId: number, quantity: string) => { try { await post(`/api/users/${userId}/inventory`, { itemId, quantity }); await fetchInventory(userId); return true; } catch (e) { setError(e instanceof Error ? e.message : "Failed to add item"); return false; } }, [fetchInventory] ); // Remove item from inventory const removeInventoryItem = useCallback( async (userId: string, itemId: number) => { try { await del(`/api/users/${userId}/inventory/${itemId}`); await fetchInventory(userId); return true; } catch (e) { setError(e instanceof Error ? e.message : "Failed to remove item"); return false; } }, [fetchInventory] ); // Set filters and reset to page 1 const setFilters = useCallback((newFilters: Partial) => { setFiltersState((prev) => ({ ...prev, ...newFilters })); setCurrentPage(1); }, []); // Debounced search setter const setSearchDebounced = useCallback( (() => { let timeoutId: NodeJS.Timeout; return (search: string) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => { setFilters({ search }); }, 300); }; })(), [setFilters] ); // Navigate to page const setPage = useCallback((page: number) => { setCurrentPage(page); }, []); // Select user and open detail panel const selectUser = useCallback( async (user: User) => { setSelectedUser(user); // Fetch fresh data const freshUser = await fetchUserById(user.id); if (freshUser) { setSelectedUser(freshUser); setUserDraft(structuredClone(freshUser)); await fetchInventory(freshUser.id); } }, [fetchUserById, fetchInventory] ); // Close detail panel const closeDetail = useCallback(() => { setSelectedUser(null); setUserDraft(null); setInventoryDraft([]); }, []); // Update draft field const updateDraft = useCallback((field: keyof User, value: unknown) => { setUserDraft((prev) => { if (!prev) return null; return { ...prev, [field]: value }; }); }, []); // Save draft changes const saveDraft = useCallback(async () => { if (!selectedUser || !userDraft) return false; const updated = await updateUser(selectedUser.id, userDraft); if (updated) { setSelectedUser(updated); setUserDraft(structuredClone(updated)); // Refresh the list to show updated data await fetchUsers(); return true; } return false; }, [selectedUser, userDraft, updateUser, fetchUsers]); // Discard draft changes const discardDraft = useCallback(() => { if (selectedUser) { setUserDraft(structuredClone(selectedUser)); } }, [selectedUser]); // Check if draft has changes const isDirty = useCallback(() => { if (!selectedUser || !userDraft) return false; return JSON.stringify(selectedUser) !== JSON.stringify(userDraft); }, [selectedUser, userDraft]); // Initial load useEffect(() => { fetchUsers(); fetchClasses(); fetchItems(); }, [fetchUsers, fetchClasses, fetchItems]); return { // User list users, total, currentPage, limit, setLimit, // Filters filters, setFilters, setSearchDebounced, // Pagination setPage, // Detail panel selectedUser, selectUser, closeDetail, // Editing userDraft, updateDraft, saveDraft, discardDraft, isDirty: isDirty(), // Inventory inventoryDraft, addInventoryItem, removeInventoryItem, // Reference data classes, items, // UI state loading, saving, error, // Actions refetch: fetchUsers, }; }