useAdminUsersQuery.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. "use client";
  2. import React from "react";
  3. import { adminListUsers } from "@/lib/frontend/apiClient";
  4. import { ADMIN_USERS_SORT } from "@/lib/frontend/admin/users/usersSorting";
  5. function normalizeQuery(query) {
  6. const q =
  7. typeof query?.q === "string" && query.q.trim() ? query.q.trim() : null;
  8. const role =
  9. typeof query?.role === "string" && query.role.trim()
  10. ? query.role.trim()
  11. : null;
  12. const branchId =
  13. typeof query?.branchId === "string" && query.branchId.trim()
  14. ? query.branchId.trim()
  15. : null;
  16. const sort =
  17. typeof query?.sort === "string" && query.sort.trim()
  18. ? query.sort.trim()
  19. : ADMIN_USERS_SORT.DEFAULT;
  20. return { q, role, branchId, sort };
  21. }
  22. function buildKey({ q, role, branchId, sort, limit }) {
  23. return `${q || ""}|${role || ""}|${branchId || ""}|${sort}|${String(limit)}`;
  24. }
  25. /**
  26. * useAdminUsersQuery
  27. *
  28. * - Loads first page on query changes.
  29. * - Supports cursor-based "load more".
  30. * - Includes race protection to avoid stale appends.
  31. */
  32. export function useAdminUsersQuery({ query, limit = 50 }) {
  33. const normalized = React.useMemo(() => {
  34. return normalizeQuery(query);
  35. }, [query?.q, query?.role, query?.branchId]);
  36. const key = React.useMemo(() => {
  37. return buildKey({ ...normalized, limit });
  38. }, [normalized.q, normalized.role, normalized.branchId, normalized.sort, limit]);
  39. const [status, setStatus] = React.useState("loading"); // loading|success|error
  40. const [items, setItems] = React.useState([]);
  41. const [nextCursor, setNextCursor] = React.useState(null);
  42. const [error, setError] = React.useState(null);
  43. const [isLoadingMore, setIsLoadingMore] = React.useState(false);
  44. const [loadMoreError, setLoadMoreError] = React.useState(null);
  45. const [refreshTick, setRefreshTick] = React.useState(0);
  46. const keyRef = React.useRef(key);
  47. React.useEffect(() => {
  48. keyRef.current = key;
  49. }, [key]);
  50. const firstReqIdRef = React.useRef(0);
  51. const moreReqIdRef = React.useRef(0);
  52. const refresh = React.useCallback(() => {
  53. setRefreshTick((n) => n + 1);
  54. }, []);
  55. const runFirstPage = React.useCallback(async () => {
  56. const reqId = ++firstReqIdRef.current;
  57. setStatus("loading");
  58. setError(null);
  59. setItems([]);
  60. setNextCursor(null);
  61. // Reset load-more state on first page load
  62. setIsLoadingMore(false);
  63. setLoadMoreError(null);
  64. try {
  65. const res = await adminListUsers({
  66. q: normalized.q,
  67. role: normalized.role,
  68. branchId: normalized.branchId,
  69. sort: normalized.sort,
  70. limit,
  71. cursor: null,
  72. });
  73. if (reqId !== firstReqIdRef.current) return;
  74. const list = Array.isArray(res?.items) ? res.items : [];
  75. const next =
  76. typeof res?.nextCursor === "string" && res.nextCursor.trim()
  77. ? res.nextCursor
  78. : null;
  79. setItems(list);
  80. setNextCursor(next);
  81. setStatus("success");
  82. } catch (err) {
  83. if (reqId !== firstReqIdRef.current) return;
  84. setError(err);
  85. setStatus("error");
  86. }
  87. }, [normalized.q, normalized.role, normalized.branchId, normalized.sort, limit]);
  88. React.useEffect(() => {
  89. runFirstPage();
  90. }, [runFirstPage, key, refreshTick]);
  91. const loadMore = React.useCallback(async () => {
  92. if (!nextCursor) return;
  93. if (isLoadingMore) return;
  94. const baseKey = keyRef.current;
  95. const reqId = ++moreReqIdRef.current;
  96. setIsLoadingMore(true);
  97. setLoadMoreError(null);
  98. try {
  99. const res = await adminListUsers({
  100. q: normalized.q,
  101. role: normalized.role,
  102. branchId: normalized.branchId,
  103. sort: normalized.sort,
  104. limit,
  105. cursor: nextCursor,
  106. });
  107. // Ignore stale
  108. if (reqId !== moreReqIdRef.current) return;
  109. if (keyRef.current !== baseKey) return;
  110. const more = Array.isArray(res?.items) ? res.items : [];
  111. const next =
  112. typeof res?.nextCursor === "string" && res.nextCursor.trim()
  113. ? res.nextCursor
  114. : null;
  115. setItems((prev) => [...prev, ...more]);
  116. setNextCursor(next);
  117. } catch (err) {
  118. setLoadMoreError(err);
  119. } finally {
  120. if (reqId === moreReqIdRef.current) {
  121. setIsLoadingMore(false);
  122. }
  123. }
  124. }, [
  125. nextCursor,
  126. isLoadingMore,
  127. normalized.q,
  128. normalized.role,
  129. normalized.branchId,
  130. normalized.sort,
  131. limit,
  132. ]);
  133. return {
  134. status,
  135. items,
  136. nextCursor,
  137. error,
  138. refresh,
  139. loadMore,
  140. isLoadingMore,
  141. loadMoreError,
  142. };
  143. }