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;