-
-
-
-
-
-
-
- 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/features/seafarer/pages/SeafarerProfilePage.tsx b/apps/portal/src/app/features/seafarer/pages/SeafarerProfilePage.tsx
new file mode 100644
index 000000000..f1e7f00f6
--- /dev/null
+++ b/apps/portal/src/app/features/seafarer/pages/SeafarerProfilePage.tsx
@@ -0,0 +1,701 @@
+import { useEffect, useState } from 'react';
+import {
+ ActionIcon,
+ Alert,
+ Avatar,
+ Badge,
+ Box,
+ Button,
+ Card,
+ Divider,
+ Group,
+ Modal,
+ Paper,
+ Select,
+ SimpleGrid,
+ Skeleton,
+ Stack,
+ Table,
+ Tabs,
+ Text,
+ TextInput,
+ Textarea,
+ ThemeIcon,
+ Title,
+ rem,
+} from '@mantine/core';
+import { useDisclosure } from '@mantine/hooks';
+import {
+ IconAnchor,
+ IconArrowLeft,
+ IconBook,
+ IconBriefcase,
+ IconCertificate,
+ IconCheck,
+ IconClock,
+ IconEdit,
+ IconFileText,
+ IconHeartbeat,
+ IconHistory,
+ IconLayoutDashboard,
+ IconPlus,
+ IconPrinter,
+ IconShip,
+ IconUser,
+ IconX,
+} from '@tabler/icons-react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { notify } from '@ema-platform/ui';
+import type { Seafarer } from './SeafarerRegistryPage';
+
+// ---------------------------------------------------------------------------
+// Extended profile types
+// ---------------------------------------------------------------------------
+interface TrainingRecord {
+ id: string;
+ course: string;
+ institution: string;
+ certNo: string;
+ issueDate: string;
+ expiry: string;
+ status: 'Approved' | 'Pending' | 'Expired';
+}
+
+interface MedicalRecord {
+ id: string;
+ examType: string;
+ issuedBy: string;
+ issueDate: string;
+ expiry: string;
+ result: 'Fit' | 'Unfit' | 'Conditional';
+ remarks: string;
+}
+
+interface SeaServiceRecord {
+ id: string;
+ vesselName: string;
+ vesselType: string;
+ rank: string;
+ flag: string;
+ from: string;
+ to: string;
+ engagementPort: string;
+}
+
+interface CertificationRecord {
+ id: string;
+ name: string;
+ certNo: string;
+ issuedBy: string;
+ issueDate: string;
+ expiry: string;
+ type: string;
+ status: 'Valid' | 'Expired' | 'Pending';
+}
+
+interface HistoryEntry {
+ id: string;
+ action: string;
+ performedBy: string;
+ date: string;
+ notes: string;
+}
+
+interface SeafarerProfile extends Seafarer {
+ dob: string;
+ nationalId: string;
+ passportNo: string;
+ bookNumber: string;
+ permanentAddress: string;
+ training: TrainingRecord[];
+ medical: MedicalRecord[];
+ seaService: SeaServiceRecord[];
+ certifications: CertificationRecord[];
+ history: HistoryEntry[];
+}
+
+// ---------------------------------------------------------------------------
+// Dummy API β replace bodies with real fetch calls
+// ---------------------------------------------------------------------------
+async function fetchSeafarerProfile(id: string): Promise {
+ await new Promise((r) => setTimeout(r, 800));
+ return {
+ id,
+ seafarerId: 'SF-2024-0001',
+ firstName: 'Abebe',
+ lastName: 'Girma',
+ email: 'abebe.g@email.com',
+ gender: 'Male',
+ nationality: 'Ethiopian',
+ mobile: '+251 911 234 567',
+ region: 'Addis Ababa',
+ registeredAt: '2024-01-10',
+ medicalStatus: 'Fit',
+ bookStatus: 'Active',
+ status: 'Active',
+ dob: '1988-03-15',
+ nationalId: 'ET-1234567',
+ passportNo: 'EP123456',
+ bookNumber: 'SB-2024-0001',
+ permanentAddress: 'Bole Sub-City, Woreda 03, House No. 456, Addis Ababa',
+ training: [
+ { id: '1', course: 'Personal Survival Techniques', institution: 'Ethiopian Maritime Institute', certNo: 'PST-2023-0456', issueDate: '2023-01-10', expiry: '2028-01-14', status: 'Approved' },
+ { id: '2', course: 'Fire Prevention and Fire Fighting', institution: 'Djibouti Maritime Academy', certNo: 'FFF-2023-0789', issueDate: '2023-03-05', expiry: '2028-03-07', status: 'Approved' },
+ { id: '3', course: 'Elementary First Aid', institution: 'Ethiopian Maritime Institute', certNo: 'EFA-2023-0102', issueDate: '2023-01-10', expiry: '2028-01-10', status: 'Approved' },
+ ],
+ medical: [
+ { id: '1', examType: 'STCW Medical Certificate', issuedBy: 'EMA Medical Center', issueDate: '2023-06-15', expiry: '2025-06-15', result: 'Fit', remarks: 'No medical conditions noted.' },
+ { id: '2', examType: 'Pre-Employment Medical', issuedBy: 'Addis Ababa General Hospital', issueDate: '2022-01-10', expiry: '2024-01-10', result: 'Fit', remarks: 'All tests within normal range.' },
+ ],
+ seaService: [
+ { id: '1', vesselName: 'MV Ethiopian Star', vesselType: 'Bulk Carrier', rank: 'Ordinary Seaman', flag: 'Ethiopia', from: '2022-03-01', to: '2023-02-28', engagementPort: 'Djibouti' },
+ { id: '2', vesselName: 'MV Red Sea Express', vesselType: 'Container Ship', rank: 'Able Seaman', flag: 'Djibouti', from: '2023-04-01', to: '2024-03-31', engagementPort: 'Berbera' },
+ ],
+ certifications: [
+ { id: '1', name: 'STCW Basic Safety Training', certNo: 'BST-2023-0001', issuedBy: 'Ethiopian Maritime Authority', issueDate: '2023-01-15', expiry: '2028-01-15', type: 'STCW', status: 'Valid' },
+ { id: '2', name: 'Certificate of Competency β Deck Rating', certNo: 'COC-2023-0234', issuedBy: 'Ethiopian Maritime Authority', issueDate: '2023-07-01', expiry: '2028-07-01', type: 'COC', status: 'Valid' },
+ ],
+ history: [
+ { id: '1', action: 'Profile Created', performedBy: 'System', date: '2024-01-10', notes: 'Initial registration submitted.' },
+ { id: '2', action: 'Status β Active', performedBy: 'Admin Officer', date: '2024-01-15', notes: 'All documents verified and approved.' },
+ { id: '3', action: 'Training Record Added', performedBy: 'Abebe Girma', date: '2024-02-20', notes: 'PST certificate uploaded.' },
+ ],
+ };
+}
+
+async function updateSeafarerStatus(_id: string, _status: string): Promise {
+ await new Promise((r) => setTimeout(r, 600));
+}
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+const STATUS_COLOR: Record = {
+ Active: 'teal', Pending: 'yellow', Suspended: 'red',
+ Approved: 'teal', Expired: 'red', Valid: 'teal',
+ Fit: 'teal', Unfit: 'red', Conditional: 'orange',
+};
+
+function Chip({ value }: { value: string }) {
+ return {value};
+}
+
+function InfoField({ label, value }: { label: string; value: string }) {
+ return (
+
+ {label}
+ {value || 'β'}
+
+ );
+}
+
+function SectionCard({ title, children, action }: { title: string; children: React.ReactNode; action?: React.ReactNode }) {
+ return (
+
+
+ {title}
+ {action}
+
+
+ {children}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Tab: Overview
+// ---------------------------------------------------------------------------
+function OverviewTab({ profile, onStatusChange }: { profile: SeafarerProfile; onStatusChange: (s: 'Active' | 'Suspended') => void }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reg. Status
+
+
+
+ Medical Status
+
+
+
+
+ Book Status
+
+
+
+
+
+ {profile.status !== 'Active' && (
+ } onClick={() => onStatusChange('Active')}>
+ Approve
+
+ )}
+ {profile.status !== 'Suspended' && (
+ } onClick={() => onStatusChange('Suspended')}>
+ Suspend
+
+ )}
+ } onClick={() => notify.info('Documents β coming soon.')}>
+ Documents
+
+
+
+
+
+ Permanent Address
+ {profile.permanentAddress || 'β'}
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Tab: Training
+// ---------------------------------------------------------------------------
+function TrainingTab({ records, onAdd }: { records: TrainingRecord[]; onAdd: () => void }) {
+ return (
+
+
+ Training Records
+ } onClick={onAdd}>+ Add Training
+
+
+
+
+
+ {['Course', 'Institution', 'Cert. No.', 'Issue Date', 'Expiry', 'Status', 'Actions'].map((h) => (
+ {h}
+ ))}
+
+
+
+ {records.map((r) => (
+
+ {r.course}
+ {r.institution}
+ {r.certNo}
+ {r.issueDate}
+ {r.expiry}
+
+
+
+
+
+ ))}
+
+
+ {records.length === 0 && No training records found.}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Tab: Medical
+// ---------------------------------------------------------------------------
+function MedicalTab({ records, onAdd }: { records: MedicalRecord[]; onAdd: () => void }) {
+ return (
+
+
+ Medical Records
+ } onClick={onAdd}>+ Add Record
+
+
+
+
+
+ {['Exam Type', 'Issued By', 'Issue Date', 'Expiry', 'Result', 'Remarks', 'Actions'].map((h) => (
+ {h}
+ ))}
+
+
+
+ {records.map((r) => (
+
+ {r.examType}
+ {r.issuedBy}
+ {r.issueDate}
+ {r.expiry}
+
+ {r.remarks}
+
+
+
+
+ ))}
+
+
+ {records.length === 0 && No medical records found.}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Tab: Sea Service
+// ---------------------------------------------------------------------------
+function SeaServiceTab({ records, onAdd }: { records: SeaServiceRecord[]; onAdd: () => void }) {
+ return (
+
+
+ Sea Service Records
+ } onClick={onAdd}>+ Add Service
+
+
+
+
+
+ {['Vessel Name', 'Type', 'Rank', 'Flag', 'From', 'To', 'Engagement Port', 'Actions'].map((h) => (
+ {h}
+ ))}
+
+
+
+ {records.map((r) => (
+
+ {r.vesselName}
+ {r.vesselType}
+ {r.rank}
+ {r.flag}
+ {r.from}
+ {r.to}
+ {r.engagementPort}
+
+
+
+
+ ))}
+
+
+ {records.length === 0 && No sea service records found.}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Tab: Certifications
+// ---------------------------------------------------------------------------
+function CertificationsTab({ records, onAdd }: { records: CertificationRecord[]; onAdd: () => void }) {
+ return (
+
+
+ Certifications
+ } onClick={onAdd}>+ Add Certification
+
+
+
+
+
+ {['Certificate', 'Cert. No.', 'Type', 'Issued By', 'Issue Date', 'Expiry', 'Status', 'Actions'].map((h) => (
+ {h}
+ ))}
+
+
+
+ {records.map((r) => (
+
+ {r.name}
+ {r.certNo}
+ {r.type}
+ {r.issuedBy}
+ {r.issueDate}
+ {r.expiry}
+
+
+
+
+
+ ))}
+
+
+ {records.length === 0 && No certifications found.}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Tab: History
+// ---------------------------------------------------------------------------
+function HistoryTab({ entries }: { entries: HistoryEntry[] }) {
+ return (
+
+ Activity History
+
+
+ {entries.map((e) => (
+
+
+
+
+
+
+ {e.action}
+ by {e.performedBy}
+
+ {e.date}
+ {e.notes && {e.notes}}
+
+
+ ))}
+ {entries.length === 0 && No history found.}
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Add Record Modal (generic)
+// ---------------------------------------------------------------------------
+function AddTrainingModal({ opened, onClose }: { opened: boolean; onClose: () => void }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function AddMedicalModal({ opened, onClose }: { opened: boolean; onClose: () => void }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function AddSeaServiceModal({ opened, onClose }: { opened: boolean; onClose: () => void }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function AddCertModal({ opened, onClose }: { opened: boolean; onClose: () => void }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Main page
+// ---------------------------------------------------------------------------
+export function SeafarerProfilePage() {
+ const { id } = useParams<{ id: string }>();
+ const navigate = useNavigate();
+ const [profile, setProfile] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [activeTab, setActiveTab] = useState('overview');
+
+ const [trainingModal, trainingModalHandlers] = useDisclosure(false);
+ const [medicalModal, medicalModalHandlers] = useDisclosure(false);
+ const [seaServiceModal, seaServiceModalHandlers] = useDisclosure(false);
+ const [certModal, certModalHandlers] = useDisclosure(false);
+
+ useEffect(() => {
+ if (!id) return;
+ fetchSeafarerProfile(id)
+ .then(setProfile)
+ .catch(() => notify.error('Failed to load seafarer profile.'))
+ .finally(() => setLoading(false));
+ }, [id]);
+
+ const handleStatusChange = async (newStatus: 'Active' | 'Suspended') => {
+ if (!profile) return;
+ try {
+ await updateSeafarerStatus(profile.id, newStatus);
+ setProfile((p) => p ? { ...p, status: newStatus } : p);
+ notify.success(`Status updated to ${newStatus}.`);
+ } catch {
+ notify.error('Failed to update status.');
+ }
+ };
+
+ const initials = profile ? `${profile.firstName[0]}${profile.lastName[0]}` : '??';
+
+ return (
+
+ {/* Breadcrumb */}
+
+ navigate('/seafarer-registry')}>
+
+
+ navigate('/seafarer-registry')}>
+ Seafarer Registry
+
+ /
+
+ {loading ? : `${profile?.firstName} ${profile?.lastName}`}
+
+
+
+ {/* Profile header card */}
+
+ {loading ? (
+
+
+
+
+
+
+
+
+ ) : profile ? (
+
+
+
+ {initials}
+
+
+
{profile.firstName} {profile.lastName}
+
+ {profile.seafarerId} Β· Registered {profile.registeredAt}
+
+
+ Gender: {profile.gender}
+ DOB: {profile.dob}
+ Nationality: {profile.nationality}
+ Mobile: {profile.mobile}
+ Email: {profile.email}
+
+
+
+
+ {profile.status}
+ } onClick={() => notify.info('Edit profile β coming soon.')}>
+ Edit Profile
+
+ } onClick={() => notify.info('Print β coming soon.')}>
+ Print Profile
+
+
+
+ ) : (
+ Profile not found.
+ )}
+
+
+ {/* Tabs */}
+ {!loading && profile && (
+
+
+ }>Overview
+ }>Training
+ }>Medical
+ }>Sea Service
+ }>Certifications
+ }>History
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Modals */}
+
+
+
+
+
+ );
+}
diff --git a/apps/portal/src/app/features/seafarer/pages/SeafarerRegistrationPage.tsx b/apps/portal/src/app/features/seafarer/pages/SeafarerRegistrationPage.tsx
new file mode 100644
index 000000000..0045c79b9
--- /dev/null
+++ b/apps/portal/src/app/features/seafarer/pages/SeafarerRegistrationPage.tsx
@@ -0,0 +1,546 @@
+import { useRef, useState } from 'react';
+import {
+ Alert,
+ Badge,
+ Box,
+ Button,
+ Card,
+ Divider,
+ FileButton,
+ Group,
+ Paper,
+ Select,
+ SimpleGrid,
+ Stack,
+ Text,
+ Textarea,
+ TextInput,
+ Title,
+ rem,
+} from '@mantine/core';
+import {
+ IconAddressBook,
+ IconAlertTriangle,
+ IconArrowLeft,
+ IconArrowRight,
+ IconCamera,
+ IconCheck,
+ IconCircleCheck,
+ IconFileDescription,
+ IconId,
+ IconInfoCircle,
+ IconSchool,
+ IconUser,
+} from '@tabler/icons-react';
+import { useNavigate } from 'react-router-dom';
+import { notify } from '@ema-platform/ui';
+
+// ---------------------------------------------------------------------------
+// Dummy API
+// ---------------------------------------------------------------------------
+async function submitSeafarerRegistration(data: unknown): Promise<{ ok: true; referenceId: string }> {
+ await new Promise((r) => setTimeout(r, 1200));
+ console.log('Seafarer registration payload:', data);
+ return { ok: true, referenceId: `SEA-${Date.now()}` };
+}
+
+// ---------------------------------------------------------------------------
+// Constants
+// ---------------------------------------------------------------------------
+const NATIONALITIES = [
+ 'Ethiopian', 'Eritrean', 'Djiboutian', 'Kenyan', 'Somali', 'Sudanese', 'Other',
+];
+const MARITAL_STATUSES = ['Single', 'Married', 'Divorced', 'Widowed'];
+const GENDERS = ['Male', 'Female'];
+const REGIONS = [
+ 'Addis Ababa', 'Dire Dawa', 'Amhara', 'Oromia', 'Tigray',
+ 'Afar', 'Somali', 'Sidama', 'South Ethiopia', 'Gambela',
+ 'Benishangul-Gumuz', 'Harari',
+];
+const RELATIONSHIPS = ['Spouse', 'Parent', 'Sibling', 'Child', 'Friend', 'Other'];
+
+const STEPS = [
+ { label: 'Personal Information' },
+ { label: 'Contact Details' },
+ { label: 'Documents Upload' },
+ { label: 'Review & Submit' },
+];
+
+interface DocSlot {
+ key: string;
+ label: string;
+ description: string;
+ required: boolean;
+ icon: typeof IconId;
+}
+
+const DOC_SLOTS: DocSlot[] = [
+ { key: 'nationalId', label: 'National ID (Front & Back)', description: 'Both sides of your national identity card', required: true, icon: IconId },
+ { key: 'passport', label: 'Passport Copy', description: 'Bio-data page of valid passport', required: false, icon: IconFileDescription },
+ { key: 'graduation', label: 'Graduation Certificate', description: 'Highest academic qualification', required: false, icon: IconSchool },
+ { key: 'photo', label: 'Passport Size Photo', description: 'Recent photo, white background, 3.5Γ4.5cm', required: true, icon: IconCamera },
+];
+
+// ---------------------------------------------------------------------------
+// Step indicator
+// ---------------------------------------------------------------------------
+function StepIndicator({ active, completed }: { active: number; completed: number[] }) {
+ return (
+
+
+ {STEPS.map((step, i) => {
+ const isDone = completed.includes(i);
+ const isCurrent = active === i;
+ return (
+
+ {/* Circle */}
+
+
+ {isDone ? (
+
+ ) : (
+
+ {i + 1}
+
+ )}
+
+
+ {isDone ? `${step.label} β` : step.label}
+
+
+
+ {/* Connector line */}
+ {i < STEPS.length - 1 && (
+
+ )}
+
+ );
+ })}
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Section heading
+// ---------------------------------------------------------------------------
+function SectionHead({ title }: { title: string }) {
+ return (
+ <>
+ {title}
+
+ >
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Review row
+// ---------------------------------------------------------------------------
+function ReviewRow({ label, value }: { label: string; value: string }) {
+ return (
+
+ {label}
+ {value || 'β'}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Document upload card
+// ---------------------------------------------------------------------------
+function DocCard({
+ slot,
+ file,
+ onFile,
+}: {
+ slot: DocSlot;
+ file: File | null;
+ onFile: (f: File | null) => void;
+}) {
+ const resetRef = useRef<() => void>(null);
+ const SlotIcon = slot.icon;
+ return (
+
+
+
+
+
+
+
+ {slot.label}
+ {slot.required && *}
+
+ {slot.description}
+
+
+
+ {file ? (
+
+
+ {file.name}
+
+
+ ) : (
+
+ {(props) => (
+
+ )}
+
+ )}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Main page
+// ---------------------------------------------------------------------------
+export function SeafarerRegistrationPage() {
+ const navigate = useNavigate();
+ const [active, setActive] = useState(0);
+ const [completed, setCompleted] = useState([]);
+ const [submitting, setSubmitting] = useState(false);
+
+ // Step 1 β Personal Information
+ const [firstName, setFirstName] = useState('');
+ const [middleName, setMiddleName] = useState('');
+ const [lastName, setLastName] = useState('');
+ const [gender, setGender] = useState(null);
+ const [dob, setDob] = useState('');
+ const [placeOfBirth, setPlaceOfBirth] = useState('');
+ const [nationality, setNationality] = useState('Ethiopian');
+ const [maritalStatus, setMaritalStatus] = useState(null);
+ const [nationalIdNumber, setNationalIdNumber] = useState('');
+ const [passportNumber, setPassportNumber] = useState('');
+ const [passportExpiry, setPassportExpiry] = useState('');
+
+ // Step 2 β Contact Details
+ const [mobile, setMobile] = useState('');
+ const [email, setEmail] = useState('');
+ const [region, setRegion] = useState(null);
+ const [city, setCity] = useState('');
+ const [permanentAddress, setPermanentAddress] = useState('');
+ const [currentAddress, setCurrentAddress] = useState('');
+ const [emergencyName, setEmergencyName] = useState('');
+ const [emergencyRel, setEmergencyRel] = useState(null);
+ const [emergencyPhone, setEmergencyPhone] = useState('');
+
+ // Step 3 β Documents
+ const [files, setFiles] = useState>({
+ nationalId: null, passport: null, graduation: null, photo: null,
+ });
+
+ const setFile = (key: string) => (f: File | null) =>
+ setFiles((prev) => ({ ...prev, [key]: f }));
+
+ const canNext = () => {
+ if (active === 0) return !!firstName.trim() && !!lastName.trim() && !!gender && !!dob && !!placeOfBirth && !!nationality && !!nationalIdNumber.trim();
+ if (active === 1) return !!mobile.trim() && !!email.trim() && !!region && !!city.trim();
+ if (active === 2) return !!files.nationalId && !!files.photo;
+ return true;
+ };
+
+ const next = () => {
+ setCompleted((prev) => prev.includes(active) ? prev : [...prev, active]);
+ setActive((c) => c + 1);
+ };
+ const prev = () => setActive((c) => c - 1);
+
+ const handleSubmit = async () => {
+ setSubmitting(true);
+ try {
+ const result = await submitSeafarerRegistration({
+ personalInfo: { firstName, middleName, lastName, gender, dob, placeOfBirth, nationality, maritalStatus, nationalIdNumber, passportNumber, passportExpiry },
+ contactDetails: { mobile, email, region, city, permanentAddress, currentAddress, emergency: { name: emergencyName, relationship: emergencyRel, phone: emergencyPhone } },
+ documents: Object.fromEntries(Object.entries(files).map(([k, v]) => [k, v?.name ?? null])),
+ });
+ notify.success(`Registration submitted! Reference: ${result.referenceId}`);
+ navigate('/applications');
+ } catch {
+ notify.error('Submission failed. Please try again.');
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const stepLabel = STEPS[active]?.label ?? '';
+ const stepIcons = [IconUser, IconAddressBook, IconFileDescription, IconCircleCheck];
+ const StepIcon = stepIcons[active];
+
+ return (
+
+ {/* Page header */}
+
+
New Seafarer Registration
+ Register a new seafarer profile β Step {active + 1} of {STEPS.length}
+
+
+ {/* Step indicator */}
+
+
+ {/* Card */}
+
+ {/* Card header */}
+
+
+
+ {stepLabel}
+
+ Step {active + 1} of {STEPS.length}
+
+
+ {/* ββ Step 1: Personal Information βββββββββββββββββββββββββββββ */}
+ {active === 0 && (
+
+
+
+ setFirstName(e.currentTarget.value)} />
+ setMiddleName(e.currentTarget.value)} />
+ setLastName(e.currentTarget.value)} />
+
+ setDob(e.currentTarget.value)} />
+ setPlaceOfBirth(e.currentTarget.value)} />
+
+
+
+
+
+
+ setNationalIdNumber(e.currentTarget.value)} />
+ setPassportNumber(e.currentTarget.value)} />
+
+ setPassportExpiry(e.currentTarget.value)} style={{ maxWidth: rem(360) }} />
+
+ }>
+ A unique Seafarer ID will be automatically generated upon approval of this registration.
+
+
+ )}
+
+ {/* ββ Step 2: Contact Details βββββββββββββββββββββββββββββββββββ */}
+ {active === 1 && (
+
+
+
+ setMobile(e.currentTarget.value)} />
+ setEmail(e.currentTarget.value)} />
+
+ setCity(e.currentTarget.value)} />
+
+
+ )}
+
+ {/* ββ Step 3: Documents Upload ββββββββββββββββββββββββββββββββββ */}
+ {active === 2 && (
+
+ }>
+ Please upload clear, readable copies of all required documents. Accepted formats: PDF, JPG, PNG (max 5MB each). Items marked with * are mandatory.
+
+
+
+ {DOC_SLOTS.map((slot) => (
+
+ ))}
+
+
+
+ Upload Progress
+
+ {DOC_SLOTS.map((slot) => (
+
+ {files[slot.key] ? (
+
+ ) : (
+
+ )}
+
+ {slot.key === 'nationalId' ? 'National ID' : slot.key === 'passport' ? 'Passport' : slot.key === 'graduation' ? 'Certificate' : 'Photo'}
+
+
+ ))}
+
+
+
+ )}
+
+ {/* ββ Step 4: Review & Submit βββββββββββββββββββββββββββββββββββ */}
+ {active === 3 && (
+
+
+ Personal Information
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Contact Details
+
+
+
+
+
+
+
+
+ {emergencyName && (
+ <>
+ Emergency Contact
+
+
+
+
+
+ >
+ )}
+
+
+
+ Documents
+
+ {DOC_SLOTS.map((slot) => (
+
+ {files[slot.key] ? (
+
+ ) : (
+
+ )}
+
+ {slot.label}
+ {slot.required && !files[slot.key] && *}
+
+ {files[slot.key] && (
+ ({files[slot.key]!.name})
+ )}
+
+ ))}
+
+
+
+ )}
+
+ {/* Navigation buttons */}
+
+
+
+ {active > 0 && (
+ } onClick={prev}>
+ Previous
+
+ )}
+ {active < STEPS.length - 1 ? (
+ }
+ onClick={next}
+ disabled={!canNext()}
+ >
+ Next Step
+
+ ) : (
+ }
+ onClick={handleSubmit}
+ loading={submitting}
+ >
+ Submit Registration
+
+ )}
+
+
+
+
+ );
+}
diff --git a/apps/portal/src/app/features/seafarer/pages/SeafarerRegistryPage.tsx b/apps/portal/src/app/features/seafarer/pages/SeafarerRegistryPage.tsx
new file mode 100644
index 000000000..cbbc6e310
--- /dev/null
+++ b/apps/portal/src/app/features/seafarer/pages/SeafarerRegistryPage.tsx
@@ -0,0 +1,379 @@
+import { useEffect, useState } from 'react';
+import {
+ ActionIcon,
+ Badge,
+ Box,
+ Button,
+ Card,
+ Group,
+ Loader,
+ Menu,
+ Paper,
+ Select,
+ SimpleGrid,
+ Skeleton,
+ Stack,
+ Table,
+ Text,
+ TextInput,
+ ThemeIcon,
+ Title,
+ rem,
+} from '@mantine/core';
+import {
+ IconAnchor,
+ IconCheck,
+ IconClock,
+ IconDotsVertical,
+ IconEdit,
+ IconEye,
+ IconFileExport,
+ IconSearch,
+ IconUserCheck,
+ IconUsers,
+ IconUserX,
+ IconX,
+} from '@tabler/icons-react';
+import { useNavigate } from 'react-router-dom';
+import { notify } from '@ema-platform/ui';
+
+// ---------------------------------------------------------------------------
+// Types
+// ---------------------------------------------------------------------------
+export interface Seafarer {
+ id: string;
+ seafarerId: string;
+ firstName: string;
+ lastName: string;
+ email: string;
+ gender: 'Male' | 'Female';
+ nationality: string;
+ mobile: string;
+ region: string;
+ registeredAt: string;
+ medicalStatus: 'Fit' | 'Unfit' | 'Pending';
+ bookStatus: 'Active' | 'Expired' | 'Suspended' | 'Pending';
+ status: 'Active' | 'Pending' | 'Suspended';
+}
+
+// ---------------------------------------------------------------------------
+// Dummy API β replace with real fetch later
+// ---------------------------------------------------------------------------
+async function fetchSeafarers(): Promise {
+ await new Promise((r) => setTimeout(r, 900));
+ return [
+ {
+ id: '1',
+ seafarerId: 'SF-2024-0001',
+ firstName: 'Abebe',
+ lastName: 'Girma',
+ email: 'abebe.g@email.com',
+ gender: 'Male',
+ nationality: 'Ethiopian',
+ mobile: '+251 911 234 567',
+ region: 'Addis Ababa',
+ registeredAt: '2024-01-10',
+ medicalStatus: 'Fit',
+ bookStatus: 'Active',
+ status: 'Active',
+ },
+ {
+ id: '2',
+ seafarerId: 'SF-2024-0002',
+ firstName: 'Sara',
+ lastName: 'Tadesse',
+ email: 'sara.t@email.com',
+ gender: 'Female',
+ nationality: 'Ethiopian',
+ mobile: '+251 922 345 678',
+ region: 'Dire Dawa',
+ registeredAt: '2024-02-14',
+ medicalStatus: 'Pending',
+ bookStatus: 'Pending',
+ status: 'Pending',
+ },
+ {
+ id: '3',
+ seafarerId: 'SF-2024-0003',
+ firstName: 'Dawit',
+ lastName: 'Bekele',
+ email: 'dawit.b@email.com',
+ gender: 'Male',
+ nationality: 'Ethiopian',
+ mobile: '+251 933 456 789',
+ region: 'Oromia',
+ registeredAt: '2024-03-05',
+ medicalStatus: 'Fit',
+ bookStatus: 'Expired',
+ status: 'Suspended',
+ },
+ {
+ id: '4',
+ seafarerId: 'SF-2024-0004',
+ firstName: 'Hana',
+ lastName: 'Mulugeta',
+ email: 'hana.m@email.com',
+ gender: 'Female',
+ nationality: 'Ethiopian',
+ mobile: '+251 944 567 890',
+ region: 'Amhara',
+ registeredAt: '2024-04-20',
+ medicalStatus: 'Fit',
+ bookStatus: 'Active',
+ status: 'Active',
+ },
+ ];
+}
+
+// ---------------------------------------------------------------------------
+// Stat card
+// ---------------------------------------------------------------------------
+function StatCard({
+ label,
+ value,
+ icon: Icon,
+ color,
+ loading,
+}: {
+ label: string;
+ value: number;
+ icon: typeof IconUsers;
+ color: string;
+ loading: boolean;
+}) {
+ return (
+
+
+
+ {loading ? (
+
+ ) : (
+
{value}
+ )}
+ {label}
+
+
+
+
+
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Status badges
+// ---------------------------------------------------------------------------
+const STATUS_COLOR: Record = {
+ Active: 'teal',
+ Pending: 'yellow',
+ Suspended: 'red',
+ Expired: 'orange',
+ Fit: 'teal',
+ Unfit: 'red',
+};
+
+function StatusBadge({ value }: { value: string }) {
+ return (
+
+ {value}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Main page
+// ---------------------------------------------------------------------------
+export function SeafarerRegistryPage() {
+ const navigate = useNavigate();
+ const [seafarers, setSeafarers] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [search, setSearch] = useState('');
+ const [statusFilter, setStatusFilter] = useState(null);
+
+ useEffect(() => {
+ fetchSeafarers()
+ .then(setSeafarers)
+ .catch(() => notify.error('Failed to load seafarers.'))
+ .finally(() => setLoading(false));
+ }, []);
+
+ const stats = {
+ total: seafarers.length,
+ active: seafarers.filter((s) => s.status === 'Active').length,
+ pending: seafarers.filter((s) => s.status === 'Pending').length,
+ suspended: seafarers.filter((s) => s.status === 'Suspended').length,
+ };
+
+ const filtered = seafarers.filter((s) => {
+ const q = search.toLowerCase();
+ const matchSearch =
+ !q ||
+ s.seafarerId.toLowerCase().includes(q) ||
+ `${s.firstName} ${s.lastName}`.toLowerCase().includes(q) ||
+ s.mobile.includes(q) ||
+ s.email.toLowerCase().includes(q);
+ const matchStatus = !statusFilter || s.status === statusFilter;
+ return matchSearch && matchStatus;
+ });
+
+ const rows = filtered.map((s) => (
+
+
+ navigate(`/seafarer-registry/${s.id}`)}>
+ {s.seafarerId}
+
+
+
+
+ {s.firstName} {s.lastName}
+ {s.email}
+
+
+ {s.gender}
+ {s.nationality}
+ {s.mobile}
+ {s.region}
+ {s.registeredAt}
+
+
+
+
+
+
+
+ ));
+
+ return (
+
+ {/* Header */}
+
+
+
Seafarer Registry
+ Manage all registered seafarers
+
+ }
+ onClick={() => navigate('/seafarer-registration')}
+ >
+ + New Registration
+
+
+
+ {/* Stats */}
+
+
+
+
+
+
+
+ {/* Table card */}
+
+ {/* Toolbar */}
+
+ Seafarer List
+
+ }
+ value={search}
+ onChange={(e) => setSearch(e.currentTarget.value)}
+ style={{ minWidth: rem(260) }}
+ size="sm"
+ rightSection={
+ search ? (
+ setSearch('')}>
+
+
+ ) : null
+ }
+ />
+
+ notify.info('Export β coming soon.')}
+ >
+
+
+
+
+
+ {/* Table */}
+ {loading ? (
+
+ {[...Array(4)].map((_, i) => )}
+
+ ) : filtered.length === 0 ? (
+
+
+
+
+ No seafarers found
+ {(search || statusFilter) && (
+
+ )}
+
+ ) : (
+
+
+
+ {['Seafarer ID', 'Name', 'Gender', 'Nationality', 'Mobile', 'Region', 'Reg. Date', 'Medical', 'Book Status', 'Status', ''].map((h) => (
+
+ {h}
+
+ ))}
+
+
+ {rows}
+
+ )}
+
+ {/* Footer */}
+ {!loading && filtered.length > 0 && (
+
+ Showing {filtered.length} of {seafarers.length} seafarers
+
+
+ Data loaded
+
+
+ )}
+
+
+ );
+}
diff --git a/apps/portal/src/app/layouts/PortalLayout.tsx b/apps/portal/src/app/layouts/PortalLayout.tsx
index 412f5418a..2ffd9f214 100644
--- a/apps/portal/src/app/layouts/PortalLayout.tsx
+++ b/apps/portal/src/app/layouts/PortalLayout.tsx
@@ -1,136 +1,270 @@
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,
+ IconAnchor,
+ IconBell,
IconCertificate,
- IconUser,
- IconHelpCircle,
- IconLogin,
- IconLogout,
+ IconChevronLeft,
IconChevronRight,
+ IconFileDescription,
+ IconFolder,
+ IconLayoutDashboard,
+ IconLifebuoy,
+ IconList,
+ IconLogout,
+ 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: '/seafarer-registry', label: 'Seafarer Registry', icon: IconList },
+ { to: '/seafarer-registration', label: 'New Registration', icon: IconAnchor },
+ { 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',
+ },
+ '/seafarer-registry': {
+ title: 'Seafarer Registry',
+ subtitle: 'Manage all registered seafarers',
+ },
+ '/seafarer-registration': {
+ title: 'New Seafarer Registration',
+ subtitle: 'Register a new seafarer profile',
+ },
+ '/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 +276,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..ea4f3fc57 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';
@@ -23,6 +17,9 @@ import { MyLicensesPage } from './features/licenses/pages/MyLicensesPage';
import { LicenseDetailPage } from './features/licenses/pages/LicenseDetailPage';
import { ProfilePage } from './features/profile/pages/ProfilePage';
import { SupportPage } from './features/support/pages/SupportPage';
+import { SeafarerRegistrationPage } from './features/seafarer/pages/SeafarerRegistrationPage';
+import { SeafarerRegistryPage } from './features/seafarer/pages/SeafarerRegistryPage';
+import { SeafarerProfilePage } from './features/seafarer/pages/SeafarerProfilePage';
// IAM (admin user management) β kept reachable but isolated under its own
// provider so it does not depend on the portal's provider tree.
@@ -43,9 +40,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: (
@@ -57,6 +53,9 @@ export const router = createBrowserRouter([
{ path: '/dashboard', element: },
{ path: '/services', element: },
{ path: '/apply', element: },
+ { path: '/seafarer-registration', element: },
+ { path: '/seafarer-registry', element: },
+ { path: '/seafarer-registry/:id', element: },
{ path: '/applications', element: },
{ path: '/applications/:id', element: },
{ path: '/licenses', element: },
@@ -66,22 +65,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}
-
-
-
-
-
- );
-}