From 1b989936f06659ae1b88f11bcb75375d0761fbf3 Mon Sep 17 00:00:00 2001 From: Estifo77 <139631617+Estifo77@users.noreply.github.com> Date: Fri, 31 Jul 2026 09:34:29 +0300 Subject: [PATCH] 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 --- .../src/app/features/payment/api/payments.ts | 1 + .../pages/SeamanBookApplicationPage.tsx | 1087 +++++++++++++---- .../seaman-book/pages/SeamanBookPage.tsx | 264 +++- .../pages/VesselRegistrationPage.tsx | 285 +++-- apps/portal/src/app/layouts/PortalLayout.tsx | 18 +- libs/ui/src/lib/data/AdvancedTable.tsx | 4 +- package-lock.json | 18 +- 7 files changed, 1281 insertions(+), 396 deletions(-) diff --git a/apps/portal/src/app/features/payment/api/payments.ts b/apps/portal/src/app/features/payment/api/payments.ts index 596317399..789bf3624 100644 --- a/apps/portal/src/app/features/payment/api/payments.ts +++ b/apps/portal/src/app/features/payment/api/payments.ts @@ -21,6 +21,7 @@ export function initiatePayment(req: PaymentRequest): Promise { /** GET /payments/intents/:intentId — used both by the poll and by manual "check again". */ export function getIntent(intentId: string): Promise { return paymentFetch(`/payments/intents/${intentId}`); + } /** diff --git a/apps/portal/src/app/features/seaman-book/pages/SeamanBookApplicationPage.tsx b/apps/portal/src/app/features/seaman-book/pages/SeamanBookApplicationPage.tsx index 1955e96f3..d43bc79ee 100644 --- a/apps/portal/src/app/features/seaman-book/pages/SeamanBookApplicationPage.tsx +++ b/apps/portal/src/app/features/seaman-book/pages/SeamanBookApplicationPage.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react'; +import { useRef, useState } from "react"; import { Alert, Badge, @@ -15,7 +15,7 @@ import { TextInput, Title, rem, -} from '@mantine/core'; +} from "@mantine/core"; import { IconAlertTriangle, IconArrowLeft, @@ -30,20 +30,20 @@ import { IconShieldCheck, IconTrash, IconUpload, -} from '@tabler/icons-react'; -import { useNavigate } from 'react-router-dom'; -import { notify, useErrorHandler } from '@ema-platform/ui'; -import { PaymentModal } from '../../payment/components/PaymentModal'; -import { toMinor } from '../../payment/utils/money'; +} from "@tabler/icons-react"; +import { useNavigate } from "react-router-dom"; +import { notify, useErrorHandler } from "@ema-platform/ui"; +import { PaymentModal } from "../../payment/components/PaymentModal"; +import { toMinor } from "../../payment/utils/money"; // --------------------------------------------------------------------------- // Steps // --------------------------------------------------------------------------- const STEPS = [ - { label: 'BST Certificates' }, - { label: 'Medical Certificate' }, - { label: 'Payment' }, - { label: 'Review & Submit' }, + { label: "BST Certificates" }, + { label: "Medical Certificate" }, + { label: "Payment" }, + { label: "Review & Submit" }, ]; // --------------------------------------------------------------------------- @@ -57,30 +57,61 @@ interface BSTSlot { } const BST_SLOTS: BSTSlot[] = [ - { key: 'pst', short: 'PST', label: 'Personal Survival Techniques (PST)', refreshYears: 5 }, - { key: 'fpff', short: 'FPFF', label: 'Fire Prevention & Fire Fighting (FPFF)', refreshYears: 5 }, - { key: 'efa', short: 'EFA', label: 'Elementary First Aid (EFA)', refreshYears: 0 }, - { key: 'pssr', short: 'PSSR', label: 'Personal Safety & Social Responsibility (PSSR)',refreshYears: 0 }, - { key: 'shp', short: 'SHPT', label: 'Sexual Harassment Prevention Training (SHPT)', refreshYears: 0 }, + { + key: "pst", + short: "PST", + label: "Personal Survival Techniques (PST)", + refreshYears: 5, + }, + { + key: "fpff", + short: "FPFF", + label: "Fire Prevention & Fire Fighting (FPFF)", + refreshYears: 5, + }, + { + key: "efa", + short: "EFA", + label: "Elementary First Aid (EFA)", + refreshYears: 0, + }, + { + key: "pssr", + short: "PSSR", + label: "Personal Safety & Social Responsibility (PSSR)", + refreshYears: 0, + }, + { + key: "shp", + short: "SHPT", + label: "Sexual Harassment Prevention Training (SHPT)", + refreshYears: 0, + }, ]; // --------------------------------------------------------------------------- // Fee table — Seaman Book + BTC shown separately, paid together // --------------------------------------------------------------------------- const FEES = [ - { label: 'Seaman Book — Application Fee', amount: 500 }, - { label: 'Seaman Book — Document Verification Fee',amount: 200 }, - { label: 'Basic Training Certificate (BTC) — Application Fee', amount: 300 }, - { label: 'BTC — Document Verification Fee', amount: 100 }, - { label: 'BSID — Application Fee', amount: 100 }, - { label: 'BSID — Card Production Fee', amount: 150 }, + { label: "Seaman Book — Application Fee", amount: 500 }, + { label: "Seaman Book — Document Verification Fee", amount: 200 }, + { label: "Basic Training Certificate (BTC) — Application Fee", amount: 300 }, + { label: "BTC — Document Verification Fee", amount: 100 }, + { label: "BSID — Application Fee", amount: 100 }, + { label: "BSID — Card Production Fee", amount: 150 }, ]; const TOTAL = FEES.reduce((s, f) => s + f.amount, 0); // --------------------------------------------------------------------------- // Step indicator // --------------------------------------------------------------------------- -function StepIndicator({ active, completed }: { active: number; completed: number[] }) { +function StepIndicator({ + active, + completed, +}: { + active: number; + completed: number[]; +}) { return ( @@ -88,30 +119,65 @@ function StepIndicator({ active, completed }: { active: number; completed: numbe const isDone = completed.includes(i); const isCurrent = active === i; return ( - + - - {isDone ? : ( - {i + 1} + + {isDone ? ( + + ) : ( + + {i + 1} + )} - + {isDone ? `${step.label} ✓` : step.label} {i < STEPS.length - 1 && ( - + )} ); @@ -125,7 +191,9 @@ function SectionHead({ title }: { title: string }) { return ( <> - {title} + + {title} + ); } @@ -133,8 +201,12 @@ function SectionHead({ title }: { title: string }) { function ReviewRow({ label, value }: { label: string; value: string }) { return (
- {label} - {value || '—'} + + {label} + + + {value || "—"} +
); } @@ -142,59 +214,157 @@ function ReviewRow({ label, value }: { label: string; value: string }) { // --------------------------------------------------------------------------- // BST upload card // --------------------------------------------------------------------------- -function BSTCard({ slot, certNumber, onCertNumber, issuer, onIssuer, issueDate, onIssueDate, expiryDate, onExpiryDate, file, onFile, resetRef }: { +function BSTCard({ + slot, + certNumber, + onCertNumber, + issuer, + onIssuer, + issueDate, + onIssueDate, + expiryDate, + onExpiryDate, + file, + onFile, + resetRef, +}: { slot: BSTSlot; - certNumber: string; onCertNumber: (v: string) => void; - issuer: string; onIssuer: (v: string) => void; - issueDate: string; onIssueDate: (v: string) => void; - expiryDate: string; onExpiryDate: (v: string) => void; - file: File | null; onFile: (f: File | null) => void; + certNumber: string; + onCertNumber: (v: string) => void; + issuer: string; + onIssuer: (v: string) => void; + issueDate: string; + onIssueDate: (v: string) => void; + expiryDate: string; + onExpiryDate: (v: string) => void; + file: File | null; + onFile: (f: File | null) => void; resetRef: React.MutableRefObject<(() => void) | null>; }) { - const isComplete = !!file && !!certNumber.trim() && !!issuer.trim() && !!issueDate; + const isComplete = + !!file && !!certNumber.trim() && !!issuer.trim() && !!issueDate; return ( - + - - + +
- {slot.short} - * - {isComplete && Done} + + {slot.short} + + + * + + {isComplete && ( + + Done + + )} - {slot.label} + + {slot.label} +
- onCertNumber(e.currentTarget.value)} /> - onIssuer(e.currentTarget.value)} /> + onCertNumber(e.currentTarget.value)} + /> + onIssuer(e.currentTarget.value)} + /> 0 ? 2 : 1} spacing="xs"> - onIssueDate(e.currentTarget.value)} /> + onIssueDate(e.currentTarget.value)} + /> {slot.refreshYears > 0 && ( - onExpiryDate(e.currentTarget.value)} /> + onExpiryDate(e.currentTarget.value)} + /> )} {file ? ( - {file.name} - ) : ( - + {(props) => ( - )} @@ -216,25 +386,52 @@ export function SeamanBookApplicationPage() { const { handleError } = useErrorHandler(); // BST - const [bstData, setBstData] = useState>(() => - Object.fromEntries(BST_SLOTS.map((s) => [s.key, { certNumber: '', issuer: '', issueDate: '', expiryDate: '', file: null }])) + const [bstData, setBstData] = useState< + Record< + string, + { + certNumber: string; + issuer: string; + issueDate: string; + expiryDate: string; + file: File | null; + } + > + >(() => + Object.fromEntries( + BST_SLOTS.map((s) => [ + s.key, + { + certNumber: "", + issuer: "", + issueDate: "", + expiryDate: "", + file: null, + }, + ]), + ), ); const bstResetRefs = useRef void) | null>>({}); const updateBst = (key: string, field: string, value: string | File | null) => - setBstData((prev) => ({ ...prev, [key]: { ...prev[key], [field]: value } })); + setBstData((prev) => ({ + ...prev, + [key]: { ...prev[key], [field]: value }, + })); // Medical - const [medCertNumber, setMedCertNumber] = useState(''); - const [medIssuer, setMedIssuer] = useState(''); - const [medIssueDate, setMedIssueDate] = useState(''); - const [medExpiryDate, setMedExpiryDate] = useState(''); + const [medCertNumber, setMedCertNumber] = useState(""); + const [medIssuer, setMedIssuer] = useState(""); + const [medIssueDate, setMedIssueDate] = useState(""); + const [medExpiryDate, setMedExpiryDate] = useState(""); const [medFile, setMedFile] = useState(null); const medResetRef = useRef<() => void>(null); // Payment - const [paymentMethod, setPaymentMethod] = useState<'cbe' | 'telebirr' | null>(null); - const [paymentRef, setPaymentRef] = useState(''); - const [paymentDate, setPaymentDate] = useState(''); + const [paymentMethod, setPaymentMethod] = useState<"cbe" | "telebirr" | null>( + null, + ); + const [paymentRef, setPaymentRef] = useState(""); + const [paymentDate, setPaymentDate] = useState(""); const [paymentFile, setPaymentFile] = useState(null); const payResetRef = useRef<() => void>(null); // Telebirr intent id, set once PaymentModal's onSuccess fires — real gateway flow, no manual ref for telebirr @@ -242,18 +439,28 @@ export function SeamanBookApplicationPage() { const [payModalOpen, setPayModalOpen] = useState(false); // Stable draft reference for this application session — there's no application id yet at // payment time (payment happens before submit), so this is what orderRef/referenceId key off. - const [payRef] = useState(() => `SB-${crypto.randomUUID().slice(0, 8).toUpperCase()}`); + const [payRef] = useState( + () => `SB-${crypto.randomUUID().slice(0, 8).toUpperCase()}`, + ); // Validation const bstComplete = BST_SLOTS.every((s) => { const d = bstData[s.key]; - return !!d.file && !!d.certNumber.trim() && !!d.issuer.trim() && !!d.issueDate; + return ( + !!d.file && !!d.certNumber.trim() && !!d.issuer.trim() && !!d.issueDate + ); }); - const medComplete = !!medFile && !!medCertNumber.trim() && !!medIssuer.trim() && !!medIssueDate && !!medExpiryDate; + const medComplete = + !!medFile && + !!medCertNumber.trim() && + !!medIssuer.trim() && + !!medIssueDate && + !!medExpiryDate; // const payComplete = !!paymentMethod && !!paymentRef.trim() && !!paymentDate; // old: telebirr used to require a manual ref too - const payComplete = paymentMethod === 'telebirr' - ? !!telebirrOrderId - : !!paymentMethod && !!paymentRef.trim() && !!paymentDate; + const payComplete = + paymentMethod === "telebirr" + ? !!telebirrOrderId + : !!paymentMethod && !!paymentRef.trim() && !!paymentDate; const canNext = () => { if (active === 0) return bstComplete; @@ -263,7 +470,7 @@ export function SeamanBookApplicationPage() { }; const next = () => { - setCompleted((prev) => prev.includes(active) ? prev : [...prev, active]); + setCompleted((prev) => (prev.includes(active) ? prev : [...prev, active])); setActive((c) => c + 1); }; const prev = () => setActive((c) => c - 1); @@ -272,8 +479,8 @@ export function SeamanBookApplicationPage() { setSubmitting(true); try { await new Promise((r) => setTimeout(r, 1400)); - notify.success('Application submitted! Reference: SB-BTC-2025-001'); - navigate('/seaman-book'); + notify.success("Application submitted! Reference: SB-BTC-2025-001"); + navigate("/seaman-book"); } catch (e) { handleError(e); } finally { @@ -286,26 +493,55 @@ export function SeamanBookApplicationPage() {
Seaman Book, BTC & BSID Application - One application covers your Seaman Book, Basic Training Certificate (BTC), and BSID — Step {active + 1} of {STEPS.length} + One application covers your Seaman Book, Basic Training Certificate + (BTC), and BSID — Step {active + 1} of {STEPS.length}
{/* What you will receive banner */} - + - - Seaman Book + + + Seaman Book + - + + + + + - - Basic Training Certificate (BTC) + + + Basic Training Certificate (BTC) + - + + + + + - - BSID (Biometric Seafarer ID) + + + BSID (Biometric Seafarer ID) + @@ -314,51 +550,84 @@ export function SeamanBookApplicationPage() { - {STEPS[active]?.label} - Step {active + 1} of {STEPS.length} + + {STEPS[active]?.label} + + + Step {active + 1} of {STEPS.length} + {/* ── Step 1: BST ─────────────────────────────────────────────── */} {active === 0 && ( - }> - Upload all 5 Basic Safety Training certificates. These training certificates issued by approved institutions are different from the EMA-issued BTC — they are the prerequisite for your BTC. + } + > + Upload all 5 Basic Safety Training certificates. These training + certificates issued by approved institutions are different from + the EMA-issued BTC — they are the prerequisite for your BTC. {BST_SLOTS.map((slot) => { const d = bstData[slot.key]; - const rRef = { current: bstResetRefs.current[slot.key] ?? null }; + const rRef = { + current: bstResetRefs.current[slot.key] ?? null, + }; return ( updateBst(slot.key, 'certNumber', v)} + onCertNumber={(v) => updateBst(slot.key, "certNumber", v)} issuer={d.issuer} - onIssuer={(v) => updateBst(slot.key, 'issuer', v)} + onIssuer={(v) => updateBst(slot.key, "issuer", v)} issueDate={d.issueDate} - onIssueDate={(v) => updateBst(slot.key, 'issueDate', v)} + onIssueDate={(v) => updateBst(slot.key, "issueDate", v)} expiryDate={d.expiryDate} - onExpiryDate={(v) => updateBst(slot.key, 'expiryDate', v)} + onExpiryDate={(v) => updateBst(slot.key, "expiryDate", v)} file={d.file} - onFile={(f) => updateBst(slot.key, 'file', f)} + onFile={(f) => updateBst(slot.key, "file", f)} resetRef={rRef} /> ); })} - Upload Progress + + Upload Progress + {BST_SLOTS.map((slot) => { - const done = !!bstData[slot.key].file && !!bstData[slot.key].certNumber; + const done = + !!bstData[slot.key].file && !!bstData[slot.key].certNumber; return ( - {done ? : ( - + {done ? ( + + ) : ( + )} - {slot.short} + + {slot.short} + ); })} @@ -370,57 +639,145 @@ export function SeamanBookApplicationPage() { {/* ── Step 2: Medical ─────────────────────────────────────────── */} {active === 1 && ( - }> - Upload your valid medical certificate from an EMA-approved medical centre. Required for both Seaman Book and BTC issuance. + } + > + Upload your valid medical certificate from an EMA-approved medical + centre. Required for both Seaman Book and BTC issuance. - setMedCertNumber(e.currentTarget.value)} /> - setMedIssuer(e.currentTarget.value)} /> + setMedCertNumber(e.currentTarget.value)} + /> + setMedIssuer(e.currentTarget.value)} + /> - setMedIssueDate(e.currentTarget.value)} /> - setMedExpiryDate(e.currentTarget.value)} /> + setMedIssueDate(e.currentTarget.value)} + /> + setMedExpiryDate(e.currentTarget.value)} + /> - + - - + +
- Medical Certificate * - PDF, JPG or PNG — max 5MB + + Medical Certificate{" "} + + * + + + + PDF, JPG or PNG — max 5MB +
{medFile ? ( - - {medFile.name} - ) : ( - + {(props) => ( - )} )}
- }> - Only certificates from EMA-approved medical centres are accepted. + } + > + Only certificates from{" "} + EMA-approved medical centres are accepted.
)} @@ -430,26 +787,49 @@ export function SeamanBookApplicationPage() { {/* Fee breakdown — SB + BTC shown separately */} - Fee Breakdown - Your payment covers both the Seaman Book and Basic Training Certificate (BTC). + + Fee Breakdown + + + Your payment covers both the Seaman Book and Basic Training + Certificate (BTC). + {/* SB fees */} - Seaman Book - {FEES.filter(f => f.label.startsWith('Seaman Book')).map(({ label, amount }) => ( - - {label.replace('Seaman Book — ', '')} - ETB {amount.toFixed(2)} - - ))} + + Seaman Book + + {FEES.filter((f) => f.label.startsWith("Seaman Book")).map( + ({ label, amount }) => ( + + {label.replace("Seaman Book — ", "")} + + ETB {amount.toFixed(2)} + + + ), + )} {/* BTC fees */} - Basic Training Certificate (BTC) - {FEES.filter(f => f.label.startsWith('Basic Training') || f.label.startsWith('BTC')).map(({ label, amount }) => ( + + Basic Training Certificate (BTC) + + {FEES.filter( + (f) => + f.label.startsWith("Basic Training") || + f.label.startsWith("BTC"), + ).map(({ label, amount }) => ( - {label.replace('Basic Training Certificate (BTC) — ', '').replace('BTC — ', '')} - ETB {amount.toFixed(2)} + + {label + .replace("Basic Training Certificate (BTC) — ", "") + .replace("BTC — ", "")} + + + ETB {amount.toFixed(2)} + ))} @@ -457,80 +837,182 @@ export function SeamanBookApplicationPage() { {/* BSID fees */} - BSID (Biometric Seafarer ID) - {FEES.filter(f => f.label.startsWith('BSID')).map(({ label, amount }) => ( - - {label.replace('BSID — ', '')} - ETB {amount.toFixed(2)} - - ))} + + BSID (Biometric Seafarer ID) + + {FEES.filter((f) => f.label.startsWith("BSID")).map( + ({ label, amount }) => ( + + {label.replace("BSID — ", "")} + + ETB {amount.toFixed(2)} + + + ), + )} - Total Amount Due - ETB {TOTAL.toFixed(2)} + + Total Amount Due + + + ETB {TOTAL.toFixed(2)} + - + {/* CBE */} - { setPaymentMethod('cbe'); setPaymentRef(''); }} + { + setPaymentMethod("cbe"); + setPaymentRef(""); + }} style={{ - cursor: 'pointer', - borderColor: paymentMethod === 'cbe' ? 'var(--mantine-color-blue-6)' : 'var(--mantine-color-default-border)', - borderWidth: paymentMethod === 'cbe' ? 2 : 1, - background: paymentMethod === 'cbe' ? 'var(--mantine-color-blue-light)' : undefined, - }}> + cursor: "pointer", + borderColor: + paymentMethod === "cbe" + ? "var(--mantine-violet-blue-6)" + : "var(--mantine-color-default-border)", + borderWidth: paymentMethod === "cbe" ? 2 : 1, + background: + paymentMethod === "cbe" + ? "var(--mantine-color-violet-light)" + : undefined, + }} + > - - + +
- CBE Bank Transfer - Commercial Bank of Ethiopia + + CBE Bank Transfer + + + Commercial Bank of Ethiopia +
- {paymentMethod === 'cbe' && } + {paymentMethod === "cbe" && ( + + )}
{/* Telebirr */} - { setPaymentMethod('telebirr'); setPaymentRef(''); }} + { + setPaymentMethod("telebirr"); + setPaymentRef(""); + }} style={{ - cursor: 'pointer', - borderColor: paymentMethod === 'telebirr' ? 'var(--mantine-color-violet-6)' : 'var(--mantine-color-default-border)', - borderWidth: paymentMethod === 'telebirr' ? 2 : 1, - background: paymentMethod === 'telebirr' ? 'var(--mantine-color-violet-light)' : undefined, - }}> + cursor: "pointer", + borderColor: + paymentMethod === "telebirr" + ? "var(--mantine-color-green-6)" + : "var(--mantine-color-default-border)", + borderWidth: paymentMethod === "telebirr" ? 2 : 1, + background: + paymentMethod === "telebirr" + ? "var(--mantine-color-green-light)" + : undefined, + }} + > - - + +
- Telebirr - Ethio Telecom Mobile Money + + Telebirr + + + Ethio Telecom Mobile Money +
- {paymentMethod === 'telebirr' && } + {paymentMethod === "telebirr" && ( + + )}
- {paymentMethod === 'cbe' && ( + {paymentMethod === "cbe" && ( <> - }> - Transfer ETB {TOTAL.toFixed(2)} to CBE Account 1000123456789 (EMA Maritime Authority). Use your full name as description. + } + > + Transfer ETB {TOTAL.toFixed(2)} to CBE + Account 1000123456789 (EMA Maritime + Authority). Use your full name as description. - setPaymentRef(e.currentTarget.value)} /> - setPaymentDate(e.currentTarget.value)} /> + setPaymentRef(e.currentTarget.value)} + /> + setPaymentDate(e.currentTarget.value)} + /> )} @@ -549,17 +1031,21 @@ export function SeamanBookApplicationPage() { )} */} - {paymentMethod === 'telebirr' && ( + {paymentMethod === "telebirr" && ( <> {telebirrOrderId ? ( - }> + } + > Payment confirmed — reference {telebirrOrderId} ) : (
) : ( - + {(props) => ( - )} @@ -634,25 +1173,52 @@ export function SeamanBookApplicationPage() { {/* ── Step 4: Review ──────────────────────────────────────────── */} {active === 3 && ( - }> - Submitting this application will initiate processing for both your Seaman Book and Basic Training Certificate (BTC). + } + > + Submitting this application will initiate processing for both your{" "} + Seaman Book and{" "} + Basic Training Certificate (BTC). - BST Certificates + + BST Certificates + {BST_SLOTS.map((slot) => { const d = bstData[slot.key]; return (
- - {slot.short} + + + {slot.short} + - {d.certNumber || '—'} - {d.issuer || '—'} - Issued: {d.issueDate || '—'} - {d.file && {d.file.name}} + + {d.certNumber || "—"} + + + {d.issuer || "—"} + + + Issued: {d.issueDate || "—"} + + {d.file && ( + + {d.file.name} + + )}
); })} @@ -660,25 +1226,47 @@ export function SeamanBookApplicationPage() {
- Medical Certificate + + Medical Certificate + - + - Payment + + Payment + - + {/* old: telebirr no longer collects a manual ref */} - + - - + +
@@ -686,17 +1274,34 @@ export function SeamanBookApplicationPage() { {/* Navigation */} - + {active > 0 && ( - + )} {active < STEPS.length - 1 ? ( - ) : ( - )} diff --git a/apps/portal/src/app/features/seaman-book/pages/SeamanBookPage.tsx b/apps/portal/src/app/features/seaman-book/pages/SeamanBookPage.tsx index 138192e30..d65528856 100644 --- a/apps/portal/src/app/features/seaman-book/pages/SeamanBookPage.tsx +++ b/apps/portal/src/app/features/seaman-book/pages/SeamanBookPage.tsx @@ -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 = { - '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 ( - + {ok ? : } - {label} + + {label} + ); } @@ -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() {
My Application — Seaman Book & BTC - 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.
@@ -128,16 +164,28 @@ export function SeamanBookPage() {
Application {MOCK_APPLICATION.id} - Submitted {MOCK_APPLICATION.submittedAt} + + Submitted {MOCK_APPLICATION.submittedAt} +
- + {MOCK_APPLICATION.status}
{MOCK_APPLICATION.remarks && ( - } mb="md" p="sm"> + } + mb="md" + p="sm" + > {MOCK_APPLICATION.remarks} )} @@ -148,15 +196,27 @@ export function SeamanBookPage() { : } + description={step.date ?? "Pending"} + icon={ + step.done ? ( + + ) : ( + + ) + } /> ))} - {MOCK_APPLICATION.status === 'Ready for Collection' && ( - } 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" && ( + } + mt="md" + > + Your Seaman Book is ready. Please visit the EMA office to collect + it. Bring your National ID. )} @@ -168,33 +228,71 @@ export function SeamanBookPage() { {/* Eligibility checklist */} - + Eligibility Requirements - - - + + + - + {ELIGIBILITY.bstItems.map((item) => ( - + ))} {!isEligible && ( - } mt="xs" p="sm"> + } + mt="xs" + p="sm" + > - Complete all requirements above before applying. Missing BST: {5 - bstDone} certificate(s). + Complete all requirements above before applying. Missing + BST: {5 - bstDone} certificate(s). )} {isEligible && ( - } mt="xs" p="sm"> - You meet all requirements. You may proceed with your application. + } + mt="xs" + p="sm" + > + + You meet all requirements. You may proceed with your + application. + )} @@ -211,24 +309,30 @@ export function SeamanBookPage() { - 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. - What will be verified: + + What will be verified: + {[ - '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) => ( - + {item} ))} @@ -241,8 +345,12 @@ export function SeamanBookPage() {
- Processing time - 5–7 working days + + Processing time + + + 5–7 working days +
@@ -250,22 +358,32 @@ export function SeamanBookPage() {
- Medical validity - 2 years (STCW) + + Medical validity + + + 2 years (STCW) +
- } p="xs"> + } + p="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. @@ -120,7 +180,9 @@ function CertificateCard({ label, description }: { label: string; description: s // --------------------------------------------------------------------------- export function VesselRegistrationPage() { const navigate = useNavigate(); - const [registration, setRegistration] = useState(null); + const [registration, setRegistration] = useState( + mockVesselRegistrations, + ); const [fetchTrigger] = useApiMutation(); 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 ( @@ -146,7 +211,9 @@ export function VesselRegistrationPage() {
Vessel Registration - Register your vessel with the Ethiopian Maritime Authority + + Register your vessel with the Ethiopian Maritime Authority +
@@ -159,16 +226,21 @@ export function VesselRegistrationPage() {
- Register Your Vessel + + Register Your Vessel + - Obtain official registration for inland waterway or sea-going vessels + Obtain official registration for inland waterway or sea-going + vessels
- Requirements + + Requirements + @@ -181,22 +253,31 @@ export function VesselRegistrationPage() { - + - About Vessel Registration + + About Vessel Registration + - Registration is valid for 5 years from the date of approval. After approval, - inland vessels receive an Inland Vessel Registration Certificate, 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 5 years from the date + of approval. After approval, inland vessels receive an{" "} + Inland Vessel Registration Certificate, while + sea-going vessels receive four certificates: Certificate of + Nationality, Certificate of Ownership, Certificate of + Registration, and Minimum Safe Manning Certificate. @@ -206,21 +287,27 @@ export function VesselRegistrationPage() { {registration && ( <> {/* Renewal alert */} - {registration.renewalStatus === 'Due Soon' && ( + {registration.renewalStatus === "Due Soon" && ( } 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. )} - {registration.renewalStatus === 'Overdue' && ( - } color="red" title="Registration Expired"> - Your vessel registration expired on {registration.expiryDate}. Immediate renewal is required. + {registration.renewalStatus === "Overdue" && ( + } + color="red" + title="Registration Expired" + > + Your vessel registration expired on {registration.expiryDate}. + Immediate renewal is required. )} @@ -232,27 +319,45 @@ export function VesselRegistrationPage() {
- {registration.vesselName} - {registration.id} + + {registration.vesselName} + + + {registration.id} +
- + {registration.status} {[ - { 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) => (
- {row.label} - {row.value || '—'} + + {row.label} + + + {row.value || "—"} +
))}
@@ -260,30 +365,44 @@ export function VesselRegistrationPage() { {registration.remarks && ( <> - Officer Remarks + + Officer Remarks + {registration.remarks} )}
{/* Timeline / status info */} - {registration.status !== 'Approved' && ( + {registration.status !== "Approved" && ( - Application Status + + Application Status + {[ - { 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) => ( - + - {step.label} + + {step.label} + ))} @@ -291,19 +410,23 @@ export function VesselRegistrationPage() { )} {/* Transfer ownership — only when approved */} - {registration.status === 'Approved' && ( + {registration.status === "Approved" && (
- Transfer Ownership - Transfer this vessel to a new owner + + Transfer Ownership + + + Transfer this vessel to a new owner +
@@ -312,22 +435,30 @@ export function VesselRegistrationPage() { )} {/* Certificates section — shown after approval */} - {registration.status === 'Approved' && ( + {registration.status === "Approved" && (
- + - {registration.category === 'Sea-going Vessel (International)' - ? 'Issued Certificates (4)' - : 'Issued Certificate'} + {registration.category === "Sea-going Vessel (International)" + ? "Issued Certificates (4)" + : "Issued Certificate"} } 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. {certs.map((cert) => ( - + ))}
diff --git a/apps/portal/src/app/layouts/PortalLayout.tsx b/apps/portal/src/app/layouts/PortalLayout.tsx index 11a973baa..b38e6fe36 100644 --- a/apps/portal/src/app/layouts/PortalLayout.tsx +++ b/apps/portal/src/app/layouts/PortalLayout.tsx @@ -156,15 +156,15 @@ const ALWAYS_VISIBLE = ["/notifications", "/profile", "/support"]; const NAV_ACCESS: Record = { 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", diff --git a/libs/ui/src/lib/data/AdvancedTable.tsx b/libs/ui/src/lib/data/AdvancedTable.tsx index 8f55811fd..a42dbc71e 100644 --- a/libs/ui/src/lib/data/AdvancedTable.tsx +++ b/libs/ui/src/lib/data/AdvancedTable.tsx @@ -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 { @@ -107,7 +107,7 @@ export function AdvancedTable({ diff --git a/package-lock.json b/package-lock.json index 66d04f06d..8ccb9923f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": {