-
-
-
-
-
-
-
- setApplicantName(e.currentTarget.value)}
- />
- setSubjectName(e.currentTarget.value)}
- />
-
-
-
-
-
- }
- >
- {t('apply.docs.hint')}
-
- {requiredDocs.map((d) => (
- toggleDoc(d)}
- />
- ))}
-
-
-
-
-
- }
- title={t('apply.review.title')}
- >
- {t('apply.review.hint')}
-
-
-
-
-
-
-
-
-
- {t('apply.docs.attached')}
-
-
- {t('apply.docs.attachedCount', {
- count: requiredDocs.filter((d) => uploaded[d]).length,
- total: requiredDocs.length,
- })}
-
-
- {!allDocsUploaded && (
-
- {t('apply.review.missingDocs')}
-
- )}
-
-
+
+
+
+
+
+
+
-
-
-
- {active < 3 ? (
-
- ) : (
-
- )}
-
+
+
+
+
+ {active === 0 && (
+
+
+ }
+ data={LICENSE_OPTIONS}
+ value={licenseType}
+ onChange={(v) => setLicenseType(v as LicenseType)}
+ searchable
+ />
+
+ }
+ data={CATEGORIES}
+ value={category}
+ onChange={setCategory}
+ />
+ }
+ data={REGIONS}
+ value={region}
+ onChange={setRegion}
+ searchable
+ />
+
+ }
+ placeholder="Abebe Bekele Tadesse"
+ value={fullName}
+ onChange={(e) => setFullName(e.currentTarget.value)}
+ />
+
+ }
+ placeholder="ET-1234567"
+ value={idNumber}
+ onChange={(e) => setIdNumber(e.currentTarget.value)}
+ />
+ }
+ placeholder="+251 911 234 567"
+ value={phone}
+ onChange={(e) => setPhone(e.currentTarget.value)}
+ />
+
+
+
+ )}
+
+ {active === 1 && (
+
+
+ setRequestType((v as RequestType) ?? 'NEW')}
+ allowDeselect={false}
+ />
+ setSubjectName(e.currentTarget.value)}
+ />
+
+ )}
+
+ {active === 2 && (
+
+
+ }>
+ All required documents must be provided before the review can be completed.
+
+ {requiredDocs.map((d) => (
+
+ setUploaded((s) => ({ ...s, [d]: !s[d] }))
+ }
+ />
+ ))}
+
+ )}
+
+ {active === 3 && (
+
+
+
+
+
+
+
+
+
+
+ {attached < requiredDocs.length && (
+
+ Some required documents are still missing. You can still submit, but
+ review may be delayed.
+
+ )}
+
+ )}
+
+
+ }
+ onClick={prev}
+ disabled={active === 0}
+ >
+ Back
+
+ {active < 3 ? (
+ }
+ onClick={next}
+ disabled={!canContinue()}
+ >
+ Continue
+
+ ) : (
+ }
+ onClick={handleSubmit}
+ >
+ Submit application
+
+ )}
+
+
+
+
+
+
+
+
+ Application Summary
+
+
+
+
+
+
+
+ {licenseType ? LICENSE_TYPE_LABELS[licenseType] : 'Select a license'}
+
+
+ {category ?? 'โ'} ยท {REQUEST_TYPE_LABELS[requestType]}
+
+
+
+
+
+
+
+
+
+
+
+ Total payable
+
+ {licenseType ? `ETB ${SERVICE_FEE[licenseType].toLocaleString()}` : 'โ'}
+
+
+
+
+
+
+
+
+
+
+ Need help?
+
+
+
+ Our support team can guide you through the documents required for this
+ license.
+
+ }
+ onClick={() => navigate('/support')}
+ >
+ Contact support
+
+
+
+
+
);
}
+function SectionHead({ title, subtitle }: { title: string; subtitle: string }) {
+ return (
+
+
{title}
+
+ {subtitle}
+
+
+ );
+}
+
+function SummaryRow({ label, value }: { label: string; value: string }) {
+ return (
+
+
+ {label}
+
+
+ {value}
+
+
+ );
+}
+
function ReviewItem({ label, value }: { label: string; value: string }) {
return (
@@ -250,3 +434,35 @@ function ReviewItem({ label, value }: { label: string; value: string }) {
);
}
+
+function Dropzone() {
+ return (
+
+
+ Supporting document
+
+
notify.info('File upload โ coming soon.')}
+ >
+
+
+
+
+
+ Drag & drop files here, or click to browse
+
+
+ PDF, JPG or PNG โ up to 10 MB
+
+
+
+
+ );
+}
diff --git a/apps/portal/src/app/layouts/PortalLayout.tsx b/apps/portal/src/app/layouts/PortalLayout.tsx
index 412f5418a..530b09dfe 100644
--- a/apps/portal/src/app/layouts/PortalLayout.tsx
+++ b/apps/portal/src/app/layouts/PortalLayout.tsx
@@ -1,136 +1,260 @@
import {
+ ActionIcon,
AppShell,
Burger,
Group,
+ Indicator,
+ Menu,
NavLink,
ScrollArea,
Stack,
Text,
- Menu,
- Avatar,
+ Tooltip,
UnstyledButton,
rem,
} from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import {
- IconLayoutDashboard,
- IconCategory,
- IconFilePlus,
- IconFileDescription,
+ IconBell,
IconCertificate,
- IconUser,
- IconHelpCircle,
- IconLogin,
- IconLogout,
+ IconChevronLeft,
IconChevronRight,
+ IconFileDescription,
+ IconFolder,
+ IconLayoutDashboard,
+ IconLifebuoy,
+ IconLogout,
+ IconPencil,
+ IconUser,
+ IconUserCircle,
} from '@tabler/icons-react';
import type { Icon } from '@tabler/icons-react';
-import { Outlet, useNavigate, useLocation } from 'react-router-dom';
-import { Logo } from '../components/Logo';
-import { useTranslation } from 'react-i18next';
+import { Outlet, useLocation, useNavigate } from 'react-router-dom';
+import { notify } from '@ema-platform/ui';
import { LanguageSwitcher } from '../components/LanguageSwitcher';
import { ColorSchemeToggle } from '../components/ColorSchemeToggle';
+import { BrandMark } from '@ema-platform/auth';
+import { BrandAvatar } from '../components/ui';
interface NavItem {
- to: string;
- labelKey: string;
+ label: string;
icon: Icon;
+ to?: string;
+ soon?: boolean;
}
const NAV_ITEMS: NavItem[] = [
- { to: '/dashboard', labelKey: 'nav.dashboard', icon: IconLayoutDashboard },
- { to: '/services', labelKey: 'nav.services', icon: IconCategory },
- { to: '/apply', labelKey: 'nav.apply', icon: IconFilePlus },
- { to: '/applications', labelKey: 'nav.applications', icon: IconFileDescription },
- { to: '/licenses', labelKey: 'nav.licenses', icon: IconCertificate },
- { to: '/profile', labelKey: 'nav.profile', icon: IconUser },
- { to: '/support', labelKey: 'nav.support', icon: IconHelpCircle },
+ { to: '/dashboard', label: 'Dashboard', icon: IconLayoutDashboard },
+ { to: '/applications', label: 'My Applications', icon: IconFileDescription },
+ { to: '/apply', label: 'Apply for License', icon: IconPencil },
+ { to: '/licenses', label: 'My Licenses', icon: IconCertificate },
+ { label: 'Documents', icon: IconFolder, soon: true },
+ { to: '/profile', label: 'Profile', icon: IconUser },
+ { to: '/support', label: 'Support', icon: IconLifebuoy },
];
-const TOKEN_KEY = 'ema-portal-auth-token';
+const PAGE_META: Record = {
+ '/dashboard': {
+ title: 'Dashboard',
+ subtitle: new Date().toLocaleDateString('en-GB', {
+ weekday: 'long',
+ day: '2-digit',
+ month: 'long',
+ year: 'numeric',
+ }),
+ },
+ '/applications': {
+ title: 'My Applications',
+ subtitle: 'Track the status of every application',
+ },
+ '/apply': {
+ title: 'Apply for a License',
+ subtitle: 'New application',
+ },
+ '/profile': {
+ title: 'Profile',
+ subtitle: 'Manage your account and preferences',
+ },
+};
export function PortalLayout() {
- const { t } = useTranslation();
const navigate = useNavigate();
const location = useLocation();
- const [opened, { toggle, close }] = useDisclosure();
+ const [navOpened, { toggle: toggleNav, close: closeNav }] = useDisclosure();
+ const [sidebarCollapsed, { toggle: toggleSidebar }] = useDisclosure(false);
- const isLoggedIn = !!localStorage.getItem(TOKEN_KEY);
-
- const go = (to: string) => {
- navigate(to);
- close();
+ const meta = PAGE_META[location.pathname] ?? {
+ title: 'EMA Portal',
+ subtitle: 'Ethiopian Maritime Authority',
};
- const logout = () => {
- localStorage.removeItem(TOKEN_KEY);
+ const go = (item: NavItem) => {
+ if (item.soon) {
+ notify.info(`${item.label} โ coming soon.`);
+ return;
+ }
+ if (item.to) {
+ navigate(item.to);
+ closeNav();
+ }
+ };
+
+ const handleLogout = () => {
navigate('/login');
};
return (
+ {/* ---- Header ---------------------------------------------------- */}
-
+
-
- go('/dashboard')}>
-
-
-
-
- {t('app.name')}
-
-
- {t('app.authority')}
-
-
-
-
+
+
+
+ {meta.title}
+
+
+ {meta.subtitle}
+
+
-
-
+
- navigate('/login')} onLogout={logout} />
+
+
+
+
+
+
+
+ {/* Profile avatar menu */}
+
-
+ {/* ---- Sidebar -------------------------------------------------- */}
+
+
+
+
+ {!sidebarCollapsed && (
+
+
+ EMA Portal
+
+
+ Ethiopian Maritime Authority
+
+
+ )}
+
+
+
-
+
{NAV_ITEMS.map((item) => {
- const ActiveIcon = item.icon;
+ const ItemIcon = item.icon;
const active =
- location.pathname === item.to ||
- location.pathname.startsWith(`${item.to}/`);
+ !!item.to &&
+ (location.pathname === item.to ||
+ location.pathname.startsWith(`${item.to}/`));
+ if (sidebarCollapsed) {
+ return (
+
+ go(item)}
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ width: '100%',
+ height: rem(40),
+ borderRadius: rem(10),
+ color: active ? 'var(--mantine-color-blue-6)' : undefined,
+ backgroundColor: active ? 'var(--mantine-color-blue-light)' : undefined,
+ }}
+ >
+
+
+
+ );
+ }
return (
}
- onClick={() => go(item.to)}
+ label={item.label}
+ leftSection={}
+ onClick={() => go(item)}
variant="light"
- styles={{ root: { borderRadius: rem(8) } }}
+ styles={{ root: { borderRadius: rem(10) }, label: { fontWeight: 500 } }}
/>
);
})}
-
-
- {t('app.authority')}
-
+ {/* Collapse toggle */}
+
+
+
+ {sidebarCollapsed ? (
+
+ ) : (
+ <>
+
+ Collapse
+ >
+ )}
+
+
@@ -142,45 +266,3 @@ export function PortalLayout() {
);
}
-
-function UserMenu({
- isLoggedIn,
- onLogin,
- onLogout,
-}: {
- isLoggedIn: boolean;
- onLogin: () => void;
- onLogout: () => void;
-}) {
- const { t } = useTranslation();
-
- return (
-
- );
-}
diff --git a/apps/portal/src/app/router.tsx b/apps/portal/src/app/router.tsx
index 3ce011417..aad29665c 100644
--- a/apps/portal/src/app/router.tsx
+++ b/apps/portal/src/app/router.tsx
@@ -10,12 +10,6 @@ import { LoginPage, SignupPage, OTPVerificationPage, ForgotPasswordPage } from '
// Portal feature pages
import { DashboardPage } from './features/dashboard/pages/DashboardPage';
import { ServicesPage } from './features/licenses/pages/ServicesPage';
-
-// Redesigned portal pages (new look & feel) โ kept alongside the originals.
-import { PortalLayoutV2 } from './v2/PortalLayoutV2';
-import { DashboardV2 } from './v2/pages/DashboardV2';
-import { ApplicationsV2 } from './v2/pages/ApplicationsV2';
-import { ApplyV2 } from './v2/pages/ApplyV2';
import { ApplyPage } from './features/licenses/pages/ApplyPage';
import { ApplicationsListPage } from './features/licenses/pages/ApplicationsListPage';
import { ApplicationDetailPage } from './features/licenses/pages/ApplicationDetailPage';
@@ -43,9 +37,8 @@ export const router = createBrowserRouter([
{ path: '/otp-verify', element: },
{ path: '/forgot-password', element: },
- // Portal (open access for this UI template).
- // Wrapped in the portal's own i18n instance so it is isolated from the
- // global i18next singleton that `@tria-plc/iamui-common` initializes.
+ // Portal โ wrapped in the portal's own i18n instance so it is isolated from
+ // the global i18next singleton that `@tria-plc/iamui-common` initializes.
{
element: (
@@ -66,22 +59,6 @@ export const router = createBrowserRouter([
],
},
- // Redesigned portal (new look & feel) under /v2 โ isolated in the portal i18n
- // instance, same as the original portal routes.
- {
- element: (
-
-
-
- ),
- children: [
- { path: '/v2', element: },
- { path: '/v2/dashboard', element: },
- { path: '/v2/applications', element: },
- { path: '/v2/apply', element: },
- ],
- },
-
// IAM admin user management (isolated providers)
{
element: (
diff --git a/apps/portal/src/app/v2/PortalLayoutV2.tsx b/apps/portal/src/app/v2/PortalLayoutV2.tsx
deleted file mode 100644
index ecb4ea3c6..000000000
--- a/apps/portal/src/app/v2/PortalLayoutV2.tsx
+++ /dev/null
@@ -1,210 +0,0 @@
-import {
- ActionIcon,
- AppShell,
- Box,
- Burger,
- Group,
- Indicator,
- NavLink,
- ScrollArea,
- Stack,
- Text,
- TextInput,
- rem,
-} from '@mantine/core';
-import { useDisclosure } from '@mantine/hooks';
-import {
- IconBell,
- IconCertificate,
- IconFileDescription,
- IconFolder,
- IconLayoutDashboard,
- IconLifebuoy,
- IconLogout,
- IconPencil,
- IconSearch,
-} from '@tabler/icons-react';
-import type { Icon } from '@tabler/icons-react';
-import { Outlet, useLocation, useNavigate } from 'react-router-dom';
-import { notify } from '@ema-platform/ui';
-import { LanguageSwitcher } from '../components/LanguageSwitcher';
-import { ColorSchemeToggle } from '../components/ColorSchemeToggle';
-import { BrandMark } from '@ema-platform/auth';
-import { BrandAvatar } from './components/ui';
-
-interface NavItem {
- label: string;
- icon: Icon;
- to?: string;
- soon?: boolean;
-}
-
-const NAV_ITEMS: NavItem[] = [
- { to: '/v2/dashboard', label: 'Dashboard', icon: IconLayoutDashboard },
- { to: '/v2/applications', label: 'My Applications', icon: IconFileDescription },
- { to: '/v2/apply', label: 'Apply for License', icon: IconPencil },
- { to: '/licenses', label: 'My Licenses', icon: IconCertificate },
- { label: 'Documents', icon: IconFolder, soon: true },
- { to: '/support', label: 'Support', icon: IconLifebuoy },
-];
-
-const PAGE_META: Record = {
- '/v2/dashboard': {
- title: 'Dashboard',
- subtitle: new Date().toLocaleDateString('en-GB', {
- weekday: 'long',
- day: '2-digit',
- month: 'long',
- year: 'numeric',
- }),
- },
- '/v2/applications': {
- title: 'My Applications',
- subtitle: 'Track the status of every application',
- },
- '/v2/apply': {
- title: 'Apply for a License',
- subtitle: 'New application',
- },
-};
-
-export function PortalLayoutV2() {
- const navigate = useNavigate();
- const location = useLocation();
- const [opened, { toggle, close }] = useDisclosure();
-
- const meta = PAGE_META[location.pathname] ?? {
- title: 'EMA Portal',
- subtitle: 'Ethiopian Maritime Authority',
- };
-
- const go = (item: NavItem) => {
- if (item.soon) {
- notify.info(`${item.label} โ coming soon.`);
- return;
- }
- if (item.to) {
- navigate(item.to);
- close();
- }
- };
-
- return (
-
- {/* ---- Topbar ---------------------------------------------------- */}
-
-
-
-
-
-
- {meta.title}
-
-
- {meta.subtitle}
-
-
-
-
-
- }
- placeholder="Search applications..."
- />
-
-
-
-
-
-
-
-
-
-
-
-
- {/* ---- Sidebar -------------------------------------------------- */}
-
-
-
-
-
-
- EMA Portal
-
-
- Ethiopian Maritime Authority
-
-
-
-
-
-
-
- {NAV_ITEMS.map((item) => {
- const ItemIcon = item.icon;
- const active =
- !!item.to &&
- (location.pathname === item.to ||
- location.pathname.startsWith(`${item.to}/`));
- return (
- }
- onClick={() => go(item)}
- variant="light"
- styles={{ root: { borderRadius: rem(10) }, label: { fontWeight: 500 } }}
- />
- );
- })}
-
-
-
-
-
-
-
-
- Abebe Bekele
-
-
- Applicant
-
-
- navigate('/login')}
- >
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/apps/portal/src/app/v2/pages/ApplicationsV2.tsx b/apps/portal/src/app/v2/pages/ApplicationsV2.tsx
deleted file mode 100644
index 57d6e5762..000000000
--- a/apps/portal/src/app/v2/pages/ApplicationsV2.tsx
+++ /dev/null
@@ -1,352 +0,0 @@
-import { useMemo, useState } from 'react';
-import {
- ActionIcon,
- Badge,
- Box,
- Button,
- Divider,
- Grid,
- Group,
- Paper,
- Stack,
- Table,
- Text,
- ThemeIcon,
- Timeline,
- Title,
- UnstyledButton,
-} from '@mantine/core';
-import {
- IconAdjustmentsHorizontal,
- IconCheck,
- IconChevronRight,
- IconClock,
- IconCircleX,
- IconDownload,
- IconMessageCircle,
-} from '@tabler/icons-react';
-import { notify } from '@ema-platform/ui';
-import { useLicenses } from '../../features/licenses/hooks/useLicenses';
-import { useLicenseLabels } from '../../features/licenses/hooks/useLicenseLabels';
-import { APPLICATION_PIPELINE } from '../../features/licenses/constants';
-import type {
- Application,
- ApplicationStatus,
- LicenseType,
-} from '../../features/licenses/types/license.types';
-import { StatusPill } from '../components/ui';
-
-const SERVICE_FEE: Record = {
- SEAFARER_COC: 1200,
- SEAFARER_COP: 900,
- SEAMAN_BOOK: 600,
- VESSEL_REGISTRATION: 3500,
- SHIP_RADIO: 900,
- TONNAGE_CERTIFICATE: 2400,
- SAFETY_MANAGEMENT: 5000,
- PORT_FACILITY: 5000,
- BOAT_OPERATOR: 700,
-};
-
-const etb = (n: number) => `ETB ${n.toLocaleString()}`;
-
-const PIPELINE_LABELS: Record = {
- SUBMITTED: 'Application submitted',
- UNDER_REVIEW: 'Document verification & review',
- PAYMENT_PENDING: 'Payment & assessment',
- APPROVED: 'Approval decision',
- ISSUED: 'License issued',
-};
-
-function stageIndex(status: ApplicationStatus): number {
- if (status === 'DRAFT') return 0;
- if (status === 'INFO_REQUESTED') return 1;
- if (status === 'REJECTED') return 3;
- const i = APPLICATION_PIPELINE.indexOf(status);
- return i < 0 ? 0 : i;
-}
-
-type TabKey = 'all' | 'review' | 'approved' | 'pending';
-
-export function ApplicationsV2() {
- const { applications } = useLicenses();
- const { licenseType, formatDate } = useLicenseLabels();
- const [tab, setTab] = useState('all');
- const [selectedId, setSelectedId] = useState(
- applications[0]?.id ?? null,
- );
-
- const counts = useMemo(
- () => ({
- all: applications.length,
- review: applications.filter((a) => a.status === 'UNDER_REVIEW').length,
- approved: applications.filter(
- (a) => a.status === 'APPROVED' || a.status === 'ISSUED',
- ).length,
- pending: applications.filter((a) =>
- ['SUBMITTED', 'INFO_REQUESTED', 'PAYMENT_PENDING'].includes(a.status),
- ).length,
- }),
- [applications],
- );
-
- const filtered = useMemo(() => {
- switch (tab) {
- case 'review':
- return applications.filter((a) => a.status === 'UNDER_REVIEW');
- case 'approved':
- return applications.filter(
- (a) => a.status === 'APPROVED' || a.status === 'ISSUED',
- );
- case 'pending':
- return applications.filter((a) =>
- ['SUBMITTED', 'INFO_REQUESTED', 'PAYMENT_PENDING'].includes(a.status),
- );
- default:
- return applications;
- }
- }, [applications, tab]);
-
- const selected =
- applications.find((a) => a.id === selectedId) ?? filtered[0] ?? applications[0];
-
- const TABS: { key: TabKey; label: string; count: number }[] = [
- { key: 'all', label: 'All', count: counts.all },
- { key: 'review', label: 'In Review', count: counts.review },
- { key: 'approved', label: 'Approved', count: counts.approved },
- { key: 'pending', label: 'Pending', count: counts.pending },
- ];
-
- return (
-
- {/* ---- Filter tabs ------------------------------------------- */}
-
-
- {TABS.map((tabItem) => {
- const active = tab === tabItem.key;
- return (
-
- );
- })}
-
- }
- >
- Filter & sort
-
-
-
-
- {/* ---- Applications table ---------------------------------- */}
-
-
-
-
-
- Application
- Type
- Submitted
- Fee
- Status
-
-
-
-
- {filtered.map((app) => {
- const isSelected = selected?.id === app.id;
- return (
- setSelectedId(app.id)}
- style={{
- cursor: 'pointer',
- background: isSelected
- ? 'var(--mantine-color-emaPrimary-light)'
- : undefined,
- }}
- >
-
-
- {app.referenceNo}
-
-
-
-
- {licenseType(app.licenseType)}
-
-
-
-
- {formatDate(app.submittedAt)}
-
-
-
-
- {etb(SERVICE_FEE[app.licenseType])}
-
-
-
-
-
-
-
-
-
- );
- })}
-
-
-
-
-
- {/* ---- Status timeline ------------------------------------- */}
-
- {selected && }
-
-
-
- );
-}
-
-function TimelinePanel({ application }: { application: Application }) {
- const { licenseType, formatDate } = useLicenseLabels();
- const current = stageIndex(application.status);
- const historyDates = new Map(
- application.history.map((h) => [h.status, h.date] as const),
- );
-
- return (
-
-
-
-
- {application.referenceNo}
-
- {licenseType(application.licenseType)}
-
-
-
-
-
-
-
- {APPLICATION_PIPELINE.map((status, i) => {
- const done = i < current;
- const isCurrent = i === current;
- const date = historyDates.get(status);
- return (
- : undefined}
- title={
-
- {PIPELINE_LABELS[status]}
-
- }
- lineVariant={done ? 'solid' : 'dashed'}
- >
-
- {date
- ? formatDate(date)
- : isCurrent
- ? 'In progress'
- : 'Pending'}
-
-
- );
- })}
-
-
-
-
-
-
-
- Last updated {formatDate(application.updatedAt)}
-
-
-
-
-
-
- }
- label="Download submission receipt"
- onClick={() => notify.info('Receipt download โ coming soon.')}
- />
- }
- label="Message case officer"
- onClick={() => notify.info('Messaging โ coming soon.')}
- />
- }
- label="Withdraw application"
- danger
- onClick={() => notify.info('Withdrawal โ coming soon.')}
- />
-
-
-
- );
-}
-
-function ActionRow({
- icon,
- label,
- onClick,
- danger,
-}: {
- icon: React.ReactNode;
- label: string;
- onClick: () => void;
- danger?: boolean;
-}) {
- return (
-
-
-
- {icon}
-
-
- {label}
-
-
-
-
- );
-}
diff --git a/apps/portal/src/app/v2/pages/ApplyV2.tsx b/apps/portal/src/app/v2/pages/ApplyV2.tsx
deleted file mode 100644
index 6c1b8550d..000000000
--- a/apps/portal/src/app/v2/pages/ApplyV2.tsx
+++ /dev/null
@@ -1,470 +0,0 @@
-import { useMemo, useState } from 'react';
-import {
- Alert,
- Box,
- Button,
- Center,
- Checkbox,
- Divider,
- Grid,
- Group,
- Paper,
- Select,
- SimpleGrid,
- Stack,
- Stepper,
- Text,
- Textarea,
- TextInput,
- ThemeIcon,
- Title,
-} from '@mantine/core';
-import {
- IconArrowLeft,
- IconArrowRight,
- IconCircleCheck,
- IconCloudUpload,
- IconDeviceMobile,
- IconId,
- IconInfoCircle,
- IconLifebuoy,
- IconMapPin,
- IconMessageCircle,
- IconShip,
- IconStack2,
- IconUser,
-} from '@tabler/icons-react';
-import { useNavigate } from 'react-router-dom';
-import { notify } from '@ema-platform/ui';
-import { useLicenses } from '../../features/licenses/hooks/useLicenses';
-import { useLicenseLabels } from '../../features/licenses/hooks/useLicenseLabels';
-import {
- LICENSE_PROCESSING_DAYS,
- LICENSE_TYPE_LABELS,
- LICENSE_VALIDITY_YEARS,
- REQUEST_TYPE_LABELS,
- REQUIRED_DOCUMENTS,
-} from '../../features/licenses/constants';
-import type { LicenseType, RequestType } from '../../features/licenses/types/license.types';
-
-const SERVICE_FEE: Record = {
- SEAFARER_COC: 1200,
- SEAFARER_COP: 900,
- SEAMAN_BOOK: 600,
- VESSEL_REGISTRATION: 3500,
- SHIP_RADIO: 900,
- TONNAGE_CERTIFICATE: 2400,
- SAFETY_MANAGEMENT: 5000,
- PORT_FACILITY: 5000,
- BOAT_OPERATOR: 700,
-};
-
-const CATEGORIES = ['Deck Officer', 'Engine Officer', 'Ratings', 'Electro-technical', 'Other'];
-const REGIONS = [
- 'Addis Ababa',
- 'Dire Dawa',
- 'Amhara',
- 'Oromia',
- 'Tigray',
- 'Afar',
- 'Somali',
- 'Sidama',
- 'South Ethiopia',
- 'Gambela',
- 'Benishangul-Gumuz',
- 'Harari',
-];
-
-const LICENSE_OPTIONS = (Object.keys(LICENSE_TYPE_LABELS) as LicenseType[]).map((v) => ({
- value: v,
- label: LICENSE_TYPE_LABELS[v],
-}));
-const REQUEST_OPTIONS = (['NEW', 'RENEWAL'] as RequestType[]).map((v) => ({
- value: v,
- label: REQUEST_TYPE_LABELS[v],
-}));
-
-export function ApplyV2() {
- const navigate = useNavigate();
- const { submit } = useLicenses();
- const { subjectLabel, doc } = useLicenseLabels();
-
- const [active, setActive] = useState(0);
- const [licenseType, setLicenseType] = useState('SEAFARER_COC');
- const [category, setCategory] = useState('Deck Officer');
- const [region, setRegion] = useState('Addis Ababa');
- const [requestType, setRequestType] = useState('NEW');
- const [fullName, setFullName] = useState('');
- const [idNumber, setIdNumber] = useState('');
- const [phone, setPhone] = useState('');
- const [subjectName, setSubjectName] = useState('');
- const [notes, setNotes] = useState('');
- const [uploaded, setUploaded] = useState>({});
-
- const requiredDocs = useMemo(
- () => (licenseType ? REQUIRED_DOCUMENTS[licenseType] : []),
- [licenseType],
- );
- const attached = requiredDocs.filter((d) => uploaded[d]).length;
-
- const next = () => setActive((c) => Math.min(c + 1, 3));
- const prev = () => setActive((c) => Math.max(c - 1, 0));
-
- const canContinue = () => {
- if (active === 0) return !!licenseType && !!fullName.trim();
- if (active === 1) return !!subjectName.trim();
- return true;
- };
-
- const handleSubmit = () => {
- if (!licenseType) return;
- submit({
- requestType,
- licenseType,
- applicantName: fullName.trim(),
- subjectName: subjectName.trim() || fullName.trim(),
- notes: notes.trim() || undefined,
- documents: requiredDocs.filter((d) => uploaded[d]),
- });
- notify.success('Your application has been submitted to EMA.');
- navigate('/v2/applications');
- };
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- {/* ---- Form card ------------------------------------------- */}
-
-
- {active === 0 && (
-
-
- }
- data={LICENSE_OPTIONS}
- value={licenseType}
- onChange={(v) => setLicenseType(v as LicenseType)}
- searchable
- />
-
- }
- data={CATEGORIES}
- value={category}
- onChange={setCategory}
- />
- }
- data={REGIONS}
- value={region}
- onChange={setRegion}
- searchable
- />
-
- }
- placeholder="Abebe Bekele Tadesse"
- value={fullName}
- onChange={(e) => setFullName(e.currentTarget.value)}
- />
-
- }
- placeholder="ET-1234567"
- value={idNumber}
- onChange={(e) => setIdNumber(e.currentTarget.value)}
- />
- }
- placeholder="+251 911 234 567"
- value={phone}
- onChange={(e) => setPhone(e.currentTarget.value)}
- />
-
-
-
- )}
-
- {active === 1 && (
-
-
- setRequestType((v as RequestType) ?? 'NEW')}
- allowDeselect={false}
- />
- setSubjectName(e.currentTarget.value)}
- />
-
- )}
-
- {active === 2 && (
-
-
- }>
- All required documents must be provided before the review can be completed.
-
- {requiredDocs.map((d) => (
-
- setUploaded((s) => ({ ...s, [d]: !s[d] }))
- }
- />
- ))}
-
- )}
-
- {active === 3 && (
-
-
-
-
-
-
-
-
-
-
- {attached < requiredDocs.length && (
-
- Some required documents are still missing. You can still submit, but
- review may be delayed.
-
- )}
-
- )}
-
-
- }
- onClick={prev}
- disabled={active === 0}
- >
- Back
-
- {active < 3 ? (
- }
- onClick={next}
- disabled={!canContinue()}
- >
- Continue
-
- ) : (
- }
- onClick={handleSubmit}
- >
- Submit application
-
- )}
-
-
-
-
- {/* ---- Summary + help -------------------------------------- */}
-
-
-
-
- Application Summary
-
-
-
-
-
-
-
- {licenseType ? LICENSE_TYPE_LABELS[licenseType] : 'Select a license'}
-
-
- {category ?? 'โ'} ยท {REQUEST_TYPE_LABELS[requestType]}
-
-
-
-
-
-
-
-
-
-
-
- Total payable
-
- {licenseType ? `ETB ${SERVICE_FEE[licenseType].toLocaleString()}` : 'โ'}
-
-
-
-
-
-
-
-
-
-
- Need help?
-
-
-
- Our support team can guide you through the documents required for this
- license.
-
- }
- onClick={() => navigate('/support')}
- >
- Contact support
-
-
-
-
-
-
- );
-}
-
-function SectionHead({ title, subtitle }: { title: string; subtitle: string }) {
- return (
-
-
{title}
-
- {subtitle}
-
-
- );
-}
-
-function SummaryRow({ label, value }: { label: string; value: string }) {
- return (
-
-
- {label}
-
-
- {value}
-
-
- );
-}
-
-function ReviewItem({ label, value }: { label: string; value: string }) {
- return (
-
-
- {label}
-
-
- {value}
-
-
- );
-}
-
-function Dropzone() {
- return (
-
-
- Supporting document
-
-
notify.info('File upload โ coming soon.')}
- >
-
-
-
-
-
- Drag & drop files here, or click to browse
-
-
- PDF, JPG or PNG โ up to 10 MB
-
-
-
-
- );
-}
diff --git a/apps/portal/src/app/v2/pages/DashboardV2.tsx b/apps/portal/src/app/v2/pages/DashboardV2.tsx
deleted file mode 100644
index 77cff8f1c..000000000
--- a/apps/portal/src/app/v2/pages/DashboardV2.tsx
+++ /dev/null
@@ -1,303 +0,0 @@
-import {
- Box,
- Button,
- Card,
- Center,
- Grid,
- Group,
- Paper,
- SimpleGrid,
- Stack,
- Table,
- Text,
- ThemeIcon,
- Title,
- UnstyledButton,
- useMantineTheme,
-} from '@mantine/core';
-import {
- IconArrowRight,
- IconAward,
- IconChevronRight,
- IconClockHour4,
- IconCircleCheck,
- IconFileText,
- IconLifebuoy,
- IconShip,
- IconSquareRoundedPlus,
- IconUpload,
-} from '@tabler/icons-react';
-import type { Icon } from '@tabler/icons-react';
-import { useNavigate } from 'react-router-dom';
-import { notify } from '@ema-platform/ui';
-import { useLicenses } from '../../features/licenses/hooks/useLicenses';
-import { useLicenseLabels } from '../../features/licenses/hooks/useLicenseLabels';
-import { StatusDonut } from '../../features/dashboard/components/StatusDonut';
-import type { ApplicationStatus } from '../../features/licenses/types/license.types';
-import { StatusPill } from '../components/ui';
-
-const OPEN_STATUSES: ApplicationStatus[] = [
- 'SUBMITTED',
- 'UNDER_REVIEW',
- 'INFO_REQUESTED',
- 'PAYMENT_PENDING',
-];
-
-export function DashboardV2() {
- const navigate = useNavigate();
- const theme = useMantineTheme();
- const { applications, licenses } = useLicenses();
- const { licenseType, formatDate } = useLicenseLabels();
-
- const activeLicenses = licenses.filter((l) => l.status === 'ACTIVE').length;
- const pending = applications.filter((a) => OPEN_STATUSES.includes(a.status)).length;
- const approved = applications.filter(
- (a) => a.status === 'APPROVED' || a.status === 'ISSUED',
- ).length;
- const documents = applications.reduce((sum, a) => sum + a.documents.length, 0);
-
- const firstName = (licenses[0]?.holderName ?? 'there').split(' ')[0];
- const recent = applications.slice(0, 6);
-
- return (
-
- {/* ---- Hero banner ------------------------------------------- */}
-
-
-
-
-
- Welcome back, {firstName} ๐
-
-
- You have {pending} applications in review and {activeLicenses} active
- licence{activeLicenses === 1 ? '' : 's'}. Start a new application or check
- your status below.
-
-
- }
- onClick={() => navigate('/v2/apply')}
- >
- Apply for a license
-
-
-
-
-
-
-
-
- {/* ---- Stat cards -------------------------------------------- */}
-
-
-
-
-
-
-
- {/* ---- Lower row --------------------------------------------- */}
-
-
-
-
- Recent Applications
- }
- onClick={() => navigate('/v2/applications')}
- >
- View all
-
-
-
-
-
-
- Application
- Type
- Submitted
- Status
-
-
-
-
- {recent.map((app) => (
- navigate('/v2/applications')}
- >
-
-
- {app.referenceNo}
-
-
-
-
- {licenseType(app.licenseType)}
-
-
-
-
- {formatDate(app.submittedAt)}
-
-
-
-
-
-
-
-
-
- ))}
-
-
-
-
-
-
-
-
-
- Application Status
-
-
-
-
-
-
- Quick actions
-
-
- navigate('/v2/apply')}
- />
- notify.info('Document upload โ coming soon.')}
- />
- navigate('/support')}
- />
-
-
-
-
-
-
- );
-}
-
-function StatCard({
- icon: CardIcon,
- color,
- value,
- label,
- trend,
- trendColor = 'gray',
-}: {
- icon: Icon;
- color: string;
- value: number;
- label: string;
- trend: string;
- trendColor?: string;
-}) {
- return (
-
-
-
-
-
-
-
- {trend}
-
-
-
- {value}
-
-
- {label}
-
-
-
- );
-}
-
-function QuickAction({
- icon: ActionIconCmp,
- color,
- label,
- onClick,
-}: {
- icon: Icon;
- color: string;
- label: string;
- onClick: () => void;
-}) {
- return (
-
-
-
-
-
-
-
- {label}
-
-
-
-
-
- );
-}