mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
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:
@@ -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}`);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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}>5–7 working days</Text>
|
||||
<Text fz="xs" c="dimmed">
|
||||
Processing time
|
||||
</Text>
|
||||
<Text fz="sm" fw={600}>
|
||||
5–7 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>
|
||||
|
||||
@@ -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,13 +190,16 @@ 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)'
|
||||
const certs =
|
||||
registration?.category === "Sea-going Vessel (International)"
|
||||
? SEAGOING_CERTIFICATES
|
||||
: INLAND_CERTIFICATES;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
18
package-lock.json
generated
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user