mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-29 23:28:12 +00:00
470 lines
17 KiB
TypeScript
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 /> },
|
|
]);
|