From ff3396ed051bd279e2abdfb8388e2f11d2fda46f Mon Sep 17 00:00:00 2001 From: estifanos Date: Tue, 1 Sep 2026 06:43:32 +0000 Subject: [PATCH 1/2] feat: add date bounds to DateField and improve validation flow in registration wizard --- .../components/fields.tsx | 4 +- .../components/steps.tsx | 4 ++ .../pages/SeafarerRegistrationPage.tsx | 42 +++++++++++++------ 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/apps/portal/src/app/features/seafarer-registration/components/fields.tsx b/apps/portal/src/app/features/seafarer-registration/components/fields.tsx index 5676ec900..95a205327 100644 --- a/apps/portal/src/app/features/seafarer-registration/components/fields.tsx +++ b/apps/portal/src/app/features/seafarer-registration/components/fields.tsx @@ -61,7 +61,7 @@ export function SelectField(p: FieldProps & { options: { value: string; label: s ); } -export function DateField(p: FieldProps) { +export function DateField(p: FieldProps & { minDate?: Date | string; maxDate?: Date | string }) { return ( p.set(p.name, v)} /> diff --git a/apps/portal/src/app/features/seafarer-registration/components/steps.tsx b/apps/portal/src/app/features/seafarer-registration/components/steps.tsx index 934244cd0..eb5abf9b8 100644 --- a/apps/portal/src/app/features/seafarer-registration/components/steps.tsx +++ b/apps/portal/src/app/features/seafarer-registration/components/steps.tsx @@ -220,6 +220,10 @@ export function EmergencyContactStep(p: StepProps) { name="medicalIssueDate" label="Issue Date" required + // Bounded here rather than only at submit: the calendar is the one + // place the applicant can see why a day is refused, and the server's + // rejection otherwise only surfaces five steps later. + maxDate={new Date()} description="Cannot be a future date. Validity is calculated from this: two years, or one year if you are under 18." /> diff --git a/apps/portal/src/app/features/seafarer-registration/pages/SeafarerRegistrationPage.tsx b/apps/portal/src/app/features/seafarer-registration/pages/SeafarerRegistrationPage.tsx index 04ff5e01f..b324f04c8 100644 --- a/apps/portal/src/app/features/seafarer-registration/pages/SeafarerRegistrationPage.tsx +++ b/apps/portal/src/app/features/seafarer-registration/pages/SeafarerRegistrationPage.tsx @@ -80,6 +80,11 @@ function answersOf(registration: SeafarerRegistration): SaveSeafarerRegistration return Object.fromEntries(ANSWER_KEYS.map((k) => [k, registration[k]])) as SaveSeafarerRegistration; } +/** Today as `yyyy-mm-dd` in the browser's own zone — en-CA is that format. */ +function todayDate(): string { + return new Date().toLocaleDateString('en-CA'); +} + function blank(value: unknown): boolean { return value === null || value === undefined || value === '' || value === false; } @@ -258,6 +263,10 @@ export function SeafarerRegistrationPage() { function set(key: AnswerKey, value: unknown) { setForm((prev) => ({ ...prev, [key]: value })); + // The submit refusal names what was wrong at the time it was refused. + // Leaving it on screen while the applicant corrects it reads as the + // correction having been ignored. + if (issues.length) setIssues([]); setErrors((prev) => { if (!prev[key]) return prev; const next = { ...prev }; @@ -280,6 +289,9 @@ export function SeafarerRegistrationPage() { found.weightKg = `Enter a weight between ${weightKg.min} and ${weightKg.max} kg.`; } } + if (index === 2 && (form.medicalIssueDate ?? '').slice(0, 10) > todayDate()) { + found.medicalIssueDate = 'The issue date cannot be in the future.'; + } setErrors(found); const missingKeys = Object.keys(found) as AnswerKey[]; if (missingKeys.length) { @@ -323,19 +335,23 @@ export function SeafarerRegistrationPage() { } async function goToStep(target: number) { - if (target <= active) { - setActive(target); - return; - } - // Going forward validates every step passed over, so a jump cannot skip a - // required field; the walk stops on the first step that fails. - for (let step = active; step < target; step++) { - if (!readOnly && !validateStep(step)) { - setActive(step); - return; + // Saved before anything can turn the navigation around. `form` is the only + // copy of what was typed, so validating first — as this used to — threw the + // edit away on every blocked step and every step back: an applicant fixing + // a field the submit check rejected watched the correction vanish. A draft + // takes any subset of the answers, so persisting an incomplete one is safe. + const saved = await saveAnswers(); + if (target > active) { + if (!saved) return; + // Going forward validates every step passed over, so a jump cannot skip a + // required field; the walk stops on the first step that fails. + for (let step = active; step < target; step++) { + if (!readOnly && !validateStep(step)) { + setActive(step); + return; + } } } - if (!(await saveAnswers())) return; setErrors({}); setActive(target); } @@ -343,8 +359,8 @@ export function SeafarerRegistrationPage() { async function handleSubmit() { if (!registration) return; setIssues([]); - if (!readOnly && !validateStep(4)) return; if (!(await saveAnswers())) return; + if (!readOnly && !validateStep(4)) return; try { await submit(registration.id).unwrap(); notifications.show({ @@ -510,7 +526,7 @@ export function SeafarerRegistrationPage() { )} - {active < STEPS.length - 1 ? ( From 570c80a91a3c493a4897d030e7d8992f0a366f45 Mon Sep 17 00:00:00 2001 From: estifanos Date: Tue, 1 Sep 2026 06:59:35 +0000 Subject: [PATCH 2/2] refactor: remove CoC and GOC endorsement queue navigation and translation keys --- apps/backoffice/src/app/i18n/locales/am.ts | 2 -- apps/backoffice/src/app/i18n/locales/en.ts | 2 -- apps/backoffice/src/app/layouts/nav-config.ts | 12 ------------ 3 files changed, 16 deletions(-) diff --git a/apps/backoffice/src/app/i18n/locales/am.ts b/apps/backoffice/src/app/i18n/locales/am.ts index ee4ee2eef..4c03f3254 100644 --- a/apps/backoffice/src/app/i18n/locales/am.ts +++ b/apps/backoffice/src/app/i18n/locales/am.ts @@ -88,8 +88,6 @@ export const am: Translations = { btcQueue: "የBTC ወረፋ", cocQueue: "የCoC ወረፋ", copQueue: "የCoP ወረፋ", - endorsementCocQueue: "የCoC እውቅና ወረፋ", - endorsementGocQueue: "የGOC እውቅና ወረፋ", endorsementQueue: "የማስተያየት ወረፋ", vesselRegistrations: "የመርከብ ምዝገባ", // vesselRegistrationReport: 'የምዝገባ ሪፖርት', diff --git a/apps/backoffice/src/app/i18n/locales/en.ts b/apps/backoffice/src/app/i18n/locales/en.ts index 342ace75b..1cfb20a56 100644 --- a/apps/backoffice/src/app/i18n/locales/en.ts +++ b/apps/backoffice/src/app/i18n/locales/en.ts @@ -88,8 +88,6 @@ export const en = { postWaiverQueue: 'Post-Waiver Queue', cocQueue: 'CoC Queue', copQueue: 'CoP Queue', - endorsementCocQueue: 'CoC Endorsement Queue', - endorsementGocQueue: 'GOC Endorsement Queue', endorsementQueue: 'Endorsement Queue', vesselRegistrations: 'Vessel Registration', vesselTransfers: 'Vessel Ownership Transfer', diff --git a/apps/backoffice/src/app/layouts/nav-config.ts b/apps/backoffice/src/app/layouts/nav-config.ts index 7df1c5bcf..9ab587db6 100644 --- a/apps/backoffice/src/app/layouts/nav-config.ts +++ b/apps/backoffice/src/app/layouts/nav-config.ts @@ -189,18 +189,6 @@ export const NAV_SECTIONS: NavSection[] = [ icon: IconShieldCheck, permissions: SEAFARER_QUEUE, }, - { - to: "/licence-review/type/ENDORSEMENT_COC", - label: "nav.endorsementCocQueue", - icon: IconRubberStamp, - permissions: [P.VIEW_SEAFARER_REGISTRY], - }, - { - to: "/licence-review/type/ENDORSEMENT_GOC", - label: "nav.endorsementGocQueue", - icon: IconRubberStamp, - permissions: [P.VIEW_SEAFARER_REGISTRY], - }, { to: "/licence-review/type/ENDORSEMENT_SEAFARER", label: "nav.endorsementQueue",