Files
emaui/libs/api/src/lib/features/seafarer-registration/seafarer-registration.constants.ts
Nati 818964a694 feat: add seafarer registration feature with multi-step form
- 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.
2026-08-20 07:14:57 +00:00

212 lines
6.8 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;
/** 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);
}