| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- "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 { isValidBranchParam } from "@/lib/frontend/params";
- import {
- parseSearchUrlState,
- 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 {
- buildSearchHref,
- buildSearchKey,
- getScopeLabel,
- needsMultiBranchSelectionHint,
- buildNextStateForScopeChange,
- buildNextStateForToggleBranch,
- buildNextStateForClearAllBranches,
- buildHrefForSingleBranchSwitch,
- } from "@/lib/frontend/search/pageHelpers";
- import { buildDateFilterValidationError } from "@/lib/frontend/search/dateFilterValidation";
- 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";
- 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");
- const parsedUrlState = React.useMemo(() => {
- return parseSearchUrlState(searchParams, { routeBranch });
- }, [searchParams, routeBranch]);
- const urlState = React.useMemo(() => {
- return normalizeSearchUrlStateForUser(parsedUrlState, {
- routeBranch,
- user,
- });
- }, [parsedUrlState, routeBranch, user]);
- const searchKey = React.useMemo(() => {
- return buildSearchKey({ routeBranch, urlState });
- }, [routeBranch, urlState]);
- const [qDraft, setQDraft] = React.useState(urlState.q || "");
- React.useEffect(() => {
- setQDraft(urlState.q || "");
- }, [urlState.q]);
- const branchesQuery = useSearchBranches({ enabled: isAdminDev });
- const query = useSearchQuery({
- searchKey,
- urlState,
- routeBranch,
- user,
- limit: urlState.limit,
- });
- const mappedError = React.useMemo(
- () => mapSearchError(query.error),
- [query.error]
- );
- const mappedLoadMoreError = React.useMemo(
- () => mapSearchError(query.loadMoreError),
- [query.loadMoreError]
- );
- // Local date validation: always run (even when q is missing) for instant UX feedback.
- const localDateValidationError = React.useMemo(() => {
- return buildDateFilterValidationError({
- from: urlState.from,
- to: urlState.to,
- });
- }, [urlState.from, urlState.to]);
- const mappedLocalDateValidation = React.useMemo(() => {
- return mapSearchError(localDateValidationError);
- }, [localDateValidationError]);
- // Validation errors should be shown near the inputs (SearchForm).
- // Prefer the query-derived validation when present, otherwise fall back to local date validation.
- const formValidationError =
- mappedError?.kind === "validation"
- ? mappedError
- : mappedLocalDateValidation?.kind === "validation"
- ? mappedLocalDateValidation
- : null;
- 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]);
- 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]
- );
- const handleSubmit = React.useCallback(() => {
- pushStateToUrl({ ...urlState, q: qDraft });
- }, [pushStateToUrl, urlState, qDraft]);
- const handleScopeChange = React.useCallback(
- (nextScope) => {
- if (!isAdminDev) return;
- replaceStateToUrl(buildNextStateForScopeChange({ urlState, nextScope }));
- },
- [isAdminDev, urlState, replaceStateToUrl]
- );
- const handleToggleBranch = React.useCallback(
- (branchId) => {
- if (!isAdminDev) return;
- replaceStateToUrl(buildNextStateForToggleBranch({ urlState, branchId }));
- },
- [isAdminDev, urlState, replaceStateToUrl]
- );
- const handleClearAllBranches = React.useCallback(() => {
- if (!isAdminDev) return;
- replaceStateToUrl(buildNextStateForClearAllBranches({ urlState }));
- }, [isAdminDev, urlState, replaceStateToUrl]);
- const handleLimitChange = React.useCallback(
- (nextLimit) => {
- replaceStateToUrl({ ...urlState, limit: nextLimit });
- },
- [urlState, replaceStateToUrl]
- );
- const handleSingleBranchChange = React.useCallback(
- (nextBranch) => {
- if (!isAdminDev) return;
- if (!isValidBranchParam(nextBranch)) return;
- const href = buildHrefForSingleBranchSwitch({ nextBranch, urlState });
- if (!href) return;
- router.push(href);
- },
- [isAdminDev, urlState, router]
- );
- const handleDateRangeChange = React.useCallback(
- ({ from, to }) => {
- replaceStateToUrl({
- ...urlState,
- from: from ?? null,
- to: to ?? null,
- });
- },
- [urlState, replaceStateToUrl]
- );
- 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 scopeLabel = getScopeLabel({ routeBranch, urlState });
- const resultsDescription = urlState.q
- ? `Suchbereich: ${scopeLabel}`
- : "Geben Sie einen Suchbegriff ein, um zu starten.";
- const needsBranchSelection = needsMultiBranchSelectionHint({
- isAdminDev,
- urlState,
- });
- 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}
- onSingleBranchChange={handleSingleBranchChange}
- availableBranches={
- branchesQuery.status === BRANCH_LIST_STATE.READY &&
- Array.isArray(branchesQuery.branches)
- ? branchesQuery.branches
- : []
- }
- branchesStatus={branchesQuery.status}
- selectedBranches={urlState.branches}
- onToggleBranch={handleToggleBranch}
- onClearAllBranches={handleClearAllBranches}
- limit={urlState.limit}
- onLimitChange={handleLimitChange}
- from={urlState.from}
- to={urlState.to}
- onDateRangeChange={handleDateRangeChange}
- validationError={formValidationError}
- />
- </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}
- needsBranchSelection={needsBranchSelection}
- />
- </ExplorerSectionCard>
- </ExplorerPageShell>
- );
- }
|