diff --git a/apps/backoffice/src/app/layouts/BackofficeLayout.tsx b/apps/backoffice/src/app/layouts/BackofficeLayout.tsx index 86163d99e..d0be5f68c 100644 --- a/apps/backoffice/src/app/layouts/BackofficeLayout.tsx +++ b/apps/backoffice/src/app/layouts/BackofficeLayout.tsx @@ -1,11 +1,11 @@ -import { useCallback, useState } from 'react'; -import { AppShell, rem } from '@mantine/core'; -import { useDisclosure } from '@mantine/hooks'; -import { Outlet, useLocation, useNavigate } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; -import { logout } from '@ema-platform/auth'; -import { AppHeader, AppSidebar } from '@ema-platform/ui'; -import type { NavItem } from '@ema-platform/ui'; +import { useCallback, useState } from "react"; +import { AppShell, rem } from "@mantine/core"; +import { useDisclosure } from "@mantine/hooks"; +import { Outlet, useLocation, useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { logout } from "@ema-platform/auth"; +import { AppHeader, AppSidebar } from "@ema-platform/ui"; +import type { NavItem } from "@ema-platform/ui"; import { IconAnchor, IconBook2, @@ -24,49 +24,104 @@ import { IconQuestionMark, IconClipboardList, IconReport, - IconAnchor, IconFilePlus, IconGauge, IconShieldOff, - IconShip, IconStack2, IconTruck, -} from '@tabler/icons-react'; -import { notify } from '@ema-platform/ui'; -import { SUPPORTED_LANGUAGES } from '../i18n/config'; -import { useAppDispatch, useAppSelector } from '../store/hooks'; + IconReportAnalytics, +} from "@tabler/icons-react"; +import { notify } from "@ema-platform/ui"; +import { SUPPORTED_LANGUAGES } from "../i18n/config"; +import { useAppDispatch, useAppSelector } from "../store/hooks"; const NAV_ITEMS: NavItem[] = [ - { to: '/dashboard', label: 'nav.dashboard', icon: IconLayoutDashboard }, - { to: '/um/user-management/dashboard', label: 'nav.userManagement', icon: IconUserShield }, - { to: '/seaman-book-queue', label: 'nav.seamanBookQueue', icon: IconBook2 }, - { to: '/vessel-registration-head-dashboard', label: 'nav.vesselRegistrationHeadDashboard', icon: IconGauge }, - { to: '/vessel-registration-queue', label: 'nav.vesselRegistrationQueue', icon: IconAnchor }, - { to: '/vessel-registration-queue/new', label: 'nav.vesselFormBuilder', icon: IconFilePlus }, - { to: '/vessel-registration-report', label: 'nav.vesselRegistrationReport', icon: IconChartBar }, - { to: '/vessel-ownership-transfer', label: 'nav.ownershipTransferQueue', icon: IconFileDescription }, - { to: '/logistics-head-dashboard', label: 'nav.logisticsHeadDashboard', icon: IconGauge }, - { to: '/freight-forwarder-license', label: 'nav.freightForwarderLicense', icon: IconTruck }, - { to: '/shipping-agent-license', label: 'nav.shippingAgentLicense', icon: IconShip }, - { to: '/combined-license', label: 'nav.combinedLicense', icon: IconStack2 }, - { to: '/joint-investment-license', label: 'nav.jointInvestmentLicense', icon: IconUsers }, - { to: '/mto-license', label: 'nav.mtoLicense', icon: IconTruck }, - { to: '/waiver', label: 'nav.waiver', icon: IconShieldOff }, - { to: '/coc-queue', label: 'nav.cocQueue', icon: IconShieldCheck }, - { to: '/endorsement-queue', label: 'nav.endorsementQueue', icon: IconRubberStamp }, - { to: '/vessel-registrations', label: 'nav.vesselRegistrations', icon: IconAnchor }, - { to: '/vessel-registration-report', label: 'nav.vesselRegistrationReport', icon: IconReportAnalytics }, - { to: '/vessel-transfers', label: 'nav.vesselTransfers', icon: IconShip }, - { to: '/seafarer-registry', label: 'nav.seafarerRegistry', icon: IconUsers }, + { to: "/dashboard", label: "nav.dashboard", icon: IconLayoutDashboard }, + { + to: "/um/user-management/dashboard", + label: "nav.userManagement", + icon: IconUserShield, + }, + { to: "/seaman-book-queue", label: "nav.seamanBookQueue", icon: IconBook2 }, + { + to: "/vessel-registration-head-dashboard", + label: "nav.vesselRegistrationHeadDashboard", + icon: IconGauge, + }, + { + to: "/vessel-registration-queue", + label: "nav.vesselRegistrationQueue", + icon: IconAnchor, + }, + { + to: "/vessel-registration-queue/new", + label: "nav.vesselFormBuilder", + icon: IconFilePlus, + }, + { + to: "/vessel-registration-report", + label: "nav.vesselRegistrationReport", + icon: IconChartBar, + }, + { + to: "/vessel-ownership-transfer", + label: "nav.ownershipTransferQueue", + icon: IconFileDescription, + }, + { + to: "/logistics-head-dashboard", + label: "nav.logisticsHeadDashboard", + icon: IconGauge, + }, + { + to: "/freight-forwarder-license", + label: "nav.freightForwarderLicense", + icon: IconTruck, + }, + { + to: "/shipping-agent-license", + label: "nav.shippingAgentLicense", + icon: IconShip, + }, + { to: "/combined-license", label: "nav.combinedLicense", icon: IconStack2 }, + { + to: "/joint-investment-license", + label: "nav.jointInvestmentLicense", + icon: IconUsers, + }, + { to: "/mto-license", label: "nav.mtoLicense", icon: IconTruck }, + { to: "/waiver", label: "nav.waiver", icon: IconShieldOff }, + { to: "/coc-queue", label: "nav.cocQueue", icon: IconShieldCheck }, + { + to: "/endorsement-queue", + label: "nav.endorsementQueue", + icon: IconRubberStamp, + }, + { + to: "/vessel-registrations", + label: "nav.vesselRegistrations", + icon: IconAnchor, + }, + { + to: "/vessel-registration-report", + label: "nav.vesselRegistrationReport", + icon: IconReportAnalytics, + }, + { to: "/vessel-transfers", label: "nav.vesselTransfers", icon: IconShip }, + { to: "/seafarer-registry", label: "nav.seafarerRegistry", icon: IconUsers }, // { to: '/applications', label: 'nav.applications', icon: IconFileDescription }, - { to: '/payment-config', label: 'nav.paymentConfig', icon: IconCreditCard }, - { to: '/analytics', label: 'nav.analytics', icon: IconChartBar }, - { to: '/medical-verification', label: 'nav.medicalVerification', icon: IconHeart }, - { to: '/questions', label: 'nav.questions', icon: IconQuestionMark }, - { to: '/exams', label: 'nav.exams', icon: IconClipboardList }, - { to: '/exam-results', label: 'nav.examResults', icon: IconReport }, - { to: '/configuration', label: 'nav.configuration', icon: IconSettings }, - { to: '/profile', label: 'nav.profile', icon: IconUser }, + { to: "/payment-config", label: "nav.paymentConfig", icon: IconCreditCard }, + { to: "/analytics", label: "nav.analytics", icon: IconChartBar }, + { + to: "/medical-verification", + label: "nav.medicalVerification", + icon: IconHeart, + }, + { to: "/questions", label: "nav.questions", icon: IconQuestionMark }, + { to: "/exams", label: "nav.exams", icon: IconClipboardList }, + { to: "/exam-results", label: "nav.examResults", icon: IconReport }, + { to: "/configuration", label: "nav.configuration", icon: IconSettings }, + { to: "/profile", label: "nav.profile", icon: IconUser }, ]; const HEADER_HEIGHT = 116; @@ -81,24 +136,29 @@ export function BackofficeLayout() { const user = useAppSelector((state) => state.auth.user); const layoutMode = useAppSelector((state) => state.preferences.layoutMode); - const displayName = user?.name?.en || user?.username || ''; + const displayName = user?.name?.en || user?.username || ""; const initials = displayName - ? displayName.split(/\s+/).map((s) => s[0]).join('').toUpperCase().slice(0, 2) - : '?'; + ? displayName + .split(/\s+/) + .map((s) => s[0]) + .join("") + .toUpperCase() + .slice(0, 2) + : "?"; const handleLogout = useCallback(() => { dispatch(logout()); - navigate('/login'); + navigate("/login"); }, [dispatch, navigate]); - const segments = location.pathname.split('/').filter(Boolean); + const segments = location.pathname.split("/").filter(Boolean); const crumbs = [ - { label: t('nav.dashboard'), path: '/dashboard' }, + { label: t("nav.dashboard"), path: "/dashboard" }, ...segments - .map((_, i) => '/' + segments.slice(0, i + 1).join('/')) - .filter((path) => path !== '/dashboard') - .filter((path) => !path.startsWith('/um')) - .map((path) => ({ label: t('nav.dashboard'), path })), + .map((_, i) => "/" + segments.slice(0, i + 1).join("/")) + .filter((path) => path !== "/dashboard") + .filter((path) => !path.startsWith("/um")) + .map((path) => ({ label: t("nav.dashboard"), path })), ]; const go = (item: NavItem) => { @@ -115,27 +175,31 @@ export function BackofficeLayout() { setCollapsed((prev) => !prev); }, []); - const isSidebar = layoutMode === 'sidebar'; + const isSidebar = layoutMode === "sidebar"; return ( -
+
@@ -152,51 +216,58 @@ export function BackofficeLayout() { {!isSidebar && (
{NAV_ITEMS.map((item) => { - const active = !!item.to && (location.pathname === item.to || location.pathname.startsWith(`${item.to}/`)); + const active = + !!item.to && + (location.pathname === item.to || + location.pathname.startsWith(`${item.to}/`)); const ItemIcon = item.icon; return ( - }>Registration not found. - - ); - } - - 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 ( - - - -
- {reg.vesselName} - {reg.id} — {reg.category} -
-
- - - - - -
- Registration Status - Submitted {reg.submitted} -
-
- {reg.status} -
- - {reg.renewal && reg.renewal !== 'OK' && ( - } - mb="md" - p="sm" - > - - Registration {reg.renewal === 'Overdue' ? 'is overdue for renewal' : 'is due for renewal soon'} - {reg.expiryDate ? ` — expires ${reg.expiryDate}` : ''}. - - - )} - - {reg.remarks && ( - } mb="md" p="sm"> - Officer Remarks - {reg.remarks} - - )} - - - {reg.timeline.map((step, i) => ( - : } - /> - ))} - - - {needsCorrection && ( - - - - )} -
- - {reg.status === 'Approved' && reg.certificates && ( - - Certificates - - {reg.certificates.map((cert) => ( -
- -
- {cert.name} - Certificate No. {cert.number} — Issued {cert.issueDate} - {cert.downloads > 0 && ( - Downloaded {cert.downloads} time{cert.downloads === 1 ? '' : 's'} - )} -
- -
- -
- ))} -
-
- )} -
- ); -} diff --git a/apps/portal/src/app/features/vessel-transfer/mock.ts b/apps/portal/src/app/features/vessel-transfer/mock.ts deleted file mode 100644 index 094723c6d..000000000 --- a/apps/portal/src/app/features/vessel-transfer/mock.ts +++ /dev/null @@ -1,96 +0,0 @@ -// ponytail: in-memory mock array, resets on reload — swap for RTK Query endpoint when backend lands. - -import type { UploadFileInfo } from '@ema-platform/api'; - -export interface Vessel { - id: string; - name: string; - regNo: string; - approved: boolean; -} - -export const MOCK_VESSELS: Vessel[] = [ - { id: 'v1', name: 'MV Abay', regNo: 'ET-VSL-2021-014', approved: true }, - { id: 'v2', name: 'MV Tana', regNo: 'ET-VSL-2022-031', approved: true }, - { id: 'v3', name: 'MV Awash', regNo: 'ET-VSL-2023-009', approved: false }, // pending registration — not selectable -]; - -export const TRANSFER_REASONS = [ - 'Sale', - 'Inheritance', - 'Gift', - 'Corporate Restructuring', - 'Court Order', - 'Other', -] as const; - -export type TransferReason = (typeof TRANSFER_REASONS)[number]; - -export type TransferStatus = 'Pending' | 'Under Review' | 'Approved' | 'Rejected'; - -export const STATUS_COLOR: Record = { - Pending: 'gray', - 'Under Review': 'blue', - Approved: 'teal', - Rejected: 'red', -}; - -export interface TransferRequest { - id: string; - vesselName: string; - vesselRegNo: string; - currentOwner: string; - newOwnerName: string; - reason: TransferReason; - status: TransferStatus; - submitted: string; - approvedOn?: string; - rejectionReason?: string; - documentFileInfo?: UploadFileInfo; -} - -export const MOCK_REQUESTS: TransferRequest[] = [ - { - id: 'VT-2025-0001', - vesselName: 'MV Abay', - vesselRegNo: 'ET-VSL-2021-014', - currentOwner: 'Solomon Bekele', - newOwnerName: 'Bahir Dar Shipping PLC', - reason: 'Corporate Restructuring', - status: 'Under Review', - submitted: '2025-06-30', - }, - { - id: 'VT-2025-0002', - vesselName: 'MV Tana', - vesselRegNo: 'ET-VSL-2022-031', - currentOwner: 'Solomon Bekele', - newOwnerName: 'Hana Girma', - reason: 'Sale', - status: 'Approved', - submitted: '2025-05-12', - approvedOn: '2025-05-28', - }, - { - id: 'VT-2025-0003', - vesselName: 'MV Abay', - vesselRegNo: 'ET-VSL-2021-014', - currentOwner: 'Solomon Bekele', - newOwnerName: 'Dawit Alemu', - reason: 'Gift', - status: 'Rejected', - submitted: '2025-04-02', - rejectionReason: 'Bill of Sale document illegible — please resubmit a clearer scan.', - }, -]; - -export function addTransferRequest(req: Omit): TransferRequest { - const created: TransferRequest = { - ...req, - id: `VT-2025-${String(MOCK_REQUESTS.length + 1).padStart(4, '0')}`, - status: 'Pending', - submitted: new Date().toISOString().slice(0, 10), - }; - MOCK_REQUESTS.unshift(created); - return created; -} diff --git a/apps/portal/src/app/features/vessel-transfer/pages/VesselTransferApplicationPage.tsx b/apps/portal/src/app/features/vessel-transfer/pages/VesselTransferApplicationPage.tsx deleted file mode 100644 index 1f2ad69b9..000000000 --- a/apps/portal/src/app/features/vessel-transfer/pages/VesselTransferApplicationPage.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - Alert, - Badge, - Button, - Divider, - FileInput, - Group, - Paper, - Select, - SimpleGrid, - Stack, - Text, - Textarea, - TextInput, - Title, -} from '@mantine/core'; -import { IconArrowLeft, IconArrowRight, IconCheck, IconInfoCircle, IconUpload } from '@tabler/icons-react'; -import { notify } from '@ema-platform/ui'; -import { isStorageUploadError, useDocumentUpload } from '@ema-platform/api'; -import { addTransferRequest, MOCK_VESSELS, TRANSFER_REASONS } from '../mock'; -import { useAppSelector } from '../../../store/hooks'; - -const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - -export function VesselTransferApplicationPage() { - const navigate = useNavigate(); - const user = useAppSelector((state) => state.auth.user); - const currentOwner = user?.name?.en || user?.username || 'Current Owner'; - - const [step, setStep] = useState(0); - - // Step 0 - const [vesselId, setVesselId] = useState(null); - const vessel = MOCK_VESSELS.find((v) => v.id === vesselId) ?? null; - - // Step 1 — new owner - const [newOwnerName, setNewOwnerName] = useState(''); - const [newOwnerIdOrTin, setNewOwnerIdOrTin] = useState(''); - const [newOwnerPhone, setNewOwnerPhone] = useState(''); - const [newOwnerEmail, setNewOwnerEmail] = useState(''); - const [newOwnerAddress, setNewOwnerAddress] = useState(''); - - // Step 2 — reason + document - const [reason, setReason] = useState(null); - const [document, setDocument] = useState(null); - - const { upload, isUploading } = useDocumentUpload(); - - const step0Ok = !!vessel; - const step1Ok = !!newOwnerName && !!newOwnerIdOrTin && !!newOwnerPhone - && EMAIL_RE.test(newOwnerEmail) && !!newOwnerAddress; - const step2Ok = !!reason && !!document; - - const handleSubmit = async () => { - if (!vessel || !reason || !document) return; - try { - const { fileInfo } = await upload(document, '/documents/get-file-upload-key'); - addTransferRequest({ - vesselName: vessel.name, - vesselRegNo: vessel.regNo, - currentOwner, - newOwnerName, - reason: reason as (typeof TRANSFER_REASONS)[number], - documentFileInfo: fileInfo, - }); - notify.success('Transfer request submitted. SMS and email confirmation sent.'); - navigate('/vessel-transfers'); - } catch (error) { - const tooLarge = isStorageUploadError(error) && error.message === 'STORAGE_UPLOAD_TOO_LARGE'; - notify.error(tooLarge ? 'File is too large to upload.' : 'Failed to upload document. Please try again.'); - } - }; - - return ( - - - -
- Request Ownership Transfer - Submit a request to transfer your vessel to a new owner. -
-
- - {/* Step 0 — Select vessel */} - {step === 0 && ( - - - Select Vessel - }> - Only approved vessels are available for transfer. - -