|
@@ -0,0 +1,165 @@
|
|
|
|
|
+"use client";
|
|
|
|
|
+
|
|
|
|
|
+import React from "react";
|
|
|
|
|
+import { adminListUsers } from "@/lib/frontend/apiClient";
|
|
|
|
|
+
|
|
|
|
|
+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;
|
|
|
|
|
+
|
|
|
|
|
+ return { q, role, branchId };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function buildKey({ q, role, branchId, limit }) {
|
|
|
|
|
+ return `${q || ""}|${role || ""}|${branchId || ""}|${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, 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,
|
|
|
|
|
+ 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, 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,
|
|
|
|
|
+ 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,
|
|
|
|
|
+ limit,
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ status,
|
|
|
|
|
+ items,
|
|
|
|
|
+ nextCursor,
|
|
|
|
|
+ error,
|
|
|
|
|
+ refresh,
|
|
|
|
|
+ loadMore,
|
|
|
|
|
+ isLoadingMore,
|
|
|
|
|
+ loadMoreError,
|
|
|
|
|
+ };
|
|
|
|
|
+}
|