Files
emaui/libs/api/src/lib/features/seafarer-registration/seafarer-registration.constants.ts

247 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type {
SeafarerRegistrationAnswers,
SeafarerRegistrationStatus,
} from './seafarer-registration.types';
/**
* Option lists and labels shared by the portal wizard and the backoffice
* review screen. Values are the server's enum members.
*/
export const GENDER_OPTIONS = [
{ value: 'MALE', label: 'Male' },
{ value: 'FEMALE', label: 'Female' },
];
export const MARITAL_STATUS_OPTIONS = [
{ value: 'SINGLE', label: 'Single' },
{ value: 'MARRIED', label: 'Married' },
{ value: 'DIVORCED', label: 'Divorced' },
{ value: 'WIDOWED', label: 'Widowed' },
];
export const DEPARTMENT_OPTIONS = [
{ value: 'DECK', label: 'Deck' },
{ value: 'ENGINE', label: 'Engine' },
{ value: 'CATERING', label: 'Catering' },
];
export const HAIR_COLOR_OPTIONS = [
{ value: 'BLACK', label: 'Black' },
{ value: 'BROWN', label: 'Brown' },
{ value: 'BLONDE', label: 'Blonde' },
{ value: 'RED', label: 'Red' },
{ value: 'GREY', label: 'Grey' },
{ value: 'WHITE', label: 'White' },
{ value: 'BALD', label: 'Bald' },
{ value: 'OTHER', label: 'Other' },
];
export const EYE_COLOR_OPTIONS = [
{ value: 'BROWN', label: 'Brown' },
{ value: 'BLACK', label: 'Black' },
{ value: 'BLUE', label: 'Blue' },
{ value: 'GREEN', label: 'Green' },
{ value: 'HAZEL', label: 'Hazel' },
{ value: 'GREY', label: 'Grey' },
{ value: 'OTHER', label: 'Other' },
];
export const BLOOD_TYPE_OPTIONS = [
{ value: 'A_POSITIVE', label: 'A+' },
{ value: 'A_NEGATIVE', label: 'A' },
{ value: 'B_POSITIVE', label: 'B+' },
{ value: 'B_NEGATIVE', label: 'B' },
{ value: 'AB_POSITIVE', label: 'AB+' },
{ value: 'AB_NEGATIVE', label: 'AB' },
{ value: 'O_POSITIVE', label: 'O+' },
{ value: 'O_NEGATIVE', label: 'O' },
{ value: 'UNKNOWN', label: 'Unknown' },
];
/** Same plausibility bounds the API enforces (PHYSICAL_BOUNDS). */
export const PHYSICAL_BOUNDS = {
heightCm: { min: 100, max: 250 },
weightKg: { min: 30, max: 250 },
} 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. */
export const SEAFARER_REGISTRATION_DOCUMENTS: {
key: string;
name: string;
description?: string;
/**
* `'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;
}[] = [
{
key: 'photo',
name: 'Passport-size Photograph',
description: 'Recent colour photograph with a plain background.',
required: true,
accept: 'image/jpeg,image/png',
},
{ key: 'nationalId', name: 'National ID (Fayda) or Kebele ID', required: 'ethiopian' },
{ key: 'passport', name: 'Passport Copy', required: 'passport' },
{ key: 'graduation', name: 'Educational Certificate', required: false },
{
key: 'medical_certificate',
name: 'Medical Certificate',
description:
'Your STCW medical fitness certificate. Must match the certificate details entered above.',
required: true,
},
{
key: 'basic_training_evidence',
name: 'Basic Training Evidence',
description:
'One file containing evidence of all five basic training competencies: Personal Survival Techniques (PST), Fire Prevention and Fire Fighting (FPFF), Elementary First Aid (EFA), Personal Safety and Social Responsibility (PSSR), and Security Awareness.',
required: true,
},
];
export const SEAFARER_REGISTRATION_STATUS_LABELS: Record<SeafarerRegistrationStatus, string> = {
DRAFT: 'Draft',
SUBMITTED: 'Submitted',
RESUBMIT_REQUIRED: 'Corrections Requested',
APPROVED: 'Approved',
REJECTED: 'Rejected',
};
/**
* Registration status → platform tone.
*
* Tones, not colours. `StatusTone` is the platform's status vocabulary and the
* one place a tone becomes a colour (`STATUS_TONE_COLOR` in
* @ema-platform/shared), so this map cannot drift the way `APPROVED: 'teal'`
* had already drifted from every other feature's green success.
*
* The union is repeated rather than imported because @ema-platform/api does not
* depend on the theme layer, and should not start to for five string literals.
*/
export const SEAFARER_REGISTRATION_STATUS_TONES: Record<
SeafarerRegistrationStatus,
'success' | 'warning' | 'danger' | 'info' | 'pending' | 'neutral'
> = {
DRAFT: 'neutral',
SUBMITTED: 'info',
RESUBMIT_REQUIRED: 'pending',
APPROVED: 'success',
REJECTED: 'danger',
};
/** Human label for each answer — the review table and the summary both use it. */
export const SEAFARER_REGISTRATION_FIELD_LABELS: Record<keyof SeafarerRegistrationAnswers, string> = {
firstName: 'First Name',
middleName: 'Middle Name',
lastName: 'Last Name',
gender: 'Gender',
dateOfBirth: 'Date of Birth',
maritalStatus: 'Marital Status',
nationality: 'Nationality',
nationalIdNumber: 'National ID (Fayda) Number',
placeOfBirth: 'Place of Birth',
passportNumber: 'Passport Number',
passportExpiry: 'Passport Expiry Date',
department: 'Department',
locationId: 'Location',
permanentAddress: 'Permanent Address',
currentAddress: 'Current Address',
emergencyContactName: 'Full Name',
emergencyContactRelationship: 'Relationship',
emergencyContactPhone: 'Phone Number',
hairColor: 'Hair Colour',
eyeColor: 'Eye Colour',
heightCm: 'Height (cm)',
weightKg: 'Weight (kg)',
bloodType: 'Blood Type',
medicalCertificateNumber: 'Certificate Number',
medicalIssuerName: 'Issuing Clinic or Practitioner',
medicalIssueDate: 'Issue Date',
declarationAccepted: 'Declaration',
};
/**
* The answers grouped the way the wizard asks them — the summary on the
* portal and the review screen in the backoffice render the same sections.
*/
export const SEAFARER_REGISTRATION_SECTIONS: {
key: string;
title: string;
fields: (keyof SeafarerRegistrationAnswers)[];
}[] = [
{
key: 'identityDetails',
title: 'Identity Details',
fields: ['firstName', 'middleName', 'lastName', 'gender', 'dateOfBirth', 'maritalStatus', 'nationality', 'nationalIdNumber'],
},
{
key: 'identity',
title: 'Identity',
fields: ['placeOfBirth', 'passportNumber', 'passportExpiry', 'department'],
},
{
key: 'address',
title: 'Address',
fields: ['locationId', 'permanentAddress', 'currentAddress'],
},
{
key: 'physicalCharacteristics',
title: 'Physical Characteristics',
fields: ['hairColor', 'eyeColor', 'heightCm', 'weightKg', 'bloodType'],
},
{
key: 'medicalCertificate',
title: 'Medical Certificate',
fields: ['medicalCertificateNumber', 'medicalIssuerName', 'medicalIssueDate'],
},
{
key: 'emergencyContact',
title: 'Emergency Contact',
fields: ['emergencyContactName', 'emergencyContactRelationship', 'emergencyContactPhone'],
},
];
const OPTION_LABELS: Partial<Record<keyof SeafarerRegistrationAnswers, { value: string; label: string }[]>> = {
gender: GENDER_OPTIONS,
maritalStatus: MARITAL_STATUS_OPTIONS,
department: DEPARTMENT_OPTIONS,
hairColor: HAIR_COLOR_OPTIONS,
eyeColor: EYE_COLOR_OPTIONS,
bloodType: BLOOD_TYPE_OPTIONS,
};
/**
* Display value for one answer: option label for enums, "—" when blank.
*
* `departmentOptions` overrides the hardcoded `DEPARTMENT_OPTIONS` fallback
* for the `department` field — the live list from `GET /departments`, so a
* department added in the backoffice after this constants file was written
* still gets its name instead of falling back to the raw code.
*/
export function displaySeafarerAnswer(
field: keyof SeafarerRegistrationAnswers,
value: unknown,
departmentOptions?: { value: string; label: string }[],
): string {
if (value === null || value === undefined || value === '') return '—';
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
const options = field === 'department' && departmentOptions ? departmentOptions : OPTION_LABELS[field];
if (options) return options.find((o) => o.value === value)?.label ?? String(value);
return String(value);
}