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).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).
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.UI RBAC (branch-level):
BranchGuard prevents branch users from accessing other branches’ URLs.GET /api/branches.Route param validation (syntactic):
branch: NL + digits (syntactic validity; existence is validated by BranchGuard for admin/dev)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)
Route: /:branch/search (protected).
URL-driven state for shareability:
q (search query)
scope semantics:
/:branch/search). No branch= query parameter is required.scope=multi&branches=NL06,NL20 (deterministic order, unique list)scope=alllimit (optional): 50 | 100 | 200 (default 100)
from/to are carried through the URL but date-range UI is still planned for later.
Cursor-based pagination (nextCursor) is not stored in the URL.
Admin/dev UX:
TopNav branch switch navigates (deep-link branch switching):
q, scope, branches, limit, from, to)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.
Branch list for admin/dev is fetched via GET /api/branches (fail-open).
Date range UI for Search (from / to) and URL sync (planned follow-up after Search scope UX).
Optional Search UX improvements:
Optional Explorer improvements:
Perceived performance polish:
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 |
/: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:
Wrap all protected pages with:
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:
File: components/app-shell/QuickNav.jsx
Purpose:
Provide direct links to:
/:branch)/:branch/search)Behavior:
Branch users: QuickNav uses the user’s branchId.
Admin/dev users:
GET /api/branches.localStorage for convenience.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 → /NL20/search?q=x&scope=multi&branches=NL06,NL20&limit=200
Single scope is kept consistent with the route branch (no divergence between UI selection and URL).
Implementation note:
lib/frontend/quickNav/branchSwitch.js.Responsive behavior:
md and up only) to keep the header compact.File: components/auth/AuthProvider.jsx
Behavior:
On mount, call apiClient.getMe().
If { user: { ... } }:
authenticatedIf { user: null }:
/login?reason=expired&next=<current-url>The next parameter:
pathname and query stringFile: components/auth/AuthGate.jsx
Behavior:
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:
UI enforces this policy as well:
autoCapitalize="none" to prevent mobile auto-capsFile: components/auth/LogoutButton.jsx
Flow:
apiClient.logout()./login?reason=logged-out.RHL-021 adds a friendly UI layer on top of backend RBAC:
Backend RBAC remains the source of truth. UI RBAC exists to:
Files:
components/auth/BranchGuard.jsxPure logic:
lib/frontend/rbac/branchAccess.jslib/frontend/rbac/branchUiDecision.jsResponsibilities:
user and status from AuthContext.Enforce branch rules:
branch → allowed only when :branch === user.branchIdadmin / dev → allowed for any branch that existsAdmin/dev branch existence validation:
BranchGuard fetches GET /api/branches and verifies the route branch exists.Fail-open policy:
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.A small hook provides consistent query state:
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.Shows:
Primary file action:
“Öffnen” opens the PDF in a new browser tab via the binary PDF endpoint:
GET /api/files/:branch/:year/:month/:day/:filenameImplementation notes:
URL construction is centralized in:
lib/frontend/explorer/pdfUrl.jsThe PDF endpoint is binary (application/pdf). The UI uses navigation (<a target="_blank">) instead of apiClient.
Route:
/: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:
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:
All: scope=all
limit:
50 | 100 | 200100from/to:
Pagination:
nextCursor is kept in client state.cursor=nextCursor and appends results.Branch users:
Admin/dev users:
Scope selector:
Single branch selection:
/:branch/search while preserving shareable query params (q, scope params, limit, from/to).Multi branch selection:
md:grid-cols-5)Branch list:
GET /api/branchesfail-open UI behavior:
if branch list fails, Search UI remains usable
(optional fallback) allow manual NLxx input for selection
Search results show at least:
Actions:
“Öffnen”
buildPdfUrl(...)<a target="_blank" rel="noopener noreferrer">)“Zum Tag”
/:branch/:year/:month/:day using dayPath(...)Sorting:
Totals:
total, the UI shows “x von y Treffern geladen”.total is null, the UI falls back to showing only the loaded count.Search uses a dedicated error mapping helper:
lib/frontend/search/errorMapping.jsMapping principles:
Key outcomes:
AUTH_UNAUTHENTICATED → redirect to login with reason=expired&next=<current-url>AUTH_FORBIDDEN_BRANCH → show Forbidden UXVALIDATION_* → show a user-friendly German validation messageUX note (RHL-037):
The Explorer + auth + search UI uses shadcn/ui primitives from components/ui/*.
Required components for the current scope:
cardinputlabelalertbuttonbreadcrumbdropdown-menuskeletontableAdditional primitives used for Search scope UX:
popovercommandbadgeTo keep the project consistent:
Use .jsx for files that contain JSX:
app/**/page.jsx, app/**/layout.jsxcomponents/**Use .js for non-JSX files:
lib/** utilities and helpersapp/api/**/route.jsmodels/**Core tests:
lib/frontend/routes.test.jslib/frontend/apiClient.test.jslib/frontend/authRedirect.test.jslib/frontend/authMessages.test.jsRBAC tests:
lib/frontend/rbac/branchAccess.test.jslib/frontend/rbac/branchUiDecision.test.jsExplorer helper tests:
lib/frontend/explorer/breadcrumbDropdowns.test.jslib/frontend/explorer/errorMapping.test.jslib/frontend/explorer/formatters.test.jslib/frontend/explorer/sorters.test.jslib/frontend/explorer/pdfUrl.test.js (RHL-023)Search helper tests:
lib/frontend/search/urlState.test.jslib/frontend/search/errorMapping.test.jslib/frontend/search/normalizeState.test.jslib/frontend/search/searchApiInput.test.jslib/frontend/search/resultsSorting.test.jsQuickNav helper tests:
lib/frontend/quickNav/branchSwitch.test.jsComponent SSR smoke test:
components/app-shell/AppShell.test.jsFrom the repo root:
npx vitest run
Optional build check:
npm run build
Start:
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)
/login?reason=expired&next=/NL01/2025/12Invalid login
Valid login
Logout
/login?reason=logged-outExplorer checks:
/:branch shows years/:branch/:year shows months/:branch/:year/:month shows days/:branch/:year/:month/:day shows filesPDF open:
On /:branch/:year/:month/:day, click “Öffnen”
Search checks:
/NL01/search
admin/dev:
pagination:
nextCursor existsTopNav QuickNav:
Deploy and verify on the server URL.
Verify:
Search UI:
Search date range UI (from / to) with shareable URL sync.
Optional Search UX improvements:
Optional Explorer improvements: