|
@@ -7,6 +7,7 @@ import { RefreshCw } from "lucide-react";
|
|
|
import { useAuth } from "@/components/auth/authContext";
|
|
import { useAuth } from "@/components/auth/authContext";
|
|
|
import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
|
|
import { buildLoginUrl, LOGIN_REASONS } from "@/lib/frontend/authRedirect";
|
|
|
import { searchPath } from "@/lib/frontend/routes";
|
|
import { searchPath } from "@/lib/frontend/routes";
|
|
|
|
|
+import { isValidBranchParam } from "@/lib/frontend/params";
|
|
|
|
|
|
|
|
import {
|
|
import {
|
|
|
parseSearchUrlState,
|
|
parseSearchUrlState,
|
|
@@ -44,12 +45,10 @@ export default function SearchPage({ branch: routeBranch }) {
|
|
|
const isAdminDev =
|
|
const isAdminDev =
|
|
|
isAuthenticated && (user.role === "admin" || user.role === "dev");
|
|
isAuthenticated && (user.role === "admin" || user.role === "dev");
|
|
|
|
|
|
|
|
- // 1) URL -> parsed state (pure helper)
|
|
|
|
|
const parsedUrlState = React.useMemo(() => {
|
|
const parsedUrlState = React.useMemo(() => {
|
|
|
return parseSearchUrlState(searchParams, { routeBranch });
|
|
return parseSearchUrlState(searchParams, { routeBranch });
|
|
|
}, [searchParams, routeBranch]);
|
|
}, [searchParams, routeBranch]);
|
|
|
|
|
|
|
|
- // 2) Normalize for user role + route context (pure helper)
|
|
|
|
|
const urlState = React.useMemo(() => {
|
|
const urlState = React.useMemo(() => {
|
|
|
return normalizeSearchUrlStateForUser(parsedUrlState, {
|
|
return normalizeSearchUrlStateForUser(parsedUrlState, {
|
|
|
routeBranch,
|
|
routeBranch,
|
|
@@ -57,21 +56,18 @@ export default function SearchPage({ branch: routeBranch }) {
|
|
|
});
|
|
});
|
|
|
}, [parsedUrlState, routeBranch, user]);
|
|
}, [parsedUrlState, routeBranch, user]);
|
|
|
|
|
|
|
|
- // 3) The identity of a first-page search (cursor intentionally excluded).
|
|
|
|
|
const searchKey = React.useMemo(() => {
|
|
const searchKey = React.useMemo(() => {
|
|
|
- return serializeSearchUrlState(urlState);
|
|
|
|
|
- }, [urlState]);
|
|
|
|
|
|
|
+ const qs = serializeSearchUrlState(urlState);
|
|
|
|
|
+ return `${routeBranch}|${qs}`;
|
|
|
|
|
+ }, [routeBranch, urlState]);
|
|
|
|
|
|
|
|
- // 4) Draft input (URL remains SoT for executed searches).
|
|
|
|
|
const [qDraft, setQDraft] = React.useState(urlState.q || "");
|
|
const [qDraft, setQDraft] = React.useState(urlState.q || "");
|
|
|
React.useEffect(() => {
|
|
React.useEffect(() => {
|
|
|
setQDraft(urlState.q || "");
|
|
setQDraft(urlState.q || "");
|
|
|
}, [urlState.q]);
|
|
}, [urlState.q]);
|
|
|
|
|
|
|
|
- // 5) Admin/dev only: branches list for multi select (fail-open)
|
|
|
|
|
const branchesQuery = useSearchBranches({ enabled: isAdminDev });
|
|
const branchesQuery = useSearchBranches({ enabled: isAdminDev });
|
|
|
|
|
|
|
|
- // 6) Data lifecycle (first page + load more)
|
|
|
|
|
const query = useSearchQuery({
|
|
const query = useSearchQuery({
|
|
|
searchKey,
|
|
searchKey,
|
|
|
urlState,
|
|
urlState,
|
|
@@ -80,7 +76,6 @@ export default function SearchPage({ branch: routeBranch }) {
|
|
|
limit: urlState.limit,
|
|
limit: urlState.limit,
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 7) Map errors to German UX copy
|
|
|
|
|
const mappedError = React.useMemo(
|
|
const mappedError = React.useMemo(
|
|
|
() => mapSearchError(query.error),
|
|
() => mapSearchError(query.error),
|
|
|
[query.error]
|
|
[query.error]
|
|
@@ -90,7 +85,6 @@ export default function SearchPage({ branch: routeBranch }) {
|
|
|
[query.loadMoreError]
|
|
[query.loadMoreError]
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- // 8) Redirect when unauthenticated mid-request
|
|
|
|
|
React.useEffect(() => {
|
|
React.useEffect(() => {
|
|
|
if (mappedError?.kind !== "unauthenticated") return;
|
|
if (mappedError?.kind !== "unauthenticated") return;
|
|
|
|
|
|
|
@@ -104,7 +98,6 @@ export default function SearchPage({ branch: routeBranch }) {
|
|
|
);
|
|
);
|
|
|
}, [mappedError?.kind, routeBranch]);
|
|
}, [mappedError?.kind, routeBranch]);
|
|
|
|
|
|
|
|
- // 9) URL write helpers (search is URL-driven)
|
|
|
|
|
const pushStateToUrl = React.useCallback(
|
|
const pushStateToUrl = React.useCallback(
|
|
|
(nextState) => {
|
|
(nextState) => {
|
|
|
router.push(buildSearchHref({ routeBranch, state: nextState }));
|
|
router.push(buildSearchHref({ routeBranch, state: nextState }));
|
|
@@ -119,16 +112,14 @@ export default function SearchPage({ branch: routeBranch }) {
|
|
|
[router, routeBranch]
|
|
[router, routeBranch]
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- // 10) Handlers
|
|
|
|
|
const handleSubmit = React.useCallback(() => {
|
|
const handleSubmit = React.useCallback(() => {
|
|
|
const nextState = {
|
|
const nextState = {
|
|
|
...urlState,
|
|
...urlState,
|
|
|
q: qDraft,
|
|
q: qDraft,
|
|
|
- branch: routeBranch,
|
|
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
pushStateToUrl(nextState);
|
|
pushStateToUrl(nextState);
|
|
|
- }, [urlState, qDraft, routeBranch, pushStateToUrl]);
|
|
|
|
|
|
|
+ }, [urlState, qDraft, pushStateToUrl]);
|
|
|
|
|
|
|
|
const handleScopeChange = React.useCallback(
|
|
const handleScopeChange = React.useCallback(
|
|
|
(nextScope) => {
|
|
(nextScope) => {
|
|
@@ -137,13 +128,12 @@ export default function SearchPage({ branch: routeBranch }) {
|
|
|
const nextState = {
|
|
const nextState = {
|
|
|
...urlState,
|
|
...urlState,
|
|
|
scope: nextScope,
|
|
scope: nextScope,
|
|
|
- branch: routeBranch,
|
|
|
|
|
branches: nextScope === SEARCH_SCOPE.MULTI ? urlState.branches : [],
|
|
branches: nextScope === SEARCH_SCOPE.MULTI ? urlState.branches : [],
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
replaceStateToUrl(nextState);
|
|
replaceStateToUrl(nextState);
|
|
|
},
|
|
},
|
|
|
- [isAdminDev, urlState, routeBranch, replaceStateToUrl]
|
|
|
|
|
|
|
+ [isAdminDev, urlState, replaceStateToUrl]
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
const handleToggleBranch = React.useCallback(
|
|
const handleToggleBranch = React.useCallback(
|
|
@@ -172,16 +162,32 @@ export default function SearchPage({ branch: routeBranch }) {
|
|
|
const nextState = {
|
|
const nextState = {
|
|
|
...urlState,
|
|
...urlState,
|
|
|
limit: nextLimit,
|
|
limit: nextLimit,
|
|
|
- branch: routeBranch,
|
|
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // Like scope changes: rerun based on URL state (executed query), not on draft.
|
|
|
|
|
replaceStateToUrl(nextState);
|
|
replaceStateToUrl(nextState);
|
|
|
},
|
|
},
|
|
|
- [urlState, routeBranch, replaceStateToUrl]
|
|
|
|
|
|
|
+ [urlState, replaceStateToUrl]
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ const handleSingleBranchChange = React.useCallback(
|
|
|
|
|
+ (nextBranch) => {
|
|
|
|
|
+ if (!isAdminDev) return;
|
|
|
|
|
+ if (!isValidBranchParam(nextBranch)) return;
|
|
|
|
|
+
|
|
|
|
|
+ const nextState = {
|
|
|
|
|
+ ...urlState,
|
|
|
|
|
+ scope: SEARCH_SCOPE.SINGLE,
|
|
|
|
|
+ branches: [],
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const base = searchPath(nextBranch);
|
|
|
|
|
+ const qs = serializeSearchUrlState(nextState);
|
|
|
|
|
+
|
|
|
|
|
+ router.push(qs ? `${base}?${qs}` : base);
|
|
|
|
|
+ },
|
|
|
|
|
+ [isAdminDev, urlState, router]
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
- // Forbidden stays consistent with Explorer UX.
|
|
|
|
|
if (mappedError?.kind === "forbidden") {
|
|
if (mappedError?.kind === "forbidden") {
|
|
|
return <ForbiddenView attemptedBranch={routeBranch} />;
|
|
return <ForbiddenView attemptedBranch={routeBranch} />;
|
|
|
}
|
|
}
|
|
@@ -205,10 +211,29 @@ export default function SearchPage({ branch: routeBranch }) {
|
|
|
</span>
|
|
</span>
|
|
|
) : null;
|
|
) : null;
|
|
|
|
|
|
|
|
|
|
+ const multiCount = Array.isArray(urlState.branches)
|
|
|
|
|
+ ? urlState.branches.length
|
|
|
|
|
+ : 0;
|
|
|
|
|
+
|
|
|
|
|
+ const scopeLabel =
|
|
|
|
|
+ urlState.scope === SEARCH_SCOPE.ALL
|
|
|
|
|
+ ? "Alle Niederlassungen"
|
|
|
|
|
+ : urlState.scope === SEARCH_SCOPE.MULTI
|
|
|
|
|
+ ? multiCount > 0
|
|
|
|
|
+ ? `${multiCount} Niederlassung${multiCount === 1 ? "" : "en"}`
|
|
|
|
|
+ : "Mehrere Niederlassungen"
|
|
|
|
|
+ : `Niederlassung ${routeBranch}`;
|
|
|
|
|
+
|
|
|
const resultsDescription = urlState.q
|
|
const resultsDescription = urlState.q
|
|
|
- ? `Niederlassung ${routeBranch}`
|
|
|
|
|
|
|
+ ? `Suchbereich: ${scopeLabel}`
|
|
|
: "Geben Sie einen Suchbegriff ein, um zu starten.";
|
|
: "Geben Sie einen Suchbegriff ein, um zu starten.";
|
|
|
|
|
|
|
|
|
|
+ const needsBranchSelection =
|
|
|
|
|
+ isAdminDev &&
|
|
|
|
|
+ urlState.scope === SEARCH_SCOPE.MULTI &&
|
|
|
|
|
+ Boolean(urlState.q) &&
|
|
|
|
|
+ multiCount === 0;
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<ExplorerPageShell
|
|
<ExplorerPageShell
|
|
|
title="Suche"
|
|
title="Suche"
|
|
@@ -229,6 +254,7 @@ export default function SearchPage({ branch: routeBranch }) {
|
|
|
isAdminDev={isAdminDev}
|
|
isAdminDev={isAdminDev}
|
|
|
scope={urlState.scope}
|
|
scope={urlState.scope}
|
|
|
onScopeChange={handleScopeChange}
|
|
onScopeChange={handleScopeChange}
|
|
|
|
|
+ onSingleBranchChange={handleSingleBranchChange}
|
|
|
availableBranches={
|
|
availableBranches={
|
|
|
branchesQuery.status === BRANCH_LIST_STATE.READY &&
|
|
branchesQuery.status === BRANCH_LIST_STATE.READY &&
|
|
|
Array.isArray(branchesQuery.branches)
|
|
Array.isArray(branchesQuery.branches)
|
|
@@ -260,6 +286,7 @@ export default function SearchPage({ branch: routeBranch }) {
|
|
|
onLoadMore={query.loadMore}
|
|
onLoadMore={query.loadMore}
|
|
|
isLoadingMore={query.isLoadingMore}
|
|
isLoadingMore={query.isLoadingMore}
|
|
|
loadMoreError={mappedLoadMoreError}
|
|
loadMoreError={mappedLoadMoreError}
|
|
|
|
|
+ needsBranchSelection={needsBranchSelection}
|
|
|
/>
|
|
/>
|
|
|
</ExplorerSectionCard>
|
|
</ExplorerSectionCard>
|
|
|
</ExplorerPageShell>
|
|
</ExplorerPageShell>
|