| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- "use client";
- import React from "react";
- import { useRouter, useSearchParams } from "next/navigation";
- import { RefreshCw } from "lucide-react";
- import { useAuth } from "@/components/auth/authContext";
- import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
- import { searchPath } from "@/lib/frontend/routes";
- import {
- parseSearchUrlState,
- serializeSearchUrlState,
- SEARCH_SCOPE,
- } from "@/lib/frontend/search/urlState";
- import { normalizeSearchUrlStateForUser } from "@/lib/frontend/search/normalizeState";
- import { mapSearchError } from "@/lib/frontend/search/errorMapping";
- import { useSearchQuery } from "@/lib/frontend/search/useSearchQuery";
- import {
- useSearchBranches,
- BRANCH_LIST_STATE,
- } from "@/lib/frontend/search/useSearchBranches";
- import ExplorerPageShell from "@/components/explorer/ExplorerPageShell";
- import ExplorerSectionCard from "@/components/explorer/ExplorerSectionCard";
- import ForbiddenView from "@/components/system/ForbiddenView";
- import { Button } from "@/components/ui/button";
- import SearchForm from "@/components/search/SearchForm";
- import SearchResults from "@/components/search/SearchResults";
- function buildSearchHref({ routeBranch, state }) {
- const base = searchPath(routeBranch);
- const qs = serializeSearchUrlState(state);
- return qs ? `${base}?${qs}` : base;
- }
- export default function SearchPage({ branch: routeBranch }) {
- const router = useRouter();
- const searchParams = useSearchParams();
- const { status: authStatus, user } = useAuth();
- const isAuthenticated = authStatus === "authenticated" && user;
- const isAdminDev =
- isAuthenticated && (user.role === "admin" || user.role === "dev");
- // 1) URL -> parsed state (pure helper)
- const parsedUrlState = React.useMemo(() => {
- return parseSearchUrlState(searchParams, { routeBranch });
- }, [searchParams, routeBranch]);
- // 2) Normalize for user role + route context (pure helper)
- const urlState = React.useMemo(() => {
- return normalizeSearchUrlStateForUser(parsedUrlState, {
- routeBranch,
- user,
- });
- }, [parsedUrlState, routeBranch, user]);
- // 3) The identity of a first-page search (cursor intentionally excluded).
- const searchKey = React.useMemo(() => {
- return serializeSearchUrlState(urlState);
- }, [urlState]);
- // 4) Draft input (URL remains SoT for executed searches).
- const [qDraft, setQDraft] = React.useState(urlState.q || "");
- React.useEffect(() => {
- setQDraft(urlState.q || "");
- }, [urlState.q]);
- // 5) Admin/dev only: branches list for multi select (fail-open)
- const branchesQuery = useSearchBranches({ enabled: isAdminDev });
- // 6) Data lifecycle (first page + load more)
- const query = useSearchQuery({
- searchKey,
- urlState,
- routeBranch,
- user,
- limit: urlState.limit,
- });
- // 7) Map errors to German UX copy
- const mappedError = React.useMemo(
- () => mapSearchError(query.error),
- [query.error]
- );
- const mappedLoadMoreError = React.useMemo(
- () => mapSearchError(query.loadMoreError),
- [query.loadMoreError]
- );
- // 8) Redirect when unauthenticated mid-request
- React.useEffect(() => {
- if (mappedError?.kind !== "unauthenticated") return;
- const next =
- typeof window !== "undefined"
- ? `${window.location.pathname}${window.location.search}`
- : searchPath(routeBranch);
- window.location.replace(
- buildLoginUrl({ reason: LOGIN_REASONS.EXPIRED, next })
- );
- }, [mappedError?.kind, routeBranch]);
- // 9) URL write helpers (search is URL-driven)
- const pushStateToUrl = React.useCallback(
- (nextState) => {
- router.push(buildSearchHref({ routeBranch, state: nextState }));
- },
- [router, routeBranch]
- );
- const replaceStateToUrl = React.useCallback(
- (nextState) => {
- router.replace(buildSearchHref({ routeBranch, state: nextState }));
- },
- [router, routeBranch]
- );
- // 10) Handlers
- const handleSubmit = React.useCallback(() => {
- const nextState = {
- ...urlState,
- q: qDraft,
- branch: routeBranch,
- };
- pushStateToUrl(nextState);
- }, [urlState, qDraft, routeBranch, pushStateToUrl]);
- const handleScopeChange = React.useCallback(
- (nextScope) => {
- if (!isAdminDev) return;
- const nextState = {
- ...urlState,
- scope: nextScope,
- branch: routeBranch,
- branches: nextScope === SEARCH_SCOPE.MULTI ? urlState.branches : [],
- };
- replaceStateToUrl(nextState);
- },
- [isAdminDev, urlState, routeBranch, replaceStateToUrl]
- );
- const handleToggleBranch = React.useCallback(
- (branchId) => {
- if (!isAdminDev) return;
- const current = Array.isArray(urlState.branches) ? urlState.branches : [];
- const set = new Set(current);
- if (set.has(branchId)) set.delete(branchId);
- else set.add(branchId);
- const nextState = {
- ...urlState,
- scope: SEARCH_SCOPE.MULTI,
- branches: Array.from(set),
- };
- replaceStateToUrl(nextState);
- },
- [isAdminDev, urlState, replaceStateToUrl]
- );
- const handleLimitChange = React.useCallback(
- (nextLimit) => {
- const nextState = {
- ...urlState,
- limit: nextLimit,
- branch: routeBranch,
- };
- // Like scope changes: rerun based on URL state (executed query), not on draft.
- replaceStateToUrl(nextState);
- },
- [urlState, routeBranch, replaceStateToUrl]
- );
- // Forbidden stays consistent with Explorer UX.
- if (mappedError?.kind === "forbidden") {
- return <ForbiddenView attemptedBranch={routeBranch} />;
- }
- const actions = (
- <Button
- variant="outline"
- size="sm"
- onClick={query.retry}
- disabled={!urlState.q || query.status === "loading"}
- title="Aktualisieren"
- >
- <RefreshCw className="h-4 w-4" />
- Aktualisieren
- </Button>
- );
- const resultsHeaderRight = urlState.q ? (
- <span className="rounded-md bg-muted px-2 py-1 text-xs text-muted-foreground">
- {urlState.q}
- </span>
- ) : null;
- const resultsDescription = urlState.q
- ? `Niederlassung ${routeBranch}`
- : "Geben Sie einen Suchbegriff ein, um zu starten.";
- return (
- <ExplorerPageShell
- title="Suche"
- description={`Lieferscheine durchsuchen • Niederlassung ${routeBranch}`}
- actions={actions}
- >
- <ExplorerSectionCard
- title="Suche"
- description="Suchbegriff und Suchbereich auswählen."
- >
- <SearchForm
- branch={routeBranch}
- qDraft={qDraft}
- onQDraftChange={setQDraft}
- onSubmit={handleSubmit}
- currentQuery={urlState.q}
- isSubmitting={query.status === "loading"}
- isAdminDev={isAdminDev}
- scope={urlState.scope}
- onScopeChange={handleScopeChange}
- availableBranches={
- branchesQuery.status === BRANCH_LIST_STATE.READY &&
- Array.isArray(branchesQuery.branches)
- ? branchesQuery.branches
- : []
- }
- branchesStatus={branchesQuery.status}
- selectedBranches={urlState.branches}
- onToggleBranch={handleToggleBranch}
- limit={urlState.limit}
- onLimitChange={handleLimitChange}
- />
- </ExplorerSectionCard>
- <ExplorerSectionCard
- title="Ergebnisse"
- description={resultsDescription}
- headerRight={resultsHeaderRight}
- >
- <SearchResults
- branch={routeBranch}
- scope={urlState.scope}
- status={query.status}
- items={query.items}
- total={query.total}
- error={mappedError}
- onRetry={query.retry}
- nextCursor={query.nextCursor}
- onLoadMore={query.loadMore}
- isLoadingMore={query.isLoadingMore}
- loadMoreError={mappedLoadMoreError}
- />
- </ExplorerSectionCard>
- </ExplorerPageShell>
- );
- }
|