Skip to content

Frontend Routing

Every URL the SPA serves, the page component behind it, and the permission (or role) that gates it. Authoritative source is src/App.tsx; this page mirrors it faithfully as of the 0.23.0 release.

Routing mechanics

  • Router: react-router-dom v6, wrapped in <BrowserRouter> with the v7 future flags v7_startTransition and v7_relativeSplatPath enabled (src/App.tsx:128-133).
  • Page components are React.lazy(...) imports, wrapped in <Suspense fallback={<RouteSkeleton />}> for bundle splitting (src/App.tsx:29-82, src/App.tsx:137).
  • Five critical pages are prefetched on idle after first render so subsequent navigation feels instant: Bookings, Finance, Groups, Customers, Visa (src/App.tsx:17-28).
  • Gating uses <ProtectedRoute> — see Permissions system. Combines allowedRoles (role filter) and requiredPermissions (permission AND-check).
// src/App.tsx:149
<Route
  path="/app"
  element={<ProtectedRoute allowedRoles={['admin', 'staff']}><Index /></ProtectedRoute>}
/>

Public routes

No auth required.

Route Component Gating
/ Home
/privacy Privacy
/terms Terms
/auth Auth (staff login)
/reset-password ResetPassword
/customer/auth CustomerAuth
/partner/auth PartnerAuth (imported as AgentAuth)

Staff / back-office routes

All gated by <ProtectedRoute requiredPermissions={[...]}> unless noted.

Dashboard

Route Component Permissions / roles
/app Index allowedRoles=['admin','staff'] (note: currently role-gated, not permission-gated — dashboard.view is seeded but not wired to this route; docs/PERMISSIONS.md §8.2, §8.8)

Inventory

Route Component Permissions
/inventory/quota AirlineBlocks inventory.view
/inventory/quota/:id AirlineBlockDetailPage inventory.view
/inventory/fit FITInventory inventory.view
/inventory/fit/:id FITDetailPage inventory.view
/inventory/b2b-flights B2BFlights inventory.view
/inventory/ground-transfers GroundTransfers inventory.view

Groups

Route Component Permissions
/groups Groups groups.view
/groups/:id GroupDetailPage groups.view

Sales

Route Component Permissions
/sales/customers Customers customers.view
/sales/leads Leads leads.view
/sales/quotations Quotations quotations.view
/sales/bookings Bookings bookings.view
/sales/bookings/:id BookingDetail bookings.view
/sales/bookings/new BookingWizard bookings.create

Partners, customers, suppliers, hotels

Route Component Permissions
/agents Partners (imported as Agents) agents.view
/people People customers.view
/people/employees Employees admin.view
/suppliers Suppliers inventory.view (drift — should be suppliers.view; docs/PERMISSIONS.md §8.5)
/suppliers/:id SupplierDetailPage inventory.view
/hotels Hotels hotels.view

Workflow (approvals, corrections, visa, tickets)

Route Component Permissions
/approvals Approvals approvals.view
/corrections CorrectionsQueue bookings.view (drift — should be approvals.view; docs/PERMISSIONS.md §8.3)
/visa VisaPipeline visa.view
/visa/:id VisaCaseDetail visa.view
/tickets Tickets tickets.view
/tickets/:id TicketDetail tickets.view

Finance & reporting

Route Component Permissions / notes
/accounts Redirect to /finance (<Navigate to="/finance" replace />)
/finance Finance finance.view
/reports Reports reports.view
/requests Requests requests.view

Communications & internal

Route Component Permissions
/communications Communications customers.view (drift — future communications.view; docs/PERMISSIONS.md §8.4)
/whatsapp WhatsAppInbox customers.view (same drift)
/internal/chat Chat (imported as ChatPage) chat.view

Admin

Route Component Permissions
/admin Admin allowedRoles=['admin'] AND requiredPermissions=['admin.view'] (double-gated)

Settings (mixed audience)

Route Component Permissions
/settings/security SecuritySettings allowedRoles=['admin','staff','agent','customer'] — available to every authenticated role

Customer portal

Role-gated; actions inside are ownership-scoped by the API (docs/PERMISSIONS.md §4.5, §6.10).

Route Component Gating
/customer CustomerPortal allowedRoles=['customer']

Partner (B2B agent) portal

Role-gated; actions inside are ownership-scoped (docs/PERMISSIONS.md §4.5, §6.11).

Route Component Gating
/partner PartnerPortal (imported as AgentPortal) allowedRoles=['agent']
/partner/bookings PartnerBookings (imported as AgentBookings) allowedRoles=['agent']
/partner/bookings/new PartnerBookingWizard allowedRoles=['agent']
/partner/invoices PartnerInvoices (imported as AgentInvoices) allowedRoles=['agent']
/partner/inventory PartnerInventory allowedRoles=['agent']
/partner/profile PartnerProfile allowedRoles=['agent']
/partner/reports PartnerReports allowedRoles=['agent']
/partner/flights PartnerFlights (imported as AgentFlights) allowedRoles=['agent']

Fallback

Route Component Notes
* NotFound Catch-all

Cross-cutting behaviour

  • Facebook PixelFacebookPixelTracker (src/App.tsx:104) fires PageView only on public marketing paths (/, /privacy, /terms). Authenticated portals are deliberately excluded (privacy policy).
  • ErrorBoundary — Every route is wrapped (src/App.tsx:135) so a crash in one page doesn't take down the shell.
  • Skip-to-contentSkipToContent + <main id="main-content" tabIndex={-1}> for keyboard users.
  • PWA update promptPWAUpdateNotifier (src/App.tsx:127) surfaces service-worker update notices.
  • React Query defaultsstaleTime: 5m, gcTime: 10m, refetchOnWindowFocus: false, retry: 1 (src/App.tsx:84-98).

Drift tracked against the matrix

Items where the route-level gate and the permissions matrix don't fully agree. Each is listed in docs/PERMISSIONS.md §8:

  • /app uses allowedRoles instead of requiredPermissions=['dashboard.view'] (§8.2, §8.8).
  • /corrections uses bookings.view instead of an approvals.view / dedicated corrections.view (§8.3).
  • /communications and /whatsapp use customers.view instead of communications.view (§8.4).
  • /suppliers uses inventory.view instead of suppliers.view (§8.5).

These are tracked as incremental work; the drift test enforces that the permissions referenced here exist in PERMISSIONS.md and vice-versa.