frontend-ui.md 23 KB

Frontend UI: App Shell, Routing, Auth/RBAC, Explorer, Search, Date Range Filter, and Navigation Polish (RHL-019 / RHL-020 / RHL-021 / RHL-022 / RHL-023 / RHL-024 / RHL-025 / RHL-037 / RHL-032)

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

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-037 + RHL-025 + RHL-032)

  • 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=<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 action 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 (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/dev): warning + one-click recovery.

1.2 Still out of scope / planned

  • 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:

    • “Herunterladen” action (download variant) next to “Öffnen”
    • a dedicated in-app PDF viewer UI (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:
    • Session check (AuthProvider)
    • 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.
  • Icons / favicon (RHL-032): metadata icons can be configured in the root layout.

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.

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 (placeholder page until profile editing is implemented).
  • Support → opens a mailto: link to info@attus.de.
  • Abmelden → calls logout and redirects to login.

Support mailto guidelines:

  • Build the mailto: query string with encodeURIComponent (not URLSearchParams) to avoid “+” rendering issues in some mail clients.
  • Include basic context in the mail body:
    • current URL
    • route path
    • timestamp
    • user role/branch
    • user-agent

5. Quick navigation (quality-of-life)

File: components/app-shell/QuickNav.jsx

Purpose:

  • Provide direct links to:

    • Explorer (/:branch)
    • Search (/:branch/search)
  • For admin/dev: enable quick branch switching while preserving the current “context”.

Behavior:

  • Branch users:

    • QuickNav is effectively fixed to their branchId.
  • Admin/dev users:

    • 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).
    • This avoids build-time issues for static/prerender contexts.

Invalid branch routes (admin/dev) (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=<current-url>

The next parameter:

  • includes the original pathname and query string
  • is sanitized to avoid open redirects (only internal paths are allowed)

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 this policy as well:
    • username input is normalized to lowercase
    • autoCapitalize="none" to prevent mobile auto-caps

7. UI RBAC, Forbidden, and NotFound (RHL-021)

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

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

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:

  • /: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

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.

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.

Files:

  • Timing constants: lib/frontend/ui/uxTimings.js

    • LOADING_UI_DELAY_MS
    • SESSION_INDICATOR_DELAY_MS
    • SESSION_INDICATOR_MIN_VISIBLE_MS
    • TOOLTIP_DELAY_MS
  • Debounce hook: lib/frontend/hooks/useDebouncedVisibility.js

Applied to:

  • Explorer level loading skeletons
  • Search results loading skeletons
  • TopNav session indicator

10. UI primitives (shadcn/ui)

The Explorer + auth + search UI uses shadcn/ui primitives from components/ui/*.

Core primitives:

  • card
  • input
  • label
  • alert
  • button
  • breadcrumb
  • dropdown-menu
  • skeleton
  • table

Additional primitives used for Search scope UX:

  • popover
  • command
  • badge
  • calendar (react-day-picker wrapper)
  • tooltip (RHL-032)

Radix integration note:

  • Radix triggers (DropdownMenuTrigger, TooltipTrigger, …) require the trigger element to support ref forwarding.
  • The project’s components/ui/button.jsx forwards refs to remain compatible with Radix asChild usage.

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

12. Tests

12.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
  • lib/frontend/search/dateRange.test.js (RHL-025)
  • lib/frontend/search/datePresets.test.js (RHL-025)
  • lib/frontend/search/dateRangePickerUtils.test.js (RHL-025)
  • lib/frontend/search/searchDateValidation.test.js (RHL-025)
  • lib/frontend/search/dateFilterValidation.test.js (RHL-025)

QuickNav helper tests:

  • lib/frontend/quickNav/branchSwitch.test.js

Component SSR smoke test:

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

12.2 Running tests

From the repo root:

npx vitest run

Optional build check:

npm run build

13. Manual verification checklist

13.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
  • Valid login

    • Expect redirect into the protected route
  • Logout

    • Expect redirect to /login?reason=logged-out

Navigation/TopNav checks (RHL-032):

  • Tooltips show consistently (no double native tooltips).
  • Theme toggle switches theme.
  • Branch switching updates the URL and keeps context (Explorer/Search).
  • Invalid branch route (/NL200) shows warning + recovery item.

Explorer checks:

  • /:branch shows years
  • /:branch/:year shows months
  • /:branch/:year/:month shows days
  • /:branch/:year/:month/:day shows files

Search checks:

  • /NL01/search

    • empty state
    • submit triggers URL update and first-page fetch
  • admin/dev:

    • scope switching (single/multi/all)
    • multi selection via checkbox grid + “Alle abwählen”
    • limit switching (50/100/200)
    • date range filter updates from/to in the URL

Debounced loading UI (RHL-032):

  • With fast connections: skeleton flashes are minimized.
  • With throttling: skeletons appear after the delay and remain stable.

13.2 Server

Deploy and verify on the server URL.

Verify:

  • Explorer navigation and PDF open
  • Search UI:
    • scopes
    • limit selection
    • date range filter + URL sync
    • open PDF / jump to day
    • TopNav branch switching keeps deep links

14. Planned follow-ups

  • Optional Search UX improvements:

    • grouping results by date / branch
    • optional date-only search mode (if desired)
  • Optional Explorer improvements:

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