This commit is contained in:
Estifo77
2026-07-29 11:28:55 +03:00
parent 8118b646bd
commit 56bf81e4ad
5 changed files with 258 additions and 795 deletions

View File

@@ -1,349 +0,0 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
ActionIcon,
Badge,
Box,
Button,
Card,
Drawer,
Group,
Paper,
Select,
SimpleGrid,
Stack,
Table,
Text,
TextInput,
ThemeIcon,
Title,
rem,
} from '@mantine/core';
import {
IconAnchor,
IconCircleCheck,
IconClock,
IconEye,
IconSearch,
IconShip,
IconX,
} from '@tabler/icons-react';
// ---------------------------------------------------------------------------
// Types & mock data
// ---------------------------------------------------------------------------
export type TransferStatus = 'Pending' | 'Under Review' | 'Approved' | 'Rejected';
export const STATUS_COLOR: Record<TransferStatus, string> = {
Pending: 'gray',
'Under Review': 'blue',
Approved: 'teal',
Rejected: 'red',
};
export interface TransferParty {
name: string;
idOrTin: string;
phone: string;
email: string;
address: string;
}
export interface TransferRequest {
id: string;
vesselName: string;
vesselRegNo: string;
currentOwner: TransferParty;
newOwner: TransferParty;
reason: string;
status: TransferStatus;
submitted: string;
remarks?: string;
approvedOn?: string;
document: { fileName: string; fileType: 'pdf' | 'image' };
}
export const MOCK_REQUESTS: TransferRequest[] = [
{
id: 'VT-2025-0001',
vesselName: 'MV Abay',
vesselRegNo: 'ET-VSL-2021-014',
currentOwner: { name: 'Solomon Bekele', idOrTin: 'ETH-ID-0012345', phone: '+251 911 234 567', email: 'solomon.b@email.com', address: 'Bole, Addis Ababa' },
newOwner: { name: 'Bahir Dar Shipping PLC', idOrTin: 'TIN-0044556677', phone: '+251 58 220 1234', email: 'info@bdshipping.com', address: 'Bahir Dar, Ethiopia' },
reason: 'Corporate Restructuring',
status: 'Under Review',
submitted: '2025-06-30',
document: { fileName: 'bill_of_sale_abay.pdf', fileType: 'pdf' },
},
{
id: 'VT-2025-0002',
vesselName: 'MV Tana',
vesselRegNo: 'ET-VSL-2022-031',
currentOwner: { name: 'Solomon Bekele', idOrTin: 'ETH-ID-0012345', phone: '+251 911 234 567', email: 'solomon.b@email.com', address: 'Bole, Addis Ababa' },
newOwner: { name: 'Hana Girma', idOrTin: 'ETH-ID-0098765', phone: '+251 922 345 678', email: 'hana.girma@email.com', address: 'Piazza, Addis Ababa' },
reason: 'Sale',
status: 'Approved',
submitted: '2025-05-12',
approvedOn: '2025-05-28',
document: { fileName: 'bill_of_sale_tana.pdf', fileType: 'pdf' },
},
{
id: 'VT-2025-0003',
vesselName: 'MV Abay',
vesselRegNo: 'ET-VSL-2021-014',
currentOwner: { name: 'Solomon Bekele', idOrTin: 'ETH-ID-0012345', phone: '+251 911 234 567', email: 'solomon.b@email.com', address: 'Bole, Addis Ababa' },
newOwner: { name: 'Dawit Alemu', idOrTin: 'ETH-ID-0011223', phone: '+251 933 456 789', email: 'dawit.alemu@email.com', address: 'Mekelle, Ethiopia' },
reason: 'Gift',
status: 'Rejected',
submitted: '2025-04-02',
remarks: 'Bill of Sale document illegible — please resubmit a clearer scan.',
document: { fileName: 'transfer_doc_scan.jpg', fileType: 'image' },
},
{
id: 'VT-2025-0004',
vesselName: 'MV Zeway',
vesselRegNo: 'ET-VSL-2020-007',
currentOwner: { name: 'Meskerem Assefa', idOrTin: 'ETH-ID-0055667', phone: '+251 944 567 890', email: 'meskerem.a@email.com', address: 'Hawassa, Ethiopia' },
newOwner: { name: 'Yonas Tesfaye', idOrTin: 'ETH-ID-0077889', phone: '+251 955 678 901', email: 'yonas.t@email.com', address: 'Adama, Ethiopia' },
reason: 'Inheritance',
status: 'Pending',
submitted: '2025-07-10',
document: { fileName: 'inheritance_transfer.pdf', fileType: 'pdf' },
},
{
id: 'VT-2025-0005',
vesselName: 'MV Awash',
vesselRegNo: 'ET-VSL-2023-009',
currentOwner: { name: 'Kidist Worku', idOrTin: 'ETH-ID-0033445', phone: '+251 966 789 012', email: 'kidist.w@email.com', address: 'Dire Dawa, Ethiopia' },
newOwner: { name: 'Nile Logistics PLC', idOrTin: 'TIN-0099887766', phone: '+251 11 550 4321', email: 'contact@nilelogistics.com', address: 'Addis Ababa, Ethiopia' },
reason: 'Court Order',
status: 'Pending',
submitted: '2025-07-15',
document: { fileName: 'court_order.pdf', fileType: 'pdf' },
},
{
id: 'VT-2025-0006',
vesselName: 'MV Genale',
vesselRegNo: 'ET-VSL-2019-002',
currentOwner: { name: 'Tesfaye Kebede', idOrTin: 'ETH-ID-0022334', phone: '+251 977 890 123', email: 'tesfaye.k@email.com', address: 'Gondar, Ethiopia' },
newOwner: { name: 'Almaz Hailu', idOrTin: 'ETH-ID-0088990', phone: '+251 988 901 234', email: 'almaz.h@email.com', address: 'Bahir Dar, Ethiopia' },
reason: 'Other',
status: 'Under Review',
submitted: '2025-07-05',
document: { fileName: 'transfer_agreement.pdf', fileType: 'pdf' },
},
];
// ---------------------------------------------------------------------------
// Quick-review drawer
// ---------------------------------------------------------------------------
function QuickReviewDrawer({
request,
opened,
onClose,
onFullReview,
}: {
request: TransferRequest | null;
opened: boolean;
onClose: () => void;
onFullReview: (id: string) => void;
}) {
if (!request) return null;
return (
<Drawer opened={opened} onClose={onClose} title={`Transfer ${request.id}`} position="right" size="lg" padding="lg">
<Stack gap="md">
<Button
variant="light"
leftSection={<IconEye size={15} />}
fullWidth
onClick={() => { onClose(); onFullReview(request.id); }}
>
Open Full Review Page
</Button>
<Paper withBorder radius="md" p="md">
<Group justify="space-between" mb="sm">
<Text fw={700} fz="sm">{request.vesselName}</Text>
<Badge color={STATUS_COLOR[request.status]} variant="light">{request.status}</Badge>
</Group>
<Text fz="xs" c="dimmed">{request.vesselRegNo}</Text>
</Paper>
<Paper withBorder radius="md" p="md">
<Text fw={700} fz="sm" mb="sm">From To</Text>
<SimpleGrid cols={2} spacing="xs">
<Box>
<Text fz="xs" c="dimmed" tt="uppercase" fw={600}>Current Owner</Text>
<Text fz="sm">{request.currentOwner.name}</Text>
</Box>
<Box>
<Text fz="xs" c="dimmed" tt="uppercase" fw={600}>New Owner</Text>
<Text fz="sm">{request.newOwner.name}</Text>
</Box>
<Box>
<Text fz="xs" c="dimmed" tt="uppercase" fw={600}>Reason</Text>
<Text fz="sm">{request.reason}</Text>
</Box>
<Box>
<Text fz="xs" c="dimmed" tt="uppercase" fw={600}>Submitted</Text>
<Text fz="sm">{request.submitted}</Text>
</Box>
</SimpleGrid>
</Paper>
{request.status === 'Rejected' && request.remarks && (
<Paper withBorder radius="md" p="md" bg="red.0">
<Text fz="xs" fw={700} c="red.7" mb={4}>Rejection Reason</Text>
<Text fz="sm">{request.remarks}</Text>
</Paper>
)}
{request.status === 'Approved' && request.approvedOn && (
<Paper withBorder radius="md" p="md" bg="teal.0">
<Text fz="sm">Transfer approved on {request.approvedOn}. New certificates issued to {request.newOwner.name}.</Text>
</Paper>
)}
</Stack>
</Drawer>
);
}
// ---------------------------------------------------------------------------
// Main page
// ---------------------------------------------------------------------------
export function VesselTransferQueuePage() {
const navigate = useNavigate();
const [requests] = useState<TransferRequest[]>(MOCK_REQUESTS);
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState<string | null>(null);
const [selected, setSelected] = useState<TransferRequest | null>(null);
const [drawerOpen, setDrawerOpen] = useState(false);
const stats = {
total: requests.length,
pending: requests.filter((r) => r.status === 'Pending').length,
underReview: requests.filter((r) => r.status === 'Under Review').length,
approved: requests.filter((r) => r.status === 'Approved').length,
};
const filtered = requests.filter((r) => {
const q = search.toLowerCase();
const matchSearch = !q
|| r.vesselName.toLowerCase().includes(q)
|| r.id.toLowerCase().includes(q)
|| r.currentOwner.name.toLowerCase().includes(q)
|| r.newOwner.name.toLowerCase().includes(q);
const matchStatus = !statusFilter || r.status === statusFilter;
return matchSearch && matchStatus;
});
const openDrawer = (req: TransferRequest) => {
setSelected(req);
setDrawerOpen(true);
};
return (
<Stack gap="md">
<div>
<Title order={3}>Vessel Ownership Transfers</Title>
<Text fz="sm" c="dimmed">Review and process vessel ownership transfer requests</Text>
</div>
{/* Stats */}
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing="sm">
{[
{ label: 'Total', value: stats.total, color: 'blue', icon: IconShip },
{ label: 'Pending', value: stats.pending, color: 'gray', icon: IconClock },
{ label: 'Under Review', value: stats.underReview, color: 'blue', icon: IconEye },
{ label: 'Approved', value: stats.approved, color: 'teal', icon: IconCircleCheck },
].map(({ label, value, color, icon: Icon }) => (
<Card key={label} withBorder radius="md" p="sm">
<Group gap="xs" wrap="nowrap">
<ThemeIcon variant="light" color={color} size={38} radius="md"><Icon size={18} /></ThemeIcon>
<div><Text fz="xl" fw={700} lh={1}>{value}</Text><Text fz="xs" c="dimmed">{label}</Text></div>
</Group>
</Card>
))}
</SimpleGrid>
{/* Table */}
<Paper withBorder radius="md">
<Group p="md" justify="space-between" wrap="wrap" gap="sm">
<Text fw={600}>Transfer Queue</Text>
<Group gap="sm" wrap="nowrap">
<TextInput
placeholder="Search by vessel, request ID or owner…"
leftSection={<IconSearch size={15} />}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
size="sm"
style={{ minWidth: rem(280) }}
rightSection={search ? <ActionIcon variant="subtle" size="sm" onClick={() => setSearch('')}><IconX size={13} /></ActionIcon> : null}
/>
<Select
placeholder="All Status"
data={['Pending', 'Under Review', 'Approved', 'Rejected']}
value={statusFilter}
onChange={setStatusFilter}
clearable
size="sm"
style={{ width: rem(180) }}
/>
</Group>
</Group>
{filtered.length === 0 ? (
<Box py="xl" ta="center">
<ThemeIcon variant="light" color="gray" size={48} radius="xl" mx="auto" mb="sm"><IconAnchor size={22} /></ThemeIcon>
<Text fz="sm" c="dimmed">No transfer requests found</Text>
</Box>
) : (
<Table highlightOnHover striped verticalSpacing="sm" fz="sm">
<Table.Thead bg="var(--mantine-color-default-hover)">
<Table.Tr>
{['Request ID', 'Vessel', 'From Owner', 'To Owner', 'Reason', 'Submitted', 'Status', ''].map((h) => (
<Table.Th key={h} style={{ fontSize: rem(11), textTransform: 'uppercase', color: 'var(--mantine-color-dimmed)', whiteSpace: 'nowrap' }}>{h}</Table.Th>
))}
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{filtered.map((req) => (
<Table.Tr key={req.id}>
<Table.Td><Text fz="xs" fw={600} c="blue.7">{req.id}</Text></Table.Td>
<Table.Td>
<Text fz="xs" fw={500}>{req.vesselName}</Text>
<Text fz="xs" c="dimmed">{req.vesselRegNo}</Text>
</Table.Td>
<Table.Td><Text fz="xs">{req.currentOwner.name}</Text></Table.Td>
<Table.Td><Text fz="xs">{req.newOwner.name}</Text></Table.Td>
<Table.Td><Text fz="xs">{req.reason}</Text></Table.Td>
<Table.Td><Text fz="xs">{req.submitted}</Text></Table.Td>
<Table.Td><Badge color={STATUS_COLOR[req.status]} variant="light" size="xs">{req.status}</Badge></Table.Td>
<Table.Td>
<Group gap={4} wrap="nowrap">
<Button size="xs" variant="subtle" onClick={() => openDrawer(req)}>Quick View</Button>
<Button size="xs" variant="light" leftSection={<IconEye size={13} />} onClick={() => navigate(`/vessel-transfers/${req.id}`)}>Review</Button>
</Group>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
)}
{filtered.length > 0 && (
<Group px="md" py="sm" justify="space-between">
<Text fz="xs" c="dimmed">Showing {filtered.length} of {requests.length} requests</Text>
</Group>
)}
</Paper>
<QuickReviewDrawer
request={selected}
opened={drawerOpen}
onClose={() => setDrawerOpen(false)}
onFullReview={(id) => navigate(`/vessel-transfers/${id}`)}
/>
</Stack>
);
}

View File

@@ -1,252 +0,0 @@
import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import {
ActionIcon,
Alert,
Badge,
Box,
Button,
Card,
Divider,
Group,
Modal,
Paper,
SimpleGrid,
Stack,
Text,
Textarea,
ThemeIcon,
Title,
rem,
} from '@mantine/core';
import {
IconAlertTriangle,
IconArrowLeft,
IconCircleCheck,
IconDownload,
IconExternalLink,
IconEye,
IconFileDescription,
IconInfoCircle,
IconUserQuestion,
IconX,
} from '@tabler/icons-react';
import { notify } from '@ema-platform/ui';
import { MOCK_REQUESTS, STATUS_COLOR, type TransferParty, type TransferStatus } from './VesselTransferQueuePage';
// ---------------------------------------------------------------------------
// Document viewer — view + download (view pattern copied from ApplicationReviewPage)
// ---------------------------------------------------------------------------
const DEMO_PDF = 'data:application/pdf;base64,JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PgplbmRvYmoKMiAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PgplbmRvYmoKMyAwIG9iago8PC9UeXBlL1BhZ2UvUGFyZW50IDIgMCBSL01lZGlhQm94WzAgMCA2MTIgNzkyXT4+CmVuZG9iagp4cmVmCjAgNAowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA1OCAwMDAwMCBuIAowMDAwMDAwMTE1IDAwMDAwIG4gCnRyYWlsZXIKPDwvU2l6ZSA0L1Jvb3QgMSAwIFI+PgpzdGFydHhyZWYKMjIwCiUlRU9G';
// ponytail: view/download hit this demo data-URI, wire to real file storage when backend lands.
function getDocUrl(fileName: string) {
return fileName.endsWith('.pdf') ? DEMO_PDF : `https://placehold.co/600x400/e9ecef/6c757d?text=${encodeURIComponent(fileName)}`;
}
function DocViewer({ fileName, fileType, label }: { fileName: string; fileType: 'pdf' | 'image'; label: string }) {
const [open, setOpen] = useState(false);
const url = getDocUrl(fileName);
return (
<>
<Card withBorder radius="md" p="sm">
<Group gap="sm" wrap="nowrap" mb="sm">
<ThemeIcon size="md" variant="light" color={fileType === 'pdf' ? 'red' : 'blue'} radius="md">
<IconFileDescription size={16} stroke={1.5} />
</ThemeIcon>
<div style={{ flex: 1, minWidth: 0 }}>
<Text fz="sm" fw={600}>{label}</Text>
<Text fz="xs" c="dimmed" truncate>{fileName}</Text>
</div>
<Badge size="xs" variant="light" color={fileType === 'pdf' ? 'red' : 'blue'}>{fileType.toUpperCase()}</Badge>
</Group>
<Box style={{ width: '100%', height: rem(220), borderRadius: rem(6), overflow: 'hidden', border: '1px solid var(--mantine-color-default-border)', background: 'var(--mantine-color-gray-0)' }}>
{fileType === 'pdf'
? <iframe src={url} style={{ width: '100%', height: '100%', border: 'none' }} title={fileName} />
: <img src={url} alt={fileName} style={{ width: '100%', height: '100%', objectFit: 'contain' }} />}
</Box>
<Group grow mt="xs">
<Button size="xs" variant="subtle" leftSection={<IconEye size={13} />} rightSection={<IconExternalLink size={13} />} onClick={() => setOpen(true)}>
View
</Button>
<Button size="xs" variant="light" component="a" href={url} download={fileName} leftSection={<IconDownload size={13} />}>
Download
</Button>
</Group>
</Card>
<Modal opened={open} onClose={() => setOpen(false)} title={<Text fw={700}>{label} {fileName}</Text>} size="90vw" styles={{ body: { padding: 0, height: '80vh' } }}>
{fileType === 'pdf'
? <iframe src={url} style={{ width: '100%', height: '100%', border: 'none' }} title={fileName} />
: <img src={url} alt={fileName} style={{ width: '100%', height: '100%', objectFit: 'contain' }} />}
</Modal>
</>
);
}
function InfoRow({ label, value }: { label: string; value: string }) {
return (
<div>
<Text fz="xs" c="dimmed" tt="uppercase" fw={600} lh={1.2}>{label}</Text>
<Text fz="sm" mt={2}>{value || '—'}</Text>
</div>
);
}
function PartyCard({ title, party }: { title: string; party: TransferParty }) {
return (
<Paper withBorder radius="lg" p="xl">
<Text fw={700} mb="md">{title}</Text>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
<InfoRow label="Full Name / Company Name" value={party.name} />
<InfoRow label="National ID / TIN" value={party.idOrTin} />
<InfoRow label="Phone Number" value={party.phone} />
<InfoRow label="Email Address" value={party.email} />
<InfoRow label="Address" value={party.address} />
</SimpleGrid>
</Paper>
);
}
// ---------------------------------------------------------------------------
// Main page
// ---------------------------------------------------------------------------
export function VesselTransferReviewPage() {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const record = MOCK_REQUESTS.find((r) => r.id === id);
const [status, setStatus] = useState<TransferStatus | undefined>(record?.status);
const [remarks, setRemarks] = useState(record?.remarks ?? '');
const [approvedOn, setApprovedOn] = useState(record?.approvedOn);
if (!record || !status) {
return (
<Stack gap="md">
<Group gap="sm">
<ActionIcon variant="subtle" size="lg" onClick={() => navigate('/vessel-transfers')}><IconArrowLeft size={18} /></ActionIcon>
<Title order={3}>Transfer Request</Title>
</Group>
<Alert color="red" icon={<IconInfoCircle size={16} />}>Transfer request not found.</Alert>
</Stack>
);
}
const isTerminal = status === 'Approved' || status === 'Rejected';
const applyAction = (newStatus: TransferStatus, label: string) => {
record.status = newStatus;
record.remarks = remarks || record.remarks;
if (newStatus === 'Approved') {
const today = record.submitted; // ponytail: no live clock in mock; stamp with submitted date placeholder
record.approvedOn = today;
setApprovedOn(today);
}
setStatus(newStatus);
notify.success(`Transfer request ${label}. SMS and email notification sent.`);
};
return (
<Stack gap="md">
{/* Header */}
<Group justify="space-between">
<Group gap="sm">
<ActionIcon variant="subtle" size="lg" onClick={() => navigate('/vessel-transfers')}>
<IconArrowLeft size={18} />
</ActionIcon>
<div>
<Title order={3}>Transfer Review {record.vesselName}</Title>
<Group gap={6} mt={2}>
<Text fz="sm" c="dimmed">{record.id}</Text>
<Text fz="sm" c="dimmed">·</Text>
<Text fz="sm" c="dimmed">Submitted {record.submitted}</Text>
</Group>
</div>
</Group>
<Badge size="lg" variant="light" color={STATUS_COLOR[status]}>{status}</Badge>
</Group>
{/* Action bar / terminal notice */}
{!isTerminal ? (
<Paper withBorder radius="lg" p="md" bg="gray.0">
<Stack gap="sm">
<Textarea
label="Officer Remarks"
placeholder="Add notes, or the reason for rejection…"
minRows={2}
value={remarks}
onChange={(e) => setRemarks(e.currentTarget.value)}
/>
<Group gap="sm" justify="flex-end">
<Button
size="sm"
color="blue"
variant="light"
leftSection={<IconUserQuestion size={15} />}
onClick={() => applyAction('Under Review', 'marked under review')}
disabled={status === 'Under Review'}
>
Mark Under Review
</Button>
<Button
size="sm"
color="red"
variant="light"
leftSection={<IconX size={15} />}
onClick={() => applyAction('Rejected', 'rejected')}
disabled={!remarks.trim()}
>
Reject
</Button>
<Button
size="sm"
color="teal"
leftSection={<IconCircleCheck size={15} />}
onClick={() => applyAction('Approved', 'approved')}
>
Approve
</Button>
</Group>
{!remarks.trim() && (
<Text fz="xs" c="dimmed" ta="right">Remarks are required to reject this request.</Text>
)}
</Stack>
</Paper>
) : (
<Alert
variant="light"
color={STATUS_COLOR[status]}
icon={status === 'Approved' ? <IconCircleCheck size={17} /> : <IconAlertTriangle size={17} />}
>
{status === 'Approved'
? <>Transfer approved on {approvedOn}. New certificates issued to {record.newOwner.name}.</>
: <>This request was rejected. {record.remarks}</>}
</Alert>
)}
{/* Vessel + reason */}
<Paper withBorder radius="lg" p="xl">
<Text fw={700} mb="md">Transfer Details</Text>
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md">
<InfoRow label="Vessel" value={`${record.vesselName}${record.vesselRegNo}`} />
<InfoRow label="Transfer Reason" value={record.reason} />
<InfoRow label="Submitted Date" value={record.submitted} />
</SimpleGrid>
</Paper>
{/* Owners */}
<SimpleGrid cols={{ base: 1, lg: 2 }} spacing="md">
<PartyCard title="Current Owner Details" party={record.currentOwner} />
<PartyCard title="New Owner Details" party={record.newOwner} />
</SimpleGrid>
{/* Document */}
<Paper withBorder radius="lg" p="xl">
<Text fw={700} mb="md">Bill of Sale / Transfer Document</Text>
<Divider mb="md" />
<Box style={{ maxWidth: rem(420) }}>
<DocViewer fileName={record.document.fileName} fileType={record.document.fileType} label="Transfer Document" />
</Box>
</Paper>
</Stack>
);
}

View File

@@ -47,8 +47,6 @@ import { MtoLicenseReviewPage } from '../features/mto-license/pages/MtoLicenseRe
import { WaiverQueuePage } from '../features/waiver/pages/WaiverQueuePage';
import { WaiverReviewPage } from '../features/waiver/pages/WaiverReviewPage';
import { LogisticsHeadDashboardPage } from '../features/logistics-head/pages/LogisticsHeadDashboardPage';
import { VesselTransferQueuePage } from '../features/vessel-transfer/pages/VesselTransferQueuePage';
import { VesselTransferReviewPage } from '../features/vessel-transfer/pages/VesselTransferReviewPage';
import { VesselRegistrationQueuePage } from '../features/vessel-registration/pages/VesselRegistrationQueuePage';
import { VesselRegistrationReviewPage } from '../features/vessel-registration/pages/VesselRegistrationReviewPage';
import { VesselRegistrationReportPage } from '../features/vessel-registration/pages/VesselRegistrationReportPage';
@@ -88,8 +86,6 @@ const router = createBrowserRouter([
{ path: 'payment-config', element: <PaymentConfigPage /> },
{ path: 'seafarer-registry', element: <SeafarerRegistryPage /> },
{ path: 'seaman-book-queue', element: <SeamanBookQueuePage /> },
{ path: 'vessel-transfers', element: <VesselTransferQueuePage /> },
{ path: 'vessel-transfers/:id', element: <VesselTransferReviewPage /> },
{ path: 'questions', element: <QuestionPage /> },
{ path: 'exams', element: <ExamPage /> },
{ path: 'exams/:id', element: <ExamDetailPage /> },

View File

@@ -0,0 +1,156 @@
import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import {
Alert,
Badge,
Button,
Divider,
Group,
Paper,
SimpleGrid,
Stack,
Stepper,
Text,
ThemeIcon,
Title,
} from '@mantine/core';
import {
IconAlertTriangle,
IconArrowLeft,
IconCircleCheck,
IconClock,
IconDownload,
IconInfoCircle,
IconShip,
} from '@tabler/icons-react';
import { notify } from '@ema-platform/ui';
import { MOCK_REGISTRATIONS, recordDownload, STATUS_COLOR } from '../mock';
// ponytail: placeholder PDF blob; wire real cert endpoint when backend lands.
const BLANK_PDF = 'data:application/pdf;base64,JVBERi0xLjQKJcfsj6IKMSAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIFI+PgplbmRvYmoKMiAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PgplbmRvYmoKMyAwIG9iago8PC9UeXBlL1BhZ2UvUGFyZW50IDIgMCBSL01lZGlhQm94WzAgMCA2MTIgNzkyXT4+CmVuZG9iagp4cmVmCjAgNAowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA1OCAwMDAwMCBuIAowMDAwMDAwMTE1IDAwMDAwIG4gCnRyYWlsZXIKPDwvU2l6ZSA0L1Jvb3QgMSAwIFI+PgpzdGFydHhyZWYKMjE3CiUlRU9G';
function downloadCertificate(filename: string) {
const a = document.createElement('a');
a.href = BLANK_PDF;
a.download = filename;
a.click();
}
export function VesselRegistrationStatusPage() {
const navigate = useNavigate();
const { id } = useParams();
const [reg] = useState(() => MOCK_REGISTRATIONS.find((r) => r.id === id) ?? null);
const [, forceUpdate] = useState(0);
if (!reg) {
return (
<Stack gap="md">
<Button variant="subtle" leftSection={<IconArrowLeft size={14} />} onClick={() => navigate('/vessel-registrations')}>Back</Button>
<Alert variant="light" color="red" icon={<IconInfoCircle size={15} />}>Registration not found.</Alert>
</Stack>
);
}
const activeStep = reg.timeline.filter((t) => t.done).length - 1;
const needsCorrection = reg.status === 'Correction Required' || reg.status === 'Rejected';
const handleDownload = (certName: string, certNumber: string) => {
downloadCertificate(`${certName.replace(/\s+/g, '-')}-${certNumber}.pdf`);
recordDownload(reg.id, certName);
forceUpdate((n) => n + 1);
notify.success(`${certName} downloaded.`);
};
return (
<Stack gap="md">
<Group gap="sm">
<Button variant="subtle" leftSection={<IconArrowLeft size={14} />} onClick={() => navigate('/vessel-registrations')}>Back</Button>
<div>
<Title order={3}>{reg.vesselName}</Title>
<Text fz="sm" c="dimmed">{reg.id} {reg.category}</Text>
</div>
</Group>
<Paper withBorder radius="lg" p="xl">
<Group justify="space-between" mb="md" wrap="wrap" gap="sm">
<Group gap="sm">
<ThemeIcon size={36} radius="md" color="blue" variant="light"><IconShip size={18} /></ThemeIcon>
<div>
<Text fw={700} fz="sm">Registration Status</Text>
<Text fz="xs" c="dimmed">Submitted {reg.submitted}</Text>
</div>
</Group>
<Badge color={STATUS_COLOR[reg.status]} variant="light" size="lg">{reg.status}</Badge>
</Group>
{reg.renewal && reg.renewal !== 'OK' && (
<Alert
variant="light"
color={reg.renewal === 'Overdue' ? 'red' : 'orange'}
icon={<IconAlertTriangle size={15} />}
mb="md"
p="sm"
>
<Text fz="sm">
Registration {reg.renewal === 'Overdue' ? 'is overdue for renewal' : 'is due for renewal soon'}
{reg.expiryDate ? ` — expires ${reg.expiryDate}` : ''}.
</Text>
</Alert>
)}
{reg.remarks && (
<Alert variant="light" color={needsCorrection ? 'orange' : 'blue'} icon={<IconInfoCircle size={15} />} mb="md" p="sm">
<Text fz="sm" fw={600} mb={2}>Officer Remarks</Text>
<Text fz="sm">{reg.remarks}</Text>
</Alert>
)}
<Stepper active={activeStep} size="sm" color="teal">
{reg.timeline.map((step, i) => (
<Stepper.Step
key={i}
label={step.event}
description={step.date ?? 'Pending'}
icon={step.done ? <IconCircleCheck size={16} /> : <IconClock size={16} />}
/>
))}
</Stepper>
{needsCorrection && (
<Group justify="flex-end" mt="md">
<Button color="orange" onClick={() => navigate('/vessel-registrations/apply')}>Resubmit Application</Button>
</Group>
)}
</Paper>
{reg.status === 'Approved' && reg.certificates && (
<Paper withBorder radius="lg" p="xl">
<Text fw={700} mb="md">Certificates</Text>
<Stack gap="sm">
{reg.certificates.map((cert) => (
<div key={cert.name}>
<Group justify="space-between" wrap="wrap" gap="sm">
<div>
<Text fw={600} fz="sm">{cert.name}</Text>
<Text fz="xs" c="dimmed">Certificate No. {cert.number} Issued {cert.issueDate}</Text>
{cert.downloads > 0 && (
<Text fz="xs" c="dimmed">Downloaded {cert.downloads} time{cert.downloads === 1 ? '' : 's'}</Text>
)}
</div>
<Button
size="xs"
leftSection={<IconDownload size={14} />}
onClick={() => handleDownload(cert.name, cert.number)}
>
Download
</Button>
</Group>
<Divider mt="sm" />
</div>
))}
</Stack>
</Paper>
)}
</Stack>
);
}

View File

@@ -1,105 +1,83 @@
import { createBrowserRouter, Navigate } from "react-router-dom";
import { I18nextProvider } from "react-i18next";
import { i18n } from "./i18n/config";
import { PortalLayout } from "./layouts/PortalLayout";
import { ProtectedRoute } from "./components/ProtectedRoute";
import { ProfileGuard } from "./components/ProfileGuard";
import { createBrowserRouter, Navigate } from 'react-router-dom';
import { I18nextProvider } from 'react-i18next';
import { i18n } from './i18n/config';
import { PortalLayout } from './layouts/PortalLayout';
import { ProtectedRoute } from './components/ProtectedRoute';
import { ProfileGuard } from './components/ProfileGuard';
// Auth (standalone pages, no portal chrome)
import {
LoginPage,
SignupPage,
OTPVerificationPage,
ForgotPasswordPage,
} from "@ema-platform/auth";
import { LoginPage, SignupPage, OTPVerificationPage, ForgotPasswordPage } from '@ema-platform/auth';
// Profile setup
import { ProfileSetupPage } from "./features/profile-setup/pages/ProfileSetupPage";
import { ProfileSetupPage } from './features/profile-setup/pages/ProfileSetupPage';
// Portal feature pages
import { DashboardPage } from "./features/dashboard/pages/DashboardPage";
import { ProfilePage } from "./features/profile/pages/ProfilePage";
import { SupportPage } from "./features/support/pages/SupportPage";
import { SeafarerRegistrationPage } from "./features/seafarer/pages/SeafarerRegistrationPage";
import { SeafarerRegistryPage } from "./features/seafarer/pages/SeafarerRegistryPage";
import { SeafarerProfilePage } from "./features/seafarer/pages/SeafarerProfilePage";
import { DashboardPage } from './features/dashboard/pages/DashboardPage';
import { ProfilePage } from './features/profile/pages/ProfilePage';
import { SupportPage } from './features/support/pages/SupportPage';
import { SeafarerRegistrationPage } from './features/seafarer/pages/SeafarerRegistrationPage';
import { SeafarerRegistryPage } from './features/seafarer/pages/SeafarerRegistryPage';
import { SeafarerProfilePage } from './features/seafarer/pages/SeafarerProfilePage';
// Phase 1 pages
import { DocumentVaultPage } from "./features/documents/pages/DocumentVaultPage";
import { SeamanBookPage } from "./features/seaman-book/pages/SeamanBookPage";
import { SeamanBookApplicationPage } from "./features/seaman-book/pages/SeamanBookApplicationPage";
import { NotificationsPage } from "./features/notifications/pages/NotificationsPage";
import { DocumentVaultPage } from './features/documents/pages/DocumentVaultPage';
import { SeamanBookPage } from './features/seaman-book/pages/SeamanBookPage';
import { SeamanBookApplicationPage } from './features/seaman-book/pages/SeamanBookApplicationPage';
import { NotificationsPage } from './features/notifications/pages/NotificationsPage';
// Phase 2 — CoC / CoP
import { CertificatesPage } from "./features/certificates/pages/CertificatesPage";
import { CoCApplicationPage } from "./features/certificates/pages/CoCApplicationPage";
import { CertificatesPage } from './features/certificates/pages/CertificatesPage';
import { CoCApplicationPage } from './features/certificates/pages/CoCApplicationPage';
// Phase 3 — Endorsement
import { EndorsementPage } from "./features/endorsement/pages/EndorsementPage";
import { VesselRegistrationPage } from "./features/vessel-registration/pages/VesselRegistrationPage";
import { VesselRegistrationApplicationPage } from "./features/vessel-registration/pages/VesselRegistrationApplicationPage";
import { OwnershipTransferPage } from "./features/vessel-registration/pages/OwnershipTransferPage";
import { VesselRegistrationDashboardPage } from "./features/vessel-registration-dashboard/pages/VesselRegistrationDashboardPage";
import { VesselOwnerLayout } from "./layouts/VesselOwnerLayout";
import { VesselOwnerLoginPage } from "./features/vessel-owner/pages/VesselOwnerLoginPage";
import { VesselOwnerRegisterPage } from "./features/vessel-owner/pages/VesselOwnerRegisterPage";
import { VesselOwnerDashboardPage } from "./features/vessel-owner/pages/VesselOwnerDashboardPage";
import { LogisticsDashboardPage } from "./features/logistics-dashboard/pages/LogisticsDashboardPage";
import { FreightForwarderLicensePage } from "./features/freight-forwarder-license/pages/FreightForwarderLicensePage";
import { FreightForwarderLicenseApplicationPage } from "./features/freight-forwarder-license/pages/FreightForwarderLicenseApplicationPage";
import { FreightForwarderLicenseRenewalPage } from "./features/freight-forwarder-license/pages/FreightForwarderLicenseRenewalPage";
import { ShippingAgentLicensePage } from "./features/shipping-agent-license/pages/ShippingAgentLicensePage";
import { ShippingAgentLicenseApplicationPage } from "./features/shipping-agent-license/pages/ShippingAgentLicenseApplicationPage";
import { ShippingAgentLicenseRenewalPage } from "./features/shipping-agent-license/pages/ShippingAgentLicenseRenewalPage";
import { CombinedLicensePage } from "./features/combined-license/pages/CombinedLicensePage";
import { CombinedLicenseApplicationPage } from "./features/combined-license/pages/CombinedLicenseApplicationPage";
import { CombinedLicenseRenewalPage } from "./features/combined-license/pages/CombinedLicenseRenewalPage";
import { JointInvestmentLicensePage } from "./features/joint-investment-license/pages/JointInvestmentLicensePage";
import { JointInvestmentLicenseApplicationPage } from "./features/joint-investment-license/pages/JointInvestmentLicenseApplicationPage";
import { JointInvestmentLicenseRenewalPage } from "./features/joint-investment-license/pages/JointInvestmentLicenseRenewalPage";
import { MtoLicensePage } from "./features/mto-license/pages/MtoLicensePage";
import { MtoLicenseApplicationPage } from "./features/mto-license/pages/MtoLicenseApplicationPage";
import { MtoLicenseRenewalPage } from "./features/mto-license/pages/MtoLicenseRenewalPage";
import { WaiverPage } from "./features/waiver/pages/WaiverPage";
import { WaiverApplicationPage } from "./features/waiver/pages/WaiverApplicationPage";
import { EndorsementPage } from './features/endorsement/pages/EndorsementPage';
import { VesselRegistrationPage } from './features/vessel-registration/pages/VesselRegistrationPage';
import { VesselRegistrationApplicationPage } from './features/vessel-registration/pages/VesselRegistrationApplicationPage';
import { OwnershipTransferPage } from './features/vessel-registration/pages/OwnershipTransferPage';
import { VesselRegistrationDashboardPage } from './features/vessel-registration-dashboard/pages/VesselRegistrationDashboardPage';
import { VesselOwnerLayout } from './layouts/VesselOwnerLayout';
import { VesselOwnerLoginPage } from './features/vessel-owner/pages/VesselOwnerLoginPage';
import { VesselOwnerRegisterPage } from './features/vessel-owner/pages/VesselOwnerRegisterPage';
import { VesselOwnerDashboardPage } from './features/vessel-owner/pages/VesselOwnerDashboardPage';
import { LogisticsDashboardPage } from './features/logistics-dashboard/pages/LogisticsDashboardPage';
import { FreightForwarderLicensePage } from './features/freight-forwarder-license/pages/FreightForwarderLicensePage';
import { FreightForwarderLicenseApplicationPage } from './features/freight-forwarder-license/pages/FreightForwarderLicenseApplicationPage';
import { FreightForwarderLicenseRenewalPage } from './features/freight-forwarder-license/pages/FreightForwarderLicenseRenewalPage';
import { ShippingAgentLicensePage } from './features/shipping-agent-license/pages/ShippingAgentLicensePage';
import { ShippingAgentLicenseApplicationPage } from './features/shipping-agent-license/pages/ShippingAgentLicenseApplicationPage';
import { ShippingAgentLicenseRenewalPage } from './features/shipping-agent-license/pages/ShippingAgentLicenseRenewalPage';
import { CombinedLicensePage } from './features/combined-license/pages/CombinedLicensePage';
import { CombinedLicenseApplicationPage } from './features/combined-license/pages/CombinedLicenseApplicationPage';
import { CombinedLicenseRenewalPage } from './features/combined-license/pages/CombinedLicenseRenewalPage';
import { JointInvestmentLicensePage } from './features/joint-investment-license/pages/JointInvestmentLicensePage';
import { JointInvestmentLicenseApplicationPage } from './features/joint-investment-license/pages/JointInvestmentLicenseApplicationPage';
import { JointInvestmentLicenseRenewalPage } from './features/joint-investment-license/pages/JointInvestmentLicenseRenewalPage';
import { MtoLicensePage } from './features/mto-license/pages/MtoLicensePage';
import { MtoLicenseApplicationPage } from './features/mto-license/pages/MtoLicenseApplicationPage';
import { MtoLicenseRenewalPage } from './features/mto-license/pages/MtoLicenseRenewalPage';
import { WaiverPage } from './features/waiver/pages/WaiverPage';
import { WaiverApplicationPage } from './features/waiver/pages/WaiverApplicationPage';
// Vessel Ownership Transfer
import { VesselTransferPage } from "./features/vessel-transfer/pages/VesselTransferPage";
import { VesselTransferApplicationPage } from "./features/vessel-transfer/pages/VesselTransferApplicationPage";
// Vessel Registration
import { VesselRegistrationStatusPage } from "./features/vessel-registration/pages/VesselRegistrationStatusPage";
import { VesselRegistrationStatusPage } from './features/vessel-registration/pages/VesselRegistrationStatusPage';
export const router = createBrowserRouter([
// Public auth pages
{ path: "/login", element: <LoginPage /> },
{ path: "/signup", element: <SignupPage /> },
{ path: '/login', element: <LoginPage /> },
{ path: '/signup', element: <SignupPage /> },
// Protected auth pages
{
element: (
<ProtectedRoute>
<OTPVerificationPage />
</ProtectedRoute>
),
path: "/otp-verify",
element: <ProtectedRoute><OTPVerificationPage /></ProtectedRoute>,
path: '/otp-verify',
},
{
element: (
<ProtectedRoute>
<ForgotPasswordPage />
</ProtectedRoute>
),
path: "/forgot-password",
element: <ProtectedRoute><ForgotPasswordPage /></ProtectedRoute>,
path: '/forgot-password',
},
{
element: (
<ProtectedRoute>
<ProfileSetupPage />
</ProtectedRoute>
),
path: "/profile-setup",
element: <ProtectedRoute><ProfileSetupPage /></ProtectedRoute>,
path: '/profile-setup',
},
// Portal — protected
@@ -114,117 +92,63 @@ export const router = createBrowserRouter([
</ProtectedRoute>
),
children: [
{ path: "/", element: <Navigate to="/dashboard" replace /> },
{ path: "/dashboard", element: <DashboardPage /> },
{ path: '/', element: <Navigate to="/dashboard" replace /> },
{ path: '/dashboard', element: <DashboardPage /> },
// Seafarer
{ path: "/seafarer-registration", element: <SeafarerRegistrationPage /> },
{ path: "/seafarer-registry", element: <SeafarerRegistryPage /> },
{ path: "/seafarer-registry/:id", element: <SeafarerProfilePage /> },
{ path: '/seafarer-registration', element: <SeafarerRegistrationPage /> },
{ path: '/seafarer-registry', element: <SeafarerRegistryPage /> },
{ path: '/seafarer-registry/:id', element: <SeafarerProfilePage /> },
// Phase 1
{ path: "/documents", element: <DocumentVaultPage /> },
{ path: "/seaman-book", element: <SeamanBookPage /> },
{ path: "/seaman-book/apply", element: <SeamanBookApplicationPage /> },
{ path: "/notifications", element: <NotificationsPage /> },
{ path: '/documents', element: <DocumentVaultPage /> },
{ path: '/seaman-book', element: <SeamanBookPage /> },
{ path: '/seaman-book/apply', element: <SeamanBookApplicationPage /> },
{ path: '/notifications', element: <NotificationsPage /> },
// Phase 2 — CoC / CoP
{ path: "/certificates", element: <CertificatesPage /> },
{ path: "/certificates/apply", element: <CoCApplicationPage /> },
{ path: '/certificates', element: <CertificatesPage /> },
{ path: '/certificates/apply', element: <CoCApplicationPage /> },
// Phase 3 — Endorsement
{ path: "/endorsements", element: <EndorsementPage /> },
{
path: "/vessel-registration-dashboard",
element: <VesselRegistrationDashboardPage />,
},
{ path: "/vessel-registration", element: <VesselRegistrationPage /> },
{
path: "/vessel-registration/apply",
element: <VesselRegistrationApplicationPage />,
},
{
path: "/vessel-registration/transfer",
element: <OwnershipTransferPage />,
},
{ path: "/logistics-dashboard", element: <LogisticsDashboardPage /> },
{
path: "/freight-forwarder-license",
element: <FreightForwarderLicensePage />,
},
{
path: "/freight-forwarder-license/apply",
element: <FreightForwarderLicenseApplicationPage />,
},
{
path: "/freight-forwarder-license/:id/renew",
element: <FreightForwarderLicenseRenewalPage />,
},
{
path: "/shipping-agent-license",
element: <ShippingAgentLicensePage />,
},
{
path: "/shipping-agent-license/apply",
element: <ShippingAgentLicenseApplicationPage />,
},
{
path: "/shipping-agent-license/:id/renew",
element: <ShippingAgentLicenseRenewalPage />,
},
{ path: "/combined-license", element: <CombinedLicensePage /> },
{
path: "/combined-license/apply",
element: <CombinedLicenseApplicationPage />,
},
{
path: "/combined-license/:id/renew",
element: <CombinedLicenseRenewalPage />,
},
{
path: "/joint-investment-license",
element: <JointInvestmentLicensePage />,
},
{
path: "/joint-investment-license/apply",
element: <JointInvestmentLicenseApplicationPage />,
},
{
path: "/joint-investment-license/:id/renew",
element: <JointInvestmentLicenseRenewalPage />,
},
{ path: "/mto-license", element: <MtoLicensePage /> },
{ path: "/mto-license/apply", element: <MtoLicenseApplicationPage /> },
{ path: "/mto-license/:id/renew", element: <MtoLicenseRenewalPage /> },
{ path: "/waiver", element: <WaiverPage /> },
{ path: "/waiver/apply", element: <WaiverApplicationPage /> },
{ path: '/endorsements', element: <EndorsementPage /> },
{ path: '/vessel-registration-dashboard', element: <VesselRegistrationDashboardPage /> },
{ path: '/vessel-registration', element: <VesselRegistrationPage /> },
{ path: '/vessel-registration/apply', element: <VesselRegistrationApplicationPage /> },
{ path: '/vessel-registration/transfer', element: <OwnershipTransferPage /> },
{ path: '/logistics-dashboard', element: <LogisticsDashboardPage /> },
{ path: '/freight-forwarder-license', element: <FreightForwarderLicensePage /> },
{ path: '/freight-forwarder-license/apply', element: <FreightForwarderLicenseApplicationPage /> },
{ path: '/freight-forwarder-license/:id/renew', element: <FreightForwarderLicenseRenewalPage /> },
{ path: '/shipping-agent-license', element: <ShippingAgentLicensePage /> },
{ path: '/shipping-agent-license/apply', element: <ShippingAgentLicenseApplicationPage /> },
{ path: '/shipping-agent-license/:id/renew', element: <ShippingAgentLicenseRenewalPage /> },
{ path: '/combined-license', element: <CombinedLicensePage /> },
{ path: '/combined-license/apply', element: <CombinedLicenseApplicationPage /> },
{ path: '/combined-license/:id/renew', element: <CombinedLicenseRenewalPage /> },
{ path: '/joint-investment-license', element: <JointInvestmentLicensePage /> },
{ path: '/joint-investment-license/apply', element: <JointInvestmentLicenseApplicationPage /> },
{ path: '/joint-investment-license/:id/renew', element: <JointInvestmentLicenseRenewalPage /> },
{ path: '/mto-license', element: <MtoLicensePage /> },
{ path: '/mto-license/apply', element: <MtoLicenseApplicationPage /> },
{ path: '/mto-license/:id/renew', element: <MtoLicenseRenewalPage /> },
{ path: '/waiver', element: <WaiverPage /> },
{ path: '/waiver/apply', element: <WaiverApplicationPage /> },
// Vessel Registration
{ path: "/vessel-registrations", element: <VesselRegistrationPage /> },
{
path: "/vessel-registrations/apply",
element: <VesselRegistrationApplicationPage />,
},
{
path: "/vessel-registrations/:id",
element: <VesselRegistrationStatusPage />,
},
{ path: '/vessel-registrations', element: <VesselRegistrationPage /> },
{ path: '/vessel-registrations/apply', element: <VesselRegistrationApplicationPage /> },
{ path: '/vessel-registrations/:id', element: <VesselRegistrationStatusPage /> },
// Vessel Ownership Transfer
{ path: "/vessel-transfers", element: <VesselTransferPage /> },
{
path: "/vessel-transfers/apply",
element: <VesselTransferApplicationPage />,
},
// General
{ path: "/profile", element: <ProfilePage /> },
{ path: "/support", element: <SupportPage /> },
{ path: '/profile', element: <ProfilePage /> },
{ path: '/support', element: <SupportPage /> },
],
},
{ path: "/vessel-owner/login", element: <VesselOwnerLoginPage /> },
{ path: "/vessel-owner/register", element: <VesselOwnerRegisterPage /> },
{ path: '/vessel-owner/login', element: <VesselOwnerLoginPage /> },
{ path: '/vessel-owner/register', element: <VesselOwnerRegisterPage /> },
{
element: (
<ProtectedRoute>
@@ -234,24 +158,12 @@ export const router = createBrowserRouter([
</ProtectedRoute>
),
children: [
{
path: "/vessel-owner/dashboard",
element: <VesselOwnerDashboardPage />,
},
{
path: "/vessel-owner/registration",
element: <VesselRegistrationPage />,
},
{
path: "/vessel-owner/registration/apply",
element: <VesselRegistrationApplicationPage />,
},
{
path: "/vessel-owner/registration/transfer",
element: <OwnershipTransferPage />,
},
{ path: '/vessel-owner/dashboard', element: <VesselOwnerDashboardPage /> },
{ path: '/vessel-owner/registration', element: <VesselRegistrationPage /> },
{ path: '/vessel-owner/registration/apply', element: <VesselRegistrationApplicationPage /> },
{ path: '/vessel-owner/registration/transfer', element: <OwnershipTransferPage /> },
],
},
{ path: "*", element: <Navigate to="/" replace /> },
{ path: '*', element: <Navigate to="/" replace /> },
]);