From e482a4aca64f8742505c378b94ed07815549200d Mon Sep 17 00:00:00 2001 From: estifanos Date: Fri, 24 Jul 2026 07:44:45 +0000 Subject: [PATCH] feat(vessel-registration): implement vessel registration feature with multi-step application process - Added VesselRegistrationApplicationPage for submitting new vessel registrations. - Created VesselRegistrationPage to list user's registrations and navigate to details. - Implemented VesselRegistrationStatusPage to display registration status and download certificates. - Integrated mock data for registrations and certificates. - Updated navigation and routing to include vessel registration paths. - Added translations for vessel registration in English and Amharic. - Documented the vessel registration workflow and API integration notes. --- .../app/features/vessel-registration/mock.ts | 433 ++++++++++++++++++ .../pages/VesselRegistrationQueuePage.tsx | 169 +++++++ .../pages/VesselRegistrationReportPage.tsx | 176 +++++++ .../pages/VesselRegistrationReviewPage.tsx | 350 ++++++++++++++ apps/backoffice/src/app/i18n/locales/am.ts | 2 + apps/backoffice/src/app/i18n/locales/en.ts | 2 + .../src/app/layouts/BackofficeLayout.tsx | 4 + apps/backoffice/src/app/router/index.tsx | 6 + .../app/features/vessel-registration/mock.ts | 286 ++++++++++++ .../VesselRegistrationApplicationPage.tsx | 367 +++++++++++++++ .../pages/VesselRegistrationPage.tsx | 81 ++++ .../pages/VesselRegistrationStatusPage.tsx | 156 +++++++ apps/portal/src/app/i18n/locales/am.ts | 1 + apps/portal/src/app/i18n/locales/en.ts | 1 + apps/portal/src/app/layouts/PortalLayout.tsx | 3 + apps/portal/src/app/router.tsx | 10 + docs/vessel-registration.md | 73 +++ 17 files changed, 2120 insertions(+) create mode 100644 apps/backoffice/src/app/features/vessel-registration/mock.ts create mode 100644 apps/backoffice/src/app/features/vessel-registration/pages/VesselRegistrationQueuePage.tsx create mode 100644 apps/backoffice/src/app/features/vessel-registration/pages/VesselRegistrationReportPage.tsx create mode 100644 apps/backoffice/src/app/features/vessel-registration/pages/VesselRegistrationReviewPage.tsx create mode 100644 apps/portal/src/app/features/vessel-registration/mock.ts create mode 100644 apps/portal/src/app/features/vessel-registration/pages/VesselRegistrationApplicationPage.tsx create mode 100644 apps/portal/src/app/features/vessel-registration/pages/VesselRegistrationPage.tsx create mode 100644 apps/portal/src/app/features/vessel-registration/pages/VesselRegistrationStatusPage.tsx create mode 100644 docs/vessel-registration.md diff --git a/apps/backoffice/src/app/features/vessel-registration/mock.ts b/apps/backoffice/src/app/features/vessel-registration/mock.ts new file mode 100644 index 000000000..7897dce21 --- /dev/null +++ b/apps/backoffice/src/app/features/vessel-registration/mock.ts @@ -0,0 +1,433 @@ +// ponytail: in-memory mock array, resets on reload — swap for RTK Query endpoint when backend lands. + +export type VesselCategory = 'Inland Waterway' | 'Sea-going'; + +export type RegistrationStatus = + | 'Pending' + | 'Under Review' + | 'Correction Required' + | 'Resubmitted' + | 'Approved' + | 'Rejected'; + +export const STATUS_COLOR: Record = { + Pending: 'gray', + 'Under Review': 'blue', + 'Correction Required': 'orange', + Resubmitted: 'cyan', + Approved: 'teal', + Rejected: 'red', +}; + +export type RenewalState = 'OK' | 'Due Soon' | 'Overdue'; + +export const RENEWAL_COLOR: Record = { + OK: 'gray', + 'Due Soon': 'orange', + Overdue: 'red', +}; + +export interface RegistrationOwner { + name: string; + idOrTin: string; + phone: string; + email: string; + address: string; +} + +export interface TimelineStep { + date: string | null; + event: string; + done: boolean; +} + +export interface RegistrationCertificate { + name: string; + number: string; + issueDate: string; +} + +export interface RegistrationDocument { + key: string; + label: string; + fileName: string; + fileType: 'pdf' | 'image'; +} + +export interface VesselRegistration { + id: string; + category: VesselCategory; + status: RegistrationStatus; + submitted: string; + remarks?: string; + correctionFields?: string[]; + expiryDate?: string; + renewal?: RenewalState; + timeline: TimelineStep[]; + documents: RegistrationDocument[]; + certificates?: RegistrationCertificate[]; + + // Vessel details + vesselName: string; + vesselType: string; + registrationArea: string; + flagState: string; + passengerCapacity?: string; + grossTonnage?: string; + length: string; + breadth: string; + depth: string; + + // Technical + imoNumber?: string; + hullNumber?: string; + shipyard: string; + yearBuilt: string; + engineType: string; + engineNumber: string; + enginePower: string; + hullMaterial: string; + + // Ownership + owner: RegistrationOwner; +} + +export const CERTIFICATES: Record = { + 'Inland Waterway': ['Inland Vessel Registration Certificate'], + 'Sea-going': [ + 'Certificate of Nationality', + 'Certificate of Ownership', + 'Certificate of Registration', + 'Minimum Safe Manning Certificate', + ], +}; + +const SUBMITTED_STEP = (date: string): TimelineStep => ({ date, event: 'Application Submitted', done: true }); +const PENDING_STEP = (event: string): TimelineStep => ({ date: null, event, done: false }); + +export const MOCK_REGISTRATIONS: VesselRegistration[] = [ + { + id: 'VR-2025-0001', + category: 'Sea-going', + status: 'Under Review', + submitted: '2025-06-20', + timeline: [ + SUBMITTED_STEP('2025-06-20'), + { date: '2025-06-22', event: 'Document Verification', done: true }, + PENDING_STEP('Inspection'), + PENDING_STEP('Approval'), + ], + documents: [ + { key: 'photos', label: 'Vessel Photos', fileName: 'nile_star_photos.pdf', fileType: 'pdf' }, + { key: 'ownership', label: 'Proof of Ownership / Bill of Sale', fileName: 'nile_star_bill_of_sale.pdf', fileType: 'pdf' }, + { key: 'particulars', label: 'Ship Particulars', fileName: 'nile_star_particulars.pdf', fileType: 'pdf' }, + { key: 'insurance', label: 'Insurance Certificate', fileName: 'nile_star_insurance.pdf', fileType: 'pdf' }, + ], + vesselName: 'MV Nile Star', + vesselType: 'Bulk Carrier', + registrationArea: 'Djibouti Corridor', + flagState: 'Ethiopia', + grossTonnage: '18500', + length: '190', + breadth: '28', + depth: '15', + imoNumber: 'IMO9876543', + shipyard: 'Hyundai Heavy Industries', + yearBuilt: '2016', + engineType: 'Diesel', + engineNumber: 'ENG-44210', + enginePower: '12000 kW', + hullMaterial: 'Steel', + owner: { + name: 'Nile Shipping PLC', + idOrTin: 'TIN-0012345678', + phone: '+251911223344', + email: 'ops@nileshipping.et', + address: 'Bole Sub-city, Addis Ababa', + }, + }, + { + id: 'VR-2025-0002', + category: 'Inland Waterway', + status: 'Correction Required', + submitted: '2025-06-10', + remarks: 'Vessel photos are blurry — please re-upload at least 2 clear photos showing the hull and registration markings.', + correctionFields: ['Vessel Photos'], + timeline: [ + SUBMITTED_STEP('2025-06-10'), + { date: '2025-06-12', event: 'Document Verification', done: true }, + { date: '2025-06-14', event: 'Correction Requested', done: true }, + PENDING_STEP('Inspection'), + PENDING_STEP('Approval'), + ], + documents: [ + { key: 'photos', label: 'Vessel Photos', fileName: 'tana_ferry3_photos.jpg', fileType: 'image' }, + ], + vesselName: 'Tana Ferry 3', + vesselType: 'Ferry', + registrationArea: 'Lake Tana', + flagState: 'Ethiopia', + passengerCapacity: '40', + length: '18', + breadth: '5', + depth: '2', + hullNumber: 'HN-2211', + shipyard: 'Bahir Dar Boat Works', + yearBuilt: '2020', + engineType: 'Outboard', + engineNumber: 'ENG-9931', + enginePower: '150 hp', + hullMaterial: 'Fiberglass', + owner: { + name: 'Getachew Alemu', + idOrTin: 'ID-4455667788', + phone: '+251922334455', + email: 'getachew.alemu@example.com', + address: 'Bahir Dar, Amhara', + }, + }, + { + id: 'VR-2025-0003', + category: 'Sea-going', + status: 'Approved', + submitted: '2025-04-05', + expiryDate: '2026-08-15', + renewal: 'Due Soon', + timeline: [ + SUBMITTED_STEP('2025-04-05'), + { date: '2025-04-08', event: 'Document Verification', done: true }, + { date: '2025-04-20', event: 'Inspection', done: true }, + { date: '2025-04-28', event: 'Approval', done: true }, + ], + documents: [ + { key: 'photos', label: 'Vessel Photos', fileName: 'abay_voyager_photos.pdf', fileType: 'pdf' }, + { key: 'ownership', label: 'Proof of Ownership / Bill of Sale', fileName: 'abay_voyager_bill_of_sale.pdf', fileType: 'pdf' }, + { key: 'particulars', label: 'Ship Particulars', fileName: 'abay_voyager_particulars.pdf', fileType: 'pdf' }, + { key: 'insurance', label: 'Insurance Certificate', fileName: 'abay_voyager_insurance.pdf', fileType: 'pdf' }, + ], + certificates: [ + { name: 'Certificate of Nationality', number: 'CN-2025-0091', issueDate: '2025-04-28' }, + { name: 'Certificate of Ownership', number: 'CO-2025-0091', issueDate: '2025-04-28' }, + { name: 'Certificate of Registration', number: 'CR-2025-0091', issueDate: '2025-04-28' }, + { name: 'Minimum Safe Manning Certificate', number: 'MSM-2025-0091', issueDate: '2025-04-28' }, + ], + vesselName: 'MV Abay Voyager', + vesselType: 'General Cargo', + registrationArea: 'Djibouti Corridor', + flagState: 'Ethiopia', + grossTonnage: '9600', + length: '140', + breadth: '21', + depth: '11', + imoNumber: 'IMO9123456', + shipyard: 'Damen Shipyards', + yearBuilt: '2012', + engineType: 'Diesel', + engineNumber: 'ENG-33012', + enginePower: '7200 kW', + hullMaterial: 'Steel', + owner: { + name: 'Abay Maritime PLC', + idOrTin: 'TIN-0098765432', + phone: '+251933445566', + email: 'contact@abaymaritime.et', + address: 'Kirkos Sub-city, Addis Ababa', + }, + }, + { + id: 'VR-2025-0004', + category: 'Inland Waterway', + status: 'Rejected', + submitted: '2025-03-02', + remarks: 'Hull number does not match the submitted proof of ownership. Application rejected — please reapply with matching documentation.', + timeline: [ + SUBMITTED_STEP('2025-03-02'), + { date: '2025-03-05', event: 'Document Verification', done: true }, + { date: '2025-03-14', event: 'Inspection', done: true }, + { date: '2025-03-18', event: 'Rejected', done: true }, + ], + documents: [ + { key: 'photos', label: 'Vessel Photos', fileName: 'awash_cargo1_photos.jpg', fileType: 'image' }, + ], + vesselName: 'Awash Cargo 1', + vesselType: 'Cargo Barge', + registrationArea: 'Awash River Basin', + flagState: 'Ethiopia', + passengerCapacity: '0', + length: '22', + breadth: '6', + depth: '3', + hullNumber: 'HN-1187', + shipyard: 'Awash River Works', + yearBuilt: '2018', + engineType: 'Inboard', + engineNumber: 'ENG-5567', + enginePower: '210 hp', + hullMaterial: 'Steel', + owner: { + name: 'Selam Tesfaye', + idOrTin: 'ID-2233445566', + phone: '+251944556677', + email: 'selam.tesfaye@example.com', + address: 'Adama, Oromia', + }, + }, + { + id: 'VR-2025-0005', + category: 'Sea-going', + status: 'Pending', + submitted: '2025-07-10', + timeline: [ + SUBMITTED_STEP('2025-07-10'), + PENDING_STEP('Document Verification'), + PENDING_STEP('Inspection'), + PENDING_STEP('Approval'), + ], + documents: [ + { key: 'photos', label: 'Vessel Photos', fileName: 'genale_pearl_photos.pdf', fileType: 'pdf' }, + { key: 'ownership', label: 'Proof of Ownership / Bill of Sale', fileName: 'genale_pearl_bill_of_sale.pdf', fileType: 'pdf' }, + { key: 'particulars', label: 'Ship Particulars', fileName: 'genale_pearl_particulars.pdf', fileType: 'pdf' }, + { key: 'insurance', label: 'Insurance Certificate', fileName: 'genale_pearl_insurance.pdf', fileType: 'pdf' }, + ], + vesselName: 'MV Genale Pearl', + vesselType: 'Container Ship', + registrationArea: 'Djibouti Corridor', + flagState: 'Ethiopia', + grossTonnage: '24300', + length: '210', + breadth: '30', + depth: '17', + imoNumber: 'IMO9345678', + shipyard: 'Samsung Heavy Industries', + yearBuilt: '2019', + engineType: 'Diesel', + engineNumber: 'ENG-51290', + enginePower: '15400 kW', + hullMaterial: 'Steel', + owner: { + name: 'Genale Maritime PLC', + idOrTin: 'TIN-0011223344', + phone: '+251911998877', + email: 'ops@genalemaritime.et', + address: 'Kirkos Sub-city, Addis Ababa', + }, + }, + { + id: 'VR-2025-0006', + category: 'Inland Waterway', + status: 'Resubmitted', + submitted: '2025-06-01', + remarks: 'Re-uploaded clearer hull and deck photos as requested.', + timeline: [ + SUBMITTED_STEP('2025-06-01'), + { date: '2025-06-03', event: 'Document Verification', done: true }, + { date: '2025-06-05', event: 'Correction Requested', done: true }, + { date: '2025-06-18', event: 'Resubmitted', done: true }, + PENDING_STEP('Approval'), + ], + documents: [ + { key: 'photos', label: 'Vessel Photos', fileName: 'zeway_runner_photos_v2.jpg', fileType: 'image' }, + ], + vesselName: 'Zeway Runner', + vesselType: 'Passenger Boat', + registrationArea: 'Lake Ziway', + flagState: 'Ethiopia', + passengerCapacity: '25', + length: '14', + breadth: '4', + depth: '1.6', + hullNumber: 'HN-3092', + shipyard: 'Ziway Boat Works', + yearBuilt: '2021', + engineType: 'Outboard', + engineNumber: 'ENG-7712', + enginePower: '90 hp', + hullMaterial: 'Fiberglass', + owner: { + name: 'Mekdes Yohannes', + idOrTin: 'ID-5566778899', + phone: '+251955667788', + email: 'mekdes.y@example.com', + address: 'Ziway, Oromia', + }, + }, + { + id: 'VR-2025-0007', + category: 'Sea-going', + status: 'Approved', + submitted: '2024-08-01', + expiryDate: '2025-08-01', + renewal: 'Overdue', + timeline: [ + SUBMITTED_STEP('2024-08-01'), + { date: '2024-08-05', event: 'Document Verification', done: true }, + { date: '2024-08-18', event: 'Inspection', done: true }, + { date: '2024-08-25', event: 'Approval', done: true }, + ], + documents: [ + { key: 'photos', label: 'Vessel Photos', fileName: 'red_sea_trader_photos.pdf', fileType: 'pdf' }, + { key: 'ownership', label: 'Proof of Ownership / Bill of Sale', fileName: 'red_sea_trader_bill_of_sale.pdf', fileType: 'pdf' }, + { key: 'particulars', label: 'Ship Particulars', fileName: 'red_sea_trader_particulars.pdf', fileType: 'pdf' }, + { key: 'insurance', label: 'Insurance Certificate', fileName: 'red_sea_trader_insurance.pdf', fileType: 'pdf' }, + ], + certificates: [ + { name: 'Certificate of Nationality', number: 'CN-2024-0058', issueDate: '2024-08-25' }, + { name: 'Certificate of Ownership', number: 'CO-2024-0058', issueDate: '2024-08-25' }, + { name: 'Certificate of Registration', number: 'CR-2024-0058', issueDate: '2024-08-25' }, + { name: 'Minimum Safe Manning Certificate', number: 'MSM-2024-0058', issueDate: '2024-08-25' }, + ], + vesselName: 'MV Red Sea Trader', + vesselType: 'Tanker', + registrationArea: 'Djibouti Corridor', + flagState: 'Ethiopia', + grossTonnage: '31200', + length: '228', + breadth: '32', + depth: '19', + imoNumber: 'IMO9456789', + shipyard: 'Mitsubishi Heavy Industries', + yearBuilt: '2009', + engineType: 'Diesel', + engineNumber: 'ENG-28871', + enginePower: '18900 kW', + hullMaterial: 'Steel', + owner: { + name: 'Red Sea Tankers PLC', + idOrTin: 'TIN-0055443322', + phone: '+251911332211', + email: 'fleet@redseatankers.et', + address: 'Yeka Sub-city, Addis Ababa', + }, + }, +]; + +// ponytail: mutate mock, no live clock — swap for real API mutation when backend lands. +export function applyDecision( + reg: VesselRegistration, + status: RegistrationStatus, + remarks?: string, + correctionFields?: string[] +): void { + reg.status = status; + if (remarks) reg.remarks = remarks; + reg.correctionFields = status === 'Correction Required' ? correctionFields : undefined; + const eventLabel: Record = { + Pending: 'Pending', + 'Under Review': 'Marked Under Review', + 'Correction Required': 'Correction Requested', + Resubmitted: 'Resubmitted', + Approved: 'Approval', + Rejected: 'Rejected', + }; + const today = reg.timeline[reg.timeline.length - 1]?.date ?? reg.submitted; + reg.timeline.push({ date: today, event: eventLabel[status], done: true }); +} + +export function generateCertificates(reg: VesselRegistration): void { + const today = reg.timeline[reg.timeline.length - 1]?.date ?? reg.submitted; + reg.certificates = CERTIFICATES[reg.category].map((name, i) => ({ + name, + number: `${name.split(' ').map((w) => w[0]).join('').toUpperCase()}-2025-${String(1000 + i)}`, + issueDate: today ?? reg.submitted, + })); +} diff --git a/apps/backoffice/src/app/features/vessel-registration/pages/VesselRegistrationQueuePage.tsx b/apps/backoffice/src/app/features/vessel-registration/pages/VesselRegistrationQueuePage.tsx new file mode 100644 index 000000000..70ed1fa2b --- /dev/null +++ b/apps/backoffice/src/app/features/vessel-registration/pages/VesselRegistrationQueuePage.tsx @@ -0,0 +1,169 @@ +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + ActionIcon, + Badge, + Box, + Button, + Card, + 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'; +import { MOCK_REGISTRATIONS, RENEWAL_COLOR, STATUS_COLOR } from '../mock'; + +export function VesselRegistrationQueuePage() { + const navigate = useNavigate(); + const [registrations] = useState(MOCK_REGISTRATIONS); + const [search, setSearch] = useState(''); + const [statusFilter, setStatusFilter] = useState(null); + const [categoryFilter, setCategoryFilter] = useState(null); + const [renewalFilter, setRenewalFilter] = useState(null); + + const stats = { + total: registrations.length, + pending: registrations.filter((r) => r.status === 'Pending').length, + underReview: registrations.filter((r) => r.status === 'Under Review').length, + approved: registrations.filter((r) => r.status === 'Approved').length, + }; + + const filtered = registrations.filter((r) => { + const q = search.toLowerCase(); + const matchSearch = !q + || r.vesselName.toLowerCase().includes(q) + || r.id.toLowerCase().includes(q) + || r.owner.name.toLowerCase().includes(q); + const matchStatus = !statusFilter || r.status === statusFilter; + const matchCategory = !categoryFilter || r.category === categoryFilter; + const matchRenewal = !renewalFilter || r.renewal === renewalFilter; + return matchSearch && matchStatus && matchCategory && matchRenewal; + }); + + return ( + +
+ Vessel Registration + Review and process vessel registration applications +
+ + {/* Stats */} + + {[ + { 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 }) => ( + + + +
{value}{label}
+
+
+ ))} +
+ + {/* Table */} + + + Registration Queue + + } + value={search} + onChange={(e) => setSearch(e.currentTarget.value)} + size="sm" + style={{ minWidth: rem(260) }} + rightSection={search ? setSearch('')}> : null} + /> + +