import { Checkbox, Grid, NumberInput, Select, Textarea, TextInput, } from '@mantine/core'; import { conditionHolds, localized, type FormFieldConfig, type FormSectionConfig, type Vessel, } from '@ema-platform/api'; import { CountrySelect } from '@ema-platform/ui'; interface Props { section: FormSectionConfig; values: Record; formData: Record>; onChange: (key: string, value: unknown) => void; disabled?: boolean; /** Keyed `${sectionKey}.${fieldKey}` — shown under the offending field. */ errors?: Record; /** The applicant's registered vessels, for the vessel-picker field. */ vessels?: Vessel[]; } /** * 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. */ const VESSEL_FIELD_FILLERS: { matches: (label: string, key: string) => boolean; value: (vessel: Vessel) => unknown; }[] = [ { matches: (l, k) => k === 'registrationNumber' || l.includes('registration number'), value: (v) => v.registrationNumber }, { matches: (l, k) => k === 'vesselName' || l.includes('vessel name'), value: (v) => v.name }, { matches: (l, k) => k === 'category' || k === 'vesselCategory' || l.includes('vessel category'), value: (v) => v.category }, { matches: (l, k) => k === 'vesselType' || l.includes('vessel type'), value: (v) => v.vesselType }, { matches: (l, k) => k === 'imoNumber' || l.includes('imo'), value: (v) => v.imoNumber }, { matches: (l, k) => k === 'hullNumber' || l.includes('hull number'), value: (v) => v.hullNumber }, { matches: (l, k) => k === 'flagState' || l.includes('flag state') || l.includes('flag'), value: (v) => v.flagState }, { matches: (l, k) => k === 'portOfRegistry' || l.includes('port of registry'), value: (v) => v.portOfRegistry }, { matches: (l, k) => k === 'grossTonnage' || l.includes('gross tonnage'), value: (v) => v.grossTonnage }, { matches: (l, k) => k === 'passengerCapacity' || l.includes('passenger capacity'), value: (v) => v.passengerCapacity }, { matches: (l, k) => k === 'lengthMeters' || l.includes('length'), value: (v) => v.lengthMeters }, { matches: (l, k) => k === 'yearBuilt' || l.includes('year built'), value: (v) => v.yearBuilt }, { matches: (l, k) => k === 'engineType' || l.includes('engine type'), value: (v) => v.engineType }, { 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 }, ]; function fillFromVessel( vessel: Vessel, fields: FormFieldConfig[], onChange: (key: string, value: unknown) => void, ) { for (const f of fields) { const label = localized(f.label).toLowerCase(); const filler = VESSEL_FIELD_FILLERS.find((m) => m.matches(label, f.key)); if (filler) onChange(f.key, filler.value(vessel) ?? ''); } } /** * Renders one form section from the license type's configuration. * * Nothing here is Freight-Forwarder specific — adding a license type or moving * a field is a backend config change, which is the whole point of the * config-driven design. */ export function ConfigDrivenSection({ section, values, formData, onChange, disabled, errors = {}, vessels = [], }: Props) { const fields = [...(section.fields ?? [])].sort( (a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0), ); return ( {fields.map((field) => { if (!conditionHolds(field.showWhen, formData)) return null; const label = localized(field.label); const value = values?.[field.key]; const error = errors[`${section.key}.${field.key}`]; const common = { label, description: localized(field.helpText) || undefined, withAsterisk: field.required, error, // Read-only fields come from the user account and must not be edited. disabled: disabled || field.readOnly, }; const span = field.type === 'TEXTAREA' ? 12 : 6; // Same field the profile Address tab collects — give it the same // searchable, flag-labeled picker instead of a plain option list. const isNationality = field.key === 'nationality' || label.toLowerCase().includes('nationality'); // Options here can't be seeded statically — they're the applicant's // own vessel register, so this overrides whatever type the backend // configured, the same way nationality overrides SELECT above. const isVesselPicker = field.key === 'vesselId' || field.key === 'vessel' || /^(select\s+)?vessel$/i.test(label.trim()); return ( {isNationality ? ( onChange(field.key, v)} /> ) : isVesselPicker ? ( // ponytail: swapped for a plain text input while testing — // vessels list is empty until the backend seed exists. Paste a // vessel id here to exercise the autofill; restore the Select // below once GET /vessels/mine returns real rows. // ({ value: o.value, label: localized(o.label), }))} value={(value as string) ?? null} onChange={(v) => onChange(field.key, v)} clearable={!field.required} /> ) : field.type === 'BOOLEAN' ? ( onChange(field.key, e.currentTarget.checked)} mt="md" /> ) : field.type === 'NUMBER' || field.type === 'MONEY' ? ( onChange(field.key, v === '' ? null : Number(v))} // Deliberately not clamped with min/max: Mantine would rewrite // the entered figure on blur, quietly turning a capital of // 900,000 into the 1,500,000 threshold. Validation reports the // problem instead, leaving what the applicant typed intact. thousandSeparator={field.type === 'MONEY' ? ',' : undefined} /> ) : field.type === 'DATE' ? ( onChange(field.key, e.currentTarget.value)} /> ) : field.type === 'TEXTAREA' ? (