frontend-ui.md 19 KB

Frontend UI: App Shell, Routing, Auth/RBAC, Explorer, and Search (RHL-019 / RHL-020 / RHL-021 / RHL-022 / RHL-023 / RHL-024)

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).

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-024)

  • 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=<original-url>.
  • 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 v1 (RHL-024)

    • Route: /:branch/search (protected).

    • URL-driven state for shareability:

    • q (search query)

    • scope params for admin/dev:

      • single: branch=NLxx
      • multi: scope=multi&branches=NL06,NL20
      • all: scope=all
    • limit (optional): 50 | 100 | 200 (default 100; only included in URL when non-default)

    • Cursor-based pagination (nextCursor) is not stored in the URL.

    • Results support:

    • Open PDF in a new tab (“Öffnen”)

    • Navigate to the Explorer day route (“Zum Tag”)

    • Search results UI includes:

    • Empty/Loading/Error states

    • Sorting dropdown (Relevanz / Datum / Dateiname)

    • “Mehr laden” button (cursor pagination)

    • “x von y” progress indicator when total is available

1.2 Still out of scope / planned

  • Date range UI for Search (from / to) and URL sync (planned follow-up after Search UI v1).

  • Optional Search UX improvements:

    • grouping results by date and/or branch
    • debounced “typeahead” search (current v1 is explicit submit)
  • 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.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.

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=<current-url>

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:

  • /:branchcomponents/explorer/levels/YearsExplorer.jsx
  • /:branch/:yearcomponents/explorer/levels/MonthsExplorer.jsx
  • /:branch/:year/:monthcomponents/explorer/levels/DaysExplorer.jsx
  • /:branch/:year/:month/:daycomponents/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 (<a target="_blank">) instead of apiClient.


8. Search UI v1 (RHL-024)

8.1 Route and state model

Route:

  • /:branch/search

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 branch: branch=NLxx
    • multi: scope=multi&branches=NL06,NL20
    • all: scope=all
  • limit:

    • allowed values: 50 | 100 | 200
    • default: 100
    • only written to URL when non-default to keep URLs readable

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 branch route context)
    • “Mehrere Niederlassungen” (multi)
    • “Alle Niederlassungen” (all)
  • Multi branch selection:

    • simple checkbox list
    • branch list is fetched from GET /api/branches
    • fail-open behavior: if branch list cannot be loaded, the rest of the Search UI remains usable

8.3 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 (<a target="_blank" rel="noopener noreferrer">)
  • “Zum Tag”

    • navigates to /:branch/:year/:month/:day using dayPath(...)

Sorting:

  • “Relevanz” (backend order)
  • “Datum (neueste zuerst)”
  • “Dateiname (A–Z)”

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.4 Error handling (Search)

Search uses a dedicated error mapping helper:

  • lib/frontend/search/errorMapping.js

Mapping principles:

  • Use stable error codes for UX decisions.
  • Do not leak raw backend messages to users.
  • Keep user-facing messages German.

Key outcomes:

  • AUTH_UNAUTHENTICATED → redirect to login with reason=expired&next=<current-url>
  • AUTH_FORBIDDEN_BRANCH → show Forbidden UX
  • VALIDATION_* → show a user-friendly German validation message
  • network/unknown → generic German error + retry

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

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

Component SSR smoke test:

  • components/app-shell/AppShell.test.js

11.2 Running tests

From the repo root:

npx vitest run

Optional build check:

npm run build

12. Manual verification checklist

12.1 Local (Docker)

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)

    • 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)
    • multi selection (checkboxes)
    • limit switching (50/100/200)
  • pagination:

    • “Mehr laden” appends results when nextCursor exists

12.2 Server

Deploy and verify on the server URL.

Verify:

  • Explorer navigation and PDF open
  • Search UI:

    • scopes
    • limit selection
    • total count (“x von y geladen”)
    • open PDF / jump to day

13. Planned follow-ups

  • Search date range UI (from / to) with shareable URL sync.

  • Optional Search UX improvements:

    • grouping results by date / branch
    • query presets
  • Optional Explorer improvements:

    • add “Herunterladen” action
    • optional in-app PDF viewer