-
-
- App ID
- Company Name
- TIN Number
- Bank Letter Amount
- Submitted
- Status
-
-
-
-
- {rows.length > 0 ? rows : (
-
-
- No applications found
-
-
- )}
-
-
- Foreign Certificate of Competency
- {app.stcwRef}
-
-
-
-
-
-
-
-
-
-
-
-
-
- } p="sm">
-
- Verification checklist (STCW Reg I/10): Confirm the issuing state is an STCW party,
- the certificate is authentic, valid, and not suspended or cancelled, and the seafarer holds the
- required sea service and medical fitness.
-
-
-
-
-
-
-
-
-
- {/* ── Supporting Documents ──────────────────────────────── */}
-
-
- }>
- Verify all supporting documents. A certified translation is required if the foreign CoC is not in English.
-
-
-
-
-
-
-
-
-
-
-
- {/* ── Payment ───────────────────────────────────────────── */}
-
-
-
-
-
-
-
- App ID
- Company Name
- TIN Number
- Bank Letter Amount
- Submitted
- Status
-
-
-
-
- {rows.length > 0 ? rows : (
-
-
- No applications found
-
-
- )}
-
-
-
-
- App ID
- Company Name
- TIN Number
- Capital Amount
- Submitted
- Status
-
-
-
-
- {rows.length > 0 ? rows : (
-
-
- No applications found
-
-
- )}
-
-
-
-
- App ID
- Company Name
- TIN Number
- Bank Letter Amount
- Submitted
- Status
-
-
-
-
- {rows.length > 0 ? rows : (
-
-
- No applications found
-
-
- )}
-
-
- Payment Configuration
- Manage certificate fees and accepted payment methods shown to applicants
+ Payment configuration
+
+ What each licence costs. Applicants are charged after approval, and
+ the amount is fixed onto the application at that moment.
+
-
- Combined Total: ETB {totalCombined.toFixed(2)}
-
+
+
+
- {/* Fee summary table */}
-
-
- Certificate Fees
- Click Edit to change fee amounts
-
+ }
+ >
+
+ A change applies to applications approved from now on. Anything
+ already approved keeps the amount it was quoted, so an edit here can
+ never alter what an applicant has already been asked to pay.
+
+
-
-
-
- App ID
- Company Name
- TIN Number
- Bank Letter Amount
- Submitted
- Status
-
-
-
-
- {rows.length > 0 ? rows : (
-
-
- No applications found
-
-
- )}
-
-
-
-
- App ID
- Company Name
- TIN Number
- Waiver Type
- Submitted
- Status
-
-
-
-
- {rows.length > 0 ? rows : (
-
-
- No applications found
-
-
- )}
-
-
- Renew Combined License
- Submit renewal documents to extend your license by one year
-
-
-
- }>
- Renewal applies the controls of both the Shipping Agent and Freight Forwarder licenses. If your vehicle or
- office rental agreement has expired since your last submission, an updated agreement is required.
-
-
-
- Renewal Documents
-
- {RENEWAL_DOCS.map((slot) => (
-
- ))}
-
-
-
- }
- loading={submitting}
- disabled={!canSubmit}
- onClick={handleSubmit}
- >
- Submit Renewal Request
-
-
-
-
- );
-}
diff --git a/apps/portal/src/app/features/dashboard/pages/DashboardPage.tsx b/apps/portal/src/app/features/dashboard/pages/DashboardPage.tsx
index c850542c1..5767846a3 100644
--- a/apps/portal/src/app/features/dashboard/pages/DashboardPage.tsx
+++ b/apps/portal/src/app/features/dashboard/pages/DashboardPage.tsx
@@ -1,239 +1,583 @@
+import { useMemo } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useSelector } from 'react-redux';
import {
+ ActionIcon,
Alert,
+ Anchor,
Badge,
+ Box,
+ Button,
Card,
Center,
+ Container,
+ Divider,
Group,
+ Loader,
Paper,
Progress,
SimpleGrid,
Stack,
+ Table,
Text,
ThemeIcon,
Title,
- UnstyledButton,
- useMantineTheme,
- rem,
+ Tooltip,
} from '@mantine/core';
import {
- IconAlertCircle,
- IconBook2,
- IconChevronRight,
+ IconAlertTriangle,
+ IconCertificate,
IconClipboardList,
- IconFileCheck,
- IconHeart,
- IconLifebuoy,
- IconShip,
+ IconClockHour4,
+ IconCreditCard,
+ IconDownload,
+ IconFileText,
IconShieldCheck,
- IconUserPlus,
- IconBell,
} from '@tabler/icons-react';
-import type { Icon } from '@tabler/icons-react';
-import { useNavigate } from 'react-router-dom';
+import { ProfileCompletionNudge } from '../../profile/components/ProfileCompletionNudge';
+import {
+ APPLICANT_ACTION_STATUSES,
+ STATUS_COLORS,
+ STATUS_LABELS,
+ STATUS_PROGRESS,
+ TERMINAL_STATUSES,
+ localized,
+ useGetCertificateUrlMutation,
+ useGetMyApplicationsQuery,
+ useGetMyLicensesQuery,
+} from '@ema-platform/api';
+import type { IssuedLicense, LicenseApplication } from '@ema-platform/api';
+import { LicenseCatalogue } from '../../licensing/components/LicenseCatalogue';
-// ---------------------------------------------------------------------------
-// Mock profile completeness — replace with real store/API data
-// ---------------------------------------------------------------------------
-const PROFILE_STEPS = [
- { label: 'Personal Information', done: true },
- { label: 'Document Upload', done: true },
- { label: 'Medical Certificate', done: false },
- { label: 'Basic Safety Training', done: false },
- { label: 'Seaman Book', done: false },
-];
+/**
+ * The applicant's home screen.
+ *
+ * Ordered by what the applicant needs from it: first anything blocked on them,
+ * then a read of where their applications stand, then the licence catalogue,
+ * then the licences they already hold. Every figure is the signed-in user's
+ * own data — there are no illustrative numbers on this page.
+ */
-const BST_ITEMS = [
- { label: 'Personal Survival Techniques (PST)', done: true, expiry: '2028-04-10' },
- { label: 'Fire Prevention & Fire Fighting (FPFF)', done: true, expiry: '2028-04-10' },
- { label: 'Elementary First Aid (EFA)', done: false, expiry: null },
- { label: 'Personal Safety & Social Responsibility (PSSR)', done: false, expiry: null },
- { label: 'Sexual Harassment Prevention', done: false, expiry: null },
-];
+/** Days before expiry at which a licence is worth flagging. */
+const EXPIRY_WARNING_DAYS = 60;
-const ALERTS = [
- { id: 1, type: 'warning', message: 'Medical certificate expires in 45 days. Please renew before it lapses.', route: '/medical-certificate' },
- { id: 2, type: 'info', message: '3 Basic Safety Training certificates are missing. Complete them to apply for a Seaman Book.', route: '/basic-safety-training' },
-];
+function daysUntil(date: string): number {
+ const ms = new Date(date).getTime() - Date.now();
+ return Math.ceil(ms / 86_400_000);
+}
+
+function formatMoney(amount: string | number | null, currency: string): string {
+ if (amount === null || amount === '') return 'No fee';
+ const value = Number(amount);
+ if (!Number.isFinite(value)) return 'No fee';
+ return `${value.toLocaleString('en-US')} ${currency}`;
+}
+
+function formatDate(value: string): string {
+ return new Date(value).toLocaleDateString('en-GB', {
+ day: 'numeric',
+ month: 'short',
+ year: 'numeric',
+ });
+}
export function DashboardPage() {
const navigate = useNavigate();
- const theme = useMantineTheme();
+ const displayName = useSelector(
+ (state: { auth: { user?: { name?: { en?: string }; username?: string } } }) =>
+ state.auth.user?.name?.en || state.auth.user?.username || '',
+ );
- const doneSteps = PROFILE_STEPS.filter((s) => s.done).length;
- const completeness = Math.round((doneSteps / PROFILE_STEPS.length) * 100);
- const bstDone = BST_ITEMS.filter((b) => b.done).length;
+ const { data: applications, isLoading } = useGetMyApplicationsQuery();
+ const { data: licenses } = useGetMyLicensesQuery();
+ const [getCertificateUrl, { isLoading: isDownloading }] =
+ useGetCertificateUrlMutation();
+
+ const items = useMemo(() => applications?.items ?? [], [applications]);
+ const heldLicenses = useMemo(() => licenses?.items ?? [], [licenses]);
+
+ const needsMe = items.filter((a) =>
+ APPLICANT_ACTION_STATUSES.includes(a.status),
+ );
+ const inProgress = items.filter(
+ (a) => !TERMINAL_STATUSES.includes(a.status) && a.status !== 'DRAFT',
+ );
+ const activeLicenses = heldLicenses.filter((l) => l.status === 'ACTIVE');
+ const expiringSoon = activeLicenses.filter((l) => {
+ const days = daysUntil(l.expiryDate);
+ return days >= 0 && days <= EXPIRY_WARNING_DAYS;
+ });
+
+ async function downloadCertificate(license: IssuedLicense) {
+ const result = await getCertificateUrl(license.id).unwrap();
+ window.open(result.url, '_blank', 'noopener');
+ }
+
+ if (isLoading) {
+ return (
+
+
+
+ );
+ }
return (
-
- {/* Hero */}
-
-
-
-
-
- Welcome to the EMA Seafarer Portal
-
-
- Manage your seafarer profile, track certificates, apply for your Seaman Book
- and monitor your maritime credentials — all in one place.
-
-
-
-
+
+
+
+ {/* A prompt, not a gate — dismissible and it never blocks the page. */}
+
+
+ {needsMe.length > 0 && (
+
+ )}
+
+ {expiringSoon.length > 0 && (
+ }
+ title={
+ expiringSoon.length === 1
+ ? 'A licence is expiring soon'
+ : `${expiringSoon.length} licences are expiring soon`
+ }
>
-
-
+
+ >
+ )}
+
+
+ My applications
+
+
+ {items.some((a) => a.status === 'PAYMENT_CONFIRMED' || a.status === 'PAID') && (
+
+
+ Your payment has been received. The certificate is being prepared
+ and will appear under My licences above once it is issued.
+
+
+ )}
+
+ {items.length === 0 ? (
+
+
+ You have not filed any applications yet.
+
+ Pick a licence above to get started.
+
+
+
+ ) : (
+
+
+
+
+ Number
+ Company
+ Status
+ Submitted
+
+
+
+
+ {items.map((app) => (
+
+
+
+ {app.applicationNumber}
+
+
+
+ {app.companyName ?? '—'}
+
+
+
+ {STATUS_LABELS[app.status]}
+
+
+
+
+ {app.submittedAt
+ ? new Date(app.submittedAt).toLocaleDateString()
+ : '—'}
+
+
+
+
+ {capabilities?.bypassEnabled && app.status === 'PAYMENT_PENDING' && (
+
+ )}
+ {/* An issued application's primary action is the
+ certificate. It used to be "View", which opened the
+ application wizard — so the one thing the applicant
+ came back for was the one thing the button did not do. */}
+ {app.status === 'CERTIFICATE_ISSUED' && (
+ }
+ onClick={() => openCertificateForApplication(app.id)}
+ >
+ Certificate
+
+ )}
+
+
+
+
+ ))}
+
+
-
- } p="xs">
-
- Your certificate will be reviewed by an EMA Medical Officer within 2 working days.
- Notifications will be sent by email and SMS.
-
-
-
- }
- onClick={handleSubmit}
- loading={submitting}
- disabled={!uploadedFile || !issuedDate || !expiryDate}
- >
- Submit for Verification
-
-
-
-
-
- {/* Notification schedule */}
-
-
-
-
-
- Expiry Notification Schedule
-
-
- } title="90 Days Before Expiry">
- First reminder — time to book your medical examination
-
- } title="60 Days Before Expiry">
- Second reminder — urgent renewal required
-
- } title="30 Days Before Expiry">
- Final reminder — certificate expires very soon
-
-
-
-
- {/* History */}
- {MOCK_HISTORY.length > 0 && (
-
- Certificate History
-
- {MOCK_HISTORY.map((cert) => (
-
-
-
-
-
-
-
- Multimodal Transport Operator License
- Apply for and manage your Multimodal Transport Operator (MTO) License
-
-
-
- {!application && (
- <>
-
-
-
-
- Apply for an MTO License
- Provide company, ownership, management, terminal, fleet, financial, and insurance information
-
-
-
- Requirements
-
-
-
-
-
-
-
-
-
-
- } onClick={() => navigate('/mto-license/apply')}>
- Start Application
-
-
-
-
-
-
- About MTO Licensing
-
-
- Applications require a physical inspection of your terminal, warehouse, trucks, office, and equipment.
- After approval, a service payment of 1000 ETB is required before certificate issuance.
- The certificate is valid for one year and must be renewed annually.
-
-
- >
- )}
-
- {application && (
- <>
- {application.status === 'Resubmit Required' && (
- } color="orange" title="Resubmission Required">
- {application.remarks || 'Please correct the requested information and resubmit.'}
-
- )}
- {application.status === 'Rejected' && (
- } color="red" title="Application Rejected">
- {application.remarks || 'Your application was rejected.'}
-
- )}
- {application.status === 'Inspection Pending' && (
- } color="grape" title="Inspection Pending">
- A physical inspection of your terminal, warehouse, trucks, office, and equipment is required. An Inspector will contact you to schedule the visit.
-
- )}
- {application.status === 'Inspection Completed' && (
- } color="indigo" title="Inspection Completed">
- Your physical inspection has been completed by an Inspector. Your application is proceeding to the next stage.
-
- )}
- {application.status === 'Payment Pending' && (
- } color="grape" title="Payment Required">
- Your application has been approved. Please pay 1000 ETB to receive your certificate.
-
-
- )}
-
-
-
-
-
-
- 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.
-
-
-
- {/* Active application status */}
- {MOCK_APPLICATION && (
-
-
-
-
-
-
-
- Ownership Transfer
- Request transfer of vessel ownership to another party
-
-
- }
- color="violet"
- onClick={() => setModalOpen(true)}
- disabled={vesselOptions.length === 0}
- >
- Request Transfer
-
-
-
- {vesselOptions.length === 0 && (
- } color="blue" variant="light">
- You must have at least one approved vessel registration to request an ownership transfer.{' '}
- navigate('/vessel-registration')}>
- View my registrations →
-
-
- )}
-
- {/* How it works */}
-
-
-
- How Ownership Transfer Works
-
-
- {[
- "Submit a transfer request with the new owner's details and a Bill of Sale",
- 'The Maritime Authority reviews and verifies the transfer documents',
- 'Upon approval, ownership is officially transferred in the registry',
- 'New certificates are automatically generated for the new owner',
- 'The new owner receives: Certificate of Nationality, Certificate of Ownership, Certificate of Registration (sea-going) or Inland Registration Certificate (inland)',
- ].map((step, i) => (
-
-
- {i + 1}
-
- {step}
-
- ))}
-
-
-
- {/* Existing transfer requests */}
-
- My Transfer Requests
- {transfers.length === 0 ? (
-
- No transfer requests submitted yet.
-
- ) : (
-
- {transfers.map((req) => )}
-
- )}
-
-
- {/* Transfer request modal */}
- { setModalOpen(false); resetForm(); }}
- title="Request Ownership Transfer"
- size="lg"
- >
-
- } color="orange" variant="light">
- Ownership transfer is permanent. Ensure all details are correct before submitting.
-
-
-
-
- {selectedVessel && (
-
- Current Owner
-
-
- Maritime Logistics Waiver
- Request authorization to use a non-ESL vessel for importing goods
-
-
-
- {!application && (
- <>
-
-
-
-
- Apply for a Waiver
- Request a Pre-Waiver (cargo not yet arrived) or Post-Waiver (cargo already arrived)
-
-
-
- Requirements
-
-
-
-
-
-
-
-
- } onClick={() => navigate('/waiver/apply')}>
- Start Application
-
-
-
-
-
-
- About the Waiver
-
-
- A waiver lets you use a non-ESL vessel to transport imported goods into Ethiopia. Once approved
- (and, for Post-Waiver, once any penalty payment is confirmed), EMA issues an official waiver letter
- addressed to your bank for import clearance. Post-Waiver is only issued once per cargo case.
-
-
- >
- )}
-
- {application && (
- <>
- {application.status === 'Resubmit Required' && (
- } color="orange" title="Resubmission Required">
- {application.remarks || 'Please correct the requested information and resubmit.'}
-
- )}
- {application.status === 'Rejected' && (
- } color="red" title="Application Rejected">
- {application.remarks || 'Your application was rejected.'}
-
- )}
- {application.status === 'Penalty Payment Pending' && (
- } color="grape" title="Penalty Payment Required">
- Your Post-Waiver has been approved. Please pay the penalty of {application.penaltyAmount?.toLocaleString() ?? '—'} ETB
- to proceed with waiver letter generation.
-
-
- )}
-
-
-
-
-
-