import { AppLayout, type SidebarItem } from "@/components/AppLayout";
import { useDisclosure } from "@mantine/hooks";
import {
Home,
Layers,
LayoutDashboard,
LifeBuoy,
Loader2,
// MapPin,
Package,
Receipt,
Settings,
} from "lucide-react";
import { useEffect, useRef } from "react";
import {
Navigate,
Outlet,
Route,
Routes,
useLocation,
useNavigate,
} from "react-router-dom";
import OnboardingResumeBanner, {
AccountReviewBanner,
} from "./components/onboarding/OnboardingResumeBanner";
import OnboardingWizardDialog from "./components/onboarding/OnboardingWizardDialog";
import { ApiErrorModal } from "./components/errors/ApiErrorModal";
import useAuth from "./hooks/useAuth";
import { useIdentify } from "./lib/posthog";
import {
startTokenRefreshScheduler,
stopTokenRefreshScheduler,
} from "./utils/refreshScheduler";
import EDRFreightLandingPage from "./pages/EDRFreightLandingPage";
import MyPortalPage from "./pages/MyPortalPage";
import MySignaturePage from "./pages/MySignaturePage";
import SettingsPage from "./pages/SettingsPage";
import ForgotPasswordPage from "./pages/accounts/ForgotPasswordPage";
import ResetPasswordLinkPage from "./pages/accounts/ResetPasswordLinkPage";
import LoginPage from "./pages/accounts/LoginPage";
import SetPasswordPage from "./pages/accounts/SetPasswordPage";
import SignupPage from "./pages/accounts/SignupPage";
import VerificationOtpPage from "./pages/accounts/VerificationOtpPage";
import InvoiceDetailPage from "./pages/billing/InvoiceDetailPage";
import InvoicesList from "./pages/billing/InvoicesList";
import BookingContractPage from "./pages/bookings/BookingContractPage";
import BookingDetailPage from "./pages/bookings/BookingDetailPage";
import BookingsListPage from "./pages/bookings/BookingsListPage";
import EditBookingPage from "./pages/bookings/EditBookingPage";
import LastMileConfirmPage from "./pages/bookings/last-mile-confirm/LastMileConfirmPage";
import LastMileContractPage from "./pages/bookings/last-mile-contract/LastMileContractPage";
import ContractDetailPage from "./pages/contracts/ContractDetailPage";
import ContractViewPage from "./pages/contracts/ContractViewPage";
import ContractsList from "./pages/contracts/ContractsList";
import NewContractPage from "./pages/contracts/NewContractPage";
import NewShipmentPage from "./pages/contracts/NewShipmentPage";
import NewShipmentRequestPage from "./pages/contracts/NewShipmentRequestPage";
import CheckPaymentPage from "./pages/payments/CheckPaymentPage";
import PaymentFailurePage from "./pages/payments/PaymentFailurePage";
import FaydaCallbackPage from "./pages/FaydaCallbackPage";
import PaymentSuccessPage from "./pages/payments/PaymentSuccessPage";
import {
ShippingLineBookingDetailPage,
ShippingLineBookingsPage,
ShippingLineCompletePage,
ShippingLineHelpPage,
ShippingLineHomePage,
ShippingLineInvoicesPage,
ShippingLineSettingsPage,
} from "./pages/shipping-line";
import {
TransitAgentBookingsPage,
TransitAgentBookingDetailPage,
TransitAgentOverviewPage,
} from "./pages/transit-agent";
import FaqPage from "./pages/support/FaqPage";
import HelpPage from "./pages/support/HelpPage";
import PrivacyPolicyPage from "./pages/support/PrivacyPolicyPage";
import TermsPage from "./pages/support/TermsPage";
import TrackingPage from "./pages/tracking/TrackingPage";
function FullScreenSpinner() {
return (
);
}
function LogoutHandler() {
const { logout } = useAuth();
const navigate = useNavigate();
const hasRun = useRef(false);
useEffect(() => {
if (hasRun.current) return;
hasRun.current = true;
logout().then(() => navigate("/login", { replace: true }));
}, []);
return ;
}
/** Blocks unauthenticated users; renders children only with a valid session. */
function RequireAuth() {
const { isPending, isAuthenticated } = useAuth();
const location = useLocation();
if (isPending) return ;
if (!isAuthenticated)
return ;
return ;
}
/**
* Waits for the company query so downstream routes can rely on it being
* resolved. Onboarding is enforced by OnboardingGate, not here.
*/
function RequireCompany() {
const { customerQuery } = useAuth();
if (customerQuery.isPending) return ;
return ;
}
/**
* Routes an un-onboarded user may still visit. The wizard auto-opens but is
* dismissable, so they can browse these freely; any other route forces the
* wizard back open and bounces them home.
*/
const ONBOARDING_ALLOWED_PATHS = ["/portal", "/signature"];
function isOnboardingAllowedPath(pathname: string): boolean {
const path = pathname.toLowerCase();
return ONBOARDING_ALLOWED_PATHS.some(
(p) => path === p || path.startsWith(p + "/"),
);
}
/**
* Enforces first-run onboarding. The home (dashboard) and signature pages stay
* reachable while onboarding is incomplete; the wizard auto-opens on login but
* can be dismissed to use those pages. Visiting any other page bounces back to
* home and re-opens the wizard. New users (no company yet) are treated the same
* as users who haven't completed onboarding.
*
* Shipping lines are exempt: staff register them with their details already
* captured, so there is nothing for them to onboard — they go straight to home.
*/
function OnboardingGate() {
const { company, onboardingCompleted, isShippingLine, isTransitAgent } =
useAuth();
const location = useLocation();
// Keyed off a positive shipping-line / transit-agent identification, never
// off "no company": that is also true mid-fetch and on error, which would let
// customers slip past onboarding whenever the request failed.
const needsOnboarding =
isShippingLine || isTransitAgent ? false : !company || !onboardingCompleted;
const allowedHere = isOnboardingAllowedPath(location.pathname);
// Open by default while onboarding is pending (covers the login case).
const [wizardOpen, { open: openWizard, close: closeWizard }] =
useDisclosure(false);
// Re-evaluate on every navigation: force the wizard open on blocked routes,
// and auto-open on first arrival while onboarding is pending.
useEffect(() => {
if (needsOnboarding && !allowedHere) {
openWizard();
}
}, [needsOnboarding, allowedHere, location.pathname, openWizard]);
// Auto-open once when onboarding becomes/loads as pending (login).
const autoOpenedRef = useRef(false);
useEffect(() => {
if (needsOnboarding && !autoOpenedRef.current) {
autoOpenedRef.current = true;
openWizard();
}
if (!needsOnboarding) autoOpenedRef.current = false;
}, [needsOnboarding, openWizard]);
if (needsOnboarding && !allowedHere) {
return ;
}
return (
<>
{needsOnboarding && }
{!needsOnboarding && }
>
);
}
/**
* Customer-only routes. A shipping line that lands on one (an old link, a
* bookmark, a hand-typed URL) is sent to its own home rather than shown a
* contract/company-shaped page that has no meaning for it.
*/
function RequireCustomer() {
const { isShippingLine, isTransitAgent, customerQuery } = useAuth();
// RequireCompany already awaits this query, but guard anyway: a refetch can
// flip `isPending` back on, and redirecting on a half-loaded account would
// throw the user into the wrong app.
if (customerQuery.isPending) return ;
if (isShippingLine) return ;
if (isTransitAgent) return ;
return ;
}
/** The mirror of RequireCustomer: shipping-line routes, closed to customers. */
function RequireShippingLine() {
const { isShippingLine, customerQuery } = useAuth();
if (customerQuery.isPending) return ;
if (!isShippingLine) return ;
return ;
}
/** Transit-agent routes, closed to every other account kind. */
function RequireTransitAgent() {
const { isTransitAgent, customerQuery } = useAuth();
if (customerQuery.isPending) return ;
if (!isTransitAgent) return ;
return ;
}
/**
* Where a signed-in account belongs. Shipping lines and customers have separate
* apps, so every "you're already logged in" redirect has to pick between them.
* Waits for the company query: `isShippingLine` is false while that request is
* still in flight, which would land a shipping line on the customer home first.
*/
function useHomeRoute(): { ready: boolean; href: string } {
const { isShippingLine, isTransitAgent, customerQuery } = useAuth();
return {
ready: !customerQuery.isPending,
href: isShippingLine
? "/shipping-line"
: isTransitAgent
? "/transit-agent"
: "/portal",
};
}
/** Keeps authenticated users off the login/signup pages. */
function RedirectIfAuthed() {
const { isPending, isAuthenticated } = useAuth();
const home = useHomeRoute();
if (isPending) return ;
if (isAuthenticated) {
if (!home.ready) return ;
return ;
}
return ;
}
/** Landing page for visitors; authenticated users go straight to the portal. */
function LandingRoute() {
const { isPending, isAuthenticated } = useAuth();
const home = useHomeRoute();
if (isPending) return ;
if (isAuthenticated) {
if (!home.ready) return ;
return ;
}
return ;
}
const sidebarItems: SidebarItem[] = [
{ label: "Home", href: "/portal", icon: },
{
label: "Contracts",
href: "/contracts",
icon: ,
},
{
label: "Bookings",
href: "/bookings",
icon: ,
},
// {
// label: "Tracking",
// href: "/tracking",
// icon: ,
// },
{
label: "Invoices",
href: "/billing",
icon: ,
},
{
section: "Account",
label: "Settings",
href: "/settings",
icon: ,
},
{
section: "Account",
label: "Help & Support",
href: "/help",
icon: ,
},
];
/**
* Sidebar for shipping lines. Intentionally its own list rather than a filtered
* view of `sidebarItems`: shipping lines have no contracts, and their Home /
* Bookings / Invoices pages are different pages at different routes.
*/
const shippingLineSidebarItems: SidebarItem[] = [
{ label: "Home", href: "/shipping-line", icon: },
{
label: "Bookings",
href: "/shipping-line/bookings",
icon: ,
},
{
label: "Invoices",
href: "/shipping-line/invoices",
icon: ,
},
{
section: "Account",
label: "Settings",
href: "/shipping-line/settings",
icon: ,
},
{
section: "Account",
label: "Help & Support",
href: "/shipping-line/help",
icon: ,
},
];
/**
* Sidebar for transit agents. Two entries only — the rest of the portal
* (contracts, invoices, settings, support) is company-scoped and has no meaning
* for an agent, so nothing is filtered in from the other lists.
*/
const transitAgentSidebarItems: SidebarItem[] = [
{
label: "Overview",
href: "/transit-agent",
icon: ,
},
{
label: "Bookings",
href: "/transit-agent/bookings",
icon: ,
},
];
const App = () => {
const navigate = useNavigate();
const location = useLocation();
const {
user,
company,
companyType,
createProfile,
reapplyProfile,
isAuthenticated,
isShippingLine,
isTransitAgent,
} = useAuth();
// Attribute replays and exceptions to the signed-in user (id/org only).
useIdentify(user, company);
// Keep the server session alive while a user is logged in. Runs after
// login, signup, and page-reload bootstrap alike.
useEffect(() => {
if (!isAuthenticated) {
stopTokenRefreshScheduler();
return;
}
startTokenRefreshScheduler();
return stopTokenRefreshScheduler;
}, [isAuthenticated]);
const displayName = user?.name?.en || user?.username || user?.email || "User";
const userEmail = user?.email;
const companyProfiles = company?.company?.companyProfiles ?? [];
return (
<>
{/* Global API error modal — shows the server's actual error message for
every failed request (suppressed on onboarding/auth pages). */}
{/* Public routes */}
} />
} />
}
/>
{/* Payment provider browser redirects (PAYMENT_RETURN_URL / PAYMENT_FAILURE_URL) */}
{/* Fayda (eSignet) redirect_uri — the whole tab lands here after
verification, completes the code/state exchange and navigates back
to the page that started it. Public on purpose: behind RequireAuth
the onboarding gate would redirect away before the exchange ran. */}
} />
} />
} />
} />
{/* Help and legal pages. Public on purpose: the auth screens link to
them before a session exists, so they carry their own chrome rather
than sitting inside the authenticated app layout. */}
} />
} />
} />
} />
{/* Auth pages — inaccessible once logged in */}
}>
} />
} />
} />
{/* Staff-issued reset links land here. Deliberately outside
RedirectIfAuthed: a customer with a stale session still needs the link
to work, and the token — not the session — is what authorises it. */}
} />
{/* Signup-flow pages; reached while a session already exists */}
} />
} />
}>
}>
{/* Shipping-line app. Its own layout and sidebar, and its own pages
at their own routes — nothing here is shared with the customer
branch below beyond the shell component itself. Contracts are
absent by design: shipping lines request bookings directly. */}
{isShippingLine && (
}>
}
>
}
/>
}
/>
}
/>
}
/>
}
/>
{/* Same detail component as the customer's /billing/:id — the
API scopes my-invoices to the signed-in payer either way,
and the page derives its back target from the URL. */}
}
/>
}
/>
}
/>
}
/>
{/* Old shared links land on the shipping-line equivalents. */}
}
/>
)}
{/* Transit-agent app. Two pages, both empty for now, behind their
own layout — there is no support widget because the chat is
company-scoped and an agent has no company, exactly as for a
shipping line. */}
{isTransitAgent && (
}>
}
>
}
/>
}
/>
}
/>
)}
{/* Customer app — unchanged. */}
}>
}
>
} />
{/* Bookings are created against a contract, but the full list is
browsable here. New-booking entry still routes via a contract. */}
} />
}
/>
}
/>
} />
}
/>
}
/>
}
/>
} />
} />
}
/>
}
/>
}
/>
{/* Completion of an initiated (bare) booking after per-booking
clearance — same form, submits to the complete endpoint. */}
}
/>
}
/>
} />
} />
} />
} />
{/* Profile was merged into Settings — keep old links working. */}
}
/>
} />
} />
} />
>
);
};
export default App;