import { ComponentType, type EastingNorthingFieldComponent } from '@defra/forms-model' import { type LanguageMessages, type ObjectSchema } from 'joi' import lowerFirst from 'lodash/lowerFirst.js' import { ComponentCollection } from '~/src/server/plugins/engine/components/ComponentCollection.js' import { FormComponent, isFormState } from '~/src/server/plugins/engine/components/FormComponent.js' import { deduplicateErrorsByHref, getLocationFieldViewModel } from '~/src/server/plugins/engine/components/LocationFieldHelpers.js' import { NumberField } from '~/src/server/plugins/engine/components/NumberField.js' import { createLowerFirstExpression } from '~/src/server/plugins/engine/components/helpers/index.js' import { type EastingNorthingState } from '~/src/server/plugins/engine/components/types.js' import { messageTemplate } from '~/src/server/plugins/engine/pageControllers/validationOptions.js' import { type ErrorMessageTemplateList, type FormPayload, type FormState, type FormStateValue, type FormSubmissionError, type FormSubmissionState } from '~/src/server/plugins/engine/types.js' import { convertToLanguageMessages } from '~/src/server/utils/type-utils.js' // British National Grid coordinate limits const DEFAULT_EASTING_MIN = 0 const DEFAULT_EASTING_MAX = 700000 const DEFAULT_NORTHING_MIN = 0 const DEFAULT_NORTHING_MAX = 1300000 export class EastingNorthingField extends FormComponent { declare options: EastingNorthingFieldComponent['options'] declare formSchema: ObjectSchema declare stateSchema: ObjectSchema declare collection: ComponentCollection constructor( def: EastingNorthingFieldComponent, props: ConstructorParameters[1] ) { super(def, props) const { name, options, schema } = def const isRequired = options.required !== false const eastingMin = schema?.easting?.min ?? DEFAULT_EASTING_MIN const eastingMax = schema?.easting?.max ?? DEFAULT_EASTING_MAX const northingMin = schema?.northing?.min ?? DEFAULT_NORTHING_MIN const northingMax = schema?.northing?.max ?? DEFAULT_NORTHING_MAX const eastingRequired = 'Enter easting' const northingRequired = 'Enter northing' const eastingDigitsMessage = `{{#label}} for ${lowerFirst(this.label)} must be between 1 and 6 digits` const northingDigitsMessage = `{{#label}} for ${lowerFirst(this.label)} must be between 1 and 7 digits` const customValidationMessages: LanguageMessages = convertToLanguageMessages({ 'any.required': eastingRequired, 'number.base': eastingRequired, 'number.min': `{{#label}} for ${lowerFirst(this.label)} must be between {{#limit}} and ${eastingMax}`, 'number.max': `{{#label}} for ${lowerFirst(this.label)} must be between ${eastingMin} and {{#limit}}`, 'number.precision': eastingDigitsMessage, 'number.integer': eastingDigitsMessage, 'number.unsafe': eastingDigitsMessage }) const northingValidationMessages: LanguageMessages = convertToLanguageMessages({ 'any.required': northingRequired, 'number.base': northingRequired, 'number.min': `{{#label}} for ${lowerFirst(this.label)} must be between {{#limit}} and ${northingMax}`, 'number.max': `{{#label}} for ${lowerFirst(this.label)} must be between ${northingMin} and {{#limit}}`, 'number.precision': northingDigitsMessage, 'number.integer': northingDigitsMessage, 'number.unsafe': northingDigitsMessage }) this.collection = new ComponentCollection( [ { type: ComponentType.NumberField, name: `${name}__easting`, title: 'Easting', schema: { min: eastingMin, max: eastingMax, precision: 0 }, options: { required: isRequired, optionalText: true, classes: 'govuk-input--width-10', customValidationMessages } }, { type: ComponentType.NumberField, name: `${name}__northing`, title: 'Northing', schema: { min: northingMin, max: northingMax, precision: 0 }, options: { required: isRequired, optionalText: true, classes: 'govuk-input--width-10', customValidationMessages: northingValidationMessages } } ], { ...props, parent: this }, { peers: [`${name}__easting`, `${name}__northing`] } ) this.options = options this.formSchema = this.collection.formSchema this.stateSchema = this.collection.stateSchema } getFormValueFromState(state: FormSubmissionState) { const value = super.getFormValueFromState(state) return EastingNorthingField.isEastingNorthing(value) ? value : undefined } getDisplayStringFromFormValue( value: EastingNorthingState | undefined ): string { if (!value) { return '' } // CYA page format: <> return `${value.easting}, ${value.northing}` } getDisplayStringFromState(state: FormSubmissionState) { const value = this.getFormValueFromState(state) return this.getDisplayStringFromFormValue(value) } getContextValueFromFormValue( value: EastingNorthingState | undefined ): string | null { if (!value) { return null } return `Easting: ${value.easting}\nNorthing: ${value.northing}` } getContextValueFromState(state: FormSubmissionState) { const value = this.getFormValueFromState(state) return this.getContextValueFromFormValue(value) } getViewModel(payload: FormPayload, errors?: FormSubmissionError[]) { const viewModel = super.getViewModel(payload, errors) return getLocationFieldViewModel(this, viewModel, payload, errors) } getViewErrors( errors?: FormSubmissionError[] ): FormSubmissionError[] | undefined { const allErrors = this.getErrors(errors) return deduplicateErrorsByHref(allErrors) } isState(value?: FormStateValue | FormState) { return EastingNorthingField.isEastingNorthing(value) } /** * For error preview page that shows all possible errors on a component */ getAllPossibleErrors(): ErrorMessageTemplateList { return EastingNorthingField.getAllPossibleErrors() } /** * Static version of getAllPossibleErrors that doesn't require a component instance. */ static getAllPossibleErrors(): ErrorMessageTemplateList { return { baseErrors: [ { type: 'required', template: messageTemplate.required }, { type: 'eastingFormat', template: createLowerFirstExpression( 'Easting for {{lowerFirst(#title)}} must be between 1 and 6 digits' ) }, { type: 'northingFormat', template: createLowerFirstExpression( 'Northing for {{lowerFirst(#title)}} must be between 1 and 7 digits' ) } ], advancedSettingsErrors: [ { type: 'eastingMin', template: createLowerFirstExpression( `Easting for {{lowerFirst(#title)}} must be between ${DEFAULT_EASTING_MIN} and ${DEFAULT_EASTING_MAX}` ) }, { type: 'eastingMax', template: createLowerFirstExpression( `Easting for {{lowerFirst(#title)}} must be between ${DEFAULT_EASTING_MIN} and ${DEFAULT_EASTING_MAX}` ) }, { type: 'northingMin', template: createLowerFirstExpression( `Northing for {{lowerFirst(#title)}} must be between ${DEFAULT_NORTHING_MIN} and ${DEFAULT_NORTHING_MAX}` ) }, { type: 'northingMax', template: createLowerFirstExpression( `Northing for {{lowerFirst(#title)}} must be between ${DEFAULT_NORTHING_MIN} and ${DEFAULT_NORTHING_MAX}` ) } ] } } static isEastingNorthing( value?: FormStateValue | FormState ): value is EastingNorthingState { return ( isFormState(value) && NumberField.isNumber(value.easting) && NumberField.isNumber(value.northing) ) } }