SearchPage.jsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. "use client";
  2. import React from "react";
  3. import { useRouter, useSearchParams } from "next/navigation";
  4. import { RefreshCw } from "lucide-react";
  5. import { useAuth } from "@/components/auth/authContext";
  6. import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
  7. import { searchPath } from "@/lib/frontend/routes";
  8. import { isValidBranchParam } from "@/lib/frontend/params";
  9. import {
  10. parseSearchUrlState,
  11. serializeSearchUrlState,
  12. SEARCH_SCOPE,
  13. } from "@/lib/frontend/search/urlState";
  14. import { normalizeSearchUrlStateForUser } from "@/lib/frontend/search/normalizeState";
  15. import { mapSearchError } from "@/lib/frontend/search/errorMapping";
  16. import { useSearchQuery } from "@/lib/frontend/search/useSearchQuery";
  17. import {
  18. useSearchBranches,
  19. BRANCH_LIST_STATE,
  20. } from "@/lib/frontend/search/useSearchBranches";
  21. import {
  22. buildSearchHref,
  23. buildSearchKey,
  24. getScopeLabel,
  25. needsMultiBranchSelectionHint,
  26. buildNextStateForScopeChange,
  27. buildNextStateForToggleBranch,
  28. buildNextStateForClearAllBranches,
  29. buildHrefForSingleBranchSwitch,
  30. } from "@/lib/frontend/search/pageHelpers";
  31. import ExplorerPageShell from "@/components/explorer/ExplorerPageShell";
  32. import ExplorerSectionCard from "@/components/explorer/ExplorerSectionCard";
  33. import ForbiddenView from "@/components/system/ForbiddenView";
  34. import { Button } from "@/components/ui/button";
  35. import SearchForm from "@/components/search/SearchForm";
  36. import SearchResults from "@/components/search/SearchResults";
  37. export default function SearchPage({ branch: routeBranch }) {
  38. const router = useRouter();
  39. const searchParams = useSearchParams();
  40. const { status: authStatus, user } = useAuth();
  41. const isAuthenticated = authStatus === "authenticated" && user;
  42. const isAdminDev =
  43. isAuthenticated && (user.role === "admin" || user.role === "dev");
  44. const parsedUrlState = React.useMemo(() => {
  45. return parseSearchUrlState(searchParams, { routeBranch });
  46. }, [searchParams, routeBranch]);
  47. const urlState = React.useMemo(() => {
  48. return normalizeSearchUrlStateForUser(parsedUrlState, {
  49. routeBranch,
  50. user,
  51. });
  52. }, [parsedUrlState, routeBranch, user]);
  53. const searchKey = React.useMemo(() => {
  54. return buildSearchKey({ routeBranch, urlState });
  55. }, [routeBranch, urlState]);
  56. const [qDraft, setQDraft] = React.useState(urlState.q || "");
  57. React.useEffect(() => {
  58. setQDraft(urlState.q || "");
  59. }, [urlState.q]);
  60. const branchesQuery = useSearchBranches({ enabled: isAdminDev });
  61. const query = useSearchQuery({
  62. searchKey,
  63. urlState,
  64. routeBranch,
  65. user,
  66. limit: urlState.limit,
  67. });
  68. const mappedError = React.useMemo(
  69. () => mapSearchError(query.error),
  70. [query.error]
  71. );
  72. const mappedLoadMoreError = React.useMemo(
  73. () => mapSearchError(query.loadMoreError),
  74. [query.loadMoreError]
  75. );
  76. React.useEffect(() => {
  77. if (mappedError?.kind !== "unauthenticated") return;
  78. const next =
  79. typeof window !== "undefined"
  80. ? `${window.location.pathname}${window.location.search}`
  81. : searchPath(routeBranch);
  82. window.location.replace(
  83. buildLoginUrl({ reason: LOGIN_REASONS.EXPIRED, next })
  84. );
  85. }, [mappedError?.kind, routeBranch]);
  86. const pushStateToUrl = React.useCallback(
  87. (nextState) => {
  88. router.push(buildSearchHref({ routeBranch, state: nextState }));
  89. },
  90. [router, routeBranch]
  91. );
  92. const replaceStateToUrl = React.useCallback(
  93. (nextState) => {
  94. router.replace(buildSearchHref({ routeBranch, state: nextState }));
  95. },
  96. [router, routeBranch]
  97. );
  98. const handleSubmit = React.useCallback(() => {
  99. pushStateToUrl({ ...urlState, q: qDraft });
  100. }, [pushStateToUrl, urlState, qDraft]);
  101. const handleScopeChange = React.useCallback(
  102. (nextScope) => {
  103. if (!isAdminDev) return;
  104. replaceStateToUrl(buildNextStateForScopeChange({ urlState, nextScope }));
  105. },
  106. [isAdminDev, urlState, replaceStateToUrl]
  107. );
  108. const handleToggleBranch = React.useCallback(
  109. (branchId) => {
  110. if (!isAdminDev) return;
  111. replaceStateToUrl(buildNextStateForToggleBranch({ urlState, branchId }));
  112. },
  113. [isAdminDev, urlState, replaceStateToUrl]
  114. );
  115. const handleClearAllBranches = React.useCallback(() => {
  116. if (!isAdminDev) return;
  117. replaceStateToUrl(buildNextStateForClearAllBranches({ urlState }));
  118. }, [isAdminDev, urlState, replaceStateToUrl]);
  119. const handleLimitChange = React.useCallback(
  120. (nextLimit) => {
  121. replaceStateToUrl({ ...urlState, limit: nextLimit });
  122. },
  123. [urlState, replaceStateToUrl]
  124. );
  125. const handleSingleBranchChange = React.useCallback(
  126. (nextBranch) => {
  127. if (!isAdminDev) return;
  128. if (!isValidBranchParam(nextBranch)) return;
  129. const href = buildHrefForSingleBranchSwitch({ nextBranch, urlState });
  130. if (!href) return;
  131. router.push(href);
  132. },
  133. [isAdminDev, urlState, router]
  134. );
  135. if (mappedError?.kind === "forbidden") {
  136. return <ForbiddenView attemptedBranch={routeBranch} />;
  137. }
  138. const actions = (
  139. <Button
  140. variant="outline"
  141. size="sm"
  142. onClick={query.retry}
  143. disabled={!urlState.q || query.status === "loading"}
  144. title="Aktualisieren"
  145. >
  146. <RefreshCw className="h-4 w-4" />
  147. Aktualisieren
  148. </Button>
  149. );
  150. const resultsHeaderRight = urlState.q ? (
  151. <span className="rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground">
  152. {urlState.q}
  153. </span>
  154. ) : null;
  155. const scopeLabel = getScopeLabel({ routeBranch, urlState });
  156. const resultsDescription = urlState.q
  157. ? `Suchbereich: ${scopeLabel}`
  158. : "Geben Sie einen Suchbegriff ein, um zu starten.";
  159. const needsBranchSelection = needsMultiBranchSelectionHint({
  160. isAdminDev,
  161. urlState,
  162. });
  163. return (
  164. <ExplorerPageShell
  165. title="Suche"
  166. description={`Lieferscheine durchsuchen • Niederlassung ${routeBranch}`}
  167. actions={actions}
  168. >
  169. <ExplorerSectionCard
  170. title="Suche"
  171. description="Suchbegriff und Suchbereich auswählen."
  172. >
  173. <SearchForm
  174. branch={routeBranch}
  175. qDraft={qDraft}
  176. onQDraftChange={setQDraft}
  177. onSubmit={handleSubmit}
  178. currentQuery={urlState.q}
  179. isSubmitting={query.status === "loading"}
  180. isAdminDev={isAdminDev}
  181. scope={urlState.scope}
  182. onScopeChange={handleScopeChange}
  183. onSingleBranchChange={handleSingleBranchChange}
  184. availableBranches={
  185. branchesQuery.status === BRANCH_LIST_STATE.READY &&
  186. Array.isArray(branchesQuery.branches)
  187. ? branchesQuery.branches
  188. : []
  189. }
  190. branchesStatus={branchesQuery.status}
  191. selectedBranches={urlState.branches}
  192. onToggleBranch={handleToggleBranch}
  193. onClearAllBranches={handleClearAllBranches}
  194. limit={urlState.limit}
  195. onLimitChange={handleLimitChange}
  196. />
  197. </ExplorerSectionCard>
  198. <ExplorerSectionCard
  199. title="Ergebnisse"
  200. description={resultsDescription}
  201. headerRight={resultsHeaderRight}
  202. >
  203. <SearchResults
  204. branch={routeBranch}
  205. scope={urlState.scope}
  206. status={query.status}
  207. items={query.items}
  208. total={query.total}
  209. error={mappedError}
  210. onRetry={query.retry}
  211. nextCursor={query.nextCursor}
  212. onLoadMore={query.loadMore}
  213. isLoadingMore={query.isLoadingMore}
  214. loadMoreError={mappedLoadMoreError}
  215. needsBranchSelection={needsBranchSelection}
  216. />
  217. </ExplorerSectionCard>
  218. </ExplorerPageShell>
  219. );
  220. }