mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 19:12:50 +00:00
- Implemented SeafarerRegistrationPage component with five-step registration process. - Added API endpoints for seafarer registration including start, save, and submit functionalities. - Created necessary types and constants for seafarer registration. - Updated router to include new registration paths and permissions. - Integrated profile defaults to pre-fill registration fields where applicable. - Added validation and error handling for registration steps. - Enhanced document upload functionality specific to seafarer registration.
212 lines
6.8 KiB
TypeScript
212 lines
6.8 KiB
TypeScript
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;
|
||
|
||
/** Upload slots, keyed as the API's submission check expects them. */
|
||
export const SEAFARER_REGISTRATION_DOCUMENTS: {
|
||
key: string;
|
||
name: string;
|
||
description?: string;
|
||
/** `'passport'`: required only once a passport number is declared. */
|
||
required: boolean | 'passport';
|
||
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: true },
|
||
{ 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',
|
||
UNDER_REVIEW: 'Under Review',
|
||
RESUBMIT_REQUIRED: 'Corrections Requested',
|
||
APPROVED: 'Approved',
|
||
REJECTED: 'Rejected',
|
||
};
|
||
|
||
export const SEAFARER_REGISTRATION_STATUS_COLORS: Record<SeafarerRegistrationStatus, string> = {
|
||
DRAFT: 'gray',
|
||
SUBMITTED: 'blue',
|
||
UNDER_REVIEW: 'indigo',
|
||
RESUBMIT_REQUIRED: 'orange',
|
||
APPROVED: 'teal',
|
||
REJECTED: 'red',
|
||
};
|
||
|
||
/** 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. */
|
||
export function displaySeafarerAnswer(
|
||
field: keyof SeafarerRegistrationAnswers,
|
||
value: unknown,
|
||
): string {
|
||
if (value === null || value === undefined || value === '') return '—';
|
||
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
|
||
const options = OPTION_LABELS[field];
|
||
if (options) return options.find((o) => o.value === value)?.label ?? String(value);
|
||
return String(value);
|
||
}
|