feat: consolidate CoC and GOC endorsement workflows into a new ENDORSEMENT_SEAFARER type and add anyOf support to conditional form field logic.

This commit is contained in:
estifanos
2026-08-26 09:39:24 +00:00
parent a86f4750bc
commit 13c3e8974d
14 changed files with 78 additions and 39 deletions

View File

@@ -33,7 +33,14 @@ import { AdvancedTable, PageLoader, notify, useServerTable } from '@ema-platform
import { useDateDisplayer } from '@ema-platform/shared';
import { endorsementColumns } from './columns';
const ENDORSEMENT_TYPE_KEYS = ['ENDORSEMENT_COC', 'ENDORSEMENT_GOC'];
// ENDORSEMENT_SEAFARER covers CoC and GOC together and is the only type new
// applications file against; the other two stay listed so an application or
// licence filed before the switch keeps showing up here.
const ENDORSEMENT_TYPE_KEYS = [
'ENDORSEMENT_SEAFARER',
'ENDORSEMENT_COC',
'ENDORSEMENT_GOC',
];
function EligibilityItem({ ok, label }: { ok: boolean; label: string }) {
return (
@@ -132,21 +139,13 @@ export function EndorsementPage() {
/>
</List>
</div>
<Stack gap="xs">
<Button
rightSection={<IconArrowRight size={16} />}
onClick={() => navigate('/licensing/ENDORSEMENT_COC/apply')}
>
{t('endorsement.endorseCoc', 'Endorse a CoC')}
</Button>
<Button
variant="light"
rightSection={<IconArrowRight size={16} />}
onClick={() => navigate('/licensing/ENDORSEMENT_GOC/apply')}
>
{t('endorsement.endorseGoc', 'Endorse a GOC')}
</Button>
</Stack>
<Button
disabled={!registered}
rightSection={<IconArrowRight size={16} />}
onClick={() => navigate('/licensing/ENDORSEMENT_SEAFARER/apply')}
>
{t('endorsement.apply', 'Apply for an endorsement')}
</Button>
</Group>
{!registered && (
<Alert color="orange" mt="md" icon={<IconInfoCircle size={16} />}>

View File

@@ -55,6 +55,10 @@ const MODE_FREE_TYPE_KEYS = [
'VESSEL_OWNERSHIP_TRANSFER',
'CERTIFICATE_OF_COMPETENCY',
'CERTIFICATE_OF_PROFICIENCY',
'ENDORSEMENT_SEAFARER',
// Retired by ENDORSEMENT_SEAFARER but kept mode-free: an in-flight
// application filed against one of these before the switch must still be
// reachable to view, correct or resubmit.
'ENDORSEMENT_COC',
'ENDORSEMENT_GOC',
'PRE_WAIVER',

View File

@@ -16,10 +16,16 @@ import { OperationsFormContent } from "../../profile/components/OperationsFormCo
* Seafarer goes to its own registration page, whose Identity Details step
* collects the profile answers itself — no detour via `/profile`. Seafarer
* wins when both are ticked; the other form is one nav click away.
*
* SEAFARER_REGISTRATION is listed before ENDORSEMENT_SEAFARER deliberately:
* `nextStepFor` takes the first key that matches, and an applicant who ticked
* both belongs in registration first — the endorsement application refuses
* submission until that registration is accepted.
*/
const NEXT_STEP: Record<string, string> = {
SEAFARER_REGISTRATION: "/seafarer-registration",
VESSEL_REGISTRATION: "/vessel-registration",
ENDORSEMENT_SEAFARER: "/endorsements",
};
function nextStepFor(selectedKeys: string[]): string {

View File

@@ -25,15 +25,21 @@ import { notify, ModalFooter } from '@ema-platform/ui';
import { useDateDisplayer } from '@ema-platform/shared';
/**
* Registrations an applicant makes for themselves rather than for a company.
* Registrations and seafarer-only services an applicant declares for
* themselves rather than for a company.
*
* Named explicitly rather than inferred from `requiresOperatorMode: false`,
* because that flag is also false for things nobody declares up front — a
* waiver is requested per shipment, not adopted as an identity.
* ENDORSEMENT_SEAFARER belongs here for the same reason as the two
* registrations: a seafarer requesting one is not declaring a logistics mode
* of operation, and without this key `accountTypeFor` below would fall the
* account through to the company-representative type instead of `SEAFARER`.
*/
const PERSONAL_REGISTRATION_KEYS = [
'SEAFARER_REGISTRATION',
'VESSEL_REGISTRATION',
'ENDORSEMENT_SEAFARER',
];
/**
@@ -56,6 +62,7 @@ function accountTypeFor(keys: string[]): string | null {
}
if (keys.includes('VESSEL_REGISTRATION')) return 'VESSEL_OWNER';
if (keys.includes('SEAFARER_REGISTRATION')) return 'SEAFARER';
if (keys.includes('ENDORSEMENT_SEAFARER')) return 'SEAFARER';
return null;
}
@@ -212,7 +219,7 @@ export function OperationsFormContent({
{personalOptions.length > 0 && (
<>
<Text size="xs" fw={600} c="dimmed" mt="sm" tt="uppercase">
Registering as an individual or vessel owner
Registering or applying as an individual or vessel owner
</Text>
{personalOptions.map((type) => (
<Checkbox

View File

@@ -63,7 +63,7 @@ export const am: Translations = {
certificates: 'የምስክር ወረቀቶች',
seamanBook: 'የመርከበኛ መጽሐፍ እና BTC',
btc: 'መሠረታዊ ሥልጠና ምስክር ወረቀት',
endorsements: 'ማረጋገጫዎች',
endorsements: 'የባህረኛ ማስተያየት',
vesselRegistrations: 'የመርከብ ምዝገባ',
vesselTransfers: 'የመርከብ ባለቤትነት ዝውውር',
documents: 'ሰነዶቼ',
@@ -1008,8 +1008,7 @@ export const am: Translations = {
registered: "የተመዘገበ መርከበኛ ({{number}})",
registrationRequired: "ንቁ የመርከበኛ ምዝገባ ያስፈልጋል",
},
endorseCoc: "CoC ያረጋግጡ",
endorseGoc: "GOC ያረጋግጡ",
apply: "ለማስተያየት ያመልክቱ",
registrationNotice: {
prefix: "መጀመሪያ የ",
link: "መርከበኛ ምዝገባዎን",

View File

@@ -63,7 +63,7 @@ export const en = {
certificates: 'Certificates',
seamanBook: 'SeamanBook and BTC',
btc: 'Basic Training Certificate',
endorsements: 'Endorsements',
endorsements: 'Endorsement Seafarer',
vesselRegistrations: 'Vessel Registration',
vesselTransfers: 'Vessel Transfers',
documents: 'My Documents',
@@ -1010,8 +1010,7 @@ export const en = {
registered: 'Registered seafarer ({{number}})',
registrationRequired: 'Active seafarer registration required',
},
endorseCoc: 'Endorse a CoC',
endorseGoc: 'Endorse a GOC',
apply: 'Apply for an endorsement',
registrationNotice: {
prefix: 'Complete your',
link: 'seafarer registration',

View File

@@ -140,7 +140,7 @@ const NAV_SECTIONS: { label?: string; items: PortalNavItem[] }[] = [
},
{
to: "/endorsements",
label: "Endorsements",
label: "Endorsement Seafarer",
i18nKey: "nav.endorsements",
icon: IconRubberStamp,
permissions: [P.VIEW_OWN_CERTIFICATES],