Browse Source

RHL-025 refactor(date-range): integrate SearchDateRangePicker and SearchDateFilterChip components for enhanced date filtering

Code_Uwe 2 weeks ago
parent
commit
d459b6a391

+ 48 - 1
components/search/SearchForm.jsx

@@ -9,6 +9,11 @@ import SearchScopeSelect from "@/components/search/form/SearchScopeSelect";
 import SearchLimitSelect from "@/components/search/form/SearchLimitSelect";
 import SearchSingleBranchCombobox from "@/components/search/form/SearchSingleBranchCombobox";
 import SearchMultiBranchPicker from "@/components/search/form/SearchMultiBranchPicker";
+import SearchDateRangePicker from "@/components/search/form/SearchDateRangePicker";
+import SearchDateFilterChip from "@/components/search/form/SearchDateFilterChip";
+
+import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
+import { AlertCircleIcon } from "lucide-react";
 
 export default function SearchForm({
 	branch,
@@ -28,6 +33,10 @@ export default function SearchForm({
 	onClearAllBranches,
 	limit,
 	onLimitChange,
+	from,
+	to,
+	onDateRangeChange,
+	validationError,
 }) {
 	const canSearch = typeof qDraft === "string" && qDraft.trim().length > 0;
 
@@ -40,6 +49,8 @@ export default function SearchForm({
 		isAdminDev && scope === SEARCH_SCOPE.SINGLE
 	);
 
+	const hasDateFilter = Boolean(from || to);
+
 	return (
 		<div className="space-y-4">
 			<form
@@ -52,7 +63,7 @@ export default function SearchForm({
 			>
 				{/* 
 					Layout goal:
-					- Desktop: everything on one line (scope + optional single combobox | query | limit)
+					- Desktop: everything on one line (scope + optional single combobox | query | date range | limit)
 					- Mobile: stack to avoid horizontal overflow
 				*/}
 				<div className="flex flex-col gap-3 lg:flex-row items-start lg:flex-nowrap">
@@ -105,6 +116,16 @@ export default function SearchForm({
 						/>
 					</div>
 
+					{/* Date range picker */}
+					<div className="shrink-0">
+						<SearchDateRangePicker
+							from={from}
+							to={to}
+							onDateRangeChange={onDateRangeChange}
+							isSubmitting={isSubmitting}
+						/>
+					</div>
+
 					{/* Right block: limit stays “content-sized” */}
 					<div className="shrink-0">
 						<SearchLimitSelect
@@ -116,6 +137,32 @@ export default function SearchForm({
 				</div>
 			</form>
 
+			{/* Validation feedback belongs near the inputs (not in results). */}
+			{validationError ? (
+				<Alert variant="destructive">
+					<AlertCircleIcon />
+					<AlertTitle>{validationError.title}</AlertTitle>
+					<AlertDescription>{validationError.description}</AlertDescription>
+				</Alert>
+			) : null}
+
+			{/* Active date filter chip (quick clear) */}
+			{hasDateFilter ? (
+				<div className="flex flex-wrap items-center gap-2">
+					<span className="text-xs text-muted-foreground">Aktive Filter:</span>
+
+					<SearchDateFilterChip
+						from={from}
+						to={to}
+						isSubmitting={isSubmitting}
+						onClear={() => {
+							if (typeof onDateRangeChange !== "function") return;
+							onDateRangeChange({ from: null, to: null });
+						}}
+					/>
+				</div>
+			) : null}
+
 			{/* Multi scope branch picker remains below */}
 			{isAdminDev && scope === SEARCH_SCOPE.MULTI ? (
 				<SearchMultiBranchPicker

+ 38 - 6
components/search/SearchPage.jsx

@@ -32,6 +32,8 @@ import {
 	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";
@@ -88,6 +90,27 @@ export default function SearchPage({ branch: routeBranch }) {
 		[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;
 
@@ -160,6 +183,17 @@ export default function SearchPage({ branch: routeBranch }) {
 		[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} />;
 	}
@@ -195,12 +229,6 @@ export default function SearchPage({ branch: routeBranch }) {
 	});
 
 	return (
-		/*
-			RHL-038:
-			- The Search page felt “too wide” on desktop.
-			- We intentionally cap the content to ~60% (centered) to improve readability.
-			- On smaller screens we keep full width for usability.
-		*/
 		<ExplorerPageShell
 			title="Suche"
 			description={`Lieferscheine durchsuchen • Niederlassung ${routeBranch}`}
@@ -233,6 +261,10 @@ export default function SearchPage({ branch: routeBranch }) {
 					onClearAllBranches={handleClearAllBranches}
 					limit={urlState.limit}
 					onLimitChange={handleLimitChange}
+					from={urlState.from}
+					to={urlState.to}
+					onDateRangeChange={handleDateRangeChange}
+					validationError={formValidationError}
 				/>
 			</ExplorerSectionCard>
 

+ 12 - 0
components/search/SearchResults.jsx

@@ -63,6 +63,18 @@ export default function SearchResults({
 	}
 
 	if (status === "error" && error) {
+		// Validation errors are rendered in the SearchForm (near the inputs).
+		// We avoid showing a second alert in the results.
+		if (error.kind === "validation") {
+			return (
+				<ExplorerEmpty
+					title="Eingaben prüfen"
+					description="Bitte prüfen Sie Ihre Filter oben."
+					upHref={null}
+				/>
+			);
+		}
+
 		return (
 			<ExplorerError
 				title={error.title}