Selaa lähdekoodia

refactor(docs): update frontend API and UI documentation for date range filter integration

Code_Uwe 1 viikko sitten
vanhempi
sitoutus
3057036d5d
2 muutettua tiedostoa jossa 175 lisäystä ja 140 poistoa
  1. 47 26
      Docs/frontend-api-usage.md
  2. 128 114
      Docs/frontend-ui.md

+ 47 - 26
Docs/frontend-api-usage.md

@@ -1,15 +1,5 @@
 <!-- --------------------------------------------------------------------------- -->
 
-<!-- Ordner: Docs -->
-
-<!-- Datei: frontend-api-usage.md -->
-
-<!-- Relativer Pfad: Docs/frontend-api-usage.md -->
-
-<!-- --------------------------------------------------------------------------- -->
-
-<!-- --------------------------------------------------------------------------- -->
-
 <!-- Ordner: docs -->
 
 <!-- Datei: frontend-api-usage.md -->
@@ -20,7 +10,7 @@
 
 # Frontend API Usage (v1)
 
-This document is the **frontend-facing** single source of truth for consuming the Lieferscheine backend APIs.
+This document is the **frontend-facing** single source of truth for consuming the RHL Lieferscheine backend APIs.
 
 Scope:
 
@@ -28,6 +18,7 @@ Scope:
 - The minimal frontend `apiClient` helper layer (`lib/frontend/apiClient.js`).
 - Practical examples for building the UI.
 - PDF streaming/opening behavior in the Explorer and in Search.
+- Search **date filters** (`from` / `to`) and **shareable URL sync** (RHL-025).
 
 > UI developers: For the app shell layout and frontend route scaffold (public vs protected routes, placeholder pages), see **`docs/frontend-ui.md`**.
 >
@@ -42,6 +33,7 @@ Notes:
 
 - The backend provides a binary PDF stream/download endpoint (RHL-015). The Explorer integrates it for “Open PDF” (RHL-023).
 - The Search UI integrates the same “Open PDF” pattern (RHL-024).
+- The Search UI supports **optional date filters** (`from` / `to`) with a date range picker and presets (RHL-025).
 
 ---
 
@@ -74,6 +66,7 @@ The core Explorer UI flow is a simple drill-down:
 Optional (admin/dev):
 
 - use the search endpoint (`GET /api/search`) to implement cross-branch search UI (see section 4.4)
+- apply date filters via `from` / `to` in `YYYY-MM-DD` format (RHL-025)
 
 ### 1.3 Example usage (client-side)
 
@@ -373,7 +366,6 @@ This endpoint returns **binary PDF data** on the happy path (not JSON).
 Frontend rules:
 
 - **Do not call this endpoint via `apiClient.apiFetch()`**.
-
   - `apiClient` is JSON-centric and will try to parse the response.
 
 - Prefer opening the endpoint URL in a **new tab** so the browser handles PDF rendering.
@@ -425,12 +417,10 @@ const href = buildPdfDownloadUrl({ branch, year, month, day, filename });
 - Use the exact `files[].name` returned by `getFiles()` (case-sensitive on Linux).
 
 - Filenames with special characters must be URL-encoded.
-
   - In particular, `#` **must** be encoded as `%23`.
     Otherwise the browser treats it as a fragment and the server receives a truncated filename.
 
 - Host consistency matters for cookies:
-
   - If you are logged in on `http://localhost:3000`, also open the PDF on `http://localhost:3000`.
   - Switching to `http://127.0.0.1:3000` will not send the cookie (different host) and results in `401`.
 
@@ -453,14 +443,13 @@ Optional filters:
 - `scope`: `branch | all | multi`
 - `branch`: single branch
 - `branches`: comma-separated branch list (for `scope=multi`)
-- `from`, `to`: `YYYY-MM-DD`
+- `from`, `to`: `YYYY-MM-DD` (inclusive)
 - `limit`: page size (default `100`, allowed `50..200`)
 - `cursor`: pagination cursor returned by the previous response
 
 Filter rule:
 
 - The backend requires **at least one filter** to avoid accidental “match everything” searches:
-
   - `q` OR `from` OR `to`
 
 If all three are missing, the API returns:
@@ -494,17 +483,44 @@ Notes:
 - `total` is the total number of matches for the current query and can be shown as “x of y loaded”.
 - `total` may be `null` if the provider cannot provide a reliable total.
 
-Recommended usage (client-side):
+##### 4.4.1 Date filters (RHL-025)
+
+The Search UI supports **optional** date filtering:
+
+- `from` / `to` are inclusive ISO date strings (`YYYY-MM-DD`).
+- `from === to` is valid (single-day filter).
+- `from > to` is invalid and must be rejected.
+
+Frontend responsibilities:
+
+- Validate locally for fast feedback (UI should not send invalid date ranges).
+- Treat backend validation as authoritative (backend may still return `VALIDATION_SEARCH_DATE` / `VALIDATION_SEARCH_RANGE`).
+
+Relevant frontend helpers:
+
+- `lib/frontend/search/dateRange.js` (pure ISO date helpers + German formatting)
+- `lib/frontend/search/searchDateValidation.js` (canonical date-range validation)
+- `lib/frontend/search/dateFilterValidation.js` (builds a local `ApiClientError` for UI rendering)
+
+##### 4.4.2 URL sync (shareable)
+
+For Search v1, the first-page identity is URL-driven (shareable).
+
+- `q`, `scope`, `branches`, `limit`, `from`, `to` are part of the shareable state.
+- `cursor` is intentionally **not** part of the shareable URL and stays in client state.
+
+##### 4.4.3 Recommended usage (client-side)
 
 ```js
 import { search, ApiClientError } from "@/lib/frontend/apiClient";
 
 export async function searchDeliveryNotesExample() {
 	try {
-		// Text search example:
 		const res = await search({
 			q: "bridgestone",
 			branch: "NL20",
+			from: "2025-12-01",
+			to: "2025-12-31",
 			limit: 100,
 		});
 
@@ -525,7 +541,10 @@ export async function searchDeliveryNotesExample() {
 }
 ```
 
-Date-range-only example (no `q`, allowed as long as at least one of `from/to` is present):
+Date-range-only example (no `q`):
+
+- The API allows this as long as at least one of `from` / `to` is provided.
+- The current v1 UI intentionally requires `q` to trigger a search to avoid accidental broad queries.
 
 ```js
 import { search } from "@/lib/frontend/apiClient";
@@ -545,11 +564,9 @@ export async function searchByDateRangeOnly() {
 Using a hit to navigate/open:
 
 - Navigate to the day folder:
-
   - `dayPath(hit.branch, hit.year, hit.month, hit.day)`
 
 - Open the PDF:
-
   - `buildPdfUrl({ branch: hit.branch, year: hit.year, month: hit.month, day: hit.day, filename: hit.filename })`
 
 Pagination:
@@ -628,7 +645,6 @@ Backend rules:
 - All route handlers are `dynamic = "force-dynamic"`.
 - All JSON responses include `Cache-Control: no-store`.
 - A small process-local TTL cache exists in `lib/storage.js`:
-
   - branches/years: 60s
   - months/days/files: 15s
 
@@ -682,7 +698,6 @@ Rules:
 - Avoid breaking changes to existing URLs, parameters, or response fields.
 
 - Prefer additive changes:
-
   - add new endpoints
   - add optional fields
 
@@ -692,14 +707,20 @@ Rules:
 
 ## 9. Out of scope / planned additions
 
-- Date range UI for Search (`from` / `to`) and URL sync (planned follow-up after Search UI v1).
+- **Date-only Search UI mode** (admin/dev):
+  - The API supports searching by date range without `q`.
+  - The current v1 UI intentionally requires `q` to trigger a search.
+  - A dedicated UI toggle (“Search by date only”) could be added later to enable this safely.
 
 - Optional Search UX improvements:
-
   - grouping results by date / branch
   - debounced search (optional; current v1 is explicit submit)
 
 - Optional Explorer UX polish:
-
   - add a dedicated “Herunterladen” UI action (download variant)
   - optional in-app PDF viewer experience (instead of a new tab)
+    ich habe zu hause einen pc zum programmieren aber auch zum zocken. ich frage mich wie teuer es wird wenn ich meine grafikkarte so update dass ich AAA-Spiele auf 4k spielen kann
+
+derzeit habe ich eine RTX4060ti eingebaut. derzeit spiele ich auch viel auf der ps5 aber ich moechte in zukunft komplett umsteigen auf pc.
+
+was brauche ich um spiele wie eafc, nba2k, anno, gta etc auf 4k mit hoher frame zu spielen?

+ 128 - 114
Docs/frontend-ui.md

@@ -1,15 +1,5 @@
 <!-- --------------------------------------------------------------------------- -->
 
-<!-- Ordner: Docs -->
-
-<!-- Datei: frontend-ui.md -->
-
-<!-- Relativer Pfad: Docs/frontend-ui.md -->
-
-<!-- --------------------------------------------------------------------------- -->
-
-<!-- --------------------------------------------------------------------------- -->
-
 <!-- Folder: docs -->
 
 <!-- File: frontend-ui.md -->
@@ -18,7 +8,7 @@
 
 <!-- --------------------------------------------------------------------------- -->
 
-# Frontend UI: App Shell, Routing, Auth/RBAC, Explorer, and Search (RHL-019 / RHL-020 / RHL-021 / RHL-022 / RHL-023 / RHL-024 / RHL-037)
+# Frontend UI: App Shell, Routing, Auth/RBAC, Explorer, Search, and Date Range Filter (RHL-019 / RHL-020 / RHL-021 / RHL-022 / RHL-023 / RHL-024 / RHL-025 / RHL-037)
 
 This document describes the **frontend routing scaffold**, the **application shell layout**, and the core UI modules for the RHL Lieferscheine app.
 
@@ -30,6 +20,7 @@ Timeline:
 - **RHL-022**: Explorer v2 (Year → Month → Day → Files) + shadcn Breadcrumbs with dropdowns.
 - **RHL-023**: Explorer file action “Open PDF” using the binary PDF endpoint (opens in a new tab).
 - **RHL-024**: Search UI v1 (URL-driven `q`, scopes, cursor pagination, open PDF + jump to day).
+- **RHL-025**: Search date range filter (from/to) with a calendar popover, presets, local validation, and URL sync.
 - **RHL-037**: Search scope UX improvements (TopNav deep-link branch switching + Single combobox + Multi selection UX + deterministic URL state).
 
 > **Language policy**
@@ -42,39 +33,33 @@ Timeline:
 
 ## 1. Scope
 
-### 1.1 Implemented (as of RHL-037)
+### 1.1 Implemented (as of RHL-037 + RHL-025)
 
 - **Public** `/login` route with a functional login form (shadcn/ui primitives).
 
 - **Protected** application shell for all authenticated routes:
-
   - Top navigation (brand, status, logout)
   - Sidebar placeholder area
   - Main content area
 
 - **Session guard** for protected routes:
-
   - Session identity is checked via `GET /api/auth/me`.
   - When unauthenticated: redirect to `/login?reason=expired&next=<original-url>`.
 
 - **In-shell auth gating (UX improvement)**:
-
   - Auth loading/error/redirect states render **inside** the AppShell main content.
   - AppShell (TopNav + sidebar) remains stable; no “blank spinner screens”.
 
 - **Logout**:
-
   - Logout button calls `GET /api/auth/logout` and redirects to `/login?reason=logged-out`.
 
 - **UI RBAC (branch-level)**:
-
   - `BranchGuard` prevents branch users from accessing other branches’ URLs.
   - Admin/dev can access multiple branches.
   - Admin/dev branch existence validation uses `GET /api/branches`.
   - Fail-open policy on validation failures (do not lock the UI on temporary API errors).
 
 - **Route param validation** (syntactic):
-
   - `branch`: `NL` + digits (syntactic validity; existence is validated by BranchGuard for admin/dev)
   - `year`: `YYYY`
   - `month`: `01–12`
@@ -82,54 +67,45 @@ Timeline:
   - Invalid params trigger `notFound()` early in layouts.
 
 - **Explorer v2** (Branch → Year → Month → Day → Files):
-
   - `/:branch` → years
   - `/:branch/:year` → months
   - `/:branch/:year/:month` → days
   - `/:branch/:year/:month/:day` → files
 
 - **Breadcrumb navigation**:
-
   - shadcn/ui `Breadcrumb` + dropdowns for year/month/day when options are available.
   - Dropdown options are derived from real API results (only show segments that exist).
 
 - **Consistent states** across Explorer levels:
-
   - Loading states (Skeleton)
   - Empty states
   - Error states with retry
   - `FS_NOT_FOUND` mapped to an Explorer “path no longer exists” card
 
 - **Explorer leaf action: Open PDF (RHL-023)**
-
   - The file list on `/:branch/:year/:month/:day` provides an **“Öffnen”** action.
   - Clicking “Öffnen” opens the selected PDF **in a new browser tab**.
   - URL construction is centralized in `lib/frontend/explorer/pdfUrl.js`.
 
 - **Search UI (RHL-024) + Scope UX improvements (RHL-037)**
-
   - Route: `/:branch/search` (protected).
 
   - URL-driven state for shareability:
-
     - `q` (search query)
 
     - scope semantics:
-
       - **Single**: route branch is the scope (`/:branch/search`). No `branch=` query parameter is required.
       - **Multi**: `scope=multi&branches=NL06,NL20` (deterministic order, unique list)
       - **All**: `scope=all`
 
     - `limit` (optional): `50 | 100 | 200` (default 100)
 
-    - `from/to` are carried through the URL but date-range UI is still planned for later.
+    - `from` / `to` (optional): ISO date range filter (`YYYY-MM-DD`) synced to the URL (RHL-025)
 
   - Cursor-based pagination (`nextCursor`) is **not stored in the URL**.
 
   - Admin/dev UX:
-
     - **TopNav branch switch navigates** (deep-link branch switching):
-
       - preserves the current deep path (Explorer and Search)
       - preserves shareable Search query params (`q`, `scope`, `branches`, `limit`, `from`, `to`)
       - keeps Single scope consistent with the route branch.
@@ -138,26 +114,31 @@ Timeline:
 
     - **Multi scope branch selection** uses a checkbox grid (optimized for large branch counts) and provides an **“Alle abwählen”** action.
 
-  - Branch list for admin/dev is fetched via `GET /api/branches` (fail-open).
+  - **Search date range filter (RHL-025)**
+    - A calendar popover allows selecting `from` and `to`.
+    - Presets (“Heute”, “Letzte 7 Tage”, …) set common ranges quickly.
+    - The active filter is shown as a compact chip and can be cleared.
+    - The filter is validated locally (fast feedback) and is also validated by the backend.
+
+    Important UX policy (current UI v1):
+    - The Search UI still triggers queries only when a non-empty `q` is present.
+    - `from` / `to` are treated as **additive filters** for a text query.
+    - The backend supports date-only searches, but the current UI intentionally avoids this to prevent accidental broad queries.
 
 ---
 
 ### 1.2 Still out of scope / planned
 
-- Date range UI for Search (`from` / `to`) and URL sync (planned follow-up after Search scope UX).
-
 - Optional Search UX improvements:
-
   - grouping results by date and/or branch
   - debounced “typeahead” search (current v1 is explicit submit)
+  - optional **date-only search mode** (allow searches with `from/to` even when `q` is empty) if desired later
 
 - Optional Explorer improvements:
-
   - “Herunterladen” action (download variant) next to “Öffnen”
   - a dedicated in-app PDF viewer UI (instead of a new tab)
 
 - Perceived performance polish:
-
   - client-side caching/prefetch
   - skeleton/layout shift reduction
 
@@ -170,15 +151,12 @@ The app uses Next.js App Router **Route Groups** to separate public and protecte
 ### 2.1 Route groups
 
 - **Public**: `app/(public)`
-
   - Routes that do **not** show the authenticated AppShell.
   - Current route: `/login`
 
 - **Protected**: `app/(protected)`
-
   - Routes that render inside the AppShell.
   - Protected routes are guarded by:
-
     1. Session check (AuthProvider)
     2. UI RBAC check for branch routes (BranchGuard)
 
@@ -229,7 +207,6 @@ File: `app/(protected)/layout.jsx`
 Responsibilities:
 
 - Wrap all protected pages with:
-
   - `AuthProvider` (session check + redirect)
   - `AppShell` (stable frame)
   - `AuthGate` (renders auth loading/error/redirect UI inside the shell)
@@ -259,7 +236,7 @@ On very wide screens the UI should remain readable.
 
 Current strategy (implemented in `AppShell.jsx` + `TopNav.jsx`):
 
-- Use a centered content column (~60% of the viewport at `2xl+`).
+- Use a centered content column (~45% of the viewport at `2xl+`).
 - Keep the Explorer/Search content inside that centered column.
 - Render the sidebar in the left gutter and align it to the right edge so it “docks” to the centered content **without consuming the centered width**.
 
@@ -276,7 +253,6 @@ Purpose:
 
 - Reduce manual URL typing during manual testing and daily usage.
 - Provide direct links to:
-
   - Explorer (`/:branch`)
   - Search (`/:branch/search`)
 
@@ -285,7 +261,6 @@ Behavior:
 - Branch users: QuickNav uses the user’s `branchId`.
 
 - Admin/dev users:
-
   - QuickNav loads branches via `GET /api/branches`.
   - Provides a branch dropdown.
   - Stores the last selected branch in `localStorage` for convenience.
@@ -293,21 +268,18 @@ Behavior:
 Implementation notes:
 
 - Branch list loading is intentionally **not** tied to the dropdown selection.
-
   - The list is fetched once for the authenticated admin/dev user (or when the user changes).
   - This avoids unnecessary refetches when switching branches in the UI.
 
 RHL-037 navigation rule:
 
 - When admin/dev selects a branch in QuickNav, the app **navigates** to the same section while replacing the path branch segment:
-
   - Explorer deep paths are preserved:
-
     - `/NL32/2025/12/31` → `/NL20/2025/12/31`
 
   - Search route is preserved and shareable query params are preserved:
-
-    - `/NL32/search?q=x&scope=multi&branches=NL06,NL20&limit=200` → `/NL20/search?q=x&scope=multi&branches=NL06,NL20&limit=200`
+    - `/NL32/search?q=x&scope=multi&branches=NL06,NL20&limit=200&from=2026-01-01&to=2026-01-31`
+      → `/NL20/search?q=x&scope=multi&branches=NL06,NL20&limit=200&from=2026-01-01&to=2026-01-31`
 
   - Single scope is kept consistent with the route branch (no divergence between UI selection and URL).
 
@@ -332,12 +304,10 @@ Behavior:
 1. On mount, call `apiClient.getMe()`.
 
 2. If `{ user: { ... } }`:
-
    - set auth state to `authenticated`
    - render protected UI
 
 3. If `{ user: null }`:
-
    - redirect to `/login?reason=expired&next=<current-url>`
 
 The `next` parameter:
@@ -367,26 +337,22 @@ Flow:
 1. Login page parses query params using `parseLoginParams(...)`.
 
 2. If `reason` is present:
-
    - `expired` → show “session expired” banner (German)
    - `logged-out` → show “logged out” banner (German)
 
 3. On submit, the form calls `apiClient.login({ username, password })`.
 
 4. On success:
-
    - redirect to `next` if present
    - otherwise redirect to `/`
 
 5. On failure:
-
    - show a safe, user-friendly error message (German)
 
 Username policy:
 
 - Backend stores usernames in lowercase and performs normalization during login.
 - UI enforces this policy as well:
-
   - username input is normalized to lowercase
   - `autoCapitalize="none"` to prevent mobile auto-caps
 
@@ -422,7 +388,6 @@ Files:
 
 - `components/auth/BranchGuard.jsx`
 - Pure logic:
-
   - `lib/frontend/rbac/branchAccess.js`
   - `lib/frontend/rbac/branchUiDecision.js`
 
@@ -430,7 +395,6 @@ Responsibilities:
 
 - Read `user` and `status` from AuthContext.
 - Enforce branch rules:
-
   - role `branch` → allowed only when `:branch === user.branchId`
   - role `admin` / `dev` → allowed for any branch that exists
 
@@ -438,7 +402,6 @@ Admin/dev branch existence validation:
 
 - `BranchGuard` fetches `GET /api/branches` and verifies the route branch exists.
 - Fail-open policy:
-
   - If fetching the list fails, do not block rendering.
   - Backend RBAC and subsequent API calls remain authoritative.
 
@@ -479,7 +442,6 @@ Routes and components:
 - All Explorer pages are **Client Components**.
 - All JSON API calls go through `lib/frontend/apiClient.js`.
 - A small hook provides consistent query state:
-
   - `lib/frontend/hooks/useExplorerQuery.js`
 
 ### 7.4 Breadcrumbs (with dropdowns)
@@ -487,12 +449,10 @@ Routes and components:
 Files:
 
 - UI component:
-
   - `components/explorer/breadcrumbs/ExplorerBreadcrumbs.jsx`
   - `components/explorer/breadcrumbs/SegmentDropdown.jsx`
 
 - Pure helpers:
-
   - `lib/frontend/explorer/breadcrumbDropdowns.js`
   - `lib/frontend/explorer/formatters.js` (German month labels)
   - `lib/frontend/explorer/sorters.js`
@@ -507,27 +467,24 @@ Files list behavior:
 
 - Uses shadcn/ui `Table`.
 - Shows:
-
   - file name
   - relative path (desktop column + mobile secondary line)
 
 Primary file action:
 
 - “Öffnen” opens the PDF in a **new browser tab** via the binary PDF endpoint:
-
   - `GET /api/files/:branch/:year/:month/:day/:filename`
 
 Implementation notes:
 
 - URL construction is centralized in:
-
   - `lib/frontend/explorer/pdfUrl.js`
 
 - The PDF endpoint is binary (`application/pdf`). The UI uses navigation (`<a target="_blank">`) instead of `apiClient`.
 
 ---
 
-## 8. Search UI (RHL-024) + Scope UX improvements (RHL-037)
+## 8. Search UI (RHL-024 / RHL-037) + Date Range Filter (RHL-025)
 
 ### 8.1 Route and state model
 
@@ -551,30 +508,25 @@ Shareable params (first page identity):
 - `q` (string)
 
 - Scope params:
-
   - **Single**: no `scope` param required; route branch defines the branch.
-
     - Example: `/NL01/search?q=reifen`
 
   - **Multi**: `scope=multi&branches=NL06,NL20`
-
     - branches list is deterministic:
-
       - unique
       - stable ordering
 
   - **All**: `scope=all`
 
 - `limit`:
-
   - allowed values: `50 | 100 | 200`
   - default: `100`
   - only written to URL when non-default to keep URLs readable
 
-- `from/to`:
-
-  - carried through the URL to prepare for the date-range UI
-  - date-range UI is still planned
+- `from/to` (RHL-025):
+  - ISO date filter in `YYYY-MM-DD`
+  - both keys are optional
+  - preserved in URLs and across branch switching
 
 Pagination:
 
@@ -591,32 +543,50 @@ Branch users:
 Admin/dev users:
 
 - Scope selector:
-
   - “Diese Niederlassung” (single: route branch)
   - “Mehrere Niederlassungen” (multi)
   - “Alle Niederlassungen” (all)
 
 - Single branch selection:
-
   - shadcn combobox
   - selecting a branch **navigates** to `/:branch/search` while preserving shareable query params (`q`, scope params, `limit`, `from/to`).
 
 - Multi branch selection:
-
   - checkbox grid optimized for large branch counts
   - “selectable card” label wrapper highlights checked items (border + background)
   - pointer cursor + hover affordances improve discoverability
-  - optional “Alle abwählen” button to reset selection
+  - “Alle abwählen” button to reset selection
 
 - Branch list:
-
   - loaded via `GET /api/branches`
   - fail-open UI behavior:
-
     - if branch list fails, Search UI remains usable
-    - (optional fallback) allow manual NLxx input for selection
+    - allow manual NLxx input as a fallback for selection
+
+### 8.3 Search form structure
+
+Files:
+
+- `components/search/SearchPage.jsx`
+- `components/search/SearchForm.jsx`
+
+Form building blocks:
+
+- `components/search/form/SearchQueryBox.jsx`
+- `components/search/form/SearchScopeSelect.jsx`
+- `components/search/form/SearchSingleBranchCombobox.jsx`
+- `components/search/form/SearchMultiBranchPicker.jsx`
+- `components/search/form/SearchLimitSelect.jsx`
+- `components/search/form/SearchDateRangePicker.jsx` (RHL-025)
+- `components/search/form/SearchDateFilterChip.jsx` (RHL-025)
 
-### 8.3 Result list and actions
+Key UX rules:
+
+- `q` is the primary trigger for searches (explicit submit, no debounce).
+- Date range changes update the URL and are applied when a query is active.
+- Validation errors are shown **near the form**, not duplicated in the results area.
+
+### 8.4 Result list and actions
 
 Search results show at least:
 
@@ -629,18 +599,16 @@ Search results show at least:
 Actions:
 
 - “Öffnen”
-
   - uses `buildPdfUrl(...)`
   - opens the binary endpoint in a new tab (`<a target="_blank" rel="noopener noreferrer">`)
 
 - “Zum Ordner”
-
   - navigates to `/:branch/:year/:month/:day` using `dayPath(...)`
 
 Implementation note:
 
 - In the Search results table, the actions are intentionally kept in a fixed-width column.
-- Buttons may render side-by-side (desktop) and can wrap on very small widths.
+- Buttons render side-by-side on desktop; on very small widths they can wrap.
 
 Sorting:
 
@@ -653,28 +621,73 @@ Totals:
 - When the backend returns `total`, the UI shows “x von y Treffern geladen”.
 - If `total` is `null`, the UI falls back to showing only the loaded count.
 
-### 8.4 Error handling (Search)
+### 8.5 Date range filter (RHL-025)
+
+#### 8.5.1 Goals
+
+- Allow narrowing down text searches by **inclusive** date filters.
+- Keep the date filter **shareable** via the URL (`from`, `to`).
+- Provide **fast feedback** (local validation) while also relying on backend validation.
+
+#### 8.5.2 URL representation
+
+- `from` and `to` are ISO strings: `YYYY-MM-DD`.
+
+- Open ranges are supported:
+  - only `from` → “ab <date>”
+  - only `to` → “bis <date>”
 
-Search uses a dedicated error mapping helper:
+- A single-day search is represented as:
+  - `from === to`
 
-- `lib/frontend/search/errorMapping.js`
+#### 8.5.3 UI controls
 
-Mapping principles:
+- Entry point: “Zeitraum” button in the Search form.
 
-- Use stable error codes for UX decisions.
-- Do not leak raw backend messages to users.
-- Keep user-facing messages German.
+- Popover contents:
+  - Two read-only inputs (“Von”, “Bis”) with clear buttons
+  - Two-month calendar view
+  - Presets (German) for common ranges
+  - Reset action
 
-Key outcomes:
+- Active filter display:
+  - A compact chip below the form controls
+  - Provides a one-click clear action
 
-- `AUTH_UNAUTHENTICATED` → redirect to login with `reason=expired&next=<current-url>`
-- `AUTH_FORBIDDEN_BRANCH` → show Forbidden UX
-- `VALIDATION_*` → show a user-friendly German validation message
-- network/unknown → generic German error + retry
+#### 8.5.4 Validation and error mapping
 
-UX note (RHL-037):
+Single Source of Truth (frontend):
 
-- Multi scope with a query but zero branches selected is treated as “not ready” and shows a friendly hint.
+- ISO parsing + range checks:
+  - `lib/frontend/search/dateRange.js`
+
+- Canonical date-range validator:
+  - `lib/frontend/search/searchDateValidation.js`
+
+Local validation:
+
+- While editing, the UI produces a local `ApiClientError` using:
+  - `lib/frontend/search/dateFilterValidation.js`
+
+- This error is mapped to German UI copy via:
+  - `lib/frontend/search/errorMapping.js`
+
+Rules:
+
+- `from > to` is invalid and produces `VALIDATION_SEARCH_RANGE`.
+- Invalid ISO strings produce `VALIDATION_SEARCH_DATE`.
+- `from === to` is valid (single day).
+
+Backend alignment:
+
+- The backend also validates `from/to` and returns the same error codes.
+- This keeps frontend and backend behavior consistent.
+
+#### 8.5.5 Calendar integration details
+
+- The calendar is loaded client-side (SSR disabled) to avoid hydration/runtime issues.
+- The hook normalizes day-click handler signatures defensively to avoid version drift.
+- When the range is invalid (`from > to`), the calendar still shows the interval but highlights it as invalid.
 
 ---
 
@@ -699,6 +712,7 @@ Additional primitives used for Search scope UX:
 - `popover`
 - `command`
 - `badge`
+- `calendar` (react-day-picker wrapper)
 
 ---
 
@@ -707,12 +721,10 @@ Additional primitives used for Search scope UX:
 To keep the project consistent:
 
 - Use **`.jsx`** for files that contain JSX:
-
   - `app/**/page.jsx`, `app/**/layout.jsx`
   - React components in `components/**`
 
 - Use **`.js`** for non-JSX files:
-
   - `lib/**` utilities and helpers
   - `app/api/**/route.js`
   - `models/**`
@@ -751,6 +763,11 @@ Search helper tests:
 - `lib/frontend/search/normalizeState.test.js`
 - `lib/frontend/search/searchApiInput.test.js`
 - `lib/frontend/search/resultsSorting.test.js`
+- `lib/frontend/search/dateRange.test.js` (RHL-025)
+- `lib/frontend/search/datePresets.test.js` (RHL-025)
+- `lib/frontend/search/dateRangePickerUtils.test.js` (RHL-025)
+- `lib/frontend/search/searchDateValidation.test.js` (RHL-025)
+- `lib/frontend/search/dateFilterValidation.test.js` (RHL-025)
 
 QuickNav helper tests:
 
@@ -789,19 +806,15 @@ docker compose -f docker-compose.yml -f docker-compose.local.yml up -d --build
 Verify flows in the browser:
 
 - Open a protected route while logged out (e.g. `/NL01/2025/12`)
-
   - Expect redirect to `/login?reason=expired&next=/NL01/2025/12`
 
 - Invalid login
-
   - Expect a German error message (e.g. “Benutzername oder Passwort ist falsch.”)
 
 - Valid login
-
   - Expect redirect into the protected route
 
 - Logout
-
   - Expect redirect to `/login?reason=logged-out`
 
 Explorer checks:
@@ -814,32 +827,36 @@ Explorer checks:
 PDF open:
 
 - On `/:branch/:year/:month/:day`, click **“Öffnen”**
-
   - Expected: opens the PDF in a new tab
   - Expected: works for filenames with spaces and special characters (URL-encoding)
 
 Search checks:
 
 - `/NL01/search`
-
   - empty state
   - submit triggers URL update and first-page fetch
 
 - admin/dev:
-
   - scope switching (single/multi/all)
   - single branch selection via combobox
   - multi selection via checkbox grid + “Alle abwählen”
   - limit switching (50/100/200)
 
-- pagination:
+- date range filter (RHL-025):
+  - open “Zeitraum” popover and pick a range
+  - verify `from/to` are written to the URL
+  - verify chip shows the German label and can clear the filter
+  - verify presets (“Heute”, “Letzte 7 Tage”, …) set the expected range
+  - invalid range:
+    - ensure UI displays a validation message
+    - ensure the calendar highlights the invalid interval
 
+- pagination:
   - “Mehr laden” appends results when `nextCursor` exists
 
 - TopNav QuickNav:
-
   - switching branch updates the URL and keeps the current deep path
-  - switching branch on Search keeps shareable query params
+  - switching branch on Search keeps shareable query params (including `from/to`)
 
 ### 12.2 Server
 
@@ -849,9 +866,9 @@ Verify:
 
 - Explorer navigation and PDF open
 - Search UI:
-
   - scopes
   - limit selection
+  - date range filter + URL sync
   - total count (“x von y geladen”)
   - open PDF / jump to day
   - TopNav branch switching keeps deep links
@@ -860,14 +877,11 @@ Verify:
 
 ## 13. Planned follow-ups
 
-- Search date range UI (`from` / `to`) with shareable URL sync.
-
 - Optional Search UX improvements:
-
   - grouping results by date / branch
-  - query presets
+  - query presets (beyond the date presets)
+  - optional date-only search mode (if desired)
 
 - Optional Explorer improvements:
-
   - add “Herunterladen” action
   - optional in-app PDF viewer