|
@@ -1,16 +1,16 @@
|
|
|
<!-- --------------------------------------------------------------------------- -->
|
|
<!-- --------------------------------------------------------------------------- -->
|
|
|
|
|
|
|
|
-<!-- Folder: Docs -->
|
|
|
|
|
|
|
+<!-- Ordner: docs -->
|
|
|
|
|
|
|
|
-<!-- File: frontend-ui.md -->
|
|
|
|
|
|
|
+<!-- Datei: frontend-ui.md -->
|
|
|
|
|
|
|
|
-<!-- Relative Path: Docs/frontend-ui.md -->
|
|
|
|
|
|
|
+<!-- Relativer Pfad: docs/frontend-ui.md -->
|
|
|
|
|
|
|
|
<!-- --------------------------------------------------------------------------- -->
|
|
<!-- --------------------------------------------------------------------------- -->
|
|
|
|
|
|
|
|
-# Frontend UI: App Shell, Routing, Auth/RBAC, and Explorer (RHL-019 / RHL-020 / RHL-021 / RHL-022 / RHL-023)
|
|
|
|
|
|
|
+# Frontend UI: App Shell, Routing, Auth/RBAC, Explorer, and Search (RHL-019 / RHL-020 / RHL-021 / RHL-022 / RHL-023 / RHL-024)
|
|
|
|
|
|
|
|
-This document describes the **frontend routing scaffold**, the **application shell layout**, and the **core navigation UI (Explorer)** 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.
|
|
|
|
|
|
|
|
Timeline:
|
|
Timeline:
|
|
|
|
|
|
|
@@ -19,6 +19,7 @@ Timeline:
|
|
|
- **RHL-021**: UI-side RBAC guard + consistent Forbidden / NotFound UX.
|
|
- **RHL-021**: UI-side RBAC guard + consistent Forbidden / NotFound UX.
|
|
|
- **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).
|
|
|
|
|
|
|
|
> **Language policy**
|
|
> **Language policy**
|
|
|
>
|
|
>
|
|
@@ -30,7 +31,7 @@ Timeline:
|
|
|
|
|
|
|
|
## 1. Scope
|
|
## 1. Scope
|
|
|
|
|
|
|
|
-### 1.1 Implemented (as of RHL-023)
|
|
|
|
|
|
|
+### 1.1 Implemented (as of RHL-024)
|
|
|
|
|
|
|
|
- **Public** `/login` route with a functional login form (shadcn/ui primitives).
|
|
- **Public** `/login` route with a functional login form (shadcn/ui primitives).
|
|
|
|
|
|
|
@@ -63,6 +64,7 @@ Timeline:
|
|
|
|
|
|
|
|
- **Route param validation** (syntactic):
|
|
- **Route param validation** (syntactic):
|
|
|
|
|
|
|
|
|
|
+ - `branch`: `NL` + digits (syntactic validity; existence is validated by BranchGuard for admin/dev)
|
|
|
- `year`: `YYYY`
|
|
- `year`: `YYYY`
|
|
|
- `month`: `01–12`
|
|
- `month`: `01–12`
|
|
|
- `day`: `01–31`
|
|
- `day`: `01–31`
|
|
@@ -85,27 +87,52 @@ Timeline:
|
|
|
- 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`.
|
|
|
|
|
+
|
|
|
|
|
+- **Search UI v1 (RHL-024)**
|
|
|
|
|
+
|
|
|
|
|
+ - Route: `/:branch/search` (protected).
|
|
|
|
|
+
|
|
|
|
|
+ - URL-driven state for shareability:
|
|
|
|
|
+
|
|
|
|
|
+ - `q` (search query)
|
|
|
|
|
+
|
|
|
|
|
+ - scope params for admin/dev:
|
|
|
|
|
+
|
|
|
|
|
+ - single: `branch=NLxx`
|
|
|
|
|
+ - multi: `scope=multi&branches=NL06,NL20`
|
|
|
|
|
+ - all: `scope=all`
|
|
|
|
|
+
|
|
|
|
|
+ - `limit` (optional): `50 | 100 | 200` (default 100; only included in URL when non-default)
|
|
|
|
|
|
|
|
- - URL construction is centralized in a pure helper:
|
|
|
|
|
|
|
+ - Cursor-based pagination (`nextCursor`) is **not stored in the URL**.
|
|
|
|
|
|
|
|
- - `lib/frontend/explorer/pdfUrl.js` (`buildPdfUrl`, optional `buildPdfDownloadUrl`)
|
|
|
|
|
|
|
+ - Results support:
|
|
|
|
|
|
|
|
- - The PDF endpoint is **binary** (`application/pdf`) and is opened via navigation (`<a target="_blank">`).
|
|
|
|
|
|
|
+ - Open PDF in a new tab (“Öffnen”)
|
|
|
|
|
+ - Navigate to the Explorer day route (“Zum Tag”)
|
|
|
|
|
|
|
|
- - The frontend does **not** use `apiClient.apiFetch()` for PDF opening (JSON-centric).
|
|
|
|
|
|
|
+ - Search results UI includes:
|
|
|
|
|
+
|
|
|
|
|
+ - Empty/Loading/Error states
|
|
|
|
|
+ - Sorting dropdown (Relevanz / Datum / Dateiname)
|
|
|
|
|
+ - “Mehr laden” button (cursor pagination)
|
|
|
|
|
+ - “x von y” progress indicator when `total` is available
|
|
|
|
|
|
|
|
### 1.2 Still out of scope / planned
|
|
### 1.2 Still out of scope / planned
|
|
|
|
|
|
|
|
-- Search UI (route exists as placeholder: `/:branch/search`).
|
|
|
|
|
|
|
+- Date range UI for Search (`from` / `to`) and URL sync (planned follow-up after Search UI v1).
|
|
|
|
|
+
|
|
|
|
|
+- Optional Search UX improvements:
|
|
|
|
|
|
|
|
-- Admin/dev branch selector in the sidebar.
|
|
|
|
|
|
|
+ - grouping results by date and/or branch
|
|
|
|
|
+ - debounced “typeahead” search (current v1 is explicit submit)
|
|
|
|
|
|
|
|
- Optional Explorer improvements:
|
|
- Optional Explorer improvements:
|
|
|
|
|
|
|
@@ -148,7 +175,7 @@ The app uses Next.js App Router **Route Groups** to separate public and protecte
|
|
|
| `/:branch/:year` | Explorer: months | Example: `/NL01/2025` |
|
|
| `/:branch/:year` | Explorer: months | Example: `/NL01/2025` |
|
|
|
| `/:branch/:year/:month` | Explorer: days | Example: `/NL01/2025/12` |
|
|
| `/:branch/:year/:month` | Explorer: days | Example: `/NL01/2025/12` |
|
|
|
| `/:branch/:year/:month/:day` | Explorer: files | Example: `/NL01/2025/12/31` |
|
|
| `/:branch/:year/:month/:day` | Explorer: files | Example: `/NL01/2025/12/31` |
|
|
|
-| `/:branch/search` | Search placeholder | Explicit segment so `search` is not interpreted as `:year` |
|
|
|
|
|
|
|
+| `/:branch/search` | Search UI (v1) | Explicit segment so `search` is not interpreted as `:year` |
|
|
|
| `/forbidden` | Forbidden page wrapper | Optional wrapper; Forbidden is typically rendered inline |
|
|
| `/forbidden` | Forbidden page wrapper | Optional wrapper; Forbidden is typically rendered inline |
|
|
|
|
|
|
|
|
Important:
|
|
Important:
|
|
@@ -197,9 +224,48 @@ UX rationale:
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 4. Authentication UX (RHL-020)
|
|
|
|
|
|
|
+## 4. App Shell & Top Navigation
|
|
|
|
|
+
|
|
|
|
|
+### 4.1 AppShell framing
|
|
|
|
|
+
|
|
|
|
|
+File: `components/app-shell/AppShell.jsx`
|
|
|
|
|
+
|
|
|
|
|
+AppShell is the stable frame for all protected pages:
|
|
|
|
|
+
|
|
|
|
|
+- TopNav is always visible.
|
|
|
|
|
+- The sidebar is currently a placeholder (reserved for future navigation/filter UI).
|
|
|
|
|
+- Route pages render inside the main content area.
|
|
|
|
|
+
|
|
|
|
|
+### 4.2 Quick navigation (quality-of-life)
|
|
|
|
|
|
|
|
-### 4.1 Session check for protected routes
|
|
|
|
|
|
|
+File: `components/app-shell/QuickNav.jsx`
|
|
|
|
|
+
|
|
|
|
|
+Purpose:
|
|
|
|
|
+
|
|
|
|
|
+- Reduce manual URL typing during manual testing and daily usage.
|
|
|
|
|
+- Provide direct links to:
|
|
|
|
|
+
|
|
|
|
|
+ - Explorer (`/:branch`)
|
|
|
|
|
+ - Search (`/:branch/search`)
|
|
|
|
|
+
|
|
|
|
|
+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.
|
|
|
|
|
+
|
|
|
|
|
+Responsive behavior:
|
|
|
|
|
+
|
|
|
|
|
+- QuickNav is hidden on small screens by default (`md` and up only) to keep the header compact.
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 5. Authentication UX (RHL-020)
|
|
|
|
|
+
|
|
|
|
|
+### 5.1 Session check for protected routes
|
|
|
|
|
|
|
|
File: `components/auth/AuthProvider.jsx`
|
|
File: `components/auth/AuthProvider.jsx`
|
|
|
|
|
|
|
@@ -221,7 +287,7 @@ The `next` parameter:
|
|
|
- includes the original `pathname` and query string
|
|
- includes the original `pathname` and query string
|
|
|
- is sanitized to avoid open redirects (only internal paths are allowed)
|
|
- is sanitized to avoid open redirects (only internal paths are allowed)
|
|
|
|
|
|
|
|
-### 4.2 AuthGate (in-shell gating)
|
|
|
|
|
|
|
+### 5.2 AuthGate (in-shell gating)
|
|
|
|
|
|
|
|
File: `components/auth/AuthGate.jsx`
|
|
File: `components/auth/AuthGate.jsx`
|
|
|
|
|
|
|
@@ -231,7 +297,7 @@ Behavior:
|
|
|
- On auth errors: show an in-shell error card + retry.
|
|
- On auth errors: show an in-shell error card + retry.
|
|
|
- On unauthenticated: show an in-shell “redirecting” message while redirect happens.
|
|
- On unauthenticated: show an in-shell “redirecting” message while redirect happens.
|
|
|
|
|
|
|
|
-### 4.3 Login page (reason / next)
|
|
|
|
|
|
|
+### 5.3 Login page (reason / next)
|
|
|
|
|
|
|
|
Files:
|
|
Files:
|
|
|
|
|
|
|
@@ -266,7 +332,7 @@ Username policy:
|
|
|
- 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
|
|
|
|
|
|
|
|
-### 4.4 Logout
|
|
|
|
|
|
|
+### 5.4 Logout
|
|
|
|
|
|
|
|
File: `components/auth/LogoutButton.jsx`
|
|
File: `components/auth/LogoutButton.jsx`
|
|
|
|
|
|
|
@@ -275,27 +341,11 @@ Flow:
|
|
|
1. Calls `apiClient.logout()`.
|
|
1. Calls `apiClient.logout()`.
|
|
|
2. Redirects to `/login?reason=logged-out`.
|
|
2. Redirects to `/login?reason=logged-out`.
|
|
|
|
|
|
|
|
-### 4.5 User status
|
|
|
|
|
-
|
|
|
|
|
-Files:
|
|
|
|
|
-
|
|
|
|
|
-- `components/auth/authContext.jsx`
|
|
|
|
|
-- `components/app-shell/UserStatus.jsx`
|
|
|
|
|
-
|
|
|
|
|
-Behavior:
|
|
|
|
|
-
|
|
|
|
|
-- AuthProvider provides a minimal auth context (`status`, `user`, `error`).
|
|
|
|
|
-- `UserStatus` renders a short indicator in the TopNav:
|
|
|
|
|
-
|
|
|
|
|
- - loading → `Lädt…`
|
|
|
|
|
- - authenticated → role + optional branchId
|
|
|
|
|
- - unauthenticated/error → fallback text
|
|
|
|
|
-
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 5. UI RBAC, Forbidden, and NotFound (RHL-021)
|
|
|
|
|
|
|
+## 6. UI RBAC, Forbidden, and NotFound (RHL-021)
|
|
|
|
|
|
|
|
-### 5.1 Goals
|
|
|
|
|
|
|
+### 6.1 Goals
|
|
|
|
|
|
|
|
RHL-021 adds a friendly UI layer on top of backend RBAC:
|
|
RHL-021 adds a friendly UI layer on top of backend RBAC:
|
|
|
|
|
|
|
@@ -308,7 +358,7 @@ Backend RBAC remains the source of truth. UI RBAC exists to:
|
|
|
- prevent “obviously forbidden” navigation in the frontend
|
|
- prevent “obviously forbidden” navigation in the frontend
|
|
|
- provide clear and consistent UX for end users
|
|
- provide clear and consistent UX for end users
|
|
|
|
|
|
|
|
-### 5.2 BranchGuard (UI-side RBAC)
|
|
|
|
|
|
|
+### 6.2 BranchGuard (UI-side RBAC)
|
|
|
|
|
|
|
|
Files:
|
|
Files:
|
|
|
|
|
|
|
@@ -334,7 +384,7 @@ Admin/dev branch existence validation:
|
|
|
- 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.
|
|
|
|
|
|
|
|
-### 5.3 Param validation (year/month/day)
|
|
|
|
|
|
|
+### 6.3 Param validation (year/month/day)
|
|
|
|
|
|
|
|
Files:
|
|
Files:
|
|
|
|
|
|
|
@@ -347,40 +397,17 @@ Layout enforcement (server-side `notFound()`):
|
|
|
- `app/(protected)/[branch]/[year]/[month]/layout.jsx`
|
|
- `app/(protected)/[branch]/[year]/[month]/layout.jsx`
|
|
|
- `app/(protected)/[branch]/[year]/[month]/[day]/layout.jsx`
|
|
- `app/(protected)/[branch]/[year]/[month]/[day]/layout.jsx`
|
|
|
|
|
|
|
|
-### 5.4 Forbidden UX
|
|
|
|
|
-
|
|
|
|
|
-Files:
|
|
|
|
|
-
|
|
|
|
|
-- `components/system/ForbiddenView.jsx`
|
|
|
|
|
-- Optional wrapper route: `app/(protected)/forbidden/page.jsx`
|
|
|
|
|
-
|
|
|
|
|
-Where Forbidden is shown:
|
|
|
|
|
-
|
|
|
|
|
-- BranchGuard renders ForbiddenView inline for branch mismatch.
|
|
|
|
|
-
|
|
|
|
|
-### 5.5 NotFound UX
|
|
|
|
|
-
|
|
|
|
|
-Files:
|
|
|
|
|
-
|
|
|
|
|
-- `components/system/NotFoundView.jsx`
|
|
|
|
|
-- Protected not-found entry: `app/(protected)/not-found.jsx`
|
|
|
|
|
-
|
|
|
|
|
-Where NotFound is shown:
|
|
|
|
|
-
|
|
|
|
|
-- invalid params in layouts via `notFound()`
|
|
|
|
|
-- admin/dev: non-existing branches via BranchGuard existence validation
|
|
|
|
|
-
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 6. Explorer v2 (RHL-022) + PDF Open (RHL-023)
|
|
|
|
|
|
|
+## 7. Explorer v2 (RHL-022) + PDF Open (RHL-023)
|
|
|
|
|
|
|
|
-### 6.1 UI goal
|
|
|
|
|
|
|
+### 7.1 UI goal
|
|
|
|
|
|
|
|
Provide a simple “file explorer” drill-down:
|
|
Provide a simple “file explorer” drill-down:
|
|
|
|
|
|
|
|
- Year → Month → Day → Files
|
|
- Year → Month → Day → Files
|
|
|
|
|
|
|
|
-### 6.2 Explorer pages
|
|
|
|
|
|
|
+### 7.2 Explorer pages
|
|
|
|
|
|
|
|
Routes and components:
|
|
Routes and components:
|
|
|
|
|
|
|
@@ -389,7 +416,7 @@ Routes and components:
|
|
|
- `/:branch/:year/:month` → `components/explorer/levels/DaysExplorer.jsx`
|
|
- `/:branch/:year/:month` → `components/explorer/levels/DaysExplorer.jsx`
|
|
|
- `/:branch/:year/:month/:day` → `components/explorer/levels/FilesExplorer.jsx`
|
|
- `/:branch/:year/:month/:day` → `components/explorer/levels/FilesExplorer.jsx`
|
|
|
|
|
|
|
|
-### 6.3 Data fetching strategy
|
|
|
|
|
|
|
+### 7.3 Data fetching strategy
|
|
|
|
|
|
|
|
- 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`.
|
|
@@ -397,13 +424,7 @@ Routes and components:
|
|
|
|
|
|
|
|
- `lib/frontend/hooks/useExplorerQuery.js`
|
|
- `lib/frontend/hooks/useExplorerQuery.js`
|
|
|
|
|
|
|
|
-Design:
|
|
|
|
|
-
|
|
|
|
|
-- predictable states: `loading | success | error`
|
|
|
|
|
-- retry mechanism exposed to the UI
|
|
|
|
|
-- no routing side effects inside the hook (routing remains in the page components)
|
|
|
|
|
-
|
|
|
|
|
-### 6.4 Breadcrumbs (with dropdowns)
|
|
|
|
|
|
|
+### 7.4 Breadcrumbs (with dropdowns)
|
|
|
|
|
|
|
|
Files:
|
|
Files:
|
|
|
|
|
|
|
@@ -418,37 +439,7 @@ Files:
|
|
|
- `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`
|
|
|
|
|
|
|
|
-Rules:
|
|
|
|
|
-
|
|
|
|
|
-- Breadcrumb shows the current path: branch → year → month → day.
|
|
|
|
|
-- Dropdowns appear only when options are available:
|
|
|
|
|
-
|
|
|
|
|
- - years dropdown on month/day/files levels
|
|
|
|
|
- - months dropdown on day/files levels
|
|
|
|
|
- - days dropdown on files level
|
|
|
|
|
-
|
|
|
|
|
-Fail-open behavior:
|
|
|
|
|
-
|
|
|
|
|
-- If dropdown option queries fail, the breadcrumb still renders the current path.
|
|
|
|
|
-
|
|
|
|
|
-### 6.5 Loading / empty / error states
|
|
|
|
|
-
|
|
|
|
|
-Shared Explorer UI building blocks:
|
|
|
|
|
-
|
|
|
|
|
-- `components/explorer/ExplorerPageShell.jsx`
|
|
|
|
|
-- `components/explorer/ExplorerSectionCard.jsx`
|
|
|
|
|
-- `components/explorer/states/*`
|
|
|
|
|
-
|
|
|
|
|
-Error mapping:
|
|
|
|
|
-
|
|
|
|
|
-- `lib/frontend/explorer/errorMapping.js` maps API client errors to UX outcomes:
|
|
|
|
|
-
|
|
|
|
|
- - `AUTH_UNAUTHENTICATED` → redirect to login (expired)
|
|
|
|
|
- - `AUTH_FORBIDDEN_BRANCH` → ForbiddenView
|
|
|
|
|
- - `FS_NOT_FOUND` → ExplorerNotFound
|
|
|
|
|
- - other errors → ExplorerError + retry
|
|
|
|
|
-
|
|
|
|
|
-### 6.6 Files list (leaf route) and “Open PDF”
|
|
|
|
|
|
|
+### 7.5 Files list (leaf route) and “Open PDF”
|
|
|
|
|
|
|
|
Leaf route:
|
|
Leaf route:
|
|
|
|
|
|
|
@@ -476,17 +467,119 @@ Implementation notes:
|
|
|
|
|
|
|
|
- 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`.
|
|
|
|
|
|
|
|
-- The filename segment must be URL-encoded (handled by `buildPdfUrl(...)`).
|
|
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 8. Search UI v1 (RHL-024)
|
|
|
|
|
+
|
|
|
|
|
+### 8.1 Route and state model
|
|
|
|
|
+
|
|
|
|
|
+Route:
|
|
|
|
|
+
|
|
|
|
|
+- `/:branch/search`
|
|
|
|
|
+
|
|
|
|
|
+State policy:
|
|
|
|
|
+
|
|
|
|
|
+- Search state is **URL-driven** to support shareable links.
|
|
|
|
|
+- Cursor-based pagination state is **not shareable** and is kept in client state.
|
|
|
|
|
+
|
|
|
|
|
+Shareable params (first page identity):
|
|
|
|
|
+
|
|
|
|
|
+- `q` (string)
|
|
|
|
|
+
|
|
|
|
|
+- Scope params:
|
|
|
|
|
+
|
|
|
|
|
+ - single branch: `branch=NLxx`
|
|
|
|
|
+ - multi: `scope=multi&branches=NL06,NL20`
|
|
|
|
|
+ - all: `scope=all`
|
|
|
|
|
+
|
|
|
|
|
+- `limit`:
|
|
|
|
|
+
|
|
|
|
|
+ - allowed values: `50 | 100 | 200`
|
|
|
|
|
+ - default: `100`
|
|
|
|
|
+ - only written to URL when non-default to keep URLs readable
|
|
|
|
|
+
|
|
|
|
|
+Pagination:
|
|
|
|
|
+
|
|
|
|
|
+- `nextCursor` is kept in client state.
|
|
|
|
|
+- “Mehr laden” triggers a request with `cursor=nextCursor` and appends results.
|
|
|
|
|
+
|
|
|
|
|
+### 8.2 Roles and scope UI
|
|
|
|
|
+
|
|
|
|
|
+Branch users:
|
|
|
|
|
+
|
|
|
|
|
+- No scope selector.
|
|
|
|
|
+- The UI always searches within the current route branch.
|
|
|
|
|
+
|
|
|
|
|
+Admin/dev users:
|
|
|
|
|
+
|
|
|
|
|
+- Scope selector:
|
|
|
|
|
+
|
|
|
|
|
+ - “Diese Niederlassung” (single branch route context)
|
|
|
|
|
+ - “Mehrere Niederlassungen” (multi)
|
|
|
|
|
+ - “Alle Niederlassungen” (all)
|
|
|
|
|
+
|
|
|
|
|
+- Multi branch selection:
|
|
|
|
|
+
|
|
|
|
|
+ - simple checkbox list
|
|
|
|
|
+ - branch list is fetched from `GET /api/branches`
|
|
|
|
|
+ - fail-open behavior: if branch list cannot be loaded, the rest of the Search UI remains usable
|
|
|
|
|
+
|
|
|
|
|
+### 8.3 Result list and actions
|
|
|
|
|
+
|
|
|
|
|
+Search results show at least:
|
|
|
|
|
|
|
|
-Accessibility:
|
|
|
|
|
|
|
+- Branch (for multi/all)
|
|
|
|
|
+- Date (German formatted)
|
|
|
|
|
+- Filename
|
|
|
|
|
+- Relative path
|
|
|
|
|
+- Optional snippet (if returned by the provider)
|
|
|
|
|
|
|
|
-- The “Öffnen” action uses an `aria-label` like `PDF öffnen: <filename>`.
|
|
|
|
|
|
|
+Actions:
|
|
|
|
|
+
|
|
|
|
|
+- “Öffnen”
|
|
|
|
|
+
|
|
|
|
|
+ - uses `buildPdfUrl(...)`
|
|
|
|
|
+ - opens the binary endpoint in a new tab (`<a target="_blank" rel="noopener noreferrer">`)
|
|
|
|
|
+
|
|
|
|
|
+- “Zum Tag”
|
|
|
|
|
+
|
|
|
|
|
+ - navigates to `/:branch/:year/:month/:day` using `dayPath(...)`
|
|
|
|
|
+
|
|
|
|
|
+Sorting:
|
|
|
|
|
+
|
|
|
|
|
+- “Relevanz” (backend order)
|
|
|
|
|
+- “Datum (neueste zuerst)”
|
|
|
|
|
+- “Dateiname (A–Z)”
|
|
|
|
|
+
|
|
|
|
|
+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)
|
|
|
|
|
+
|
|
|
|
|
+Search uses a dedicated error mapping helper:
|
|
|
|
|
+
|
|
|
|
|
+- `lib/frontend/search/errorMapping.js`
|
|
|
|
|
+
|
|
|
|
|
+Mapping principles:
|
|
|
|
|
+
|
|
|
|
|
+- Use stable error codes for UX decisions.
|
|
|
|
|
+- Do not leak raw backend messages to users.
|
|
|
|
|
+- Keep user-facing messages German.
|
|
|
|
|
+
|
|
|
|
|
+Key outcomes:
|
|
|
|
|
+
|
|
|
|
|
+- `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
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 7. UI primitives (shadcn/ui)
|
|
|
|
|
|
|
+## 9. UI primitives (shadcn/ui)
|
|
|
|
|
|
|
|
-The Explorer + auth UI uses shadcn/ui primitives from `components/ui/*`.
|
|
|
|
|
|
|
+The Explorer + auth + search UI uses shadcn/ui primitives from `components/ui/*`.
|
|
|
|
|
|
|
|
Required components for the current scope:
|
|
Required components for the current scope:
|
|
|
|
|
|
|
@@ -502,7 +595,7 @@ Required components for the current scope:
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 8. File Naming Convention (.js vs .jsx)
|
|
|
|
|
|
|
+## 10. File Naming Convention (.js vs .jsx)
|
|
|
|
|
|
|
|
To keep the project consistent:
|
|
To keep the project consistent:
|
|
|
|
|
|
|
@@ -520,9 +613,9 @@ To keep the project consistent:
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 9. Tests
|
|
|
|
|
|
|
+## 11. Tests
|
|
|
|
|
|
|
|
-### 9.1 Unit tests
|
|
|
|
|
|
|
+### 11.1 Unit tests
|
|
|
|
|
|
|
|
Core tests:
|
|
Core tests:
|
|
|
|
|
|
|
@@ -544,11 +637,19 @@ Explorer helper tests:
|
|
|
- `lib/frontend/explorer/sorters.test.js`
|
|
- `lib/frontend/explorer/sorters.test.js`
|
|
|
- `lib/frontend/explorer/pdfUrl.test.js` (RHL-023)
|
|
- `lib/frontend/explorer/pdfUrl.test.js` (RHL-023)
|
|
|
|
|
|
|
|
|
|
+Search helper tests:
|
|
|
|
|
+
|
|
|
|
|
+- `lib/frontend/search/urlState.test.js`
|
|
|
|
|
+- `lib/frontend/search/errorMapping.test.js`
|
|
|
|
|
+- `lib/frontend/search/normalizeState.test.js`
|
|
|
|
|
+- `lib/frontend/search/searchApiInput.test.js`
|
|
|
|
|
+- `lib/frontend/search/resultsSorting.test.js`
|
|
|
|
|
+
|
|
|
Component SSR smoke test:
|
|
Component SSR smoke test:
|
|
|
|
|
|
|
|
- `components/app-shell/AppShell.test.js`
|
|
- `components/app-shell/AppShell.test.js`
|
|
|
|
|
|
|
|
-### 9.2 Running tests
|
|
|
|
|
|
|
+### 11.2 Running tests
|
|
|
|
|
|
|
|
From the repo root:
|
|
From the repo root:
|
|
|
|
|
|
|
@@ -564,9 +665,9 @@ npm run build
|
|
|
|
|
|
|
|
---
|
|
---
|
|
|
|
|
|
|
|
-## 10. Manual verification checklist
|
|
|
|
|
|
|
+## 12. Manual verification checklist
|
|
|
|
|
|
|
|
-### 10.1 Local (Docker)
|
|
|
|
|
|
|
+### 12.1 Local (Docker)
|
|
|
|
|
|
|
|
Start:
|
|
Start:
|
|
|
|
|
|
|
@@ -592,14 +693,6 @@ Verify flows in the browser:
|
|
|
|
|
|
|
|
- Expect redirect to `/login?reason=logged-out`
|
|
- Expect redirect to `/login?reason=logged-out`
|
|
|
|
|
|
|
|
-RBAC checks:
|
|
|
|
|
-
|
|
|
|
|
-- Branch user:
|
|
|
|
|
-
|
|
|
|
|
- - `/NL01/...` works (own branch)
|
|
|
|
|
- - `/NL02/...` shows Forbidden
|
|
|
|
|
- - invalid params (e.g. `/NL01/abcd`, `/NL01/2024/99/01`) show NotFound
|
|
|
|
|
-
|
|
|
|
|
Explorer checks:
|
|
Explorer checks:
|
|
|
|
|
|
|
|
- `/:branch` shows years
|
|
- `/:branch` shows years
|
|
@@ -607,46 +700,56 @@ Explorer checks:
|
|
|
- `/:branch/:year/:month` shows days
|
|
- `/:branch/:year/:month` shows days
|
|
|
- `/:branch/:year/:month/:day` shows files
|
|
- `/:branch/:year/:month/:day` shows files
|
|
|
|
|
|
|
|
-PDF open (RHL-023):
|
|
|
|
|
|
|
+PDF open:
|
|
|
|
|
|
|
|
-- On `/:branch/:year/:month/:day`, click **“Öffnen”** on multiple files
|
|
|
|
|
|
|
+- 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)
|
|
|
|
|
|
|
|
-### 10.2 Server
|
|
|
|
|
|
|
+Search checks:
|
|
|
|
|
|
|
|
-Deploy and verify on the server URL.
|
|
|
|
|
|
|
+- `/NL01/search`
|
|
|
|
|
|
|
|
-Verify flows:
|
|
|
|
|
|
|
+ - empty state
|
|
|
|
|
+ - submit triggers URL update and first-page fetch
|
|
|
|
|
|
|
|
-- Unauthenticated redirect + `next`
|
|
|
|
|
-- Valid login sets cookie and redirects back to `next`
|
|
|
|
|
-- Logout clears session and shows `reason=logged-out`
|
|
|
|
|
|
|
+- admin/dev:
|
|
|
|
|
|
|
|
-Admin/dev checks:
|
|
|
|
|
|
|
+ - scope switching (single/multi/all)
|
|
|
|
|
+ - multi selection (checkboxes)
|
|
|
|
|
+ - limit switching (50/100/200)
|
|
|
|
|
|
|
|
-- existing branches render
|
|
|
|
|
-- non-existing branch (e.g. `/NL9999`) shows NotFound (existence validation)
|
|
|
|
|
|
|
+- pagination:
|
|
|
|
|
|
|
|
-PDF open (RHL-023):
|
|
|
|
|
|
|
+ - “Mehr laden” appends results when `nextCursor` exists
|
|
|
|
|
|
|
|
-- Repeat the local PDF open checks against real NAS data
|
|
|
|
|
|
|
+### 12.2 Server
|
|
|
|
|
|
|
|
----
|
|
|
|
|
|
|
+Deploy and verify on the server URL.
|
|
|
|
|
|
|
|
-## 11. Planned follow-ups
|
|
|
|
|
|
|
+Verify:
|
|
|
|
|
|
|
|
-- Search UI and filters (`/:branch/search`).
|
|
|
|
|
|
|
+- Explorer navigation and PDF open
|
|
|
|
|
+- Search UI:
|
|
|
|
|
|
|
|
-- Optional Explorer UI enhancements:
|
|
|
|
|
|
|
+ - scopes
|
|
|
|
|
+ - limit selection
|
|
|
|
|
+ - total count (“x von y geladen”)
|
|
|
|
|
+ - open PDF / jump to day
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
|
|
|
- - add a secondary action “Herunterladen” (download variant)
|
|
|
|
|
- - optional in-app PDF viewer experience (instead of a new tab)
|
|
|
|
|
|
|
+## 13. Planned follow-ups
|
|
|
|
|
|
|
|
-- Admin/dev branch selector in the sidebar.
|
|
|
|
|
|
|
+- Search date range UI (`from` / `to`) with shareable URL sync.
|
|
|
|
|
|
|
|
-- Smooth navigation / perceived performance improvements:
|
|
|
|
|
|
|
+- Optional Search UX improvements:
|
|
|
|
|
+
|
|
|
|
|
+ - grouping results by date / branch
|
|
|
|
|
+ - query presets
|
|
|
|
|
+
|
|
|
|
|
+- Optional Explorer improvements:
|
|
|
|
|
|
|
|
- - reduce skeleton/layout shift
|
|
|
|
|
- - client-side caching / prefetching for Explorer drill-down
|
|
|
|
|
|
|
+ - add “Herunterladen” action
|
|
|
|
|
+ - optional in-app PDF viewer
|