diff --git a/apps/portal/src/app/features/licensing/components/ConfigDrivenSection.tsx b/apps/portal/src/app/features/licensing/components/ConfigDrivenSection.tsx index 00cc4346f..c036a86fc 100644 --- a/apps/portal/src/app/features/licensing/components/ConfigDrivenSection.tsx +++ b/apps/portal/src/app/features/licensing/components/ConfigDrivenSection.tsx @@ -25,15 +25,23 @@ interface Props { errors?: Record; /** The applicant's registered vessels, for the vessel-picker field. */ vessels?: Vessel[]; + /** + * Fired when the vessel picker's value resolves to a known vessel. Vessel + * Information and Current Ownership are separate form sections, so this + * component (one instance per section) can only fill its own fields — + * the parent uses this to fill the rest via the same `VESSEL_FIELD_FILLERS`. + */ + onVesselSelected?: (vessel: Vessel) => void; } /** - * Maps a Vessel's own fields onto whichever sibling fields the backend put - * in the same section — same dual key/label matching the nationality prefill - * in LicenseApplicationPage uses, so this doesn't depend on exact field-key - * naming in the seeded formSchema. + * Maps a Vessel's own fields onto whichever fields the backend put in the + * license form — same dual key/label matching the nationality prefill in + * LicenseApplicationPage uses, so this doesn't depend on exact field-key + * naming in the seeded formSchema. Exported so the parent page can apply it + * across every section, not just the one the vessel picker lives in. */ -const VESSEL_FIELD_FILLERS: { +export const VESSEL_FIELD_FILLERS: { matches: (label: string, key: string) => boolean; value: (vessel: Vessel) => unknown; }[] = [ @@ -53,9 +61,12 @@ const VESSEL_FIELD_FILLERS: { { matches: (l, k) => k === 'enginePowerKw' || l.includes('engine power'), value: (v) => v.enginePowerKw }, { matches: (l, k) => k === 'numberOfEngines' || l.includes('number of engines'), value: (v) => v.numberOfEngines }, { matches: (l, k) => k === 'hullMaterial' || l.includes('hull material'), value: (v) => v.hullMaterial }, + // Current Ownership: only the "current" owner, never "new owner" — + // that's who the vessel is being transferred to, not on file anywhere. + { matches: (l, k) => k === 'currentOwnerName' || k === 'currentOwner' || l.includes('current owner'), value: (v) => v.ownerName }, ]; -function fillFromVessel( +export function fillFromVessel( vessel: Vessel, fields: FormFieldConfig[], onChange: (key: string, value: unknown) => void, @@ -82,6 +93,7 @@ export function ConfigDrivenSection({ disabled, errors = {}, vessels = [], + onVesselSelected, }: Props) { const fields = [...(section.fields ?? [])].sort( (a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0), @@ -134,7 +146,7 @@ export function ConfigDrivenSection({ // onChange={(v) => { // onChange(field.key, v); // const vessel = vessels.find((x) => x.id === v); - // if (vessel) fillFromVessel(vessel, fields, onChange); + // if (vessel) { fillFromVessel(vessel, fields, onChange); onVesselSelected?.(vessel); } // }} // searchable // clearable={!field.required} @@ -147,7 +159,10 @@ export function ConfigDrivenSection({ const v = e.currentTarget.value; onChange(field.key, v); const vessel = vessels.find((x) => x.id === v); - if (vessel) fillFromVessel(vessel, fields, onChange); + if (vessel) { + fillFromVessel(vessel, fields, onChange); + onVesselSelected?.(vessel); + } }} /> ) : field.type === 'SELECT' ? ( diff --git a/apps/portal/src/app/features/licensing/pages/LicenseApplicationPage.tsx b/apps/portal/src/app/features/licensing/pages/LicenseApplicationPage.tsx index 802a2630e..2849e99ae 100644 --- a/apps/portal/src/app/features/licensing/pages/LicenseApplicationPage.tsx +++ b/apps/portal/src/app/features/licensing/pages/LicenseApplicationPage.tsx @@ -50,10 +50,11 @@ import { type FieldErrors, type FormFieldConfig, type ValidationIssue, + type Vessel, } from '@ema-platform/api'; import { getCountryCode, getCountryName, ModalFooter } from '@ema-platform/ui'; import { useCurrentProfile } from '@ema-platform/auth'; -import { ConfigDrivenSection } from '../components/ConfigDrivenSection'; +import { ConfigDrivenSection, fillFromVessel } from '../components/ConfigDrivenSection'; import { DocumentSlots } from '../components/DocumentSlots'; import { StaffEvidence } from '../components/StaffEvidence'; @@ -210,6 +211,20 @@ export function LicenseApplicationPage() { const readOnly = !['DRAFT', 'RESUBMIT_REQUIRED'].includes(application.status); + // Vessel Information and Current Ownership are separate form sections, so + // ConfigDrivenSection (one instance per section) can't fill both itself — + // it reports the pick up here and this fans it out across every section. + function handleVesselSelected(vessel: Vessel) { + for (const section of config?.licenseType.formSchema.sections ?? []) { + fillFromVessel(vessel, section.fields, (key, value) => { + setDraft((prev) => ({ + ...prev, + [section.key]: { ...(prev[section.key] ?? {}), [key]: value }, + })); + }); + } + } + async function saveSection(sectionKey: string) { // During an adjustment round only flagged sections are editable, so don't // even attempt a write the server would reject. @@ -459,6 +474,7 @@ export function LicenseApplicationPage() { section={section} values={draft[section.key] ?? {}} formData={draft} + onVesselSelected={handleVesselSelected} errors={fieldErrors} vessels={vessels} disabled={readOnly || locked} @@ -589,6 +605,7 @@ export function LicenseApplicationPage() {