Prechádzať zdrojové kódy

RHL-037 feat(search): enhance SearchPage and SearchResults for branch selection handling and improved UX

Code_Uwe 3 týždňov pred
rodič
commit
60208f0054

+ 48 - 21
components/search/SearchPage.jsx

@@ -7,6 +7,7 @@ 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,
@@ -44,12 +45,10 @@ export default function SearchPage({ branch: routeBranch }) {
 	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,
@@ -57,21 +56,18 @@ export default function SearchPage({ branch: routeBranch }) {
 		});
 	}, [parsedUrlState, routeBranch, user]);
 
-	// 3) The identity of a first-page search (cursor intentionally excluded).
 	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 || "");
 	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,
@@ -80,7 +76,6 @@ export default function SearchPage({ branch: routeBranch }) {
 		limit: urlState.limit,
 	});
 
-	// 7) Map errors to German UX copy
 	const mappedError = React.useMemo(
 		() => mapSearchError(query.error),
 		[query.error]
@@ -90,7 +85,6 @@ export default function SearchPage({ branch: routeBranch }) {
 		[query.loadMoreError]
 	);
 
-	// 8) Redirect when unauthenticated mid-request
 	React.useEffect(() => {
 		if (mappedError?.kind !== "unauthenticated") return;
 
@@ -104,7 +98,6 @@ export default function SearchPage({ branch: routeBranch }) {
 		);
 	}, [mappedError?.kind, routeBranch]);
 
-	// 9) URL write helpers (search is URL-driven)
 	const pushStateToUrl = React.useCallback(
 		(nextState) => {
 			router.push(buildSearchHref({ routeBranch, state: nextState }));
@@ -119,16 +112,14 @@ export default function SearchPage({ branch: routeBranch }) {
 		[router, routeBranch]
 	);
 
-	// 10) Handlers
 	const handleSubmit = React.useCallback(() => {
 		const nextState = {
 			...urlState,
 			q: qDraft,
-			branch: routeBranch,
 		};
 
 		pushStateToUrl(nextState);
-	}, [urlState, qDraft, routeBranch, pushStateToUrl]);
+	}, [urlState, qDraft, pushStateToUrl]);
 
 	const handleScopeChange = React.useCallback(
 		(nextScope) => {
@@ -137,13 +128,12 @@ export default function SearchPage({ branch: routeBranch }) {
 			const nextState = {
 				...urlState,
 				scope: nextScope,
-				branch: routeBranch,
 				branches: nextScope === SEARCH_SCOPE.MULTI ? urlState.branches : [],
 			};
 
 			replaceStateToUrl(nextState);
 		},
-		[isAdminDev, urlState, routeBranch, replaceStateToUrl]
+		[isAdminDev, urlState, replaceStateToUrl]
 	);
 
 	const handleToggleBranch = React.useCallback(
@@ -172,16 +162,32 @@ export default function SearchPage({ branch: routeBranch }) {
 			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]
+		[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") {
 		return <ForbiddenView attemptedBranch={routeBranch} />;
 	}
@@ -205,10 +211,29 @@ export default function SearchPage({ branch: routeBranch }) {
 		</span>
 	) : 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
-		? `Niederlassung ${routeBranch}`
+		? `Suchbereich: ${scopeLabel}`
 		: "Geben Sie einen Suchbegriff ein, um zu starten.";
 
+	const needsBranchSelection =
+		isAdminDev &&
+		urlState.scope === SEARCH_SCOPE.MULTI &&
+		Boolean(urlState.q) &&
+		multiCount === 0;
+
 	return (
 		<ExplorerPageShell
 			title="Suche"
@@ -229,6 +254,7 @@ export default function SearchPage({ branch: routeBranch }) {
 					isAdminDev={isAdminDev}
 					scope={urlState.scope}
 					onScopeChange={handleScopeChange}
+					onSingleBranchChange={handleSingleBranchChange}
 					availableBranches={
 						branchesQuery.status === BRANCH_LIST_STATE.READY &&
 						Array.isArray(branchesQuery.branches)
@@ -260,6 +286,7 @@ export default function SearchPage({ branch: routeBranch }) {
 					onLoadMore={query.loadMore}
 					isLoadingMore={query.isLoadingMore}
 					loadMoreError={mappedLoadMoreError}
+					needsBranchSelection={needsBranchSelection}
 				/>
 			</ExplorerSectionCard>
 		</ExplorerPageShell>

+ 11 - 0
components/search/SearchResults.jsx

@@ -30,6 +30,7 @@ export default function SearchResults({
 	onLoadMore,
 	isLoadingMore,
 	loadMoreError,
+	needsBranchSelection = false,
 }) {
 	const [sortMode, setSortMode] = React.useState(SEARCH_RESULTS_SORT.RELEVANCE);
 
@@ -38,6 +39,16 @@ export default function SearchResults({
 	}, [items, sortMode]);
 
 	if (status === "idle") {
+		if (needsBranchSelection) {
+			return (
+				<ExplorerEmpty
+					title="Niederlassungen auswählen"
+					description="Bitte wählen Sie mindestens eine Niederlassung aus, um die Suche zu starten."
+					upHref={null}
+				/>
+			);
+		}
+
 		return (
 			<ExplorerEmpty
 				title="Suche starten"