@@ -1,642 +0,0 @@
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 (
< Box mb = { 32 } >
< Group gap = { 0 } align = "center" wrap = "nowrap" >
{ STEPS . map ( ( step , i ) = > {
const isDone = completed . includes ( i ) ;
const isCurrent = active === i ;
return (
< Group key = { i } gap = { 0 } align = "center" style = { { flex : i < STEPS . length - 1 ? 1 : 'none' } } >
< Stack gap = { 4 } align = "center" style = { { minWidth : rem ( 40 ) } } >
< Box
style = { {
width : rem ( 40 ) ,
height : rem ( 40 ) ,
borderRadius : '50%' ,
display : 'flex' ,
alignItems : 'center' ,
justifyContent : 'center' ,
backgroundColor : isDone
? 'var(--mantine-color-blue-8)'
: isCurrent
? 'var(--mantine-color-blue-7)'
: 'var(--mantine-color-gray-1)' ,
border : isCurrent ? '2.5px solid var(--mantine-color-blue-5)' : '2px solid transparent' ,
boxShadow : isCurrent || isDone ? '0 2px 8px rgba(34, 139, 230, 0.2)' : 'none' ,
flexShrink : 0 ,
transition : 'all 0.2s ease' ,
} }
>
{ isDone ? (
< IconCheck size = { 18 } color = "white" stroke = { 2.5 } / >
) : (
< Text fw = { 700 } fz = "sm" c = { isCurrent ? 'white' : 'gray.5' } >
{ i + 1 }
< / Text >
) }
< / Box >
< Text
fz = "xs"
fw = { isCurrent ? 700 : 400 }
c = { isCurrent ? 'blue.7' : 'dimmed' }
style = { { whiteSpace : 'nowrap' } }
>
{ isDone ? ` ${ step . label } ✓ ` : step . label }
< / Text >
< / Stack >
{ i < STEPS . length - 1 && (
< Box
style = { {
flex : 1 ,
height : rem ( 2 ) ,
backgroundColor : isDone
? 'var(--mantine-color-blue-8)'
: 'var(--mantine-color-gray-2)' ,
marginBottom : rem ( 22 ) ,
} }
/ >
) }
< / Group >
) ;
} ) }
< / Group >
< / Box >
) ;
}
// ---------------------------------------------------------------------------
// Section heading
// ---------------------------------------------------------------------------
function SectionHead ( { title } : { title : string } ) {
return (
< >
< Divider mt = "md" mb = "xs" / >
< Text fw = { 600 } fz = "sm" tt = "uppercase" c = "gray.6" > { title } < / Text >
< / >
) ;
}
// ---------------------------------------------------------------------------
// Review row
// ---------------------------------------------------------------------------
function ReviewRow ( { label , value } : { label : string ; value : string } ) {
return (
< div >
< Text fz = "xs" c = "dimmed" tt = "uppercase" fw = { 600 } lh = { 1.2 } > { label } < / Text >
< Text fz = "sm" mt = { 2 } > { value || '—' } < / Text >
< / div >
) ;
}
// ---------------------------------------------------------------------------
// 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 (
< Card
withBorder
radius = "md"
p = "md"
style = { {
borderStyle : 'dashed' ,
borderColor : file
? 'var(--mantine-color-teal-5)'
: 'var(--mantine-color-default-border)' ,
} }
>
< Group gap = "sm" mb = "sm" wrap = "nowrap" >
< Box
style = { {
width : rem ( 44 ) ,
height : rem ( 44 ) ,
borderRadius : rem ( 8 ) ,
background : 'var(--mantine-color-blue-light)' ,
display : 'flex' ,
alignItems : 'center' ,
justifyContent : 'center' ,
flexShrink : 0 ,
} }
>
< SlotIcon size = { 22 } color = "var(--mantine-color-blue-6)" stroke = { 1.5 } / >
< / Box >
< div >
< Text fw = { 600 } fz = "sm" >
{ slot . label }
{ slot . required && < Text span c = "red" ml = { 3 } > * < / Text > }
< / Text >
< Text fz = "xs" c = "dimmed" > { slot . description } < / Text >
< / div >
< / Group >
{ file ? (
< Group gap = "xs" align = "center" >
< IconCircleCheck size = { 16 } color = "var(--mantine-color-teal-6)" / >
< Text fz = "xs" c = "teal.7" style = { { flex : 1 } } truncate > { file . name } < / Text >
< Button
size = "xs"
variant = "subtle"
color = "red"
onClick = { ( ) = > { onFile ( null ) ; resetRef . current ? . ( ) ; } }
>
Remove
< / Button >
< / Group >
) : (
< FileButton resetRef = { resetRef } onChange = { onFile } accept = "application/pdf,image/jpeg,image/png" >
{ ( props ) = > (
< Button size = "xs" variant = "default" { ...props } >
Choose File
< / Button >
) }
< / FileButton >
) }
< / Card >
) ;
}
// ---------------------------------------------------------------------------
// 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 < number [ ] > ( [ ] ) ;
const [ submitting , setSubmitting ] = useState ( false ) ;
const [ updateProfile ] = useUpdateMyProfileMutation ( ) ;
const [ saveAddress ] = useSaveMyAddressMutation ( ) ;
// Step 1 — Personal Information
const [ firstName , setFirstName ] = useState < BilingualValue > ( { en : '' , am : '' } ) ;
const [ middleName , setMiddleName ] = useState < BilingualValue > ( { en : '' , am : '' } ) ;
const [ lastName , setLastName ] = useState < BilingualValue > ( { en : '' , am : '' } ) ;
const [ gender , setGender ] = useState < string | null > ( null ) ;
const [ dob , setDob ] = useState < Date | null > ( null ) ;
const [ placeOfBirth , setPlaceOfBirth ] = useState ( '' ) ;
const [ nationality , setNationality ] = useState < string | null > ( 'Ethiopian' ) ;
const [ maritalStatus , setMaritalStatus ] = useState < string | null > ( 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 < string | null > ( null ) ;
const [ permanentAddress , setPermanentAddress ] = useState ( '' ) ;
const [ currentAddress , setCurrentAddress ] = useState ( '' ) ;
const [ emergencyName , setEmergencyName ] = useState ( '' ) ;
const [ emergencyRel , setEmergencyRel ] = useState < string | null > ( null ) ;
const [ emergencyPhone , setEmergencyPhone ] = useState ( '' ) ;
// Step 3 — Documents
const [ files , setFiles ] = useState < Record < string , File | null > > ( {
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 = (
< Stack gap = "md" >
< Paper withBorder radius = "md" p = "md" >
< Text fw = { 700 } fz = "sm" mb = "sm" > Personal Information < / Text >
< SimpleGrid cols = { { base : 1 , sm : 2 , lg : 3 } } spacing = "md" >
< ReviewRow label = "First Name" value = { ` ${ firstName . en } ${ firstName . am ? ` / ${ firstName . am } ` : '' } ` } / >
< ReviewRow label = "Middle Name" value = { ` ${ middleName . en } ${ middleName . am ? ` / ${ middleName . am } ` : '' } ` } / >
< ReviewRow label = "Last Name" value = { ` ${ lastName . en } ${ lastName . am ? ` / ${ lastName . am } ` : '' } ` } / >
< ReviewRow label = "Gender" value = { gender ? ? '' } / >
< ReviewRow label = "Date of Birth" value = { ` ${ dob ? . toLocaleDateString ( 'en-US' ) ? ? '' } ${ dob ? ` ( ${ toEthiopicDateLabel ( dob ) } ) ` : '' } ` } / >
< ReviewRow label = "Place of Birth" value = { placeOfBirth } / >
< ReviewRow label = "Nationality" value = { nationality ? ? '' } / >
< ReviewRow label = "Marital Status" value = { maritalStatus ? ? '' } / >
< ReviewRow label = "National ID No." value = { nationalIdNumber } / >
< ReviewRow label = "Passport No." value = { passportNumber } / >
< ReviewRow label = "Passport Expiry" value = { passportExpiry } / >
< / SimpleGrid >
< / Paper >
< Paper withBorder radius = "md" p = "md" >
< Text fw = { 700 } fz = "sm" mb = "sm" > Contact Details < / Text >
< SimpleGrid cols = { { base : 1 , sm : 2 , lg : 3 } } spacing = "md" >
< ReviewRow label = "Mobile" value = { mobile } / >
< ReviewRow label = "Email" value = { email } / >
< ReviewRow label = "Location" value = { locationId ? ? '' } / >
< ReviewRow label = "Permanent Address" value = { permanentAddress } / >
< ReviewRow label = "Current Address" value = { currentAddress } / >
< / SimpleGrid >
{ emergencyName && (
< >
< Divider mt = "md" mb = "sm" / >
< Text fw = { 600 } fz = "sm" mb = "sm" > Emergency Contact < / Text >
< SimpleGrid cols = { { base : 1 , sm : 2 , lg : 3 } } spacing = "md" >
< ReviewRow label = "Name" value = { emergencyName } / >
< ReviewRow label = "Relationship" value = { emergencyRel ? ? '' } / >
< ReviewRow label = "Phone" value = { emergencyPhone } / >
< / SimpleGrid >
< / >
) }
< / Paper >
< Paper withBorder radius = "md" p = "md" >
< Text fw = { 700 } fz = "sm" mb = "sm" > Documents < / Text >
< SimpleGrid cols = { { base : 1 , sm : 2 , lg : 4 } } spacing = "md" >
{ DOC_SLOTS . map ( ( slot ) = > (
< Group key = { slot . key } gap = "xs" align = "center" >
{ files [ slot . key ] ? (
< IconCircleCheck size = { 18 } color = "var(--mantine-color-teal-6)" / >
) : (
< Box style = { { width : 18 , height : 18 , borderRadius : '50%' , border : '2px solid var(--mantine-color-gray-3)' , flexShrink : 0 } } / >
) }
< div >
< Text fz = "sm" c = { files [ slot . key ] ? undefined : 'dimmed' } >
{ slot . label }
{ slot . required && ! files [ slot . key ] && < Text span c = "red" ml = { 3 } > * < / Text > }
< / Text >
{ files [ slot . key ] && (
< Text fz = "xs" c = "dimmed" truncate maw = { 160 } > { files [ slot . key ] ! . name } < / Text >
) }
< / div >
< / Group >
) ) }
< / SimpleGrid >
< / Paper >
< / Stack >
) ;
const stepLabel = STEPS [ active ] ? . label ? ? '' ;
const stepIcons = [ IconUser , IconAddressBook , IconFileDescription , IconCircleCheck ] ;
const StepIcon = stepIcons [ active ] ;
if ( registered && ! editing ) {
const status = profile . seafarerStatus ? ? 'PENDING' ;
return (
< Stack gap = "md" >
< Group justify = "space-between" align = "flex-start" >
< div >
< Title order = { 3 } > Seafarer Registration < / Title >
< Text fz = "sm" c = "dimmed" >
{ profile . seafarerNumber
? ` Seafarer ID ${ profile . seafarerNumber } `
: 'Submitted — a Seafarer ID is issued once EMA approves your registration.' }
< / Text >
< / div >
< Group gap = "sm" >
< Badge variant = "light" color = { status === 'ACTIVE' ? 'teal' : status === 'PENDING' ? 'yellow' : 'red' } >
{ status }
< / Badge >
< Button variant = "default" onClick = { ( ) = > setEditing ( true ) } > Edit details < / Button >
< / Group >
< / Group >
{ review }
< / Stack >
) ;
}
return (
< Stack gap = "md" >
{ /* Page header */ }
< div >
< Title order = { 3 } > New Seafarer Registration < / Title >
< Text fz = "sm" c = "dimmed" > Register a new seafarer profile — Step { active + 1 } of { STEPS . length } < / Text >
< / div >
{ /* Step indicator */ }
< StepIndicator active = { active } completed = { completed } / >
{ /* Card */ }
< Paper withBorder radius = "lg" p = "xl" >
{ /* Card header */ }
< Group justify = "space-between" mb = "lg" >
< Group gap = "xs" >
< StepIcon size = { 20 } stroke = { 1.6 } / >
< Text fw = { 700 } fz = "lg" > { stepLabel } < / Text >
< / Group >
< Badge variant = "light" color = "blue" radius = "md" > Step { active + 1 } of { STEPS . length } < / Badge >
< / Group >
{ /* ── Step 1: Personal Information ───────────────────────────── */ }
{ active === 0 && (
< Stack gap = "md" >
< SectionHead title = "Identity Details" / >
< SimpleGrid cols = { { base : 1 , sm : 2 , lg : 3 } } spacing = "md" >
< BilingualInput label = "First Name" required value = { firstName } onChange = { setFirstName } / >
< BilingualInput label = "Middle Name" value = { middleName } onChange = { setMiddleName } / >
< BilingualInput label = "Last Name" required value = { lastName } onChange = { setLastName } / >
< Select label = "Gender" placeholder = "Select" required data = { GENDERS } value = { gender } onChange = { setGender } / >
< AmharicDatePicker label = "Date of Birth" required value = { dob } onChange = { setDob } / >
< TextInput label = "Place of Birth" placeholder = "City, Region" required value = { placeOfBirth } onChange = { ( e ) = > setPlaceOfBirth ( e . currentTarget . value ) } / >
< Select label = "Nationality" required data = { NATIONALITIES } value = { nationality } onChange = { setNationality } searchable / >
< Select label = "Marital Status" placeholder = "Select" data = { MARITAL_STATUSES } value = { maritalStatus } onChange = { setMaritalStatus } / >
< / SimpleGrid >
< SectionHead title = "Identity Documents" / >
< SimpleGrid cols = { { base : 1 , sm : 2 , lg : 3 } } spacing = "md" >
< TextInput label = "National ID Number" placeholder = "ET-000000" required value = { nationalIdNumber } onChange = { ( e ) = > setNationalIdNumber ( e . currentTarget . value ) } / >
< TextInput label = "Passport Number" placeholder = "EP000000" value = { passportNumber } onChange = { ( e ) = > setPassportNumber ( e . currentTarget . value ) } / >
< TextInput label = "Passport Expiry Date" type = "date" value = { passportExpiry } onChange = { ( e ) = > setPassportExpiry ( e . currentTarget . value ) } / >
< / SimpleGrid >
< Alert variant = "light" color = "blue" icon = { < IconInfoCircle size = { 17 } / > } >
A unique Seafarer ID will be automatically generated upon approval of this registration .
< / Alert >
< / Stack >
) }
{ /* ── Step 2: Contact Details ─────────────────────────────────── */ }
{ active === 1 && (
< Stack gap = "md" >
< SectionHead title = "Contact Information" / >
< SimpleGrid cols = { { base : 1 , sm : 2 , lg : 3 } } spacing = "md" >
< TextInput label = "Mobile Number" placeholder = "+251 9XX XXX XXX" required value = { mobile } onChange = { ( e ) = > setMobile ( e . currentTarget . value ) } / >
< TextInput label = "Email Address" placeholder = "email@example.com" type = "email" required value = { email } onChange = { ( e ) = > setEmail ( e . currentTarget . value ) } / >
< / SimpleGrid >
< SectionHead title = "Location" / >
< LocationPicker
value = { locationId ? ? undefined }
onChange = { setLocationId }
required
/ >
< SectionHead title = "Address" / >
< Textarea label = "Permanent Address" placeholder = "Full permanent address" autosize minRows = { 2 } value = { permanentAddress } onChange = { ( e ) = > setPermanentAddress ( e . currentTarget . value ) } / >
< Textarea
label = { < > < Text span fz = "sm" fw = { 500 } > Current Address < / Text > < Text span fz = "xs" c = "dimmed" ml = { 6 } > ( If different from permanent ) < / Text > < / > }
placeholder = "Full current address"
autosize
minRows = { 2 }
value = { currentAddress }
onChange = { ( e ) = > setCurrentAddress ( e . currentTarget . value ) }
/ >
< SectionHead title = "Emergency Contact" / >
< SimpleGrid cols = { { base : 1 , sm : 2 , lg : 3 } } spacing = "md" >
< TextInput label = "Contact Name" placeholder = "Full name" value = { emergencyName } onChange = { ( e ) = > setEmergencyName ( e . currentTarget . value ) } / >
< Select label = "Relationship" placeholder = "Select" data = { RELATIONSHIPS } value = { emergencyRel } onChange = { setEmergencyRel } / >
< TextInput label = "Phone Number" placeholder = "+251 9XX XXX XXX" value = { emergencyPhone } onChange = { ( e ) = > setEmergencyPhone ( e . currentTarget . value ) } / >
< / SimpleGrid >
< / Stack >
) }
{ /* ── Step 3: Documents Upload ────────────────────────────────── */ }
{ active === 2 && (
< Stack gap = "md" >
< Alert variant = "light" color = "yellow" icon = { < IconAlertTriangle size = { 17 } / > } >
Please upload clear , readable copies of all required documents . Accepted formats : PDF , JPG , PNG ( max 5 MB each ) . Items marked with * are mandatory .
< / Alert >
< SimpleGrid cols = { { base : 1 , sm : 2 , lg : 4 } } spacing = "md" >
{ DOC_SLOTS . map ( ( slot ) = > (
< DocCard
key = { slot . key }
slot = { slot }
file = { files [ slot . key ] }
onFile = { setFile ( slot . key ) }
/ >
) ) }
< / SimpleGrid >
< Paper withBorder radius = "md" p = "md" bg = "gray.0" >
< Text fw = { 600 } fz = "sm" mb = "sm" > Upload Progress < / Text >
< SimpleGrid cols = { { base : 2 , sm : 4 } } spacing = "sm" >
{ DOC_SLOTS . map ( ( slot ) = > (
< Group key = { slot . key } gap = { 6 } align = "center" >
{ files [ slot . key ] ? (
< IconCircleCheck size = { 16 } color = "var(--mantine-color-teal-6)" / >
) : (
< Box style = { { width : 16 , height : 16 , borderRadius : '50%' , border : '2px solid var(--mantine-color-gray-3)' } } / >
) }
< Text fz = "xs" c = { files [ slot . key ] ? 'teal.7' : 'dimmed' } fw = { files [ slot . key ] ? 600 : 400 } >
{ slot . key === 'nationalId' ? 'National ID' : slot . key === 'passport' ? 'Passport' : slot . key === 'graduation' ? 'Certificate' : 'Photo' }
< / Text >
< / Group >
) ) }
< / SimpleGrid >
< / Paper >
< / Stack >
) }
{ /* ── Step 4: Review & Submit ─────────────────────────────────── */ }
{ active === 3 && review }
{ /* Navigation buttons */ }
< Group justify = "space-between" mt = "xl" >
< Button variant = "default" onClick = { ( ) = > navigate ( '/applications' ) } >
Cancel
< / Button >
< Group gap = "sm" >
{ active > 0 && (
< Button variant = "default" leftSection = { < IconArrowLeft size = { 16 } / > } onClick = { prev } >
Previous
< / Button >
) }
{ active < STEPS . length - 1 ? (
< Button
rightSection = { < IconArrowRight size = { 16 } / > }
onClick = { next }
disabled = { ! canNext ( ) }
>
Next Step
< / Button >
) : (
< Button
color = "blue"
leftSection = { < IconCircleCheck size = { 16 } / > }
onClick = { handleSubmit }
loading = { submitting }
>
Submit Registration
< / Button >
) }
< / Group >
< / Group >
< / Paper >
< / Stack >
) ;
}