This document describes the frontend routing scaffold, the application shell layout, and the core UI modules for the RHL Lieferscheine app.
Timeline:
q, scopes, cursor pagination, open PDF + jump to day).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).
Public /login route with a functional login form (shadcn/ui primitives).
Protected application shell for all authenticated routes:
Session guard for protected routes:
GET /api/auth/me./login?reason=expired&next=<original-url>.In-shell auth gating (UX improvement):
Logout:
GET /api/auth/logout and redirects to /login?reason=logged-out.User dropdown menu (RHL-032):
Profile password change (RHL-009):
/profile (protected)apiClient.changePassword({ currentPassword, newPassword }).Global toast notifications (Sonner):
lib/frontend/ui/toast.js) to keep copy and behavior consistent.Role model (RHL-041):
branch | admin | superadmin | dev.lib/frontend/auth/roles.js
isAdminLike(role) → admin | superadmin | devcanManageUsers(role) → superadmin | dev (RHL-012 prerequisite, no UI screens yet)UI RBAC (branch-level):
BranchGuard prevents branch users from accessing other branches’ URLs.admin/superadmin/dev) can access multiple branches.GET /api/branches.Route param validation (syntactic):
branch: NL + digits (syntactic validity; existence is validated by BranchGuard for admin-like)year: YYYYmonth: 01–12day: 01–31notFound() early in layouts.Explorer v2 (Branch → Year → Month → Day → Files):
/:branch → years/:branch/:year → months/:branch/:year/:month → days/:branch/:year/:month/:day → filesBreadcrumb navigation:
Breadcrumb + dropdowns for year/month/day when options are available.Consistent states across Explorer levels:
FS_NOT_FOUND mapped to an Explorer “path no longer exists” cardExplorer leaf action: Open PDF (RHL-023)
/:branch/:year/:month/:day provides an “Öffnen” action.lib/frontend/explorer/pdfUrl.js.Search UI (RHL-024) + Scope UX improvements (RHL-037)
/:branch/search (protected).q, scope, branches, limit, from, to.nextCursor) is not stored in the URL.Search date range filter (RHL-025)
from and to.TopNav / navigation polish (RHL-032)
Implementation note:
unoptimized) to avoid browser-specific “pending” indicators caused by aborted /_next/image optimization requests.User management UI + APIs (RHL-012):
canManageUsers(role).superadmin/dev.Email-based password reset/recovery:
Optional Search UX improvements:
from/to even when q is empty) if desired laterOptional Explorer improvements:
The app uses Next.js App Router Route Groups to separate public and protected UI.
Public: app/(public)
/loginProtected: app/(protected)
| 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:
/search route. Visiting /search matches /:branch with branch = "search".File: app/layout.jsx
Responsibilities:
app/globals.css).File: app/(public)/layout.jsx
Responsibilities:
/login (and potential future public routes).File: app/(protected)/layout.jsx
Responsibilities:
AuthProvider (session check + redirect)AppShell (stable frame)AuthGate (renders auth loading/error/redirect UI inside the shell)UX rationale:
File: components/app-shell/AppShell.jsx
AppShell is the stable frame for all protected pages:
On very wide screens the UI should remain readable.
Current strategy (implemented in AppShell.jsx + TopNav.jsx):
2xl+).Below 2xl:
File: components/app-shell/TopNav.jsx
TopNav is a sticky header and is the primary navigation surface.
Layout groups (left → right):
/ (light + dark variants).Design note:
Asset convention:
public/brand/.dark: classes.Implementation note:
unoptimized) to avoid repeated aborted /_next/image requests on some browsers.File: components/app-shell/ThemeToggleButton.jsx
next-themes.File: components/app-shell/SessionIndicator.jsx
Goal:
Policy:
title attributes to avoid double tooltips.TooltipProvider is mounted at TopNav scope so all triggers share the same delay configuration.File: components/app-shell/UserStatus.jsx
Menu items (German):
/profile (account info + password change)mailto: link to support.Role label mapping:
branch → “Niederlassung”admin → “Admin”superadmin → “Superadmin”dev → “Entwicklung”File: components/app-shell/QuickNav.jsx
Purpose:
Provide direct links to:
/:branch)/:branch/search)For admin-like users: enable quick branch switching while preserving the current “context”.
Behavior:
Branch users:
branchId.Admin-like users (admin/superadmin/dev):
GET /api/branches.localStorage (rhl_last_branch) for convenience.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.
/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:
lib/frontend/quickNav/branchSwitch.js.useSearchParams() inside QuickNav for “current query string” access.
window.location.search at click-time instead (client-only).Invalid branch routes (admin-like) (RHL-032):
/NL200):
File: components/auth/AuthProvider.jsx
Behavior:
On mount, call apiClient.getMe().
If { user: { ... } }:
authenticatedIf { user: null }:
/login?reason=expired&next=<current-url>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.
File: components/auth/AuthGate.jsx
Behavior:
RHL-032 note:
Files:
app/(public)/login/page.jsx (Server Component)components/auth/LoginForm.jsx (Client Component)Flow:
Login page parses query params using parseLoginParams(...).
If reason is present:
expired → show “session expired” banner (German)logged-out → show “logged out” banner (German)On submit, the form calls apiClient.login({ username, password }).
On success:
next if present/On failure:
Username policy:
autoCapitalize="none" to prevent mobile auto-capsUI-side RBAC exists for UX (backend RBAC remains authoritative):
Files:
components/auth/BranchGuard.jsxPure logic:
lib/frontend/rbac/branchAccess.jslib/frontend/rbac/branchUiDecision.jsResponsibilities:
user and status from AuthContext.branch → allowed only when :branch === user.branchIdadmin/superadmin/dev) → allowed for any branch that existsAdmin-like branch existence validation:
BranchGuard fetches GET /api/branches and verifies the route branch exists.Files:
lib/frontend/params.jsLayout enforcement (server-side notFound()):
app/(protected)/[branch]/layout.jsx (branch syntax)app/(protected)/[branch]/[year]/layout.jsxapp/(protected)/[branch]/[year]/[month]/layout.jsxapp/(protected)/[branch]/[year]/[month]/[day]/layout.jsxProvide a simple “file explorer” drill-down:
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.jsxlib/frontend/apiClient.js.lib/frontend/hooks/useExplorerQuery.jsFiles:
UI component:
components/explorer/breadcrumbs/ExplorerBreadcrumbs.jsxcomponents/explorer/breadcrumbs/SegmentDropdown.jsxPure helpers:
lib/frontend/explorer/breadcrumbDropdowns.jslib/frontend/explorer/formatters.js (German month labels)lib/frontend/explorer/sorters.jsLeaf route:
/:branch/:year/:month/:dayFiles list behavior:
Table.Primary file action:
GET /api/files/:branch/:year/:month/:day/:filenameImplementation notes:
lib/frontend/explorer/pdfUrl.jsRoute:
/:branch/searchSingle Source of Truth rule (RHL-037):
/:branch/search is the source of truth for the current branch context.branch= for Single.URL-driven state policy:
Admin-like scope availability:
admin/superadmin/dev) can switch between:
Branch users:
Shareable params (first page identity):
q (string)
Scope params:
scope param required; route branch defines the branch.scope=multi&branches=NL06,NL20 (deterministic)scope=alllimit:
50 | 100 | 200100from/to (RHL-025):
YYYY-MM-DDGoal:
Approach:
useDebouncedVisibility(...) and centralized timing constants.The project uses Sonner (shadcn/ui integration) for toast notifications.
app/layout.jsx (root layout) and respects the current theme.lib/frontend/ui/toast.js./profile (protected)components/profile/ProfilePage.jsx
components/profile/ChangePasswordCard.jsx
apiClient.changePassword({ currentPassword, newPassword }).The Explorer + auth + search UI uses shadcn/ui primitives from components/ui/*.
To keep the project consistent:
.jsx for files that contain JSX..js for non-JSX files.Role helper tests (RHL-041):
lib/frontend/auth/roles.test.jsExisting tests remain as documented in the project.
From the repo root:
npx vitest run
Optional build check:
npm run build
requireUserManagement(session) (backend).canManageUsers(role) (frontend).