refactor: standardize import statements and formatting across SeamanBookPage and VesselRegistrationPage

feat: add mock data for vessel registration and seaman book application

fix: update eligibility and status handling in SeamanBookPage

style: improve readability and consistency in PortalLayout and AdvancedTable components

chore: update package-lock.json for dependency management
This commit is contained in:
Estifo77
2026-07-31 09:34:29 +03:00
parent 0db451bdec
commit 1b989936f0
7 changed files with 1281 additions and 396 deletions

View File

@@ -21,6 +21,7 @@ export function initiatePayment(req: PaymentRequest): Promise<PaymentIntent> {
/** GET /payments/intents/:intentId — used both by the poll and by manual "check again". */
export function getIntent(intentId: string): Promise<PaymentIntent> {
return paymentFetch<PaymentIntent>(`/payments/intents/${intentId}`);
}
/**

View File

@@ -1,5 +1,5 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import {
Alert,
Badge,
@@ -17,7 +17,7 @@ import {
Timeline,
Title,
rem,
} from '@mantine/core';
} from "@mantine/core";
import {
IconAlertCircle,
IconBook2,
@@ -30,8 +30,8 @@ import {
IconPrinter,
IconShield,
IconX,
} from '@tabler/icons-react';
import { notify } from '@ema-platform/ui';
} from "@tabler/icons-react";
import { notify } from "@ema-platform/ui";
// ---------------------------------------------------------------------------
// Mock data — replace with real API
@@ -40,19 +40,46 @@ const ELIGIBILITY = {
hasProfile: true,
hasNationalId: true,
hasMedicalCert: true,
medicalExpiry: '2026-03-14',
medicalExpiry: "2026-03-14",
bstComplete: true,
bstItems: [
{ label: 'Personal Survival Techniques (PST)', done: true },
{ label: 'Fire Prevention & Fire Fighting (FPFF)', done: true },
{ label: 'Elementary First Aid (EFA)', done: true },
{ label: 'Personal Safety & Social Responsibility (PSSR)', done: true },
{ label: 'Sexual Harassment Prevention', done: true },
{ label: "Personal Survival Techniques (PST)", done: true },
{ label: "Fire Prevention & Fire Fighting (FPFF)", done: true },
{ label: "Elementary First Aid (EFA)", done: true },
{ label: "Personal Safety & Social Responsibility (PSSR)", done: true },
{ label: "Sexual Harassment Prevention", done: true },
],
};
const MOCK_APPLICATION: SeamanBookApp | null = null;
// const MOCK_APPLICATION: SeamanBookApp | null = null;
const MOCK_APPLICATION: SeamanBookApp = {
id: "SB-2026-001",
submittedAt: "2026-07-02T09:15:00Z",
status: "Approved",
remarks: "Seaman Book issued successfully.",
timeline: [
{
date: "2026-07-02T09:15:00Z",
event: "Application Submitted",
done: true,
},
{
date: "2026-07-03T11:30:00Z",
event: "Document Verification",
done: true,
},
{
date: "2026-07-04T14:10:00Z",
event: "Application Reviewed",
done: true,
},
{
date: "2026-07-05T10:00:00Z",
event: "Seaman Book Issued",
done: true,
},
],
};
interface SeamanBookApp {
id: string;
submittedAt: string;
@@ -62,20 +89,27 @@ interface SeamanBookApp {
}
const STATUS_COLOR: Record<string, string> = {
'Under Review': 'yellow',
'Approved': 'teal',
'Rejected': 'red',
'Correction Required': 'orange',
'Ready for Collection': 'blue',
"Under Review": "yellow",
Approved: "teal",
Rejected: "red",
"Correction Required": "orange",
"Ready for Collection": "blue",
};
function EligibilityItem({ label, ok }: { label: string; ok: boolean }) {
return (
<Group gap="xs">
<ThemeIcon size={22} radius="xl" variant={ok ? 'filled' : 'light'} color={ok ? 'teal' : 'red'}>
<ThemeIcon
size={22}
radius="xl"
variant={ok ? "filled" : "light"}
color={ok ? "teal" : "red"}
>
{ok ? <IconCheck size={13} /> : <IconX size={13} />}
</ThemeIcon>
<Text fz="sm" c={ok ? undefined : 'dimmed'}>{label}</Text>
<Text fz="sm" c={ok ? undefined : "dimmed"}>
{label}
</Text>
</Group>
);
}
@@ -100,7 +134,9 @@ export function SeamanBookPage() {
await new Promise((r) => setTimeout(r, 1400));
setSubmitting(false);
setSubmitted(true);
notify.success('Seaman Book application submitted successfully! Reference: SB-APP-2024-002');
notify.success(
"Seaman Book application submitted successfully! Reference: SB-APP-2024-002",
);
};
const activeStep = MOCK_APPLICATION
@@ -113,8 +149,8 @@ export function SeamanBookPage() {
<div>
<Title order={3}>My Application Seaman Book & BTC</Title>
<Text fz="sm" c="dimmed">
A Seaman Book is your official maritime identity document. It records your sea service and must be
held before joining any vessel.
A Seaman Book is your official maritime identity document. It records
your sea service and must be held before joining any vessel.
</Text>
</div>
@@ -128,16 +164,28 @@ export function SeamanBookPage() {
</ThemeIcon>
<div>
<Text fw={700}>Application {MOCK_APPLICATION.id}</Text>
<Text fz="xs" c="dimmed">Submitted {MOCK_APPLICATION.submittedAt}</Text>
<Text fz="xs" c="dimmed">
Submitted {MOCK_APPLICATION.submittedAt}
</Text>
</div>
</Group>
<Badge color={STATUS_COLOR[MOCK_APPLICATION.status] ?? 'gray'} variant="light" size="lg">
<Badge
color={STATUS_COLOR[MOCK_APPLICATION.status] ?? "gray"}
variant="light"
size="lg"
>
{MOCK_APPLICATION.status}
</Badge>
</Group>
{MOCK_APPLICATION.remarks && (
<Alert variant="light" color="blue" icon={<IconInfoCircle size={15} />} mb="md" p="sm">
<Alert
variant="light"
color="blue"
icon={<IconInfoCircle size={15} />}
mb="md"
p="sm"
>
<Text fz="sm">{MOCK_APPLICATION.remarks}</Text>
</Alert>
)}
@@ -148,15 +196,27 @@ export function SeamanBookPage() {
<Stepper.Step
key={i}
label={step.event}
description={step.date ?? 'Pending'}
icon={step.done ? <IconCircleCheck size={16} /> : <IconClock size={16} />}
description={step.date ?? "Pending"}
icon={
step.done ? (
<IconCircleCheck size={16} />
) : (
<IconClock size={16} />
)
}
/>
))}
</Stepper>
{MOCK_APPLICATION.status === 'Ready for Collection' && (
<Alert variant="light" color="teal" icon={<IconPrinter size={17} />} mt="md">
Your Seaman Book is ready. Please visit the EMA office to collect it. Bring your National ID.
{MOCK_APPLICATION.status === "Ready for Collection" && (
<Alert
variant="light"
color="teal"
icon={<IconPrinter size={17} />}
mt="md"
>
Your Seaman Book is ready. Please visit the EMA office to collect
it. Bring your National ID.
</Alert>
)}
</Paper>
@@ -168,33 +228,71 @@ export function SeamanBookPage() {
{/* Eligibility checklist */}
<Paper withBorder radius="lg" p="lg">
<Group mb="md" gap="xs">
<ThemeIcon variant="light" color={isEligible ? 'teal' : 'orange'} size={36} radius="md">
<ThemeIcon
variant="light"
color={isEligible ? "teal" : "orange"}
size={36}
radius="md"
>
<IconShield size={18} />
</ThemeIcon>
<Text fw={700}>Eligibility Requirements</Text>
</Group>
<Stack gap="sm">
<EligibilityItem label="Profile completed (name, DOB, nationality)" ok={ELIGIBILITY.hasProfile} />
<EligibilityItem label="National ID / Fayda uploaded" ok={ELIGIBILITY.hasNationalId} />
<EligibilityItem label="Valid medical certificate uploaded" ok={ELIGIBILITY.hasMedicalCert} />
<EligibilityItem
label="Profile completed (name, DOB, nationality)"
ok={ELIGIBILITY.hasProfile}
/>
<EligibilityItem
label="National ID / Fayda uploaded"
ok={ELIGIBILITY.hasNationalId}
/>
<EligibilityItem
label="Valid medical certificate uploaded"
ok={ELIGIBILITY.hasMedicalCert}
/>
<Divider label="Basic Safety Training (all 5 required)" labelPosition="left" my={4} />
<Divider
label="Basic Safety Training (all 5 required)"
labelPosition="left"
my={4}
/>
{ELIGIBILITY.bstItems.map((item) => (
<EligibilityItem key={item.label} label={item.label} ok={item.done} />
<EligibilityItem
key={item.label}
label={item.label}
ok={item.done}
/>
))}
{!isEligible && (
<Alert variant="light" color="orange" icon={<IconAlertCircle size={15} />} mt="xs" p="sm">
<Alert
variant="light"
color="orange"
icon={<IconAlertCircle size={15} />}
mt="xs"
p="sm"
>
<Text fz="xs">
Complete all requirements above before applying. Missing BST: {5 - bstDone} certificate(s).
Complete all requirements above before applying. Missing
BST: {5 - bstDone} certificate(s).
</Text>
</Alert>
)}
{isEligible && (
<Alert variant="light" color="teal" icon={<IconCircleCheck size={15} />} mt="xs" p="sm">
<Text fz="xs">You meet all requirements. You may proceed with your application.</Text>
<Alert
variant="light"
color="teal"
icon={<IconCircleCheck size={15} />}
mt="xs"
p="sm"
>
<Text fz="xs">
You meet all requirements. You may proceed with your
application.
</Text>
</Alert>
)}
</Stack>
@@ -211,24 +309,30 @@ export function SeamanBookPage() {
<Stack gap="sm">
<Text fz="sm" c="dimmed" lh={1.6}>
Upon submitting your application, EMA Registration Officers will verify your profile,
documents, medical certificate, and Basic Safety Training certificates. You will be
notified at each stage by email and SMS.
Upon submitting your application, EMA Registration Officers will
verify your profile, documents, medical certificate, and Basic
Safety Training certificates. You will be notified at each stage
by email and SMS.
</Text>
<Divider />
<Text fw={600} fz="sm">What will be verified:</Text>
<Text fw={600} fz="sm">
What will be verified:
</Text>
<Stack gap={6}>
{[
'Full seafarer profile',
'National ID / Fayda authenticity',
'Medical certificate validity',
'All 5 Basic Safety Training certificates',
'Passport size photo',
"Full seafarer profile",
"National ID / Fayda authenticity",
"Medical certificate validity",
"All 5 Basic Safety Training certificates",
"Passport size photo",
].map((item) => (
<Group key={item} gap="xs">
<IconCircleCheck size={15} color="var(--mantine-color-teal-6)" />
<IconCircleCheck
size={15}
color="var(--mantine-color-teal-6)"
/>
<Text fz="sm">{item}</Text>
</Group>
))}
@@ -241,8 +345,12 @@ export function SeamanBookPage() {
<Group gap="xs">
<IconClock size={15} color="var(--mantine-color-blue-6)" />
<div>
<Text fz="xs" c="dimmed">Processing time</Text>
<Text fz="sm" fw={600}>57 working days</Text>
<Text fz="xs" c="dimmed">
Processing time
</Text>
<Text fz="sm" fw={600}>
57 working days
</Text>
</div>
</Group>
</Card>
@@ -250,22 +358,32 @@ export function SeamanBookPage() {
<Group gap="xs">
<IconHeart size={15} color="var(--mantine-color-red-6)" />
<div>
<Text fz="xs" c="dimmed">Medical validity</Text>
<Text fz="sm" fw={600}>2 years (STCW)</Text>
<Text fz="xs" c="dimmed">
Medical validity
</Text>
<Text fz="sm" fw={600}>
2 years (STCW)
</Text>
</div>
</Group>
</Card>
</SimpleGrid>
<Alert variant="light" color="blue" icon={<IconInfoCircle size={15} />} p="xs">
<Alert
variant="light"
color="blue"
icon={<IconInfoCircle size={15} />}
p="xs"
>
<Text fz="xs">
Application fee will be communicated during the review process. Payment can be made online or at the EMA office.
Application fee will be communicated during the review
process. Payment can be made online or at the EMA office.
</Text>
</Alert>
<Button
leftSection={<IconBook2 size={16} />}
onClick={() => navigate('/seaman-book/apply')}
onClick={() => navigate("/seaman-book/apply")}
loading={submitting}
disabled={!isEligible}
size="md"
@@ -287,20 +405,38 @@ export function SeamanBookPage() {
<Paper withBorder radius="lg" p="lg" bg="var(--mantine-color-blue-light)">
<Group gap="xs" mb="sm">
<IconInfoCircle size={18} color="var(--mantine-color-blue-6)" />
<Text fw={700} fz="sm">About the Seaman Book</Text>
<Text fw={700} fz="sm">
About the Seaman Book
</Text>
</Group>
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing="md">
{[
{ icon: IconBook2, title: 'Official Identity', desc: 'Internationally recognized maritime identity document required before joining any vessel.' },
{ icon: IconFileDescription, title: 'Service Record', desc: 'Records all your sea service, vessel assignments, and employment history.' },
{ icon: IconShield, title: 'STCW Compliance', desc: 'Required under STCW for all seafarers. Must be renewed and kept valid throughout your career.' },
{
icon: IconBook2,
title: "Official Identity",
desc: "Internationally recognized maritime identity document required before joining any vessel.",
},
{
icon: IconFileDescription,
title: "Service Record",
desc: "Records all your sea service, vessel assignments, and employment history.",
},
{
icon: IconShield,
title: "STCW Compliance",
desc: "Required under STCW for all seafarers. Must be renewed and kept valid throughout your career.",
},
].map(({ icon: Icon, title, desc }) => (
<Box key={title}>
<Group gap="xs" mb={4}>
<Icon size={16} color="var(--mantine-color-blue-6)" />
<Text fz="sm" fw={600}>{title}</Text>
<Text fz="sm" fw={600}>
{title}
</Text>
</Group>
<Text fz="xs" c="dimmed" lh={1.5}>{desc}</Text>
<Text fz="xs" c="dimmed" lh={1.5}>
{desc}
</Text>
</Box>
))}
</SimpleGrid>

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import {
Alert,
Badge,
@@ -14,7 +14,7 @@ import {
ThemeIcon,
Title,
rem,
} from '@mantine/core';
} from "@mantine/core";
import {
IconAnchor,
IconCheck,
@@ -27,16 +27,23 @@ import {
IconInfoCircle,
IconClockHour4,
IconTransferIn,
} from '@tabler/icons-react';
import { useApiMutation } from '@ema-platform/api';
import { authStorage } from '@ema-platform/auth';
} from "@tabler/icons-react";
import { useApiMutation } from "@ema-platform/api";
import { authStorage } from "@ema-platform/auth";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
type VesselRegStatus = 'Pending' | 'Under Review' | 'Approved' | 'Rejected' | 'Correction Required';
type VesselCategory = 'Inland Waterway Vessel' | 'Sea-going Vessel (International)';
type RenewalStatus = 'Valid' | 'Due Soon' | 'Overdue' | 'Not Applicable';
type VesselRegStatus =
| "Pending"
| "Under Review"
| "Approved"
| "Rejected"
| "Correction Required";
type VesselCategory =
| "Inland Waterway Vessel"
| "Sea-going Vessel (International)";
type RenewalStatus = "Valid" | "Due Soon" | "Overdue" | "Not Applicable";
interface VesselRegistration {
id: string;
@@ -45,7 +52,7 @@ interface VesselRegistration {
vesselType: string;
flagState: string;
portOfRegistry: string;
capacityLabel: 'Passenger Capacity' | 'Gross Tonnage (GT)';
capacityLabel: "Passenger Capacity" | "Gross Tonnage (GT)";
capacityValue: number;
vesselLengthM: number;
imoOrHullNumber: string;
@@ -57,26 +64,63 @@ interface VesselRegistration {
renewalStatus: RenewalStatus;
expiryDate: string | null;
}
const mockVesselRegistrations: VesselRegistration = {
id: "VR-001",
vesselName: "MV Blue Horizon",
category: "Sea-going Vessel (International)",
vesselType: "Container Ship",
flagState: "Ethiopia",
portOfRegistry: "Djibouti",
capacityLabel: "Gross Tonnage (GT)",
capacityValue: 18500,
vesselLengthM: 210,
imoOrHullNumber: "IMO9384721",
ownerName: "Blue Ocean Shipping Ltd.",
status: "Approved",
submittedDate: "2026-06-10",
approvalDate: "2026-06-15",
remarks: "Registration approved successfully.",
renewalStatus: "Due Soon",
expiryDate: "2031-06-15",
};
const STATUS_COLOR: Record<string, string> = {
Pending: 'gray',
'Under Review': 'yellow',
Approved: 'teal',
Rejected: 'red',
'Correction Required': 'orange',
Pending: "gray",
"Under Review": "yellow",
Approved: "teal",
Rejected: "red",
"Correction Required": "orange",
};
// Inland vessel certificates (1)
const INLAND_CERTIFICATES = [
{ label: 'Inland Vessel Registration Certificate', description: 'Official government registration document for inland waterway operation' },
{
label: "Inland Vessel Registration Certificate",
description:
"Official government registration document for inland waterway operation",
},
];
// Sea-going vessel certificates (4)
const SEAGOING_CERTIFICATES = [
{ label: 'Certificate of Nationality', description: 'Certifies the vessel\'s nationality and right to fly the Ethiopian flag' },
{ label: 'Certificate of Ownership', description: 'Confirms legal ownership of the vessel' },
{ label: 'Certificate of Registration', description: 'Official registration document for international sea-going operation' },
{ label: 'Minimum Safe Manning Certificate', description: 'Specifies the minimum crew required for safe operation of the vessel' },
{
label: "Certificate of Nationality",
description:
"Certifies the vessel's nationality and right to fly the Ethiopian flag",
},
{
label: "Certificate of Ownership",
description: "Confirms legal ownership of the vessel",
},
{
label: "Certificate of Registration",
description:
"Official registration document for international sea-going operation",
},
{
label: "Minimum Safe Manning Certificate",
description:
"Specifies the minimum crew required for safe operation of the vessel",
},
];
// ---------------------------------------------------------------------------
@@ -96,7 +140,13 @@ function RequirementItem({ label }: { label: string }) {
// ---------------------------------------------------------------------------
// Certificate card (shown after approval)
// ---------------------------------------------------------------------------
function CertificateCard({ label, description }: { label: string; description: string }) {
function CertificateCard({
label,
description,
}: {
label: string;
description: string;
}) {
return (
<Card withBorder radius="md" p="md">
<Group gap="sm" mb="xs" wrap="nowrap">
@@ -104,11 +154,21 @@ function CertificateCard({ label, description }: { label: string; description: s
<IconCertificate size={20} />
</ThemeIcon>
<div>
<Text fw={600} fz="sm">{label}</Text>
<Text fz="xs" c="dimmed">{description}</Text>
<Text fw={600} fz="sm">
{label}
</Text>
<Text fz="xs" c="dimmed">
{description}
</Text>
</div>
</Group>
<Button size="xs" variant="light" color="teal" leftSection={<IconDownload size={14} />} fullWidth>
<Button
size="xs"
variant="light"
color="teal"
leftSection={<IconDownload size={14} />}
fullWidth
>
Download Certificate
</Button>
</Card>
@@ -120,7 +180,9 @@ function CertificateCard({ label, description }: { label: string; description: s
// ---------------------------------------------------------------------------
export function VesselRegistrationPage() {
const navigate = useNavigate();
const [registration, setRegistration] = useState<VesselRegistration | null>(null);
const [registration, setRegistration] = useState<VesselRegistration | null>(
mockVesselRegistrations,
);
const [fetchTrigger] = useApiMutation<VesselRegistration>();
const fetched = useRef(false);
@@ -128,15 +190,18 @@ export function VesselRegistrationPage() {
const profileId = authStorage.getProfileId();
if (!profileId || fetched.current) return;
fetched.current = true;
fetchTrigger({ url: '/vessel-registrations/my', method: 'GET' })
fetchTrigger({ url: "/vessel-registrations/my", method: "GET" })
.unwrap()
.then((data) => setRegistration(data))
.catch(() => {/* no registration yet */});
.catch(() => {
/* no registration yet */
});
}, [fetchTrigger]);
const certs = registration?.category === 'Sea-going Vessel (International)'
? SEAGOING_CERTIFICATES
: INLAND_CERTIFICATES;
const certs =
registration?.category === "Sea-going Vessel (International)"
? SEAGOING_CERTIFICATES
: INLAND_CERTIFICATES;
return (
<Stack gap="md">
@@ -146,7 +211,9 @@ export function VesselRegistrationPage() {
</ThemeIcon>
<div>
<Title order={3}>Vessel Registration</Title>
<Text fz="sm" c="dimmed">Register your vessel with the Ethiopian Maritime Authority</Text>
<Text fz="sm" c="dimmed">
Register your vessel with the Ethiopian Maritime Authority
</Text>
</div>
</Group>
@@ -159,16 +226,21 @@ export function VesselRegistrationPage() {
<IconAnchor size={28} />
</ThemeIcon>
<div>
<Text fw={700} fz="lg">Register Your Vessel</Text>
<Text fw={700} fz="lg">
Register Your Vessel
</Text>
<Text fz="sm" c="dimmed">
Obtain official registration for inland waterway or sea-going vessels
Obtain official registration for inland waterway or sea-going
vessels
</Text>
</div>
</Group>
<Divider mb="md" />
<Text fw={600} fz="sm" mb="xs">Requirements</Text>
<Text fw={600} fz="sm" mb="xs">
Requirements
</Text>
<Stack gap={6} mb="xl">
<RequirementItem label="Proof of Ownership / Bill of Sale" />
<RequirementItem label="Builder's Certificate or Technical Specifications" />
@@ -181,22 +253,31 @@ export function VesselRegistrationPage() {
<Button
size="md"
leftSection={<IconAnchor size={18} />}
onClick={() => navigate('/vessel-registration/apply')}
onClick={() => navigate("/vessel-registration/apply")}
>
Start Vessel Registration
</Button>
</Paper>
<Paper withBorder radius="md" p="md" bg="var(--mantine-color-blue-light)">
<Paper
withBorder
radius="md"
p="md"
bg="var(--mantine-color-blue-light)"
>
<Group gap="xs" mb={4}>
<IconInfoCircle size={16} color="var(--mantine-color-blue-7)" />
<Text fw={600} fz="sm" c="blue.7">About Vessel Registration</Text>
<Text fw={600} fz="sm" c="blue.7">
About Vessel Registration
</Text>
</Group>
<Text fz="sm" c="dimmed">
Registration is valid for <strong>5 years</strong> from the date of approval. After approval,
inland vessels receive an <strong>Inland Vessel Registration Certificate</strong>, while
sea-going vessels receive four certificates: Certificate of Nationality, Certificate of
Ownership, Certificate of Registration, and Minimum Safe Manning Certificate.
Registration is valid for <strong>5 years</strong> from the date
of approval. After approval, inland vessels receive an{" "}
<strong>Inland Vessel Registration Certificate</strong>, while
sea-going vessels receive four certificates: Certificate of
Nationality, Certificate of Ownership, Certificate of
Registration, and Minimum Safe Manning Certificate.
</Text>
</Paper>
</>
@@ -206,21 +287,27 @@ export function VesselRegistrationPage() {
{registration && (
<>
{/* Renewal alert */}
{registration.renewalStatus === 'Due Soon' && (
{registration.renewalStatus === "Due Soon" && (
<Alert
icon={<IconAlertCircle size={17} />}
color="orange"
title="Renewal Due Soon"
>
Your vessel registration expires on {registration.expiryDate}. Please initiate renewal to avoid expiry.
Your vessel registration expires on {registration.expiryDate}.
Please initiate renewal to avoid expiry.
<Button size="xs" variant="white" color="orange" mt="xs">
Start Renewal
</Button>
</Alert>
)}
{registration.renewalStatus === 'Overdue' && (
<Alert icon={<IconAlertCircle size={17} />} color="red" title="Registration Expired">
Your vessel registration expired on {registration.expiryDate}. Immediate renewal is required.
{registration.renewalStatus === "Overdue" && (
<Alert
icon={<IconAlertCircle size={17} />}
color="red"
title="Registration Expired"
>
Your vessel registration expired on {registration.expiryDate}.
Immediate renewal is required.
</Alert>
)}
@@ -232,27 +319,45 @@ export function VesselRegistrationPage() {
<IconAnchor size={22} />
</ThemeIcon>
<div>
<Text fw={700} fz="lg">{registration.vesselName}</Text>
<Text fz="xs" c="dimmed">{registration.id}</Text>
<Text fw={700} fz="lg">
{registration.vesselName}
</Text>
<Text fz="xs" c="dimmed">
{registration.id}
</Text>
</div>
</Group>
<Badge color={STATUS_COLOR[registration.status] ?? 'gray'} size="lg" variant="light">
<Badge
color={STATUS_COLOR[registration.status] ?? "gray"}
size="lg"
variant="light"
>
{registration.status}
</Badge>
</Group>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
{[
{ label: 'Category', value: registration.category },
{ label: 'Vessel Type', value: registration.vesselType },
{ label: 'Flag State', value: registration.flagState },
{ label: 'Port of Registry', value: registration.portOfRegistry },
{ label: registration.capacityLabel, value: String(registration.capacityValue) },
{ label: 'Submitted', value: registration.submittedDate },
{ label: "Category", value: registration.category },
{ label: "Vessel Type", value: registration.vesselType },
{ label: "Flag State", value: registration.flagState },
{
label: "Port of Registry",
value: registration.portOfRegistry,
},
{
label: registration.capacityLabel,
value: String(registration.capacityValue),
},
{ label: "Submitted", value: registration.submittedDate },
].map((row) => (
<div key={row.label}>
<Text fz="xs" c="dimmed" tt="uppercase" fw={600} lh={1.2}>{row.label}</Text>
<Text fz="sm" mt={2}>{row.value || '—'}</Text>
<Text fz="xs" c="dimmed" tt="uppercase" fw={600} lh={1.2}>
{row.label}
</Text>
<Text fz="sm" mt={2}>
{row.value || "—"}
</Text>
</div>
))}
</SimpleGrid>
@@ -260,30 +365,44 @@ export function VesselRegistrationPage() {
{registration.remarks && (
<>
<Divider my="md" />
<Text fz="xs" c="dimmed" tt="uppercase" fw={600} mb={4}>Officer Remarks</Text>
<Text fz="xs" c="dimmed" tt="uppercase" fw={600} mb={4}>
Officer Remarks
</Text>
<Text fz="sm">{registration.remarks}</Text>
</>
)}
</Paper>
{/* Timeline / status info */}
{registration.status !== 'Approved' && (
{registration.status !== "Approved" && (
<Paper withBorder radius="md" p="md">
<Group gap="xs" mb="sm">
<IconClockHour4 size={16} />
<Text fw={600} fz="sm">Application Status</Text>
<Text fw={600} fz="sm">
Application Status
</Text>
</Group>
<Stack gap={6}>
{[
{ label: 'Submitted', done: true },
{ label: 'Under Review', done: registration.status !== 'Pending' },
{ label: 'Approved', done: false },
{ label: "Submitted", done: true },
{
label: "Under Review",
done: registration.status !== "Pending",
},
{ label: "Approved", done: false },
].map((step) => (
<Group key={step.label} gap="xs">
<ThemeIcon size={20} radius="xl" color={step.done ? 'teal' : 'gray'} variant={step.done ? 'filled' : 'light'}>
<ThemeIcon
size={20}
radius="xl"
color={step.done ? "teal" : "gray"}
variant={step.done ? "filled" : "light"}
>
<IconCheck size={12} />
</ThemeIcon>
<Text fz="sm" c={step.done ? undefined : 'dimmed'}>{step.label}</Text>
<Text fz="sm" c={step.done ? undefined : "dimmed"}>
{step.label}
</Text>
</Group>
))}
</Stack>
@@ -291,19 +410,23 @@ export function VesselRegistrationPage() {
)}
{/* Transfer ownership — only when approved */}
{registration.status === 'Approved' && (
{registration.status === "Approved" && (
<Paper withBorder radius="md" p="md">
<Group justify="space-between">
<div>
<Text fw={600} fz="sm">Transfer Ownership</Text>
<Text fz="xs" c="dimmed">Transfer this vessel to a new owner</Text>
<Text fw={600} fz="sm">
Transfer Ownership
</Text>
<Text fz="xs" c="dimmed">
Transfer this vessel to a new owner
</Text>
</div>
<Button
leftSection={<IconTransferIn size={15} />}
color="violet"
variant="light"
size="sm"
onClick={() => navigate('/vessel-registration/transfer')}
onClick={() => navigate("/vessel-registration/transfer")}
>
Request Transfer
</Button>
@@ -312,22 +435,30 @@ export function VesselRegistrationPage() {
)}
{/* Certificates section — shown after approval */}
{registration.status === 'Approved' && (
{registration.status === "Approved" && (
<div>
<Group gap="xs" mb="sm">
<IconShieldCheck size={18} color="var(--mantine-color-teal-6)" />
<IconShieldCheck
size={18}
color="var(--mantine-color-teal-6)"
/>
<Text fw={700} fz="md">
{registration.category === 'Sea-going Vessel (International)'
? 'Issued Certificates (4)'
: 'Issued Certificate'}
{registration.category === "Sea-going Vessel (International)"
? "Issued Certificates (4)"
: "Issued Certificate"}
</Text>
</Group>
<Alert color="teal" icon={<IconCircleCheck size={17} />} mb="md">
Your vessel registration has been approved. You may download your certificate(s) below.
Your vessel registration has been approved. You may download
your certificate(s) below.
</Alert>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
{certs.map((cert) => (
<CertificateCard key={cert.label} label={cert.label} description={cert.description} />
<CertificateCard
key={cert.label}
label={cert.label}
description={cert.description}
/>
))}
</SimpleGrid>
</div>

View File

@@ -156,15 +156,15 @@ const ALWAYS_VISIBLE = ["/notifications", "/profile", "/support"];
const NAV_ACCESS: Record<string, string[] | "*"> = {
null: "*",
cadet: "*",
// cadet: [
// "/dashboard",
// "/seafarer-registry",
// "/seaman-book",
// "/certificates",
// "/endorsements",
// "/documents",
// ],
//cadet: "*",
cadet: [
"/dashboard",
"/seafarer-registry",
"/seaman-book",
"/certificates",
"/endorsements",
"/documents",
],
registration: [
"/vessel-registration-dashboard",
"/vessel-registration",

View File

@@ -12,7 +12,7 @@ import {
Paper,
Badge,
} from "@mantine/core";
import { IconRefresh, IconEye, IconInbox } from "@tabler/icons-react";
import { IconRefresh, IconAdjustmentsHorizontal , IconInbox, } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
export interface AdvancedColumn<T> {
@@ -107,7 +107,7 @@ export function AdvancedTable<T extends { id?: string | number }>({
<Button
variant="default"
size="sm"
leftSection={<IconEye size={16} />}
leftSection={<IconAdjustmentsHorizontal size={16} />}
>
{t("common.view", "View")}
</Button>

18
package-lock.json generated
View File

@@ -3873,6 +3873,18 @@
}
}
},
"node_modules/@mui/x-date-pickers/node_modules/@types/react": {
"version": "18.3.31",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
"integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
}
},
"node_modules/@mui/x-date-pickers/node_modules/react-is": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz",
@@ -11954,7 +11966,7 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"iconv-lite": "^0.6.2"
@@ -13745,7 +13757,7 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@@ -17931,7 +17943,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/sax": {