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..cd0e7bee4
--- /dev/null
+++ b/apps/portal/src/app/features/seafarer/pages/SeafarerRegistrationPage.tsx
@@ -0,0 +1,642 @@
+import { useEffect, useRef, useState } from 'react';
+import { useCurrentProfile, useUpdateMyProfileMutation } from '@ema-platform/auth';
+import { useAppSelector } from '../../../store/hooks';
+import { useSaveMyAddressMutation } from '../../profile/api/address-api';
+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';
+import { BilingualInput } from '../../../components/BilingualInput';
+import type { BilingualValue } from '../../../components/BilingualInput';
+import { AmharicDatePicker, toEthiopicDateLabel } from '../../../components/AmharicDatePicker';
+import { LocationPicker } from '../../location/components/LocationPicker';
+
+
+// ---------------------------------------------------------------------------
+// Constants
+// ---------------------------------------------------------------------------
+const NATIONALITIES = [
+ 'Ethiopian', 'Eritrean', 'Djiboutian', 'Kenyan', 'Somali', 'Sudanese', 'Other',
+];
+const MARITAL_STATUSES = ['Single', 'Married', 'Divorced', 'Widowed'];
+const GENDERS = ['Male', 'Female'];
+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 (
+
+
+
+ {isDone ? (
+
+ ) : (
+
+ {i + 1}
+
+ )}
+
+
+ {isDone ? `${step.label} ✓` : step.label}
+
+
+
+ {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}
+ { onFile(null); resetRef.current?.(); }}
+ >
+ Remove
+
+
+ ) : (
+
+ {(props) => (
+
+ Choose File
+
+ )}
+
+ )}
+
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Main page
+// ---------------------------------------------------------------------------
+export function SeafarerRegistrationPage() {
+ const navigate = useNavigate();
+ // Every signed-in user already has a profile (`/profiles/me` provisions
+ // one), so registering fills that row in rather than creating a second —
+ // POST /profiles trips the unique user_id constraint.
+ const { profileId, profile } = useCurrentProfile();
+ const user = useAppSelector((state) => state.auth.user);
+ const [active, setActive] = useState(0);
+ const [completed, setCompleted] = useState([]);
+ const [submitting, setSubmitting] = useState(false);
+ const [updateProfile] = useUpdateMyProfileMutation();
+ const [saveAddress] = useSaveMyAddressMutation();
+
+ // Step 1 — Personal Information
+ const [firstName, setFirstName] = useState({ en: '', am: '' });
+ const [middleName, setMiddleName] = useState({ en: '', am: '' });
+ const [lastName, setLastName] = useState({ en: '', am: '' });
+ const [gender, setGender] = useState(null);
+ const [dob, setDob] = useState(null);
+ 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 [locationId, setLocationId] = useState(null);
+ 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 }));
+
+ // Signup already asked for name, email and phone — start from those (and
+ // whatever is on the profile) instead of making the applicant retype them.
+ // Only blank fields are filled, so nothing typed here is overwritten.
+ useEffect(() => {
+ const [enFirst = '', ...enRest] = (user?.name.en ?? '').trim().split(/\s+/);
+ const [amFirst = '', ...amRest] = (user?.name.am ?? '').trim().split(/\s+/);
+ const orEmpty = (v?: string | null) => v ?? '';
+ // Profile stores MALE / SINGLE; the selects here list Male / Single.
+ const title = (v: string) => v.charAt(0) + v.slice(1).toLowerCase();
+ setFirstName((c) => c.en ? c : { en: profile?.firstName || enFirst, am: amFirst });
+ setMiddleName((c) => c.en ? c : { en: profile?.middleName || enRest.slice(0, -1).join(' '), am: amRest.slice(0, -1).join(' ') });
+ setLastName((c) => c.en ? c : { en: profile?.lastName || orEmpty(enRest.at(-1)), am: orEmpty(amRest.at(-1)) });
+ if (profile?.gender) setGender((c) => c ?? title(profile.gender));
+ if (profile?.dob) setDob((c) => c ?? new Date(profile.dob));
+ if (profile?.pob) setPlaceOfBirth((c) => c || profile.pob);
+ if (profile?.maritalStatus) setMaritalStatus((c) => c ?? title(profile.maritalStatus));
+ setEmail((c) => c || profile?.address?.email || user?.email || '');
+ setMobile((c) => c || profile?.address?.primaryPhoneNumber || user?.phoneNumber || '');
+ if (profile?.address?.idNumber) setNationalIdNumber((c) => c || profile.address.idNumber);
+ if (profile?.address?.nationality) setNationality((c) => c ?? profile.address.nationality);
+ if (profile?.address?.streetAddress) setPermanentAddress((c) => c || orEmpty(profile.address.streetAddress));
+ if (profile?.address?.emergencyContactName) setEmergencyName((c) => c || orEmpty(profile.address.emergencyContactName));
+ if (profile?.address?.emergencyContactPhone) setEmergencyPhone((c) => c || orEmpty(profile.address.emergencyContactPhone));
+ if (profile?.address?.emergencyContactRelation) setEmergencyRel((c) => c ?? profile.address.emergencyContactRelation);
+ }, [user, profile]);
+
+ // Registration writes the profile as SEAFARER with personal details, so a
+ // profile in that state is a submitted registration: show it read-only
+ // instead of an empty wizard. "Edit" reopens the wizard on the same data.
+ const registered = profile?.type === 'SEAFARER' && !!profile.firstName && !!profile.dob;
+ const [editing, setEditing] = useState(false);
+
+ const canNext = () => {
+ if (active === 0) return !!firstName.en.trim() && !!lastName.en.trim() && !!gender && !!dob && !!placeOfBirth && !!nationality && !!nationalIdNumber.trim();
+ if (active === 1) return !!mobile.trim() && !!email.trim() && !!locationId;
+ 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 () => {
+ if (!profileId) return;
+ setSubmitting(true);
+ try {
+ await updateProfile({
+ id: profileId,
+ body: {
+ type: 'SEAFARER',
+ firstName: firstName.en,
+ middleName: middleName.en || undefined,
+ lastName: lastName.en,
+ gender: gender?.toUpperCase() ?? 'MALE',
+ dob: dob?.toISOString().split('T')[0] ?? '',
+ pob: placeOfBirth || undefined,
+ maritalStatus: maritalStatus?.toUpperCase() ?? 'SINGLE',
+ },
+ }).unwrap();
+
+ await saveAddress({
+ profileId,
+ body: {
+ idType: 'NID',
+ idNumber: nationalIdNumber,
+ nationality: nationality ?? 'Ethiopian',
+ primaryPhoneNumber: mobile,
+ email: email || undefined,
+ website: null,
+ streetAddress: permanentAddress || undefined,
+ emergencyContactName: emergencyName || undefined,
+ emergencyContactPhone: emergencyPhone || undefined,
+ emergencyContactRelation: emergencyRel || undefined,
+ },
+ }).unwrap();
+
+ notify.success(`Registration submitted! Profile ID: ${profileId.slice(0, 8).toUpperCase()}`);
+ setEditing(false);
+ setActive(0);
+ setCompleted([]);
+ } catch {
+ notify.error('Submission failed. Please try again.');
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const review = (
+
+
+ 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}
+ )}
+
+
+ ))}
+
+
+
+ );
+
+ const stepLabel = STEPS[active]?.label ?? '';
+ const stepIcons = [IconUser, IconAddressBook, IconFileDescription, IconCircleCheck];
+ const StepIcon = stepIcons[active];
+
+ if (registered && !editing) {
+ const status = profile.seafarerStatus ?? 'PENDING';
+ return (
+
+
+
+
Seafarer Registration
+
+ {profile.seafarerNumber
+ ? `Seafarer ID ${profile.seafarerNumber}`
+ : 'Submitted — a Seafarer ID is issued once EMA approves your registration.'}
+
+
+
+
+ {status}
+
+ setEditing(true)}>Edit details
+
+
+ {review}
+
+ );
+ }
+
+ 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 && (
+
+
+
+
+
+
+
+
+ setPlaceOfBirth(e.currentTarget.value)} />
+
+
+
+
+
+
+ setNationalIdNumber(e.currentTarget.value)} />
+ setPassportNumber(e.currentTarget.value)} />
+ setPassportExpiry(e.currentTarget.value)} />
+
+
+ }>
+ 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)} />
+
+
+
+
+
+
+
+ )}
+
+ {/* ── 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 && review}
+
+ {/* Navigation buttons */}
+
+ navigate('/applications')}>
+ Cancel
+
+
+ {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/router.tsx b/apps/portal/src/app/router.tsx
index 77bea3f5f..5692b1187 100644
--- a/apps/portal/src/app/router.tsx
+++ b/apps/portal/src/app/router.tsx
@@ -27,6 +27,7 @@ import { RequireSeafarerProfile } from "./features/profile/components/RequireSea
import { OperationsOnboardingPage } from "./features/onboarding/pages/OperationsOnboardingPage";
import { ProfilePage } from "./features/profile/pages/ProfilePage";
import { SupportPage } from "./features/support/pages/SupportPage";
+import { SeafarerRegistrationPage } from "./features/seafarer/pages/SeafarerRegistrationPage";
import { MySeaRecordsPage } from "./features/seafarer/pages/MySeaRecordsPage";
import { ExamsPage } from "./features/exams/pages/ExamsPage";
@@ -152,13 +153,17 @@ export const router = createBrowserRouter([
},
// Seafarer
- // The standalone wizard is gone — registration is the config-driven
- // licensing flow like every other licence type, gated by
- // RequireSeafarerProfile + RequirePermission the same way
- // /licensing/:typeCode/apply already is.
{
path: "/seafarer-registration",
- element: ,
+ // No profile gate: this wizard asks for the personal, identity and
+ // contact details itself across its four steps, so sending the
+ // applicant to /profile first made the approved form unreachable —
+ // they were bounced out before ever seeing it.
+ element: (
+
+
+
+ ),
},
{
path: "/seafarer/records",