203 lines
10 KiB
JavaScript
203 lines
10 KiB
JavaScript
import { Suspense, lazy } from 'react';
|
|
import AppToaster from '@/components/third-party/AppToaster';
|
|
import { QueryClientProvider } from '@tanstack/react-query';
|
|
import { queryClientInstance } from '@/lib/query-client';
|
|
import { Navigate, BrowserRouter as Router, Route, Routes } from 'react-router-dom';
|
|
import PageNotFound from './lib/PageNotFound';
|
|
import { AuthProvider, useAuth } from '@/lib/AuthContext';
|
|
import { ZoneProvider } from '@/lib/ZoneContext';
|
|
import ScrollToTop from './components/ScrollToTop';
|
|
import ProtectedRoute from '@/components/ProtectedRoute';
|
|
import AdminLayout from '@/layouts/AdminLayout';
|
|
import Login from '@/pages/Login';
|
|
|
|
/* Every console page is code-split. The Dispatch board alone pulls the map,
|
|
the solver payload builders and the comparison panels; loading that on the
|
|
way to the Hubs table would make every first paint pay for it. */
|
|
/* Doormile-only pages. A client login is sent home rather than shown a page
|
|
whose every request the server refuses. */
|
|
function StaffOnly({ children }) {
|
|
const { isClient } = useAuth();
|
|
return isClient ? <Navigate to="/doormile/home" replace /> : children;
|
|
}
|
|
|
|
const Dispatch = lazy(() => import('@/pages/doormile/dispatch/Dispatch.jsx'));
|
|
const DispatchPreview = lazy(() => import('@/pages/doormile/dispatch/Preview.jsx'));
|
|
|
|
const Home = lazy(() => import('@/pages/doormile/home/Home.jsx'));
|
|
const Orders = lazy(() => import('@/pages/doormile/orders/Orders'));
|
|
const Bookings = lazy(() => import('@/pages/doormile/bookings/Bookings'));
|
|
const OrdersPreview = lazy(() => import('@/pages/doormile/orders/OrdersPreview'));
|
|
const CreateOrder = lazy(() => import('@/pages/doormile/orders/CreateOrder'));
|
|
const MultipleOrders = lazy(() => import('@/pages/doormile/orders/MultipleOrders'));
|
|
|
|
const Deliveries = lazy(() => import('@/pages/doormile/deliveries/Deliveries'));
|
|
const Returns = lazy(() => import('@/pages/doormile/returns/Returns'));
|
|
|
|
const Tenants = lazy(() => import('@/pages/doormile/clients/Tenants'));
|
|
const CreateClient = lazy(() => import('@/pages/doormile/clients/CreateClient'));
|
|
const ClientOnboarding = lazy(() => import('@/pages/doormile/clients/ClientOnboarding'));
|
|
const CreateCustomer = lazy(() => import('@/pages/doormile/clients/CreateCustomer'));
|
|
|
|
const Pricing = lazy(() => import('@/pages/doormile/pricing/Pricing'));
|
|
|
|
const Riders = lazy(() => import('@/pages/doormile/riders/Riders'));
|
|
const CreateRider = lazy(() => import('@/pages/doormile/riders/CreateRider'));
|
|
const EditRider = lazy(() => import('@/pages/doormile/riders/EditRider'));
|
|
|
|
const Customers = lazy(() => import('@/pages/doormile/customers/Customers'));
|
|
const Hubs = lazy(() => import('@/pages/doormile/hubs/Hubs'));
|
|
const Vehicles = lazy(() => import('@/pages/doormile/vehicles/Vehicles'));
|
|
const AppUsers = lazy(() => import('@/pages/doormile/appUsers/AppUsers'));
|
|
const Tripsheets = lazy(() => import('@/pages/doormile/tripsheets/Tripsheets'));
|
|
const Exceptions = lazy(() => import('@/pages/doormile/exceptions/Exceptions'));
|
|
const CompetitiveIntel = lazy(() => import('@/pages/doormile/competitiveIntel/CompetitiveIntel'));
|
|
|
|
const OrdersSummary = lazy(() => import('@/pages/doormile/reports/OrdersSummary'));
|
|
const OrdersDetails = lazy(() => import('@/pages/doormile/reports/OrdersDetails'));
|
|
const RidersSummary = lazy(() => import('@/pages/doormile/reports/RidersSummary'));
|
|
const Profitability = lazy(() => import('@/pages/doormile/reports/Profitability'));
|
|
|
|
const ViewProfile = lazy(() => import('@/pages/doormile/ViewProfile'));
|
|
const Settings = lazy(() => import('@/pages/doormile/settings/Settings'));
|
|
const Agents = lazy(() => import('@/pages/doormile/agents/Agents'));
|
|
|
|
const PageFallback = () => (
|
|
<div className="flex min-h-[60vh] items-center justify-center">
|
|
<div className="h-8 w-8 animate-spin rounded-full border-4 border-slate-200 border-t-slate-800" />
|
|
</div>
|
|
);
|
|
|
|
const AuthenticatedApp = () => {
|
|
const { isLoadingAuth, isLoadingPublicSettings } = useAuth();
|
|
|
|
if (isLoadingPublicSettings || isLoadingAuth) {
|
|
return (
|
|
<div className="fixed inset-0 flex items-center justify-center">
|
|
<div className="w-8 h-8 border-4 border-slate-200 border-t-slate-800 rounded-full animate-spin"></div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Suspense fallback={<PageFallback />}>
|
|
<Routes>
|
|
{/* Sign-in — outside the protected boundary. */}
|
|
<Route path="/login" element={<Login />} />
|
|
|
|
<Route element={<ProtectedRoute />}>
|
|
{/* Dispatch is the console's home: it is the board operators sit on
|
|
all day, and the one page that answers "what is happening right
|
|
now" without a filter being set first. */}
|
|
<Route path="/" element={<Navigate to="/doormile/home" replace />} />
|
|
<Route path="/home" element={<Navigate to="/doormile/home" replace />} />
|
|
<Route path="/customers" element={<Navigate to="/doormile/customers" replace />} />
|
|
<Route path="/customer" element={<Navigate to="/doormile/customers" replace />} />
|
|
|
|
<Route path="/doormile" element={<AdminLayout />}>
|
|
<Route index element={<Navigate to="/doormile/home" replace />} />
|
|
|
|
<Route path="home" element={<Home />} />
|
|
{/* Control X — the board formerly routed at /doormile/dispatch.
|
|
|
|
The directory, the component and the .dispatch-container CSS
|
|
namespace keep their old names on purpose: they are internal,
|
|
and renaming a 5,800-line stylesheet's root class is a large
|
|
diff with no user-visible benefit and real risk of missing a
|
|
selector. Only what an operator sees and types changes. */}
|
|
<Route path="control-x" element={<Dispatch />} />
|
|
<Route path="control-x/preview" element={<DispatchPreview />} />
|
|
|
|
{/* The old paths keep working.
|
|
|
|
This board is the page operators sit on all day, so it is the
|
|
one most likely to be bookmarked, pinned in a browser tab
|
|
group, or pasted into a message. Renaming the route without
|
|
this would turn every one of those into a 404 — a support
|
|
problem with no upside, avoided by two lines. `replace` so the
|
|
dead path does not linger in history behind a Back press. */}
|
|
<Route path="dispatch" element={<Navigate to="/doormile/control-x" replace />} />
|
|
<Route path="dispatch/preview" element={<Navigate to="/doormile/control-x/preview" replace />} />
|
|
|
|
{/* Static before dynamic, so `orders/create` cannot be read as an
|
|
order whose id is "create". */}
|
|
<Route path="orders" element={<Orders />} />
|
|
<Route path="orders/create" element={<CreateOrder />} />
|
|
<Route path="orders/multiple" element={<MultipleOrders />} />
|
|
<Route path="orders/createorders" element={<MultipleOrders />} />
|
|
<Route path="orders/preview" element={<OrdersPreview />} />
|
|
|
|
<Route path="bookings" element={<Bookings />} />
|
|
|
|
<Route path="deliveries" element={<Deliveries />} />
|
|
{/* Reverse logistics: parcels going back to the sender (RTO). */}
|
|
<Route path="returns" element={<Returns />} />
|
|
|
|
<Route path="tenants" element={<StaffOnly><Tenants /></StaffOnly>} />
|
|
<Route path="clients/create" element={<StaffOnly><CreateClient /></StaffOnly>} />
|
|
{/* Owner-only (admin@doormile.com); the page and the server both check. */}
|
|
<Route path="clients/onboard" element={<StaffOnly><ClientOnboarding /></StaffOnly>} />
|
|
<Route path="customer/create" element={<CreateCustomer />} />
|
|
|
|
<Route path="pricing" element={<Pricing />} />
|
|
|
|
<Route path="milers" element={<Riders />} />
|
|
<Route path="milers/create" element={<CreateRider />} />
|
|
<Route path="milers/edit" element={<EditRider />} />
|
|
|
|
{/* Backwards-compatible aliases */}
|
|
<Route path="riders" element={<Navigate to="/doormile/milers" replace />} />
|
|
<Route path="riders/create" element={<Navigate to="/doormile/milers/create" replace />} />
|
|
<Route path="riders/edit" element={<Navigate to="/doormile/milers/edit" replace />} />
|
|
|
|
<Route path="customers" element={<Customers />} />
|
|
<Route path="hubs" element={<Hubs />} />
|
|
<Route path="vehicles" element={<StaffOnly><Vehicles /></StaffOnly>} />
|
|
<Route path="app-users" element={<StaffOnly><AppUsers /></StaffOnly>} />
|
|
<Route path="tripsheets" element={<StaffOnly><Tripsheets /></StaffOnly>} />
|
|
<Route path="exceptions" element={<Exceptions />} />
|
|
<Route path="competitive-intel" element={<StaffOnly><CompetitiveIntel /></StaffOnly>} />
|
|
|
|
<Route path="reports/orderssummary" element={<OrdersSummary />} />
|
|
<Route path="reports/ordersdetails" element={<OrdersDetails />} />
|
|
<Route path="reports/riderssummary" element={<RidersSummary />} />
|
|
<Route path="reports/profitability" element={<Profitability />} />
|
|
|
|
<Route path="profile" element={<ViewProfile />} />
|
|
<Route path="settings" element={<Settings />} />
|
|
|
|
{/* The AI_engine swarm. Read-only: it reports what the agents are
|
|
and what they last did, and has no control surface, because
|
|
every autonomy gate lives in that repository's own env. */}
|
|
<Route path="agents" element={<StaffOnly><Agents /></StaffOnly>} />
|
|
</Route>
|
|
</Route>
|
|
|
|
<Route path="*" element={<PageNotFound />} />
|
|
</Routes>
|
|
</Suspense>
|
|
);
|
|
};
|
|
|
|
function App() {
|
|
return (
|
|
<AuthProvider>
|
|
<QueryClientProvider client={queryClientInstance}>
|
|
<ZoneProvider>
|
|
<Router>
|
|
<ScrollToTop />
|
|
<AuthenticatedApp />
|
|
</Router>
|
|
{/* Every toast in the app (OpenToast, the design system's `toast`,
|
|
direct react-hot-toast calls) renders here: top-right, one card
|
|
design. Without a mounted renderer every toast fires into
|
|
nothing — including the data layer's failure notices. */}
|
|
<AppToaster />
|
|
</ZoneProvider>
|
|
</QueryClientProvider>
|
|
</AuthProvider>
|
|
);
|
|
}
|
|
|
|
export default App;
|