mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
Implement nationality-based document requirements and add cancellation feature for seafarer registration
This commit is contained in:
@@ -3,6 +3,7 @@ import { Alert, Badge, Button, Card, FileButton, Group, Loader, Stack, Text } fr
|
|||||||
import { IconAlertTriangle, IconCheck, IconFileUpload } from '@tabler/icons-react';
|
import { IconAlertTriangle, IconCheck, IconFileUpload } from '@tabler/icons-react';
|
||||||
import {
|
import {
|
||||||
SEAFARER_REGISTRATION_DOCUMENTS,
|
SEAFARER_REGISTRATION_DOCUMENTS,
|
||||||
|
isEthiopianNationality,
|
||||||
uploadDocument,
|
uploadDocument,
|
||||||
type Attachment,
|
type Attachment,
|
||||||
} from '@ema-platform/api';
|
} from '@ema-platform/api';
|
||||||
@@ -10,22 +11,25 @@ import {
|
|||||||
const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
|
const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
|
||||||
|
|
||||||
/** The document slots a registration asks for. */
|
/** The document slots a registration asks for. */
|
||||||
export function documentSlots(passportDeclared: boolean) {
|
export function documentSlots(passportDeclared: boolean, nationality?: string | null) {
|
||||||
|
const ethiopian = isEthiopianNationality(nationality);
|
||||||
return SEAFARER_REGISTRATION_DOCUMENTS.map((d) => ({
|
return SEAFARER_REGISTRATION_DOCUMENTS.map((d) => ({
|
||||||
...d,
|
...d,
|
||||||
isRequired: d.required === 'passport' ? passportDeclared : d.required,
|
isRequired: d.required === 'passport' ? passportDeclared : d.required === 'ethiopian' ? ethiopian : d.required,
|
||||||
})).filter((d) => d.required !== 'passport' || passportDeclared);
|
})).filter((d) => (d.required !== 'passport' || passportDeclared) && (d.required !== 'ethiopian' || ethiopian));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RegistrationDocuments({
|
export function RegistrationDocuments({
|
||||||
registrationId,
|
registrationId,
|
||||||
passportDeclared,
|
passportDeclared,
|
||||||
|
nationality,
|
||||||
attachments,
|
attachments,
|
||||||
readOnly,
|
readOnly,
|
||||||
onUploaded,
|
onUploaded,
|
||||||
}: {
|
}: {
|
||||||
registrationId: string;
|
registrationId: string;
|
||||||
passportDeclared: boolean;
|
passportDeclared: boolean;
|
||||||
|
nationality?: string | null;
|
||||||
attachments: Attachment[];
|
attachments: Attachment[];
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
onUploaded: () => void;
|
onUploaded: () => void;
|
||||||
@@ -62,7 +66,7 @@ export function RegistrationDocuments({
|
|||||||
{error}
|
{error}
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
{documentSlots(passportDeclared).map((slot) => {
|
{documentSlots(passportDeclared, nationality).map((slot) => {
|
||||||
const existing = attachments.find((a) => a.documentKey === slot.key);
|
const existing = attachments.find((a) => a.documentKey === slot.key);
|
||||||
const uploaded = Boolean(existing?.files?.length);
|
const uploaded = Boolean(existing?.files?.length);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
GENDER_OPTIONS,
|
GENDER_OPTIONS,
|
||||||
HAIR_COLOR_OPTIONS,
|
HAIR_COLOR_OPTIONS,
|
||||||
MARITAL_STATUS_OPTIONS,
|
MARITAL_STATUS_OPTIONS,
|
||||||
|
isEthiopianNationality,
|
||||||
useGetActiveDepartmentsQuery,
|
useGetActiveDepartmentsQuery,
|
||||||
useLocalized,
|
useLocalized,
|
||||||
} from '@ema-platform/api';
|
} from '@ema-platform/api';
|
||||||
@@ -44,6 +45,7 @@ function SectionTitle({ title, description }: { title: string; description?: str
|
|||||||
export function IdentityDetailsStep(
|
export function IdentityDetailsStep(
|
||||||
p: StepProps & { account: { email?: string; phoneNumber?: string } },
|
p: StepProps & { account: { email?: string; phoneNumber?: string } },
|
||||||
) {
|
) {
|
||||||
|
const ethiopian = isEthiopianNationality(p.form.nationality);
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<SectionTitle title="Contact Details" />
|
<SectionTitle title="Contact Details" />
|
||||||
@@ -68,7 +70,18 @@ export function IdentityDetailsStep(
|
|||||||
<DateField {...p} name="dateOfBirth" label="Date of Birth" required />
|
<DateField {...p} name="dateOfBirth" label="Date of Birth" required />
|
||||||
<SelectField {...p} name="maritalStatus" label="Marital Status" required options={MARITAL_STATUS_OPTIONS} />
|
<SelectField {...p} name="maritalStatus" label="Marital Status" required options={MARITAL_STATUS_OPTIONS} />
|
||||||
<NationalityField {...p} name="nationality" label="Nationality" required />
|
<NationalityField {...p} name="nationality" label="Nationality" required />
|
||||||
<TextField {...p} name="nationalIdNumber" label="National ID (Fayda) Number" required maxLength={64} />
|
{ethiopian ? (
|
||||||
|
<TextField {...p} name="nationalIdNumber" label="National ID (Fayda) Number" required maxLength={64} />
|
||||||
|
) : (
|
||||||
|
<TextField
|
||||||
|
{...p}
|
||||||
|
name="passportNumber"
|
||||||
|
label="Passport Number"
|
||||||
|
required
|
||||||
|
maxLength={32}
|
||||||
|
description="Required for non-Ethiopian applicants in place of a National ID."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
@@ -88,19 +101,24 @@ export function ApplicantDetailsStep(p: StepProps) {
|
|||||||
const departmentOptions =
|
const departmentOptions =
|
||||||
departments?.map((d) => ({ value: d.code, label: localized(d.name) })) ??
|
departments?.map((d) => ({ value: d.code, label: localized(d.name) })) ??
|
||||||
DEPARTMENT_OPTIONS;
|
DEPARTMENT_OPTIONS;
|
||||||
|
// Non-Ethiopians already declare their passport number as their primary ID
|
||||||
|
// on the Identity step — asking again here would just duplicate the field.
|
||||||
|
const ethiopian = isEthiopianNationality(p.form.nationality);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<SectionTitle title="Identity" />
|
<SectionTitle title="Identity" />
|
||||||
<Grid>
|
<Grid>
|
||||||
<TextField {...p} name="placeOfBirth" label="Place of Birth" required maxLength={128} />
|
<TextField {...p} name="placeOfBirth" label="Place of Birth" required maxLength={128} />
|
||||||
<TextField
|
{ethiopian && (
|
||||||
{...p}
|
<TextField
|
||||||
name="passportNumber"
|
{...p}
|
||||||
label="Passport Number"
|
name="passportNumber"
|
||||||
maxLength={32}
|
label="Passport Number"
|
||||||
description="Required later for international sea service; optional at registration."
|
maxLength={32}
|
||||||
/>
|
description="Required later for international sea service; optional at registration."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{p.form.passportNumber && (
|
{p.form.passportNumber && (
|
||||||
<DateField {...p} name="passportExpiry" label="Passport Expiry Date" />
|
<DateField {...p} name="passportExpiry" label="Passport Expiry Date" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
@@ -8,13 +9,14 @@ import {
|
|||||||
Grid,
|
Grid,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
|
Modal,
|
||||||
Paper,
|
Paper,
|
||||||
Stack,
|
Stack,
|
||||||
Stepper,
|
Stepper,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { IconAlertTriangle, IconCheck, IconInfoCircle, IconPencil } from '@tabler/icons-react';
|
import { IconAlertTriangle, IconCheck, IconInfoCircle, IconPencil, IconTrash } from '@tabler/icons-react';
|
||||||
import { notifications } from '@mantine/notifications';
|
import { notifications } from '@mantine/notifications';
|
||||||
import {
|
import {
|
||||||
PHYSICAL_BOUNDS,
|
PHYSICAL_BOUNDS,
|
||||||
@@ -23,6 +25,8 @@ import {
|
|||||||
SEAFARER_REGISTRATION_STATUS_LABELS,
|
SEAFARER_REGISTRATION_STATUS_LABELS,
|
||||||
extractErrorMessage,
|
extractErrorMessage,
|
||||||
extractValidationIssues,
|
extractValidationIssues,
|
||||||
|
isEthiopianNationality,
|
||||||
|
useCancelSeafarerRegistrationMutation,
|
||||||
useGetAttachmentsQuery,
|
useGetAttachmentsQuery,
|
||||||
useGetMySeafarerRegistrationQuery,
|
useGetMySeafarerRegistrationQuery,
|
||||||
useSaveSeafarerRegistrationMutation,
|
useSaveSeafarerRegistrationMutation,
|
||||||
@@ -48,15 +52,26 @@ const STEPS = [
|
|||||||
{ label: 'Review', description: 'Check & submit' },
|
{ label: 'Review', description: 'Check & submit' },
|
||||||
];
|
];
|
||||||
|
|
||||||
/** Which answers each step must have before "Continue" — mirrors the API's submission check. */
|
/**
|
||||||
|
* Which answers each step must have before "Continue" — mirrors the API's
|
||||||
|
* submission check. National ID vs Passport Number depends on the declared
|
||||||
|
* nationality, so that slot is added dynamically in `requiredForStep`.
|
||||||
|
*/
|
||||||
const REQUIRED_BY_STEP: AnswerKey[][] = [
|
const REQUIRED_BY_STEP: AnswerKey[][] = [
|
||||||
['firstName', 'lastName', 'gender', 'dateOfBirth', 'maritalStatus', 'nationality', 'nationalIdNumber'],
|
['firstName', 'lastName', 'gender', 'dateOfBirth', 'maritalStatus', 'nationality'],
|
||||||
['placeOfBirth', 'department', 'locationId', 'hairColor', 'eyeColor', 'heightCm', 'weightKg'],
|
['placeOfBirth', 'department', 'locationId', 'hairColor', 'eyeColor', 'heightCm', 'weightKg'],
|
||||||
['medicalCertificateNumber', 'medicalIssuerName', 'medicalIssueDate'],
|
['medicalCertificateNumber', 'medicalIssuerName', 'medicalIssueDate'],
|
||||||
[],
|
[],
|
||||||
['declarationAccepted'],
|
['declarationAccepted'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** Ethiopians must give a National ID; everyone else must give a Passport Number instead. */
|
||||||
|
function requiredForStep(index: number, nationality: string | null | undefined): AnswerKey[] {
|
||||||
|
const base = REQUIRED_BY_STEP[index] ?? [];
|
||||||
|
if (index !== 0) return base;
|
||||||
|
return [...base, isEthiopianNationality(nationality) ? 'nationalIdNumber' : 'passportNumber'];
|
||||||
|
}
|
||||||
|
|
||||||
const ANSWER_KEYS = Object.keys(SEAFARER_REGISTRATION_FIELD_LABELS) as AnswerKey[];
|
const ANSWER_KEYS = Object.keys(SEAFARER_REGISTRATION_FIELD_LABELS) as AnswerKey[];
|
||||||
|
|
||||||
function answersOf(registration: SeafarerRegistration): SaveSeafarerRegistration {
|
function answersOf(registration: SeafarerRegistration): SaveSeafarerRegistration {
|
||||||
@@ -119,15 +134,18 @@ function withProfileDefaults(
|
|||||||
* still missing. A submitted registration opens to a read-only summary.
|
* still missing. A submitted registration opens to a read-only summary.
|
||||||
*/
|
*/
|
||||||
export function SeafarerRegistrationPage() {
|
export function SeafarerRegistrationPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
const accountUser = useAppSelector((state) => state.auth.user);
|
const accountUser = useAppSelector((state) => state.auth.user);
|
||||||
const { profile } = useCurrentProfile();
|
const { profile } = useCurrentProfile();
|
||||||
const { data, isLoading } = useGetMySeafarerRegistrationQuery();
|
const { data, isLoading } = useGetMySeafarerRegistrationQuery();
|
||||||
const registration = data?.registration ?? null;
|
const registration = data?.registration ?? null;
|
||||||
|
|
||||||
const [start] = useStartSeafarerRegistrationMutation();
|
const [start] = useStartSeafarerRegistrationMutation();
|
||||||
|
const [cancelDraft, { isLoading: cancelling }] = useCancelSeafarerRegistrationMutation();
|
||||||
const [save, { isLoading: saving }] = useSaveSeafarerRegistrationMutation();
|
const [save, { isLoading: saving }] = useSaveSeafarerRegistrationMutation();
|
||||||
const [submit, { isLoading: submitting }] = useSubmitSeafarerRegistrationMutation();
|
const [submit, { isLoading: submitting }] = useSubmitSeafarerRegistrationMutation();
|
||||||
const [startError, setStartError] = useState<string | null>(null);
|
const [startError, setStartError] = useState<string | null>(null);
|
||||||
|
const [confirmingCancel, setConfirmingCancel] = useState(false);
|
||||||
const started = useRef(false);
|
const started = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -204,7 +222,7 @@ export function SeafarerRegistrationPage() {
|
|||||||
|
|
||||||
function validateStep(index: number): boolean {
|
function validateStep(index: number): boolean {
|
||||||
const found: Partial<Record<AnswerKey, string>> = {};
|
const found: Partial<Record<AnswerKey, string>> = {};
|
||||||
for (const key of REQUIRED_BY_STEP[index] ?? []) {
|
for (const key of requiredForStep(index, form.nationality)) {
|
||||||
if (blank(form[key])) found[key] = `${SEAFARER_REGISTRATION_FIELD_LABELS[key]} is required.`;
|
if (blank(form[key])) found[key] = `${SEAFARER_REGISTRATION_FIELD_LABELS[key]} is required.`;
|
||||||
}
|
}
|
||||||
if (index === 1) {
|
if (index === 1) {
|
||||||
@@ -232,7 +250,7 @@ export function SeafarerRegistrationPage() {
|
|||||||
}
|
}
|
||||||
if (index === 3) {
|
if (index === 3) {
|
||||||
const supplied = new Set(attachments.filter((a) => a.files?.length).map((a) => a.documentKey));
|
const supplied = new Set(attachments.filter((a) => a.files?.length).map((a) => a.documentKey));
|
||||||
const missing = documentSlots(Boolean(form.passportNumber))
|
const missing = documentSlots(Boolean(form.passportNumber), form.nationality)
|
||||||
.filter((d) => d.isRequired && !supplied.has(d.key))
|
.filter((d) => d.isRequired && !supplied.has(d.key))
|
||||||
.map((d) => d.name);
|
.map((d) => d.name);
|
||||||
if (missing.length) {
|
if (missing.length) {
|
||||||
@@ -302,6 +320,19 @@ export function SeafarerRegistrationPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleCancel() {
|
||||||
|
if (!registration) return;
|
||||||
|
try {
|
||||||
|
await cancelDraft(registration.id).unwrap();
|
||||||
|
notifications.show({ color: 'teal', title: 'Draft discarded', message: 'Nothing was saved.' });
|
||||||
|
navigate('/dashboard');
|
||||||
|
} catch (err) {
|
||||||
|
notifications.show({ color: 'red', title: 'Could not discard the draft', message: extractErrorMessage(err) });
|
||||||
|
} finally {
|
||||||
|
setConfirmingCancel(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const stepProps = { form, set, errors, disabled: readOnly };
|
const stepProps = { form, set, errors, disabled: readOnly };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -319,11 +350,24 @@ export function SeafarerRegistrationPage() {
|
|||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
</div>
|
</div>
|
||||||
{showSummary && !readOnly && (
|
<Group gap="xs">
|
||||||
<Button size="xs" variant="default" leftSection={<IconPencil size={14} />} onClick={() => setViewingSummary(false)}>
|
{registration.status === 'DRAFT' && (
|
||||||
Edit details
|
<Button
|
||||||
</Button>
|
size="xs"
|
||||||
)}
|
variant="subtle"
|
||||||
|
color="red"
|
||||||
|
leftSection={<IconTrash size={14} />}
|
||||||
|
onClick={() => setConfirmingCancel(true)}
|
||||||
|
>
|
||||||
|
Cancel & discard draft
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{showSummary && !readOnly && (
|
||||||
|
<Button size="xs" variant="default" leftSection={<IconPencil size={14} />} onClick={() => setViewingSummary(false)}>
|
||||||
|
Edit details
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{registration.status === 'APPROVED' && (
|
{registration.status === 'APPROVED' && (
|
||||||
@@ -386,6 +430,7 @@ export function SeafarerRegistrationPage() {
|
|||||||
<RegistrationDocuments
|
<RegistrationDocuments
|
||||||
registrationId={registration.id}
|
registrationId={registration.id}
|
||||||
passportDeclared={Boolean(form.passportNumber)}
|
passportDeclared={Boolean(form.passportNumber)}
|
||||||
|
nationality={form.nationality}
|
||||||
attachments={attachments}
|
attachments={attachments}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
onUploaded={refetchAttachments}
|
onUploaded={refetchAttachments}
|
||||||
@@ -426,6 +471,23 @@ export function SeafarerRegistrationPage() {
|
|||||||
</Group>
|
</Group>
|
||||||
</Paper>
|
</Paper>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<Modal opened={confirmingCancel} onClose={() => setConfirmingCancel(false)} title="Discard this draft?" centered>
|
||||||
|
<Stack>
|
||||||
|
<Text size="sm">
|
||||||
|
Everything you have entered will be deleted, including any documents already uploaded. This cannot be
|
||||||
|
undone. You can start a new registration at any time.
|
||||||
|
</Text>
|
||||||
|
<Group justify="flex-end">
|
||||||
|
<Button variant="default" onClick={() => setConfirmingCancel(false)} disabled={cancelling}>
|
||||||
|
Keep draft
|
||||||
|
</Button>
|
||||||
|
<Button color="red" loading={cancelling} onClick={handleCancel}>
|
||||||
|
Discard draft
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -298,6 +298,22 @@ function SeaServiceTab() {
|
|||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// Shown once the field has something in it — a blank required field is left
|
||||||
|
// to the button being disabled, same as the rest of the form; only an
|
||||||
|
// actual too-short value gets called out.
|
||||||
|
const vesselNameError =
|
||||||
|
form.vesselName && form.vesselName.trim().length <= 1
|
||||||
|
? t('seaRecords.seaService.fields.vesselNameTooShort', {
|
||||||
|
defaultValue: 'Must be at least 2 characters.',
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
const rankError =
|
||||||
|
form.rank && form.rank.trim().length <= 1
|
||||||
|
? t('seaRecords.seaService.fields.rankTooShort', {
|
||||||
|
defaultValue: 'Must be at least 2 characters.',
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
const valid =
|
const valid =
|
||||||
form.vesselName.trim().length > 1 &&
|
form.vesselName.trim().length > 1 &&
|
||||||
form.rank.trim().length > 1 &&
|
form.rank.trim().length > 1 &&
|
||||||
@@ -391,6 +407,7 @@ function SeaServiceTab() {
|
|||||||
required
|
required
|
||||||
value={form.vesselName}
|
value={form.vesselName}
|
||||||
onChange={(e) => setForm({ ...form, vesselName: e.target.value })}
|
onChange={(e) => setForm({ ...form, vesselName: e.target.value })}
|
||||||
|
error={vesselNameError}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t('seaRecords.seaService.fields.imoNumber')}
|
label={t('seaRecords.seaService.fields.imoNumber')}
|
||||||
@@ -418,9 +435,13 @@ function SeaServiceTab() {
|
|||||||
</Group>
|
</Group>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={t('seaRecords.seaService.fields.rank')}
|
label={t('seaRecords.seaService.fields.rank')}
|
||||||
|
description={t('seaRecords.seaService.fields.rankHint', {
|
||||||
|
defaultValue: 'Must be at least 2 characters, e.g. "AB" or "2nd Officer".',
|
||||||
|
})}
|
||||||
required
|
required
|
||||||
value={form.rank}
|
value={form.rank}
|
||||||
onChange={(e) => setForm({ ...form, rank: e.target.value })}
|
onChange={(e) => setForm({ ...form, rank: e.target.value })}
|
||||||
|
error={rankError}
|
||||||
/>
|
/>
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<AmharicDatePicker
|
<AmharicDatePicker
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ export const seafarerRegistrationApi = baseApi
|
|||||||
invalidatesTags: (_r, error) => (error ? [] : [LIST]),
|
invalidatesTags: (_r, error) => (error ? [] : [LIST]),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
cancelSeafarerRegistration: builder.mutation<void, string>({
|
||||||
|
query: (id) => ({ url: `/seafarer-registrations/${id}`, method: 'DELETE' }),
|
||||||
|
invalidatesTags: (_r, error, id) => (error ? [] : [LIST, item(id)]),
|
||||||
|
}),
|
||||||
|
|
||||||
saveSeafarerRegistration: builder.mutation<
|
saveSeafarerRegistration: builder.mutation<
|
||||||
SeafarerRegistration,
|
SeafarerRegistration,
|
||||||
{ id: string; body: SaveSeafarerRegistration }
|
{ id: string; body: SaveSeafarerRegistration }
|
||||||
@@ -108,6 +113,7 @@ export const seafarerRegistrationApi = baseApi
|
|||||||
export const {
|
export const {
|
||||||
useGetMySeafarerRegistrationQuery,
|
useGetMySeafarerRegistrationQuery,
|
||||||
useStartSeafarerRegistrationMutation,
|
useStartSeafarerRegistrationMutation,
|
||||||
|
useCancelSeafarerRegistrationMutation,
|
||||||
useSaveSeafarerRegistrationMutation,
|
useSaveSeafarerRegistrationMutation,
|
||||||
useSubmitSeafarerRegistrationMutation,
|
useSubmitSeafarerRegistrationMutation,
|
||||||
useListSeafarerRegistrationsQuery,
|
useListSeafarerRegistrationsQuery,
|
||||||
|
|||||||
@@ -64,13 +64,28 @@ export const PHYSICAL_BOUNDS = {
|
|||||||
weightKg: { min: 30, max: 250 },
|
weightKg: { min: 30, max: 250 },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How the platform spells Ethiopia in the `nationality` free-text field
|
||||||
|
* (CountrySelect's country name — matches the API's ETHIOPIAN_NATIONALITY).
|
||||||
|
*/
|
||||||
|
export const ETHIOPIAN_NATIONALITY = 'Ethiopia';
|
||||||
|
|
||||||
|
/** Whether a declared nationality is Ethiopian — drives National ID vs Passport requirements. */
|
||||||
|
export function isEthiopianNationality(nationality: string | null | undefined): boolean {
|
||||||
|
return nationality === ETHIOPIAN_NATIONALITY;
|
||||||
|
}
|
||||||
|
|
||||||
/** Upload slots, keyed as the API's submission check expects them. */
|
/** Upload slots, keyed as the API's submission check expects them. */
|
||||||
export const SEAFARER_REGISTRATION_DOCUMENTS: {
|
export const SEAFARER_REGISTRATION_DOCUMENTS: {
|
||||||
key: string;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
/** `'passport'`: required only once a passport number is declared. */
|
/**
|
||||||
required: boolean | 'passport';
|
* `'passport'`: required once a passport number is declared (always true
|
||||||
|
* for non-Ethiopians, who must declare one). `'ethiopian'`: required only
|
||||||
|
* for applicants who declared Ethiopian nationality.
|
||||||
|
*/
|
||||||
|
required: boolean | 'passport' | 'ethiopian';
|
||||||
accept?: string;
|
accept?: string;
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
@@ -80,7 +95,7 @@ export const SEAFARER_REGISTRATION_DOCUMENTS: {
|
|||||||
required: true,
|
required: true,
|
||||||
accept: 'image/jpeg,image/png',
|
accept: 'image/jpeg,image/png',
|
||||||
},
|
},
|
||||||
{ key: 'nationalId', name: 'National ID (Fayda) or Kebele ID', required: true },
|
{ key: 'nationalId', name: 'National ID (Fayda) or Kebele ID', required: 'ethiopian' },
|
||||||
{ key: 'passport', name: 'Passport Copy', required: 'passport' },
|
{ key: 'passport', name: 'Passport Copy', required: 'passport' },
|
||||||
{ key: 'graduation', name: 'Educational Certificate', required: false },
|
{ key: 'graduation', name: 'Educational Certificate', required: false },
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user