# Frontend UI: App Shell, Routing, Auth/RBAC, Explorer, Search, Date Range Filter, Navigation Polish, Profile Password Change, and Role Refinement (RHL-019 / RHL-020 / RHL-021 / RHL-022 / RHL-023 / RHL-024 / RHL-025 / RHL-037 / RHL-032 / RHL-009 / RHL-041) 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). - **RHL-032**: Navigation UX polish (TopNav branding, theme toggle, user menu, tooltips, session indicator without content flicker, favicon, active states, and debounced loading UI). - **RHL-009**: Profile password change UI (Change Password form + toasts). - **RHL-041**: Role refinement (`superadmin`) + capability separation for future user management (RHL-012). > **Language policy** > > - 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-041 + RHL-037 + RHL-025 + RHL-032 + RHL-009) - **Public** `/login` route with a functional login form (shadcn/ui primitives). - **Protected** application shell for all authenticated routes: - Top navigation (branding, quick navigation, actions) - 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 action calls `GET /api/auth/logout` and redirects to `/login?reason=logged-out`. - **User dropdown menu (RHL-032)**: - Profile / Support / Logout - **Profile password change (RHL-009)**: - Route: `/profile` (protected) - Profile shows read-only account/session metadata (role, branch, email). - Password change form calls `apiClient.changePassword({ currentPassword, newPassword })`. - Uses inline validation for actionable form errors. - Uses toast notifications (Sonner) for success/error feedback. - **Global toast notifications (Sonner)**: - Sonner Toaster is mounted once in the root layout. - UI code uses a small wrapper (`lib/frontend/ui/toast.js`) to keep copy and behavior consistent. - **Role model (RHL-041)**: - Backend roles: `branch | admin | superadmin | dev`. - UI treats **branch access** and **user management** as separate capabilities. - UI introduces a pure role helper: - `lib/frontend/auth/roles.js` - `isAdminLike(role)` → `admin | superadmin | dev` - `canManageUsers(role)` → `superadmin | dev` (RHL-012 prerequisite, no UI screens yet) - **UI RBAC (branch-level)**: - `BranchGuard` prevents branch users from accessing other branches’ URLs. - Admin-like users (`admin/superadmin/dev`) can access multiple branches. - Admin-like 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-like) - `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`, `scope`, `branches`, `limit`, `from`, `to`. - Cursor-based pagination (`nextCursor`) is **not stored in the URL**. - **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. - **TopNav / navigation polish (RHL-032)** - Solid header (no blur/transparency) for crisp borders. - Branding logo (light + dark assets). - Icon-only theme toggle. - User dropdown menu (Profile / Support / Logout). - Consistent tooltips across navigation. - Session check indicator in TopNav (debounced) to avoid content flicker. - Clear active states for Explorer and Search. - Safe handling of invalid branch routes (admin-like): warning + one-click recovery. Implementation note: - For small static brand assets (logos), Next Image optimization is disabled (`unoptimized`) to avoid browser-specific “pending” indicators caused by aborted `/_next/image` optimization requests. --- ### 1.2 Still out of scope / planned - User management UI + APIs (RHL-012): - The UI role helpers already define `canManageUsers(role)`. - RHL-012 will add screens and API endpoints and must enforce `superadmin/dev`. - Email-based password reset/recovery: - Planned as a separate follow-up ticket/phase. - Rationale: higher security surface (token handling and non-leakage), external SMTP/IT dependencies, and separate rate limiting ticket (RHL-031) to avoid scope creep. - 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: - add a dedicated “Herunterladen” UI action (download variant) - optional in-app PDF viewer experience (instead of a new tab) --- ## 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 | | `/profile` | Profile | Password change is implemented here (RHL-009) | | `/: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. - Mount global UI providers: - theme provider - Sonner Toaster (global notifications) ### 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: - Keep the AppShell frame visible while auth/session checks run. - Avoid 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 TopNav structure (RHL-032) File: `components/app-shell/TopNav.jsx` TopNav is a sticky header and is the primary navigation surface. Layout groups (left → right): - **Brand**: logo link to `/` (light + dark variants). - **Primary navigation**: QuickNav (branch selector + Explorer/Search buttons). - **Actions**: theme toggle + session indicator. - **User menu**: dropdown trigger (Profile / Support / Logout). Design note: - The header is intentionally rendered as a **solid** background (no backdrop blur) to keep borders/buttons crisp. ### 4.3 Branding (logo assets) Asset convention: - Store brand assets under `public/brand/`. - Use two assets when needed (light/dark) and toggle them via Tailwind `dark:` classes. Implementation note: - For small static logo assets, Next Image optimization is disabled (`unoptimized`) to avoid repeated aborted `/_next/image` requests on some browsers. ### 4.4 Theme toggle File: `components/app-shell/ThemeToggleButton.jsx` - Icon-only toggle (sun/moon) using `next-themes`. - Preference respects system theme by default. ### 4.5 Session indicator (no content flicker) File: `components/app-shell/SessionIndicator.jsx` Goal: - Avoid “session checking” cards flashing in the main content area. - Surface transient checks as a small indicator in the TopNav. Policy: - The indicator is **debounced** to avoid flicker on fast connections. - If the session check completes before the delay, nothing is shown. ### 4.6 Tooltips policy (RHL-032) - Use shadcn/Radix tooltips for TopNav actions. - Prefer tooltips over native `title` attributes to avoid double tooltips. - A single `TooltipProvider` is mounted at TopNav scope so all triggers share the same delay configuration. ### 4.7 User menu File: `components/app-shell/UserStatus.jsx` Menu items (German): - **Profil** → `/profile` (account info + password change) - **Support** → opens a `mailto:` link to support. - **Abmelden** → calls logout and redirects to login. Role label mapping: - `branch` → “Niederlassung” - `admin` → “Admin” - `superadmin` → “Superadmin” - `dev` → “Entwicklung” --- ## 5. Quick navigation (quality-of-life) File: `components/app-shell/QuickNav.jsx` Purpose: - Provide direct links to: - Explorer (`/:branch`) - Search (`/:branch/search`) - For admin-like users: enable quick branch switching while preserving the current “context”. Behavior: - Branch users: - QuickNav is effectively fixed to their `branchId`. - Admin-like users (`admin/superadmin/dev`): - Loads branches via `GET /api/branches`. - Stores the last selected branch in `localStorage` (`rhl_last_branch`) for convenience. - Keeps `selectedBranch` stable and avoids update loops (guarded initialization). Branch switching rule (RHL-037): - Selecting a branch navigates to the same section while replacing the first path segment. - Explorer deep paths preserved: - `/NL32/2025/12/31` → `/NL20/2025/12/31` - Search route preserved and shareable params preserved: - `/NL32/search?q=x&scope=multi&branches=NL06,NL20&limit=200&from=...&to=...` → `/NL20/search?q=x&scope=multi&branches=NL06,NL20&limit=200&from=...&to=...` Implementation notes: - Deep-path branch switching logic is centralized in `lib/frontend/quickNav/branchSwitch.js`. - Avoid using `useSearchParams()` inside QuickNav for “current query string” access. - Use `window.location.search` at click-time instead (client-only). Invalid branch routes (admin-like) (RHL-032): - If the user manually navigates to a syntactically valid but non-existent branch (e.g. `/NL200`): - QuickNav shows a warning indicator. - The dropdown shows a warning explanation. - A one-click recovery item is available: “Zur letzten gültigen Niederlassung”. --- ## 6. Authentication UX (RHL-020) ### 6.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=` Notes: - `GET /api/auth/me` returns minimal identity data for the UI: - `userId`, `role`, `branchId`, and (optionally) `email`. - Role is one of: `branch | admin | superadmin | dev`. ### 6.2 AuthGate (in-shell gating) File: `components/auth/AuthGate.jsx` Behavior: - While session is loading: render a minimal in-shell state. - On auth errors: show an in-shell error card + retry. - On unauthenticated: show a short “redirecting” message while redirect happens. RHL-032 note: - Prefer surfacing transient “session checking” feedback in the TopNav (SessionIndicator) to avoid content flicker. ### 6.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 the same policy: - username input is normalized to lowercase - `autoCapitalize="none"` to prevent mobile auto-caps --- ## 7. UI RBAC, Forbidden, and NotFound (RHL-021 / RHL-041) ### 7.1 Goals UI-side RBAC exists for UX (backend RBAC remains authoritative): - Branch users must not access other branches’ URLs. - Admin-like users may access any existing branch. - Invalid route parameters (year/month/day) should surface as NotFound. ### 7.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` - admin-like (`admin/superadmin/dev`) → allowed for any branch that exists Admin-like 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. ### 7.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` --- ## 8. Explorer v2 (RHL-022) + PDF Open (RHL-023) ### 8.1 UI goal Provide a simple “file explorer” drill-down: - Year → Month → Day → Files ### 8.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` ### 8.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` ### 8.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` ### 8.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` --- ## 9. Search UI (RHL-024 / RHL-037) + Date Range Filter (RHL-025) ### 9.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. Admin-like scope availability: - Admin-like users (`admin/superadmin/dev`) can switch between: - Single (route branch) - Multi (selected branches) - All (global) Branch users: - Are forced to Single on their own branch. Shareable params (first page identity): - `q` (string) - Scope params: - **Single**: no `scope` param required; route branch defines the branch. - **Multi**: `scope=multi&branches=NL06,NL20` (deterministic) - **All**: `scope=all` - `limit`: - allowed values: `50 | 100 | 200` - default: `100` - `from/to` (RHL-025): - ISO date filter in `YYYY-MM-DD` - preserved in URLs and across branch switching ### 9.2 Debounced loading UI (RHL-032) Goal: - Avoid visible “skeleton flashes” for fast requests. Approach: - Loading UI is shown only after a small delay. - This is implemented with `useDebouncedVisibility(...)` and centralized timing constants. --- ## 10. Toast notifications (Sonner) The project uses Sonner (shadcn/ui integration) for toast notifications. - The Toaster is mounted once in `app/layout.jsx` (root layout) and respects the current theme. - UI code should use the wrapper in `lib/frontend/ui/toast.js`. --- ## 11. Profile: Password change (RHL-009) ### 11.1 Route - `/profile` (protected) ### 11.2 Components - `components/profile/ProfilePage.jsx` - Shows read-only account/session info (role, branch, email). - `components/profile/ChangePasswordCard.jsx` - Calls `apiClient.changePassword({ currentPassword, newPassword })`. - Maps password policy errors into user-friendly German hints. --- ## 12. UI primitives (shadcn/ui) The Explorer + auth + search UI uses shadcn/ui primitives from `components/ui/*`. --- ## 13. File Naming Convention (.js vs .jsx) To keep the project consistent: - Use **`.jsx`** for files that contain JSX. - Use **`.js`** for non-JSX files. --- ## 14. Tests ### 14.1 Unit tests Role helper tests (RHL-041): - `lib/frontend/auth/roles.test.js` Existing tests remain as documented in the project. ### 14.2 Running tests From the repo root: ```bash npx vitest run ``` Optional build check: ```bash npm run build ``` --- ## 15. Manual verification checklist ### 15.1 Local (Docker) - Login and verify TopNav role label per account. - Branch switching for admin-like users. - Explorer drill-down. - Search scopes (admin-like) + Search restrictions (branch). - Open PDFs. ### 15.2 Server - Repeat the local checks on the real server. - Validate RBAC behavior: - branch user → forbidden on foreign branches - admin-like → notfound on non-existent branches --- ## 16. Planned follow-ups - User management UI + APIs (RHL-012): - Guard endpoints using `requireUserManagement(session)` (backend). - Gate UI screens using `canManageUsers(role)` (frontend).