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 Pixel —
FacebookPixelTracker (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-content —
SkipToContent + <main id="main-content" tabIndex={-1}> for keyboard users.
- PWA update prompt —
PWAUpdateNotifier (src/App.tsx:127) surfaces service-worker update notices.
- React Query defaults —
staleTime: 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.