# 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. Timeline: - **RHL-019**: Public vs protected route scaffold + AppShell + placeholder pages. - **RHL-020**: Real login flow + session handling and redirects. - **RHL-021**: UI-side RBAC guard + consistent Forbidden / NotFound UX. - **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** > > - Conversation and developer coordination can be German. > - **Code, comments, tests, and documentation are English.** > - **All user-facing UI strings are German** (labels, button text, alerts, hints). --- ## 1. Scope ### 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=`. - **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` - `day`: `01–31` - 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` (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. - **Single scope branch selection** uses a shadcn combobox to switch the _route branch_. - **Multi scope branch selection** uses a checkbox grid (optimized for large branch counts) and provides an **“Alle abwählen”** action. - **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 - 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 --- ## 2. Route Groups & URL Structure The app uses Next.js App Router **Route Groups** to separate public and protected UI. ### 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) ### 2.2 URL map | URL | Purpose | Notes | | ---------------------------- | --------------------------- | ---------------------------------------------------------- | | `/login` | Login page | Supports `reason` and `next` query params | | `/` | Protected entry placeholder | Rendered only when authenticated | | `/:branch` | Explorer: years | Example: `/NL01` | | `/:branch/:year` | Explorer: months | Example: `/NL01/2025` | | `/:branch/:year/:month` | Explorer: days | Example: `/NL01/2025/12` | | `/:branch/:year/:month/:day` | Explorer: files | Example: `/NL01/2025/12/31` | | `/: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 | Important: - There is **no** standalone `/search` route. Visiting `/search` matches `/:branch` with `branch = "search"`. --- ## 3. Layouts ### 3.1 Root layout File: `app/layout.jsx` Responsibilities: - Global CSS imports (`app/globals.css`). - Theme provider setup (shadcn/ui + next-themes wrapper). - Base HTML/body structure. ### 3.2 Public layout File: `app/(public)/layout.jsx` Responsibilities: - Minimal centered layout for public routes. - Intended for `/login` (and potential future public routes). ### 3.3 Protected layout 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) UX rationale: - We keep the AppShell frame visible while auth/session checks run. - This avoids full-screen “blank spinners” on slow connections. --- ## 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.1.1 Width policy and sidebar docking (2xl+) On very wide screens the UI should remain readable. Current strategy (implemented in `AppShell.jsx` + `TopNav.jsx`): - 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**. Below `2xl`: - Use full width for usability. - Hide the sidebar placeholder to avoid shrinking the main content area. ### 4.2 Quick navigation (quality-of-life) 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. 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&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). Implementation note: - Helper logic for deep-path branch switching is centralized under `lib/frontend/quickNav/branchSwitch.js`. 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` 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=` The `next` parameter: - includes the original `pathname` and query string - is sanitized to avoid open redirects (only internal paths are allowed) ### 5.2 AuthGate (in-shell gating) File: `components/auth/AuthGate.jsx` Behavior: - While session is loading: show an in-shell loading card. - On auth errors: show an in-shell error card + retry. - On unauthenticated: show an in-shell “redirecting” message while redirect happens. ### 5.3 Login page (reason / next) Files: - `app/(public)/login/page.jsx` (Server Component) - `components/auth/LoginForm.jsx` (Client Component) 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 ### 5.4 Logout File: `components/auth/LogoutButton.jsx` Flow: 1. Calls `apiClient.logout()`. 2. Redirects to `/login?reason=logged-out`. --- ## 6. UI RBAC, Forbidden, and NotFound (RHL-021) ### 6.1 Goals RHL-021 adds a friendly UI layer on top of backend RBAC: - Branch users must not access other branches’ URLs. - Admin/dev users may access any existing branch. - Invalid route parameters (year/month/day) should surface as NotFound. Backend RBAC remains the source of truth. UI RBAC exists to: - prevent “obviously forbidden” navigation in the frontend - provide clear and consistent UX for end users ### 6.2 BranchGuard (UI-side RBAC) Files: - `components/auth/BranchGuard.jsx` - Pure logic: - `lib/frontend/rbac/branchAccess.js` - `lib/frontend/rbac/branchUiDecision.js` 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 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. ### 6.3 Param validation (year/month/day) Files: - `lib/frontend/params.js` Layout enforcement (server-side `notFound()`): - `app/(protected)/[branch]/layout.jsx` (branch syntax) - `app/(protected)/[branch]/[year]/layout.jsx` - `app/(protected)/[branch]/[year]/[month]/layout.jsx` - `app/(protected)/[branch]/[year]/[month]/[day]/layout.jsx` --- ## 7. Explorer v2 (RHL-022) + PDF Open (RHL-023) ### 7.1 UI goal Provide a simple “file explorer” drill-down: - Year → Month → Day → Files ### 7.2 Explorer pages Routes and components: - `/:branch` → `components/explorer/levels/YearsExplorer.jsx` - `/:branch/:year` → `components/explorer/levels/MonthsExplorer.jsx` - `/:branch/:year/:month` → `components/explorer/levels/DaysExplorer.jsx` - `/:branch/:year/:month/:day` → `components/explorer/levels/FilesExplorer.jsx` ### 7.3 Data fetching strategy - 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) 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` ### 7.5 Files list (leaf route) and “Open PDF” Leaf route: - `/:branch/:year/:month/:day` 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 (``) instead of `apiClient`. --- ## 8. Search UI (RHL-024 / RHL-037) + Date Range Filter (RHL-025) ### 8.1 Route and state model Route: - `/:branch/search` Single Source of Truth rule (RHL-037): - The **path segment** `/:branch/search` is the source of truth for the **current branch context**. - Single-scope search uses the **route branch**. - Shareable URLs for search do not need to carry `branch=` for Single. URL-driven 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**: 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` (RHL-025): - ISO date filter in `YYYY-MM-DD` - both keys are optional - preserved in URLs and across branch switching 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: 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 - “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 - 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) 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: - Branch (for multi/all) - Date (German formatted) - Filename - Relative path - Optional snippet (if returned by the provider) Actions: - “Öffnen” - uses `buildPdfUrl(...)` - opens the binary endpoint in a new tab (``) - “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 render side-by-side on desktop; on very small widths they can wrap. Sorting: - “Relevanz” (backend order) - “Datum (neueste zuerst)” - “Niederlassung” (NLxx ascending; with stable tie-breakers by date and filename) 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.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 ” - only `to` → “bis ” - A single-day search is represented as: - `from === to` #### 8.5.3 UI controls - Entry point: “Zeitraum” button in the Search form. - Popover contents: - Two read-only inputs (“Von”, “Bis”) with clear buttons - Two-month calendar view - Presets (German) for common ranges - Reset action - Active filter display: - A compact chip below the form controls - Provides a one-click clear action #### 8.5.4 Validation and error mapping Single Source of Truth (frontend): - 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. --- ## 9. UI primitives (shadcn/ui) The Explorer + auth + search UI uses shadcn/ui primitives from `components/ui/*`. Required components for the current scope: - `card` - `input` - `label` - `alert` - `button` - `breadcrumb` - `dropdown-menu` - `skeleton` - `table` Additional primitives used for Search scope UX: - `popover` - `command` - `badge` - `calendar` (react-day-picker wrapper) --- ## 10. File Naming Convention (.js vs .jsx) 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/**` - tests that do not contain JSX --- ## 11. Tests ### 11.1 Unit tests Core tests: - `lib/frontend/routes.test.js` - `lib/frontend/apiClient.test.js` - `lib/frontend/authRedirect.test.js` - `lib/frontend/authMessages.test.js` RBAC tests: - `lib/frontend/rbac/branchAccess.test.js` - `lib/frontend/rbac/branchUiDecision.test.js` Explorer helper tests: - `lib/frontend/explorer/breadcrumbDropdowns.test.js` - `lib/frontend/explorer/errorMapping.test.js` - `lib/frontend/explorer/formatters.test.js` - `lib/frontend/explorer/sorters.test.js` - `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` - `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: - `lib/frontend/quickNav/branchSwitch.test.js` Component SSR smoke test: - `components/app-shell/AppShell.test.js` ### 11.2 Running tests From the repo root: ```bash npx vitest run ``` Optional build check: ```bash npm run build ``` --- ## 12. Manual verification checklist ### 12.1 Local (Docker) Start: ```bash 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: - `/:branch` shows years - `/:branch/:year` shows months - `/:branch/:year/:month` shows days - `/:branch/:year/:month/:day` shows files 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) - 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 (including `from/to`) ### 12.2 Server Deploy and verify on the server URL. 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 --- ## 13. Planned follow-ups - Optional Search UX improvements: - grouping results by date / branch - query presets (beyond the date presets) - optional date-only search mode (if desired) - Optional Explorer improvements: - add “Herunterladen” action - optional in-app PDF viewer