Routing

React Router v7 (react-router-dom 7.15.1, exact-pinned) with centralized route definitions in frontend/src/App.tsx. Routes are organized into protected (JWT), portal (token-based), and public (auth) groups.

Route Hierarchy

As of 2026-06:

/ (ProtectedRoute + Layout -- requires JWT auth)
├── index                               → HomeRedirect (first starred sidebar item,
│                                         fallback "Meine Aufgaben" — sidebarService)
├── /buildings                          → Buildings list
├── /buildings/:id                      → Building dashboard (tabs incl. site protocols)
├── /buildings/:buildingId/projects/:projectId
│   └── /scenarios/:scenarioId          → Scenario detail
├── /funding-applications               → Funding applications list
├── /quotes/*                           → Quotes (nested routes)
├── /contacts, /companies, /leads       → CRM pages (+ /:id detail)
├── /profile                            → User profile (tabbed)
├── /admin/*                            → Admin section
│   ├── /settings, /responsibilities    (old /users, /team-admin, /departments redirect)
│   ├── /funding-programs/* (versions, measures, loans), /forms, /form-builder,
│   │   /form-fields, /form-submissions, /products, /u-values
│   ├── /hubspot (consolidated), /dashboard
│   ├── /document-templates, /pdf-templates, /engagement-task-templates
├── /internal/*                         → Internal tools
│   ├── /document-obtaining/*           → Document collection suite
│   ├── /workflow (+ /settings/task-types) → Workflow v4 board
│   ├── /billing, /team, /changelog, /help-requests
│   ├── /collection-settings/{email-templates,rejection-reasons,flow-definitions}
│   ├── /portal-preview/:id, /entra-groups (+ /:id)
├── /files-demo
└── /helper/{floor-plan-capture,pdf-export}

/internal/document-review/:collectionId/:fulfillmentId
    → ProtectedRoute WITHOUT Layout (fullscreen review)

/portal (token-based auth from URL or localStorage)
├── /portal, /portal/collections/:id    → Portal dashboard / document upload
├── /portal/appointments/:proposalId    → Appointment view
└── /portal/forms/:formKey[/:submissionId]
    → ProtectedRoute (JWT!) -- Form Builder v3 portal forms

(public auth)
├── /login, /signup, /check-email, /verify-email
├── /forgot-password, /reset-password
├── /phone-number, /phone-verification
├── /magic-link, /auth/magic-link
└── /auth/callback                      → OAuth redirect

Legacy paths are kept alive via <Navigate replace /> redirects (e.g. /admin/users/admin/responsibilities?tab=employees, /admin/hubspot-monitoring/admin/hubspot).

Route Protection

GuardWhereMechanism
ProtectedRoutedefined inline in frontend/src/App.tsxChecks isAuthenticated from the Zustand auth store; redirects to /login with a sanitized returnUrl (getSafeReturnUrl prevents the “log in twice” loop, fluffy-doodle#43)
Portal authcomponents/portal/Token from URL or localStorage, no session refresh
Public routesNo guard, accessible without auth

Note: /portal/forms/* is the exception in the portal group — it requires JWT auth (ProtectedRoute).

See Authentication for JWT lifecycle, Portal for token-based access.

Code Splitting

All page components are lazily loaded (71 lazy() imports in App.tsx as of 2026-06):

const BuildingDetail = lazy(() => import('./pages/BuildingDetail'));

A shared <LoadingFallback /> displays during chunk loading. This keeps the initial bundle small and loads page code on demand.

Admin Route Access

Admin routes are guarded by RBAC Authorization checks. The sidebar (components/sidebar/, with components/topnav/ — redesign landed via the PR#1567 saga, closed 2026-05-06) conditionally renders admin links based on user role; getDefaultRoute() in sidebarService.ts picks the landing route from the user’s first starred sidebar item (fallback: Meine Aufgaben).