Files
emaui/apps/portal/src/app/router.tsx

470 lines
17 KiB
TypeScript

import { createBrowserRouter, Navigate } from "react-router-dom";
import { ThemeGallery } from "@ema-platform/ui";
import { PortalLayout } from "./layouts/PortalLayout";
import { ProtectedRoute } from "./components/ProtectedRoute";
import { LandingRoute } from "./components/LandingRoute";
// Auth (standalone pages, no portal chrome)
import {
LoginPage,
SignupPage,
OTPVerificationPage,
ForgotPasswordPage,
SetPasswordPage,
RequirePermission,
LICENSE_PERMISSIONS,
PORTAL_PERMISSIONS,
} from "@ema-platform/auth";
// Same grant keys as the matching PortalLayout nav items; the API enforces
// the real rule, this just keeps direct URLs consistent with the hidden nav.
const P = PORTAL_PERMISSIONS;
const L = LICENSE_PERMISSIONS;
// Portal feature pages
import { DashboardPage } from "./features/dashboard/pages/DashboardPage";
import { RequireOperations } from "./features/onboarding/components/RequireOperations";
import { OperationsOnboardingPage } from "./features/onboarding/pages/OperationsOnboardingPage";
import { ProfilePage } from "./features/profile/pages/ProfilePage";
import { SupportPage } from "./features/support/pages/SupportPage";
import { MedicalRecordsPage, SeaServicePage } from "./features/seafarer/pages/SeaRecords";
import { SeafarerRegistrationPage } from "./features/seafarer-registration/pages/SeafarerRegistrationPage";
import { ExamsPage } from "./features/exams/pages/ExamsPage";
import { ExamAttemptPage } from "./features/exam-attempt/pages/ExamAttemptPage";
// Phase 1 pages
import { DocumentVaultPage } from "./features/documents/pages/DocumentVaultPage";
import { SeamanBookPage } from "./features/seaman-book/pages/SeamanBookPage";
import { BasicSafetyTrainingPage } from "./features/basic-safety-training/pages/BasicSafetyTrainingPage";
import { NotificationsPage } from "./features/notifications/pages/NotificationsPage";
// Phase 2 — CoC / CoP
import { CertificatesPage } from "./features/certificates/pages/CertificatesPage";
// Phase 3 — Endorsement
import { EndorsementPage } from "./features/endorsement/pages/EndorsementPage";
import { VesselRegistrationPage } from "./features/vessel-registration/pages/VesselRegistrationPage";
import { VesselTransferPage } from "./features/vessel-registration/pages/VesselTransferPage";
import { MyApplicationsPage } from "./features/licensing/pages/MyApplicationsPage";
import { PaymentCheckPage } from "./features/payments/pages/PaymentCheckPage";
import { PaymentSuccessPage } from "./features/payments/pages/PaymentSuccessPage";
import { PaymentFailurePage } from "./features/payments/pages/PaymentFailurePage";
import { LicenseApplicationPage } from "./features/licensing/pages/LicenseApplicationPage";
import { ApplicationRedirectPage } from "./features/licensing/pages/ApplicationRedirectPage";
import { WaiverPage } from "./features/waiver/pages/WaiverPage";
import { VesselRegistrationStatusPage } from "./features/vessel-registration/pages/VesselRegistrationStatusPage";
export const router = createBrowserRouter([
// Public landing page — institutional overview + role-based entry points.
{ path: "/", element: <LandingRoute /> },
// Theme visual-regression surface. Unauthenticated by design — it renders
// only static primitives, so it needs no API and cannot flake.
{ path: "/__gallery", element: <ThemeGallery /> },
// Public auth pages
{ path: "/login", element: <LoginPage /> },
{ path: "/signup", element: <SignupPage /> },
// Completes the forgot-password flow; the reset message links here. The
// IAM package generates `/reset-password` links, `/set-password` is the
// first-time-credential variant — one page serves both.
{ path: "/set-password", element: <SetPasswordPage /> },
{ path: "/reset-password", element: <SetPasswordPage /> },
// Reached from the login page by a logged-out user, so it must stay
// public — ProtectedRoute would bounce them straight to the landing page.
{ path: "/forgot-password", element: <ForgotPasswordPage /> },
// Protected auth pages
{
element: (
<ProtectedRoute>
<OTPVerificationPage />
</ProtectedRoute>
),
path: "/otp-verify",
},
// The two-step setup wizard is gone. Signing up lands on the dashboard, and
// profile details are collected where they are actually needed: on /profile,
// via the dashboard nudge, or inline in an application flow. The path stays
// as a redirect so existing bookmarks and emailed links do not 404.
{ path: "/profile-setup", element: <Navigate to="/profile" replace /> },
// Portal — protected.
{
element: (
<ProtectedRoute>
{/* Asks what the applicant operates as before the rest of the
portal, which is filtered by that answer. */}
<RequireOperations>
<PortalLayout />
</RequireOperations>
</ProtectedRoute>
),
children: [
{ path: "/dashboard", element: <DashboardPage /> },
{ path: "/onboarding/operations", element: <OperationsOnboardingPage /> },
// Config-driven licensing: one set of pages serves every licence type.
{
path: "/licensing/applications",
element: (
<RequirePermission anyOf={[L.VIEW_OWN_APPLICATIONS]}>
<MyApplicationsPage />
</RequirePermission>
),
},
// Telebirr returns the applicant to these.
{ path: "/payments/check", element: <PaymentCheckPage /> },
{ path: "/payments/success", element: <PaymentSuccessPage /> },
{ path: "/payments/failure", element: <PaymentFailurePage /> },
// Seaman Book and BTC are auto-opened together after seafarer approval.
// They use the shared status/payment page, never the generic form wizard.
{
path: "/licensing/BTC_BASIC_TRAINING/apply",
element: <Navigate to="/basic-training-certificate" replace />,
},
{
path: "/licensing/BTC_BASIC_TRAINING/applications/:applicationId",
element: <Navigate to="/basic-training-certificate" replace />,
},
{
path: "/licensing/SEAMAN_BOOK/apply",
element: <Navigate to="/seaman-book" replace />,
},
{
path: "/licensing/SEAMAN_BOOK/applications/:applicationId",
element: <Navigate to="/seaman-book" replace />,
},
// Seafarer registration is not a licence: it has its own page and API.
{
path: "/licensing/SEAFARER_REGISTRATION/apply",
element: <Navigate to="/seafarer-registration" replace />,
},
{
path: "/licensing/SEAFARER_REGISTRATION/applications/:applicationId",
element: <Navigate to="/seafarer-registration" replace />,
},
{
path: "/licensing/:typeCode/apply",
element: (
<RequirePermission anyOf={[L.CREATE_APPLICATION]}>
<LicenseApplicationPage />
</RequirePermission>
),
},
{
path: "/licensing/:typeCode/applications/:applicationId",
element: (
<RequirePermission anyOf={[L.VIEW_OWN_APPLICATIONS]}>
<LicenseApplicationPage />
</RequirePermission>
),
},
// Notification / email deep links arrive as /applications/<id>; resolve the
// licence type and forward to the canonical route.
{
path: "/applications/:applicationId",
element: (
<RequirePermission anyOf={[L.VIEW_OWN_APPLICATIONS]}>
<ApplicationRedirectPage />
</RequirePermission>
),
},
// Seafarer
//
// Registration is its own five-step form over its own endpoints —
// not a configured licence type. A draft opens on first visit; once
// submitted the page shows the registration's status and answers.
{
path: "/seafarer-registration",
element: (
<RequirePermission anyOf={[P.APPLY_SEAFARER_REGISTRATION]}>
<SeafarerRegistrationPage />
</RequirePermission>
),
},
// Sea service and medical certificates each have a page of their own.
{
path: "/seafarer/sea-service",
element: (
<RequirePermission anyOf={[P.VIEW_OWN_SEA_SERVICE]}>
<SeaServicePage />
</RequirePermission>
),
},
{
path: "/seafarer/medical",
element: (
<RequirePermission anyOf={[P.VIEW_OWN_MEDICAL]}>
<MedicalRecordsPage />
</RequirePermission>
),
},
{ path: "/seafarer/records", element: <Navigate to="/seafarer/sea-service" replace /> },
{
path: "/exams",
element: (
<RequirePermission anyOf={[P.VIEW_OWN_EXAM, P.APPLY_EXAM]}>
<ExamsPage />
</RequirePermission>
),
},
{
path: "/exams/:examId/take",
element: (
<RequirePermission anyOf={[P.APPLY_EXAM, P.VIEW_OWN_EXAM]}>
<ExamAttemptPage />
</RequirePermission>
),
},
// The public-facing registry was a hardcoded mock and does not belong in
// the applicant portal; officers browse seafarers in the backoffice.
{
path: "/seafarer-registry",
element: <Navigate to="/seafarer-registration" replace />,
},
{
path: "/seafarer-registry/:id",
element: <Navigate to="/seafarer-registration" replace />,
},
// Phase 1
{ path: "/documents", element: <DocumentVaultPage /> },
{
path: "/seaman-book",
element: (
<RequirePermission
anyOf={[P.VIEW_OWN_SEA_SERVICE, P.VIEW_OWN_MEDICAL]}
>
<SeamanBookPage />
</RequirePermission>
),
},
{
path: "/basic-training-certificate",
element: (
<RequirePermission anyOf={[P.VIEW_OWN_CERTIFICATES]}>
<SeamanBookPage />
</RequirePermission>
),
},
// Requested automatically with the seafarer registration — nothing to file.
{ path: "/seaman-book/apply", element: <Navigate to="/seaman-book" replace /> },
{ path: "/medical", element: <Navigate to="/seafarer/medical" replace /> },
{
path: "/basic-safety-training",
element: (
<RequirePermission anyOf={[P.VIEW_OWN_CERTIFICATES]}>
<BasicSafetyTrainingPage />
</RequirePermission>
),
},
{ path: "/notifications", element: <NotificationsPage /> },
// Phase 2 — CoC / CoP
{
path: "/certificates",
element: (
<RequirePermission anyOf={[P.VIEW_OWN_CERTIFICATES]}>
<CertificatesPage />
</RequirePermission>
),
},
// CoC/CoP applications go through the generic license wizard —
// /licensing/CERTIFICATE_OF_COMPETENCY/apply and
// /licensing/CERTIFICATE_OF_PROFICIENCY/apply, wired below.
// Phase 3 — Endorsement
{
path: "/endorsements",
element: (
<RequirePermission anyOf={[P.VIEW_OWN_CERTIFICATES]}>
<EndorsementPage />
</RequirePermission>
),
},
{
path: "/vessel-registration",
element: (
<RequirePermission
anyOf={[P.APPLY_VESSEL_REGISTRATION, P.VIEW_OWN_VESSELS]}
>
<VesselRegistrationPage />
</RequirePermission>
),
},
// The registration wizard is the config-driven licensing flow; the old
// standalone wizard posted to endpoints that never existed.
{
path: "/vessel-registration/apply",
element: <Navigate to="/licensing/VESSEL_REGISTRATION/apply" replace />,
},
{
path: "/vessel-registration-dashboard",
element: <Navigate to="/vessel-registration" replace />,
},
{
path: "/vessel-ownership-transfer",
element: (
<RequirePermission
anyOf={[P.APPLY_VESSEL_REGISTRATION, P.VIEW_OWN_VESSELS]}
>
<VesselTransferPage />
</RequirePermission>
),
},
// The nav item used to nest this under /vessel-registration, which made
// the sidebar's prefix-match (nav-utils.ts isItemActive) light up both
// items at once. Kept as a redirect for old bookmarks/links.
{
path: "/vessel-registration/transfer",
element: <Navigate to="/vessel-ownership-transfer" replace />,
},
// Legacy per-licence-type URLs. Each once had its own hand-written page
// that posted to a `/logistics-licenses/*` endpoint the API never had,
// and dropped every uploaded document on the floor. They are kept as
// redirects so old bookmarks land somewhere real; the config-driven
// wizard below serves every licence type from one place.
{
path: "/logistics-dashboard",
element: <Navigate to="/dashboard" replace />,
},
{
path: "/freight-forwarder-license",
element: <Navigate to="/licensing/applications" replace />,
},
{
path: "/freight-forwarder-license/apply",
element: <Navigate to="/licensing/FREIGHT_FORWARDER/apply" replace />,
},
{
path: "/freight-forwarder-license/:id/renew",
element: <Navigate to="/licensing/applications" replace />,
},
{
path: "/shipping-agent-license",
element: <Navigate to="/licensing/applications" replace />,
},
{
path: "/shipping-agent-license/apply",
element: <Navigate to="/licensing/SHIPPING_AGENT/apply" replace />,
},
{
path: "/shipping-agent-license/:id/renew",
element: <Navigate to="/licensing/applications" replace />,
},
{
path: "/combined-license",
element: <Navigate to="/licensing/applications" replace />,
},
{
path: "/combined-license/apply",
element: <Navigate to="/licensing/COMBINED_SA_FF/apply" replace />,
},
{
path: "/combined-license/:id/renew",
element: <Navigate to="/licensing/applications" replace />,
},
{
path: "/joint-investment-license",
element: <Navigate to="/licensing/applications" replace />,
},
{
path: "/joint-investment-license/apply",
element: <Navigate to="/licensing/JOINT_INVESTOR/apply" replace />,
},
{
path: "/joint-investment-license/:id/renew",
element: <Navigate to="/licensing/applications" replace />,
},
{
path: "/mto-license",
element: <Navigate to="/licensing/applications" replace />,
},
{
path: "/mto-license/apply",
element: (
<Navigate
to="/licensing/MULTIMODAL_TRANSPORT_OPERATOR/apply"
replace
/>
),
},
{
path: "/mto-license/:id/renew",
element: <Navigate to="/licensing/applications" replace />,
},
// Waiver has no backend yet, so it says so rather than pretending.
{
path: "/waiver",
element: (
<RequirePermission anyOf={[P.APPLY_WAIVER, P.VIEW_WAIVER_LETTER]}>
<WaiverPage />
</RequirePermission>
),
},
{ path: "/waiver/apply", element: <Navigate to="/waiver" replace /> },
// Vessel Registration
{
path: "/vessel-registrations",
element: (
<RequirePermission
anyOf={[P.APPLY_VESSEL_REGISTRATION, P.VIEW_OWN_VESSELS]}
>
<VesselRegistrationPage />
</RequirePermission>
),
},
// { path: '/vessel-registrations/apply', element: <VesselRegistrationPage /> },
{
path: "/vessel-registrations/:id",
element: (
<RequirePermission
anyOf={[P.APPLY_VESSEL_REGISTRATION, P.VIEW_OWN_VESSELS]}
>
<VesselRegistrationStatusPage />
</RequirePermission>
),
},
// General
{ path: "/profile", element: <ProfilePage /> },
{ path: "/support", element: <SupportPage /> },
],
},
// The separate vessel-owner login/portal was mock-only and called auth
// endpoints that never existed; vessel owners are ordinary portal users.
{ path: "/vessel-owner/login", element: <Navigate to="/login" replace /> },
{
path: "/vessel-owner/register",
element: <Navigate to="/signup" replace />,
},
{
path: "/vessel-owner/dashboard",
element: <Navigate to="/vessel-registration" replace />,
},
{
path: "/vessel-owner/registration",
element: <Navigate to="/vessel-registration" replace />,
},
{
path: "/vessel-owner/registration/apply",
element: <Navigate to="/licensing/VESSEL_REGISTRATION/apply" replace />,
},
{
path: "/vessel-owner/registration/transfer",
element: <Navigate to="/vessel-ownership-transfer" replace />,
},
// A typo'd URL should not maroon a signed-in user on the marketing page —
// ProtectedRoute sends anonymous visitors on to / (landing) instead.
{ path: "*", element: <Navigate to="/dashboard" replace /> },
]);