useAdminUsersQuery.js 3.8 KB

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