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 (branch access vs user management)./profile redirect, and post-change resume flow).NL + numeric draft) + branch existence validation with fail-open note,75%).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:
Width policy:
desktop (lg+): centered w-3/4 content for a wider but bounded workspace
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 }).Must-change-password enforcement (RHL-044):
GET /api/auth/me) includes mustChangePassword.AuthGate centrally enforces:mustChangePassword=true, redirect any protected non-profile route to /profile./profile until password is changed./profile?mustChangePasswordGate=1&next=<safe-internal-url>mustChangePassword and refreshes the session cookie.next when available./profile shows:mustChangePassword=trueGlobal 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 | devUser management UI (RHL-012 + RHL-043 hardening):
/admin/users (protected)superadmin, devforbidden: admin, branch
Features:
list users with filters (username/email search, role, branchId)
loaded-count indicator (X Benutzer geladen) in the users toolbar
sort toolbar (Standard, Rolle (Rechte), Niederlassung (NL))
create user dialog (initial password + must-change-password flag on create)
create/edit branch input hardening (NL prefix + numeric input draft)
create/edit branch existence checks against /api/branches with fail-open note
update user (role/branchId consistency enforced)
delete user with typed-username confirmation gate
temporary password controls (reset, reveal/hide, copy) in table + edit dialog
sticky right action column with icon-only actions (edit/delete)
cursor-based pagination (“Mehr laden”), consistent with selected sort mode
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.Search history (recent queries, RHL-042)
localStorage per authenticated user:rhl.searchHistory.v1.<userId>10 (LRU: reused query moves to top)routeBranch, q, scope, branches, limit, from, to, createdAtcursor is intentionally never stored./NL32/search -> /NL17/search).Verlauf löschen).TopNav / navigation polish (RHL-032)
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.
The “Benutzerverwaltung” entry is only shown when the user has the user-management capability.
Email-based password reset/recovery:
Optional Search UX improvements:
from/to even when q is empty) if desired laterOptional Explorer improvements:
Optional user management enhancements (not implemented in RHL-043):
The app uses Next.js App Router Route Groups to separate public and protected UI.
Public: app/(public)
/loginProtected: app/(protected)
AuthGate, RHL-044)| 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) |
/admin/users |
Benutzerverwaltung | Only superadmin/dev (RHL-012) |
/: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:
Current strategy (implemented in AppShell.jsx + TopNav.jsx):
lg:w-3/4 container on desktop for both:
This keeps header and content visually aligned and avoids horizontal “jump” between navigation and page body.
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)/admin/users (only superadmin / dev)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, email, mustChangePassword.Role is one of: branch | admin | superadmin | dev.
File: components/auth/AuthGate.jsx
Behavior:
mustChangePassword=true:
/profile unless already on /profile.next.mustChangePassword=false and gate marker present:
next automatically (if present and safe).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-DDSearch history is a frontend-only convenience feature built on top of the URL-driven first-page state model.
Storage model:
rhl.searchHistory.v1.<userId>10routeBranch, q, scope, branches, limit, from, toWrite trigger policy:
Restore behavior:
/:branch/search.routeBranch) when needed.cursor remains excluded).UI behavior:
Goal:
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./admin/users (protected)canManageUsers(role) from lib/frontend/auth/roles.js.requireUserManagement(session).Files:
components/admin/users/AdminUsersPage.jsx (gating)components/admin/users/AdminUsersClient.jsx (list + filters + actions)components/admin/users/AdminUsersTableToolbar.jsx (loaded count + sort dropdown)components/admin/users/UsersTable.jsx (table)components/admin/users/UserTemporaryPasswordField.jsx (temporary password controls)components/admin/users/DeleteUserDialog.jsx (delete safety dialog)components/admin/users/BranchNumberInput.jsx (fixed NL prefix + numeric input)components/admin/users/CreateUserDialog.jsx + components/admin/users/create-user/* (create flow)components/admin/users/EditUserDialog.jsx + components/admin/users/edit-user/* (edit flow)The UI uses the API client wrappers from lib/frontend/apiClient.js:
adminListUsersadminCreateUseradminUpdateUseradminDeleteUseradminResetUserPasswordDelete flow requires explicit typed confirmation:
Branch-role input hardening (role=branch) in create and edit:
NL,1 -> NL0132 -> NL32200 -> NL20001 remain visible while typing.Branch existence validation:
/api/branches is available and selected branch does not exist:/api/branches cannot be loaded:Users toolbar (above table):
X Benutzer geladen)StandardRolle (Rechte)Niederlassung (NL)Table layout:
Aktion column for edit/delete actions,Temporary password column:
••••••),navigator.clipboard is not usable.Tooltips:
/api/admin/users?sort=...) and remains stable across Mehr laden.sort so changing sort resets the list cleanly./profile (protected)components/profile/ProfilePage.jsx
mustChangePassword=true.components/profile/ChangePasswordCard.jsx
apiClient.changePassword({ currentPassword, newPassword }).retry() from AuthContext).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
Verlauf löschenNL + number) and branch existence warning/blockingStandard, Rolle (Rechte), Niederlassung (NL))/admin/usersmustChangePassword=true is forced to /profilenext is available