"use client"; import React from "react"; import { adminListUsers } from "@/lib/frontend/apiClient"; import { ADMIN_USERS_SORT } from "@/lib/frontend/admin/users/usersSorting"; function normalizeQuery(query) { const q = typeof query?.q === "string" && query.q.trim() ? query.q.trim() : null; const role = typeof query?.role === "string" && query.role.trim() ? query.role.trim() : null; const branchId = typeof query?.branchId === "string" && query.branchId.trim() ? query.branchId.trim() : null; const sort = typeof query?.sort === "string" && query.sort.trim() ? query.sort.trim() : ADMIN_USERS_SORT.DEFAULT; return { q, role, branchId, sort }; } function buildKey({ q, role, branchId, sort, limit }) { return `${q || ""}|${role || ""}|${branchId || ""}|${sort}|${String(limit)}`; } /** * useAdminUsersQuery * * - Loads first page on query changes. * - Supports cursor-based "load more". * - Includes race protection to avoid stale appends. */ export function useAdminUsersQuery({ query, limit = 50 }) { const normalized = React.useMemo(() => { return normalizeQuery(query); }, [query?.q, query?.role, query?.branchId]); const key = React.useMemo(() => { return buildKey({ ...normalized, limit }); }, [normalized.q, normalized.role, normalized.branchId, normalized.sort, limit]); const [status, setStatus] = React.useState("loading"); // loading|success|error const [items, setItems] = React.useState([]); const [nextCursor, setNextCursor] = React.useState(null); const [error, setError] = React.useState(null); const [isLoadingMore, setIsLoadingMore] = React.useState(false); const [loadMoreError, setLoadMoreError] = React.useState(null); const [refreshTick, setRefreshTick] = React.useState(0); const keyRef = React.useRef(key); React.useEffect(() => { keyRef.current = key; }, [key]); const firstReqIdRef = React.useRef(0); const moreReqIdRef = React.useRef(0); const refresh = React.useCallback(() => { setRefreshTick((n) => n + 1); }, []); const runFirstPage = React.useCallback(async () => { const reqId = ++firstReqIdRef.current; setStatus("loading"); setError(null); setItems([]); setNextCursor(null); // Reset load-more state on first page load setIsLoadingMore(false); setLoadMoreError(null); try { const res = await adminListUsers({ q: normalized.q, role: normalized.role, branchId: normalized.branchId, sort: normalized.sort, limit, cursor: null, }); if (reqId !== firstReqIdRef.current) return; const list = Array.isArray(res?.items) ? res.items : []; const next = typeof res?.nextCursor === "string" && res.nextCursor.trim() ? res.nextCursor : null; setItems(list); setNextCursor(next); setStatus("success"); } catch (err) { if (reqId !== firstReqIdRef.current) return; setError(err); setStatus("error"); } }, [normalized.q, normalized.role, normalized.branchId, normalized.sort, limit]); React.useEffect(() => { runFirstPage(); }, [runFirstPage, key, refreshTick]); const loadMore = React.useCallback(async () => { if (!nextCursor) return; if (isLoadingMore) return; const baseKey = keyRef.current; const reqId = ++moreReqIdRef.current; setIsLoadingMore(true); setLoadMoreError(null); try { const res = await adminListUsers({ q: normalized.q, role: normalized.role, branchId: normalized.branchId, sort: normalized.sort, limit, cursor: nextCursor, }); // Ignore stale if (reqId !== moreReqIdRef.current) return; if (keyRef.current !== baseKey) return; const more = Array.isArray(res?.items) ? res.items : []; const next = typeof res?.nextCursor === "string" && res.nextCursor.trim() ? res.nextCursor : null; setItems((prev) => [...prev, ...more]); setNextCursor(next); } catch (err) { setLoadMoreError(err); } finally { if (reqId === moreReqIdRef.current) { setIsLoadingMore(false); } } }, [ nextCursor, isLoadingMore, normalized.q, normalized.role, normalized.branchId, normalized.sort, limit, ]); return { status, items, nextCursor, error, refresh, loadMore, isLoadingMore, loadMoreError, }; }