|
@@ -1,15 +1,5 @@
|
|
|
<!-- --------------------------------------------------------------------------- -->
|
|
<!-- --------------------------------------------------------------------------- -->
|
|
|
|
|
|
|
|
-<!-- Ordner: Docs -->
|
|
|
|
|
-
|
|
|
|
|
-<!-- Datei: frontend-ui.md -->
|
|
|
|
|
-
|
|
|
|
|
-<!-- Relativer Pfad: Docs/frontend-ui.md -->
|
|
|
|
|
-
|
|
|
|
|
-<!-- --------------------------------------------------------------------------- -->
|
|
|
|
|
-
|
|
|
|
|
-<!-- --------------------------------------------------------------------------- -->
|
|
|
|
|
-
|
|
|
|
|
<!-- Folder: docs -->
|
|
<!-- Folder: docs -->
|
|
|
|
|
|
|
|
<!-- File: frontend-ui.md -->
|
|
<!-- 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.
|
|
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-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-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-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).
|
|
- **RHL-037**: Search scope UX improvements (TopNav deep-link branch switching + Single combobox + Multi selection UX + deterministic URL state).
|
|
|
|
|
|
|
|
> **Language policy**
|
|
> **Language policy**
|
|
@@ -42,39 +33,33 @@ Timeline:
|
|
|
|
|
|
|
|
## 1. Scope
|
|
## 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).
|
|
- **Public** `/login` route with a functional login form (shadcn/ui primitives).
|
|
|
|
|
|
|
|
- **Protected** application shell for all authenticated routes:
|
|
- **Protected** application shell for all authenticated routes:
|
|
|
-
|
|
|
|
|
- Top navigation (brand, status, logout)
|
|
- Top navigation (brand, status, logout)
|
|
|
- Sidebar placeholder area
|
|
- Sidebar placeholder area
|
|
|
- Main content area
|
|
- Main content area
|
|
|
|
|
|
|
|
- **Session guard** for protected routes:
|
|
- **Session guard** for protected routes:
|
|
|
-
|
|
|
|
|
- Session identity is checked via `GET /api/auth/me`.
|
|
- Session identity is checked via `GET /api/auth/me`.
|
|
|
- When unauthenticated: redirect to `/login?reason=expired&next=<original-url>`.
|
|
- When unauthenticated: redirect to `/login?reason=expired&next=<original-url>`.
|
|
|
|
|
|
|
|
- **In-shell auth gating (UX improvement)**:
|
|
- **In-shell auth gating (UX improvement)**:
|
|
|
-
|
|
|
|
|
- Auth loading/error/redirect states render **inside** the AppShell main content.
|
|
- Auth loading/error/redirect states render **inside** the AppShell main content.
|
|
|
- AppShell (TopNav + sidebar) remains stable; no “blank spinner screens”.
|
|
- AppShell (TopNav + sidebar) remains stable; no “blank spinner screens”.
|
|
|
|
|
|
|
|
- **Logout**:
|
|
- **Logout**:
|
|
|
-
|
|
|
|
|
- Logout button calls `GET /api/auth/logout` and redirects to `/login?reason=logged-out`.
|
|
- Logout button calls `GET /api/auth/logout` and redirects to `/login?reason=logged-out`.
|
|
|
|
|
|
|
|
- **UI RBAC (branch-level)**:
|
|
- **UI RBAC (branch-level)**:
|
|
|
-
|
|
|
|
|
- `BranchGuard` prevents branch users from accessing other branches’ URLs.
|
|
- `BranchGuard` prevents branch users from accessing other branches’ URLs.
|
|
|
- Admin/dev can access multiple branches.
|
|
- Admin/dev can access multiple branches.
|
|
|
- Admin/dev branch existence validation uses `GET /api/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).
|
|
- Fail-open policy on validation failures (do not lock the UI on temporary API errors).
|
|
|
|
|
|
|
|
- **Route param validation** (syntactic):
|
|
- **Route param validation** (syntactic):
|
|
|
-
|
|
|
|
|
- `branch`: `NL` + digits (syntactic validity; existence is validated by BranchGuard for admin/dev)
|
|
- `branch`: `NL` + digits (syntactic validity; existence is validated by BranchGuard for admin/dev)
|
|
|
- `year`: `YYYY`
|
|
- `year`: `YYYY`
|
|
|
- `month`: `01–12`
|
|
- `month`: `01–12`
|
|
@@ -82,54 +67,45 @@ Timeline:
|
|
|
- Invalid params trigger `notFound()` early in layouts.
|
|
- Invalid params trigger `notFound()` early in layouts.
|
|
|
|
|
|
|
|
- **Explorer v2** (Branch → Year → Month → Day → Files):
|
|
- **Explorer v2** (Branch → Year → Month → Day → Files):
|
|
|
-
|
|
|
|
|
- `/:branch` → years
|
|
- `/:branch` → years
|
|
|
- `/:branch/:year` → months
|
|
- `/:branch/:year` → months
|
|
|
- `/:branch/:year/:month` → days
|
|
- `/:branch/:year/:month` → days
|
|
|
- `/:branch/:year/:month/:day` → files
|
|
- `/:branch/:year/:month/:day` → files
|
|
|
|
|
|
|
|
- **Breadcrumb navigation**:
|
|
- **Breadcrumb navigation**:
|
|
|
-
|
|
|
|
|
- shadcn/ui `Breadcrumb` + dropdowns for year/month/day when options are available.
|
|
- 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).
|
|
- Dropdown options are derived from real API results (only show segments that exist).
|
|
|
|
|
|
|
|
- **Consistent states** across Explorer levels:
|
|
- **Consistent states** across Explorer levels:
|
|
|
-
|
|
|
|
|
- Loading states (Skeleton)
|
|
- Loading states (Skeleton)
|
|
|
- Empty states
|
|
- Empty states
|
|
|
- Error states with retry
|
|
- Error states with retry
|
|
|
- `FS_NOT_FOUND` mapped to an Explorer “path no longer exists” card
|
|
- `FS_NOT_FOUND` mapped to an Explorer “path no longer exists” card
|
|
|
|
|
|
|
|
- **Explorer leaf action: Open PDF (RHL-023)**
|
|
- **Explorer leaf action: Open PDF (RHL-023)**
|
|
|
-
|
|
|
|
|
- The file list on `/:branch/:year/:month/:day` provides an **“Öffnen”** action.
|
|
- The file list on `/:branch/:year/:month/:day` provides an **“Öffnen”** action.
|
|
|
- Clicking “Öffnen” opens the selected PDF **in a new browser tab**.
|
|
- Clicking “Öffnen” opens the selected PDF **in a new browser tab**.
|
|
|
- URL construction is centralized in `lib/frontend/explorer/pdfUrl.js`.
|
|
- URL construction is centralized in `lib/frontend/explorer/pdfUrl.js`.
|
|
|
|
|
|
|
|
- **Search UI (RHL-024) + Scope UX improvements (RHL-037)**
|
|
- **Search UI (RHL-024) + Scope UX improvements (RHL-037)**
|
|
|
-
|
|
|
|
|
- Route: `/:branch/search` (protected).
|
|
- Route: `/:branch/search` (protected).
|
|
|
|
|
|
|
|
- URL-driven state for shareability:
|
|
- URL-driven state for shareability:
|
|
|
-
|
|
|
|
|
- `q` (search query)
|
|
- `q` (search query)
|
|
|
|
|
|
|
|
- scope semantics:
|
|
- scope semantics:
|
|
|
-
|
|
|
|
|
- **Single**: route branch is the scope (`/:branch/search`). No `branch=` query parameter is required.
|
|
- **Single**: route branch is the scope (`/:branch/search`). No `branch=` query parameter is required.
|
|
|
- **Multi**: `scope=multi&branches=NL06,NL20` (deterministic order, unique list)
|
|
- **Multi**: `scope=multi&branches=NL06,NL20` (deterministic order, unique list)
|
|
|
- **All**: `scope=all`
|
|
- **All**: `scope=all`
|
|
|
|
|
|
|
|
- `limit` (optional): `50 | 100 | 200` (default 100)
|
|
- `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**.
|
|
- Cursor-based pagination (`nextCursor`) is **not stored in the URL**.
|
|
|
|
|
|
|
|
- Admin/dev UX:
|
|
- Admin/dev UX:
|
|
|
-
|
|
|
|
|
- **TopNav branch switch navigates** (deep-link branch switching):
|
|
- **TopNav branch switch navigates** (deep-link branch switching):
|
|
|
-
|
|
|
|
|
- preserves the current deep path (Explorer and Search)
|
|
- preserves the current deep path (Explorer and Search)
|
|
|
- preserves shareable Search query params (`q`, `scope`, `branches`, `limit`, `from`, `to`)
|
|
- preserves shareable Search query params (`q`, `scope`, `branches`, `limit`, `from`, `to`)
|
|
|
- keeps Single scope consistent with the route branch.
|
|
- 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.
|
|
- **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
|
|
### 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:
|
|
- Optional Search UX improvements:
|
|
|
-
|
|
|
|
|
- grouping results by date and/or branch
|
|
- grouping results by date and/or branch
|
|
|
- debounced “typeahead” search (current v1 is explicit submit)
|
|
- 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:
|
|
- Optional Explorer improvements:
|
|
|
-
|
|
|
|
|
- “Herunterladen” action (download variant) next to “Öffnen”
|
|
- “Herunterladen” action (download variant) next to “Öffnen”
|
|
|
- a dedicated in-app PDF viewer UI (instead of a new tab)
|
|
- a dedicated in-app PDF viewer UI (instead of a new tab)
|
|
|
|
|
|
|
|
- Perceived performance polish:
|
|
- Perceived performance polish:
|
|
|
-
|
|
|
|
|
- client-side caching/prefetch
|
|
- client-side caching/prefetch
|
|
|
- skeleton/layout shift reduction
|
|
- 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
|
|
### 2.1 Route groups
|
|
|
|
|
|
|
|
- **Public**: `app/(public)`
|
|
- **Public**: `app/(public)`
|
|
|
-
|
|
|
|
|
- Routes that do **not** show the authenticated AppShell.
|
|
- Routes that do **not** show the authenticated AppShell.
|
|
|
- Current route: `/login`
|
|
- Current route: `/login`
|
|
|
|
|
|
|
|
- **Protected**: `app/(protected)`
|
|
- **Protected**: `app/(protected)`
|
|
|
-
|
|
|
|
|
- Routes that render inside the AppShell.
|
|
- Routes that render inside the AppShell.
|
|
|
- Protected routes are guarded by:
|
|
- Protected routes are guarded by:
|
|
|
-
|
|
|
|
|
1. Session check (AuthProvider)
|
|
1. Session check (AuthProvider)
|
|
|
2. UI RBAC check for branch routes (BranchGuard)
|
|
2. UI RBAC check for branch routes (BranchGuard)
|
|
|
|
|
|
|
@@ -229,7 +207,6 @@ File: `app/(protected)/layout.jsx`
|
|
|
Responsibilities:
|
|
Responsibilities:
|
|
|
|
|
|
|
|
- Wrap all protected pages with:
|
|
- Wrap all protected pages with:
|
|
|
-
|
|
|
|
|
- `AuthProvider` (session check + redirect)
|
|
- `AuthProvider` (session check + redirect)
|
|
|
- `AppShell` (stable frame)
|
|
- `AppShell` (stable frame)
|
|
|
- `AuthGate` (renders auth loading/error/redirect UI inside the shell)
|
|
- `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`):
|
|
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.
|
|
- 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**.
|
|
- 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.
|
|
- Reduce manual URL typing during manual testing and daily usage.
|
|
|
- Provide direct links to:
|
|
- Provide direct links to:
|
|
|
-
|
|
|
|
|
- Explorer (`/:branch`)
|
|
- Explorer (`/:branch`)
|
|
|
- Search (`/:branch/search`)
|
|
- Search (`/:branch/search`)
|
|
|
|
|
|
|
@@ -285,7 +261,6 @@ Behavior:
|
|
|
- Branch users: QuickNav uses the user’s `branchId`.
|
|
- Branch users: QuickNav uses the user’s `branchId`.
|
|
|
|
|
|
|
|
- Admin/dev users:
|
|
- Admin/dev users:
|
|
|
-
|
|
|
|
|
- QuickNav loads branches via `GET /api/branches`.
|
|
- QuickNav loads branches via `GET /api/branches`.
|
|
|
- Provides a branch dropdown.
|
|
- Provides a branch dropdown.
|
|
|
- Stores the last selected branch in `localStorage` for convenience.
|
|
- Stores the last selected branch in `localStorage` for convenience.
|
|
@@ -293,21 +268,18 @@ Behavior:
|
|
|
Implementation notes:
|
|
Implementation notes:
|
|
|
|
|
|
|
|
- Branch list loading is intentionally **not** tied to the dropdown selection.
|
|
- 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).
|
|
- 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.
|
|
- This avoids unnecessary refetches when switching branches in the UI.
|
|
|
|
|
|
|
|
RHL-037 navigation rule:
|
|
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:
|
|
- 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:
|
|
- Explorer deep paths are preserved:
|
|
|
-
|
|
|
|
|
- `/NL32/2025/12/31` → `/NL20/2025/12/31`
|
|
- `/NL32/2025/12/31` → `/NL20/2025/12/31`
|
|
|
|
|
|
|
|
- Search route is preserved and shareable query params are preserved:
|
|
- 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).
|
|
- 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()`.
|
|
1. On mount, call `apiClient.getMe()`.
|
|
|
|
|
|
|
|
2. If `{ user: { ... } }`:
|
|
2. If `{ user: { ... } }`:
|
|
|
-
|
|
|
|
|
- set auth state to `authenticated`
|
|
- set auth state to `authenticated`
|
|
|
- render protected UI
|
|
- render protected UI
|
|
|
|
|
|
|
|
3. If `{ user: null }`:
|
|
3. If `{ user: null }`:
|
|
|
-
|
|
|
|
|
- redirect to `/login?reason=expired&next=<current-url>`
|
|
- redirect to `/login?reason=expired&next=<current-url>`
|
|
|
|
|
|
|
|
The `next` parameter:
|
|
The `next` parameter:
|
|
@@ -367,26 +337,22 @@ Flow:
|
|
|
1. Login page parses query params using `parseLoginParams(...)`.
|
|
1. Login page parses query params using `parseLoginParams(...)`.
|
|
|
|
|
|
|
|
2. If `reason` is present:
|
|
2. If `reason` is present:
|
|
|
-
|
|
|
|
|
- `expired` → show “session expired” banner (German)
|
|
- `expired` → show “session expired” banner (German)
|
|
|
- `logged-out` → show “logged out” banner (German)
|
|
- `logged-out` → show “logged out” banner (German)
|
|
|
|
|
|
|
|
3. On submit, the form calls `apiClient.login({ username, password })`.
|
|
3. On submit, the form calls `apiClient.login({ username, password })`.
|
|
|
|
|
|
|
|
4. On success:
|
|
4. On success:
|
|
|
-
|
|
|
|
|
- redirect to `next` if present
|
|
- redirect to `next` if present
|
|
|
- otherwise redirect to `/`
|
|
- otherwise redirect to `/`
|
|
|
|
|
|
|
|
5. On failure:
|
|
5. On failure:
|
|
|
-
|
|
|
|
|
- show a safe, user-friendly error message (German)
|
|
- show a safe, user-friendly error message (German)
|
|
|
|
|
|
|
|
Username policy:
|
|
Username policy:
|
|
|
|
|
|
|
|
- Backend stores usernames in lowercase and performs normalization during login.
|
|
- Backend stores usernames in lowercase and performs normalization during login.
|
|
|
- UI enforces this policy as well:
|
|
- UI enforces this policy as well:
|
|
|
-
|
|
|
|
|
- username input is normalized to lowercase
|
|
- username input is normalized to lowercase
|
|
|
- `autoCapitalize="none"` to prevent mobile auto-caps
|
|
- `autoCapitalize="none"` to prevent mobile auto-caps
|
|
|
|
|
|
|
@@ -422,7 +388,6 @@ Files:
|
|
|
|
|
|
|
|
- `components/auth/BranchGuard.jsx`
|
|
- `components/auth/BranchGuard.jsx`
|
|
|
- Pure logic:
|
|
- Pure logic:
|
|
|
-
|
|
|
|
|
- `lib/frontend/rbac/branchAccess.js`
|
|
- `lib/frontend/rbac/branchAccess.js`
|
|
|
- `lib/frontend/rbac/branchUiDecision.js`
|
|
- `lib/frontend/rbac/branchUiDecision.js`
|
|
|
|
|
|
|
@@ -430,7 +395,6 @@ Responsibilities:
|
|
|
|
|
|
|
|
- Read `user` and `status` from AuthContext.
|
|
- Read `user` and `status` from AuthContext.
|
|
|
- Enforce branch rules:
|
|
- Enforce branch rules:
|
|
|
-
|
|
|
|
|
- role `branch` → allowed only when `:branch === user.branchId`
|
|
- role `branch` → allowed only when `:branch === user.branchId`
|
|
|
- role `admin` / `dev` → allowed for any branch that exists
|
|
- 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.
|
|
- `BranchGuard` fetches `GET /api/branches` and verifies the route branch exists.
|
|
|
- Fail-open policy:
|
|
- Fail-open policy:
|
|
|
-
|
|
|
|
|
- If fetching the list fails, do not block rendering.
|
|
- If fetching the list fails, do not block rendering.
|
|
|
- Backend RBAC and subsequent API calls remain authoritative.
|
|
- Backend RBAC and subsequent API calls remain authoritative.
|
|
|
|
|
|
|
@@ -479,7 +442,6 @@ Routes and components:
|
|
|
- All Explorer pages are **Client Components**.
|
|
- All Explorer pages are **Client Components**.
|
|
|
- All JSON API calls go through `lib/frontend/apiClient.js`.
|
|
- All JSON API calls go through `lib/frontend/apiClient.js`.
|
|
|
- A small hook provides consistent query state:
|
|
- A small hook provides consistent query state:
|
|
|
-
|
|
|
|
|
- `lib/frontend/hooks/useExplorerQuery.js`
|
|
- `lib/frontend/hooks/useExplorerQuery.js`
|
|
|
|
|
|
|
|
### 7.4 Breadcrumbs (with dropdowns)
|
|
### 7.4 Breadcrumbs (with dropdowns)
|
|
@@ -487,12 +449,10 @@ Routes and components:
|
|
|
Files:
|
|
Files:
|
|
|
|
|
|
|
|
- UI component:
|
|
- UI component:
|
|
|
-
|
|
|
|
|
- `components/explorer/breadcrumbs/ExplorerBreadcrumbs.jsx`
|
|
- `components/explorer/breadcrumbs/ExplorerBreadcrumbs.jsx`
|
|
|
- `components/explorer/breadcrumbs/SegmentDropdown.jsx`
|
|
- `components/explorer/breadcrumbs/SegmentDropdown.jsx`
|
|
|
|
|
|
|
|
- Pure helpers:
|
|
- Pure helpers:
|
|
|
-
|
|
|
|
|
- `lib/frontend/explorer/breadcrumbDropdowns.js`
|
|
- `lib/frontend/explorer/breadcrumbDropdowns.js`
|
|
|
- `lib/frontend/explorer/formatters.js` (German month labels)
|
|
- `lib/frontend/explorer/formatters.js` (German month labels)
|
|
|
- `lib/frontend/explorer/sorters.js`
|
|
- `lib/frontend/explorer/sorters.js`
|
|
@@ -507,27 +467,24 @@ Files list behavior:
|
|
|
|
|
|
|
|
- Uses shadcn/ui `Table`.
|
|
- Uses shadcn/ui `Table`.
|
|
|
- Shows:
|
|
- Shows:
|
|
|
-
|
|
|
|
|
- file name
|
|
- file name
|
|
|
- relative path (desktop column + mobile secondary line)
|
|
- relative path (desktop column + mobile secondary line)
|
|
|
|
|
|
|
|
Primary file action:
|
|
Primary file action:
|
|
|
|
|
|
|
|
- “Öffnen” opens the PDF in a **new browser tab** via the binary PDF endpoint:
|
|
- “Öffnen” opens the PDF in a **new browser tab** via the binary PDF endpoint:
|
|
|
-
|
|
|
|
|
- `GET /api/files/:branch/:year/:month/:day/:filename`
|
|
- `GET /api/files/:branch/:year/:month/:day/:filename`
|
|
|
|
|
|
|
|
Implementation notes:
|
|
Implementation notes:
|
|
|
|
|
|
|
|
- URL construction is centralized in:
|
|
- URL construction is centralized in:
|
|
|
-
|
|
|
|
|
- `lib/frontend/explorer/pdfUrl.js`
|
|
- `lib/frontend/explorer/pdfUrl.js`
|
|
|
|
|
|
|
|
- The PDF endpoint is binary (`application/pdf`). The UI uses navigation (`<a target="_blank">`) instead of `apiClient`.
|
|
- 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
|
|
### 8.1 Route and state model
|
|
|
|
|
|
|
@@ -551,30 +508,25 @@ Shareable params (first page identity):
|
|
|
- `q` (string)
|
|
- `q` (string)
|
|
|
|
|
|
|
|
- Scope params:
|
|
- Scope params:
|
|
|
-
|
|
|
|
|
- **Single**: no `scope` param required; route branch defines the branch.
|
|
- **Single**: no `scope` param required; route branch defines the branch.
|
|
|
-
|
|
|
|
|
- Example: `/NL01/search?q=reifen`
|
|
- Example: `/NL01/search?q=reifen`
|
|
|
|
|
|
|
|
- **Multi**: `scope=multi&branches=NL06,NL20`
|
|
- **Multi**: `scope=multi&branches=NL06,NL20`
|
|
|
-
|
|
|
|
|
- branches list is deterministic:
|
|
- branches list is deterministic:
|
|
|
-
|
|
|
|
|
- unique
|
|
- unique
|
|
|
- stable ordering
|
|
- stable ordering
|
|
|
|
|
|
|
|
- **All**: `scope=all`
|
|
- **All**: `scope=all`
|
|
|
|
|
|
|
|
- `limit`:
|
|
- `limit`:
|
|
|
-
|
|
|
|
|
- allowed values: `50 | 100 | 200`
|
|
- allowed values: `50 | 100 | 200`
|
|
|
- default: `100`
|
|
- default: `100`
|
|
|
- only written to URL when non-default to keep URLs readable
|
|
- 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:
|
|
Pagination:
|
|
|
|
|
|
|
@@ -591,32 +543,50 @@ Branch users:
|
|
|
Admin/dev users:
|
|
Admin/dev users:
|
|
|
|
|
|
|
|
- Scope selector:
|
|
- Scope selector:
|
|
|
-
|
|
|
|
|
- “Diese Niederlassung” (single: route branch)
|
|
- “Diese Niederlassung” (single: route branch)
|
|
|
- “Mehrere Niederlassungen” (multi)
|
|
- “Mehrere Niederlassungen” (multi)
|
|
|
- “Alle Niederlassungen” (all)
|
|
- “Alle Niederlassungen” (all)
|
|
|
|
|
|
|
|
- Single branch selection:
|
|
- Single branch selection:
|
|
|
-
|
|
|
|
|
- shadcn combobox
|
|
- shadcn combobox
|
|
|
- selecting a branch **navigates** to `/:branch/search` while preserving shareable query params (`q`, scope params, `limit`, `from/to`).
|
|
- selecting a branch **navigates** to `/:branch/search` while preserving shareable query params (`q`, scope params, `limit`, `from/to`).
|
|
|
|
|
|
|
|
- Multi branch selection:
|
|
- Multi branch selection:
|
|
|
-
|
|
|
|
|
- checkbox grid optimized for large branch counts
|
|
- checkbox grid optimized for large branch counts
|
|
|
- “selectable card” label wrapper highlights checked items (border + background)
|
|
- “selectable card” label wrapper highlights checked items (border + background)
|
|
|
- pointer cursor + hover affordances improve discoverability
|
|
- pointer cursor + hover affordances improve discoverability
|
|
|
- - optional “Alle abwählen” button to reset selection
|
|
|
|
|
|
|
+ - “Alle abwählen” button to reset selection
|
|
|
|
|
|
|
|
- Branch list:
|
|
- Branch list:
|
|
|
-
|
|
|
|
|
- loaded via `GET /api/branches`
|
|
- loaded via `GET /api/branches`
|
|
|
- fail-open UI behavior:
|
|
- fail-open UI behavior:
|
|
|
-
|
|
|
|
|
- if branch list fails, Search UI remains usable
|
|
- 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:
|
|
Search results show at least:
|
|
|
|
|
|
|
@@ -629,18 +599,16 @@ Search results show at least:
|
|
|
Actions:
|
|
Actions:
|
|
|
|
|
|
|
|
- “Öffnen”
|
|
- “Öffnen”
|
|
|
-
|
|
|
|
|
- uses `buildPdfUrl(...)`
|
|
- uses `buildPdfUrl(...)`
|
|
|
- opens the binary endpoint in a new tab (`<a target="_blank" rel="noopener noreferrer">`)
|
|
- opens the binary endpoint in a new tab (`<a target="_blank" rel="noopener noreferrer">`)
|
|
|
|
|
|
|
|
- “Zum Ordner”
|
|
- “Zum Ordner”
|
|
|
-
|
|
|
|
|
- navigates to `/:branch/:year/:month/:day` using `dayPath(...)`
|
|
- navigates to `/:branch/:year/:month/:day` using `dayPath(...)`
|
|
|
|
|
|
|
|
Implementation note:
|
|
Implementation note:
|
|
|
|
|
|
|
|
- In the Search results table, the actions are intentionally kept in a fixed-width column.
|
|
- 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:
|
|
Sorting:
|
|
|
|
|
|
|
@@ -653,28 +621,73 @@ Totals:
|
|
|
- When the backend returns `total`, the UI shows “x von y Treffern geladen”.
|
|
- 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.
|
|
- 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`
|
|
- `popover`
|
|
|
- `command`
|
|
- `command`
|
|
|
- `badge`
|
|
- `badge`
|
|
|
|
|
+- `calendar` (react-day-picker wrapper)
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
@@ -707,12 +721,10 @@ Additional primitives used for Search scope UX:
|
|
|
To keep the project consistent:
|
|
To keep the project consistent:
|
|
|
|
|
|
|
|
- Use **`.jsx`** for files that contain JSX:
|
|
- Use **`.jsx`** for files that contain JSX:
|
|
|
-
|
|
|
|
|
- `app/**/page.jsx`, `app/**/layout.jsx`
|
|
- `app/**/page.jsx`, `app/**/layout.jsx`
|
|
|
- React components in `components/**`
|
|
- React components in `components/**`
|
|
|
|
|
|
|
|
- Use **`.js`** for non-JSX files:
|
|
- Use **`.js`** for non-JSX files:
|
|
|
-
|
|
|
|
|
- `lib/**` utilities and helpers
|
|
- `lib/**` utilities and helpers
|
|
|
- `app/api/**/route.js`
|
|
- `app/api/**/route.js`
|
|
|
- `models/**`
|
|
- `models/**`
|
|
@@ -751,6 +763,11 @@ Search helper tests:
|
|
|
- `lib/frontend/search/normalizeState.test.js`
|
|
- `lib/frontend/search/normalizeState.test.js`
|
|
|
- `lib/frontend/search/searchApiInput.test.js`
|
|
- `lib/frontend/search/searchApiInput.test.js`
|
|
|
- `lib/frontend/search/resultsSorting.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:
|
|
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:
|
|
Verify flows in the browser:
|
|
|
|
|
|
|
|
- Open a protected route while logged out (e.g. `/NL01/2025/12`)
|
|
- Open a protected route while logged out (e.g. `/NL01/2025/12`)
|
|
|
-
|
|
|
|
|
- Expect redirect to `/login?reason=expired&next=/NL01/2025/12`
|
|
- Expect redirect to `/login?reason=expired&next=/NL01/2025/12`
|
|
|
|
|
|
|
|
- Invalid login
|
|
- Invalid login
|
|
|
-
|
|
|
|
|
- Expect a German error message (e.g. “Benutzername oder Passwort ist falsch.”)
|
|
- Expect a German error message (e.g. “Benutzername oder Passwort ist falsch.”)
|
|
|
|
|
|
|
|
- Valid login
|
|
- Valid login
|
|
|
-
|
|
|
|
|
- Expect redirect into the protected route
|
|
- Expect redirect into the protected route
|
|
|
|
|
|
|
|
- Logout
|
|
- Logout
|
|
|
-
|
|
|
|
|
- Expect redirect to `/login?reason=logged-out`
|
|
- Expect redirect to `/login?reason=logged-out`
|
|
|
|
|
|
|
|
Explorer checks:
|
|
Explorer checks:
|
|
@@ -814,32 +827,36 @@ Explorer checks:
|
|
|
PDF open:
|
|
PDF open:
|
|
|
|
|
|
|
|
- On `/:branch/:year/:month/:day`, click **“Öffnen”**
|
|
- On `/:branch/:year/:month/:day`, click **“Öffnen”**
|
|
|
-
|
|
|
|
|
- Expected: opens the PDF in a new tab
|
|
- Expected: opens the PDF in a new tab
|
|
|
- Expected: works for filenames with spaces and special characters (URL-encoding)
|
|
- Expected: works for filenames with spaces and special characters (URL-encoding)
|
|
|
|
|
|
|
|
Search checks:
|
|
Search checks:
|
|
|
|
|
|
|
|
- `/NL01/search`
|
|
- `/NL01/search`
|
|
|
-
|
|
|
|
|
- empty state
|
|
- empty state
|
|
|
- submit triggers URL update and first-page fetch
|
|
- submit triggers URL update and first-page fetch
|
|
|
|
|
|
|
|
- admin/dev:
|
|
- admin/dev:
|
|
|
-
|
|
|
|
|
- scope switching (single/multi/all)
|
|
- scope switching (single/multi/all)
|
|
|
- single branch selection via combobox
|
|
- single branch selection via combobox
|
|
|
- multi selection via checkbox grid + “Alle abwählen”
|
|
- multi selection via checkbox grid + “Alle abwählen”
|
|
|
- limit switching (50/100/200)
|
|
- 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
|
|
- “Mehr laden” appends results when `nextCursor` exists
|
|
|
|
|
|
|
|
- TopNav QuickNav:
|
|
- TopNav QuickNav:
|
|
|
-
|
|
|
|
|
- switching branch updates the URL and keeps the current deep path
|
|
- 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
|
|
### 12.2 Server
|
|
|
|
|
|
|
@@ -849,9 +866,9 @@ Verify:
|
|
|
|
|
|
|
|
- Explorer navigation and PDF open
|
|
- Explorer navigation and PDF open
|
|
|
- Search UI:
|
|
- Search UI:
|
|
|
-
|
|
|
|
|
- scopes
|
|
- scopes
|
|
|
- limit selection
|
|
- limit selection
|
|
|
|
|
+ - date range filter + URL sync
|
|
|
- total count (“x von y geladen”)
|
|
- total count (“x von y geladen”)
|
|
|
- open PDF / jump to day
|
|
- open PDF / jump to day
|
|
|
- TopNav branch switching keeps deep links
|
|
- TopNav branch switching keeps deep links
|
|
@@ -860,14 +877,11 @@ Verify:
|
|
|
|
|
|
|
|
## 13. Planned follow-ups
|
|
## 13. Planned follow-ups
|
|
|
|
|
|
|
|
-- Search date range UI (`from` / `to`) with shareable URL sync.
|
|
|
|
|
-
|
|
|
|
|
- Optional Search UX improvements:
|
|
- Optional Search UX improvements:
|
|
|
-
|
|
|
|
|
- grouping results by date / branch
|
|
- grouping results by date / branch
|
|
|
- - query presets
|
|
|
|
|
|
|
+ - query presets (beyond the date presets)
|
|
|
|
|
+ - optional date-only search mode (if desired)
|
|
|
|
|
|
|
|
- Optional Explorer improvements:
|
|
- Optional Explorer improvements:
|
|
|
-
|
|
|
|
|
- add “Herunterladen” action
|
|
- add “Herunterladen” action
|
|
|
- optional in-app PDF viewer
|
|
- optional in-app PDF viewer
|