diff --git a/apps/e2e/src/seafarer-registration.spec.ts b/apps/e2e/src/seafarer-registration.spec.ts index 2f03ff5ed..b68b7fc07 100644 --- a/apps/e2e/src/seafarer-registration.spec.ts +++ b/apps/e2e/src/seafarer-registration.spec.ts @@ -23,7 +23,12 @@ import { approveRegistration, runWorkflow } from './support/workflow'; */ /** - * Fills the fields `RequireSeafarerProfile` refuses to open the wizard without. + * Fills the profile the seafarer wizard prefills its Identity Details step + * from. + * + * No longer a precondition for reaching the wizard — that redirect is gone and + * the step collects these itself — but a populated profile is the returning + * applicant's case, and it is the prefill that keeps them from retyping. * * They are split across two tabs, and every tab's panel is in the DOM whether * or not it is showing — so each one has to be selected before its inputs can diff --git a/apps/portal/src/app/features/licensing/pages/LicenseApplicationPage.tsx b/apps/portal/src/app/features/licensing/pages/LicenseApplicationPage.tsx index 39cbdd9d3..5352f520c 100644 --- a/apps/portal/src/app/features/licensing/pages/LicenseApplicationPage.tsx +++ b/apps/portal/src/app/features/licensing/pages/LicenseApplicationPage.tsx @@ -227,12 +227,17 @@ export function LicenseApplicationPage() { detail?.application?.formData, ]); - // Generic fill for every field the config marks `readOnly` with a - // `source` — e.g. seafarer registration's read-only Identity Details step, - // which shows what's already on the profile instead of asking again. - // `readOnly` fields are never sent by the applicant and the server skips - // them at validation, so this is display-only; the profile itself is what - // an edit has to go through. + // Generic fill for every field the config gives a `source` — the profile + // value the applicant would otherwise retype. Seafarer registration's + // Identity Details step is the case that drives this: it collects name, + // gender, DOB and national ID *in the wizard* rather than sending the + // applicant to `/profile` first, so those fields are editable and this is a + // prefill, not a display. + // + // Editable sourced fields are filled only while still blank. Re-running + // this effect (a refetched profile, a saved draft) must not overwrite what + // the applicant has since typed — for a `readOnly` field the profile stays + // authoritative, so those keep tracking it. useEffect(() => { if (!profile || !config) return; const context = { user: profile.user, profile }; @@ -242,10 +247,14 @@ export function LicenseApplicationPage() { const next = { ...prev }; for (const section of config.licenseType.formSchema.sections) { for (const field of section.fields) { - if (!field.readOnly || !field.source) continue; + if (!field.source) continue; + const current = next[section.key]?.[field.key]; + const untouched = + current === undefined || current === null || current === ""; + if (!field.readOnly && !untouched) continue; const value = readSourcePath(context, field.source); if (value === undefined || value === null || value === "") continue; - if (next[section.key]?.[field.key] === value) continue; + if (current === value) continue; next[section.key] = { ...next[section.key], [field.key]: value }; changed = true; } diff --git a/apps/portal/src/app/features/profile/components/RequireSeafarerProfile.tsx b/apps/portal/src/app/features/profile/components/RequireSeafarerProfile.tsx index 02cca1bf0..5856d706d 100644 --- a/apps/portal/src/app/features/profile/components/RequireSeafarerProfile.tsx +++ b/apps/portal/src/app/features/profile/components/RequireSeafarerProfile.tsx @@ -1,24 +1,14 @@ -import { useEffect } from 'react'; import { Center, Loader } from '@mantine/core'; -import { Navigate, useLocation, useParams } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; -import { notify } from '@ema-platform/ui'; -import { - PROFILE_FIELD_SECTION, - useCurrentProfile, - type ProfileRequirement, -} from '@ema-platform/auth'; +import { Navigate, useParams } from 'react-router-dom'; +import { useCurrentProfile, type ProfileRequirement } from '@ema-platform/auth'; /** - * Seafarer registration is filled in from the profile (nationality, ID, - * names, contact details) — the server refuses an application missing them, - * so they're asked for up front instead of at submit time. + * The identity the seafarer wizard needs before it can produce a registration. * - * Only the fields the Personal, Maritime Profile and Address tabs actually - * mark required — matches `profileSchema` / `addressSchema`, so the gate is - * always satisfiable by finishing those tabs and never blocks on an optional - * field (place of birth, region/city/woreda, emergency contact) the forms - * don't star. + * No longer a gate on opening the wizard: the Identity Details step collects + * these itself, so an applicant with an empty profile starts in registration + * rather than being sent to `/profile` to prepare for it. Kept because + * `ProfilePage` still reads it to show what a seafarer registration will need. */ export const SEAFARER_PROFILE_REQUIREMENT: ProfileRequirement = { fields: [ @@ -42,35 +32,26 @@ export const SEAFARER_PROFILE_REQUIREMENT: ProfileRequirement = { const REGISTRATION_TYPE_KEY = 'SEAFARER_REGISTRATION'; /** - * Sends an applicant with an incomplete profile to `/profile` before they can - * reach seafarer registration. Wraps `/seafarer-registration` directly and - * `/licensing/:typeCode/apply` when `typeCode` is the seafarer type — the - * latter is the shared wizard route every licence type renders through, so - * without it the gate is a decoration a deep link skips. + * Guards the seafarer wizard against the one case it cannot serve: an + * applicant who already holds a seafarer number. * - * Fires before the wizard starts, not mid-application, so nothing is lost — - * unlike the case `ProfileRequirementGate`'s doc comment warns against - * (mid-flow redirects on the old, deleted setup wizard). + * It deliberately does *not* gate on profile completeness any more. Selecting + * Seafarer Registration now opens the wizard, and the Identity Details step + * collects name, gender, DOB, marital status, nationality and national ID + * itself — an empty profile is a thing the wizard fills, not a reason to be + * sent away from it. Those answers reach the profile when a reviewer approves + * the registration (`CompletionEffectService.registerSeafarer`). + * + * Wraps `/seafarer-registration` directly and `/licensing/:typeCode/apply` + * when `typeCode` is the seafarer type — the latter is the shared wizard route + * every licence type renders through, so without it a deep link skips this. */ export function RequireSeafarerProfile({ children }: { children: React.ReactNode }) { - const { t } = useTranslation(); const { typeCode } = useParams(); - const { pathname } = useLocation(); - const { isLoading, isFetching, error, gapsFor, profile } = useCurrentProfile(); + const { isLoading, error, profile } = useCurrentProfile(); - // Shared wizard route — only the seafarer type is gated here. + // Shared wizard route — only the seafarer type is checked here. const gated = !typeCode || typeCode === REGISTRATION_TYPE_KEY; - const gaps = gated ? gapsFor(SEAFARER_PROFILE_REQUIREMENT) : []; - const redirecting = gated && !isLoading && !error && !isFetching && gaps.length > 0; - - useEffect(() => { - if (!redirecting) return; - const fields = gaps.map((field) => t(`profileFields.${field}`, field)).join(', '); - notify.info(t('profileGate.seafarerRedirect', { fields })); - // Fire once per redirect, not on every render while gaps/gapsFor are - // recreated — the toast content is captured at the moment it fires. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [redirecting, pathname]); if (!gated) return <>{children}; @@ -91,24 +72,10 @@ export function RequireSeafarerProfile({ children }: { children: React.ReactNode return ; } - // A failed lookup must not lock anyone out — the server still refuses the - // application for a profile it can't fill in from. + // A failed lookup must not lock anyone out — an unreadable profile says + // nothing about whether this applicant is already registered, and the + // server refuses a duplicate registration regardless. if (error) return <>{children}; - if (gaps.length === 0) return <>{children}; - - // Gaps while a save is still landing are not an answer yet. Saving a - // profile tab invalidates this query and the applicant may already be - // headed back here in the same tick — deciding on the pre-save cache would - // bounce them off the screen they just finished. - if (isFetching) { - return ( -
- -
- ); - } - - const target = PROFILE_FIELD_SECTION[gaps[0]]; - return ; + return <>{children}; } diff --git a/apps/portal/src/app/i18n/locales/am.ts b/apps/portal/src/app/i18n/locales/am.ts index b1f4d1cf3..16ace7369 100644 --- a/apps/portal/src/app/i18n/locales/am.ts +++ b/apps/portal/src/app/i18n/locales/am.ts @@ -234,8 +234,8 @@ export const am: Translations = { addDetails: 'እነዚህን መረጃዎች ጨምር', viewProfile: 'ሙሉ መገለጫ ይመልከቱ', seafarerReason: 'የባህረኞች ምዝገባ የሚዘጋጀው ከመገለጫዎ ነው — እነዚህ መረጃዎች ራሱ ይሞላሉ።', - seafarerRedirect: 'የባህረኛ ምዝገባ ለማድረግ መገለጫዎን ያጠናቅቁ። የሚያስፈልጉ፡ {{fields}}', - seafarerBanner: 'የባህረኛ ምዝገባ ለማድረግ የመገለጫ መረጃ ያስፈልጋል።', + seafarerBanner: + 'የባህረኛ ምዝገባ እነዚህን መረጃዎች ይጠይቃል፤ ከጸደቀ በኋላም መገለጫዎን ያዘምናል። እዚህ አስቀድመው ቢሞሏቸው እዚያ እንደገና መተየብ አይኖርብዎትም።', }, profileSections: { diff --git a/apps/portal/src/app/i18n/locales/en.ts b/apps/portal/src/app/i18n/locales/en.ts index 52e486ba0..81220e078 100644 --- a/apps/portal/src/app/i18n/locales/en.ts +++ b/apps/portal/src/app/i18n/locales/en.ts @@ -234,8 +234,8 @@ export const en = { viewProfile: 'View full profile', seafarerReason: 'Seafarer registration is built from your profile — these details fill it in for you.', - seafarerRedirect: 'Finish your profile to register as a seafarer. Still needed: {{fields}}', - seafarerBanner: 'Profile details are needed for seafarer registration.', + seafarerBanner: + 'Seafarer registration asks for these details and updates your profile once approved. Filling them in here first saves you typing them there.', }, profileSections: { diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 000000000..e8b78c852 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +allowBuilds: + core-js: set this to true or false + esbuild: set this to true or false + nx: set this to true or false