frontend-ui.md 26 KB

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

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

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

  • 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 (RHL-024) + Scope UX improvements (RHL-037)

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

    • URL-driven state for shareability:

    • q (search query)

    • scope semantics:

      • Single: route branch is the scope (/:branch/search). No branch= query parameter is required.
      • Multi: scope=multi&branches=NL06,NL20 (deterministic order, unique list)
      • All: scope=all
    • limit (optional): 50 | 100 | 200 (default 100)

    • from / to (optional): ISO date range filter (YYYY-MM-DD) synced to the URL (RHL-025)

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

    • Admin/dev UX:

    • TopNav branch switch navigates (deep-link branch switching):

      • preserves the current deep path (Explorer and Search)
      • preserves shareable Search query params (q, scope, branches, limit, from, to)
      • keeps Single scope consistent with the route branch.
    • 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.

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

    Important UX policy (current UI v1):

    • The Search UI still triggers queries only when a non-empty q is present.
    • from / to are treated as additive filters for a text query.
    • The backend supports date-only searches, but the current UI intentionally avoids this to prevent accidental broad queries.

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

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

Implementation notes:

  • Branch list loading is intentionally not tied to the dropdown selection.
    • The list is fetched once for the authenticated admin/dev user (or when the user changes).
    • This avoids unnecessary refetches when switching branches in the UI.

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&from=2026-01-01&to=2026-01-31/NL20/search?q=x&scope=multi&branches=NL06,NL20&limit=200&from=2026-01-01&to=2026-01-31

    • Single scope is kept consistent with the route branch (no divergence between UI selection and URL).

Implementation note:

  • Helper logic for deep-path branch switching is centralized under lib/frontend/quickNav/branchSwitch.js.

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 (RHL-024 / RHL-037) + Date Range Filter (RHL-025)

8.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.
    • Example: /NL01/search?q=reifen

    • Multi: scope=multi&branches=NL06,NL20

    • branches list is deterministic:

      • unique
      • stable ordering
    • All: scope=all

  • limit:

    • allowed values: 50 | 100 | 200
    • default: 100
    • only written to URL when non-default to keep URLs readable
  • from/to (RHL-025):

    • ISO date filter in YYYY-MM-DD
    • both keys are optional
    • preserved in URLs and across branch switching

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

    • shadcn combobox
    • selecting a branch navigates to /:branch/search while preserving shareable query params (q, scope params, limit, from/to).
  • Multi branch selection:

    • checkbox grid optimized for large branch counts
    • “selectable card” label wrapper highlights checked items (border + background)
    • pointer cursor + hover affordances improve discoverability
    • “Alle abwählen” button to reset selection
  • Branch list:

    • loaded via GET /api/branches
    • fail-open UI behavior:
    • if branch list fails, Search UI remains usable
    • allow manual NLxx input as a fallback for selection

8.3 Search form structure

Files:

  • components/search/SearchPage.jsx
  • components/search/SearchForm.jsx

Form building blocks:

  • components/search/form/SearchQueryBox.jsx
  • components/search/form/SearchScopeSelect.jsx
  • components/search/form/SearchSingleBranchCombobox.jsx
  • components/search/form/SearchMultiBranchPicker.jsx
  • components/search/form/SearchLimitSelect.jsx
  • components/search/form/SearchDateRangePicker.jsx (RHL-025)
  • components/search/form/SearchDateFilterChip.jsx (RHL-025)

Key UX rules:

  • q is the primary trigger for searches (explicit submit, no debounce).
  • Date range changes update the URL and are applied when a query is active.
  • Validation errors are shown near the form, not duplicated in the results area.

8.4 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 Ordner”

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

Implementation note:

  • In the Search results table, the actions are intentionally kept in a fixed-width column.
  • Buttons render side-by-side on desktop; on very small widths they can wrap.

Sorting:

  • “Relevanz” (backend order)
  • “Datum (neueste zuerst)”
  • “Niederlassung” (NLxx ascending; with stable tie-breakers by date and filename)

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.5 Date range filter (RHL-025)

8.5.1 Goals

  • Allow narrowing down text searches by inclusive date filters.
  • Keep the date filter shareable via the URL (from, to).
  • Provide fast feedback (local validation) while also relying on backend validation.

8.5.2 URL representation

  • from and to are ISO strings: YYYY-MM-DD.

  • Open ranges are supported:

    • only from → “ab ”
    • only to → “bis ”
    • A single-day search is represented as:

      • from === to

    8.5.3 UI controls

    • Entry point: “Zeitraum” button in the Search form.

    • Popover contents:

      • Two read-only inputs (“Von”, “Bis”) with clear buttons
      • Two-month calendar view
      • Presets (German) for common ranges
      • Reset action
    • Active filter display:

      • A compact chip below the form controls
      • Provides a one-click clear action

    8.5.4 Validation and error mapping

    Single Source of Truth (frontend):

    • ISO parsing + range checks:

      • lib/frontend/search/dateRange.js
    • Canonical date-range validator:

      • lib/frontend/search/searchDateValidation.js

    Local validation:

    • While editing, the UI produces a local ApiClientError using:

      • lib/frontend/search/dateFilterValidation.js
    • This error is mapped to German UI copy via:

      • lib/frontend/search/errorMapping.js

    Rules:

    • from > to is invalid and produces VALIDATION_SEARCH_RANGE.
    • Invalid ISO strings produce VALIDATION_SEARCH_DATE.
    • from === to is valid (single day).

    Backend alignment:

    • The backend also validates from/to and returns the same error codes.
    • This keeps frontend and backend behavior consistent.

    8.5.5 Calendar integration details

    • The calendar is loaded client-side (SSR disabled) to avoid hydration/runtime issues.
    • The hook normalizes day-click handler signatures defensively to avoid version drift.
    • When the range is invalid (from > to), the calendar still shows the interval but highlights it as invalid.

    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

    Additional primitives used for Search scope UX:

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

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

    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)
      • single branch selection via combobox
      • multi selection via checkbox grid + “Alle abwählen”
      • limit switching (50/100/200)
    • date range filter (RHL-025):

      • open “Zeitraum” popover and pick a range
      • verify from/to are written to the URL
      • verify chip shows the German label and can clear the filter
      • verify presets (“Heute”, “Letzte 7 Tage”, …) set the expected range
      • invalid range:
      • ensure UI displays a validation message
      • ensure the calendar highlights the invalid interval
    • pagination:

      • “Mehr laden” appends results when nextCursor exists
    • TopNav QuickNav:

      • switching branch updates the URL and keeps the current deep path
      • switching branch on Search keeps shareable query params (including from/to)

    12.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
      • total count (“x von y geladen”)
      • open PDF / jump to day
      • TopNav branch switching keeps deep links

    13. Planned follow-ups

    • Optional Search UX improvements:

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

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