From 3e73c4be859ffe10f455361e22b2184eb247bfe0 Mon Sep 17 00:00:00 2001 From: David Inga Date: Tue, 17 Oct 2023 10:06:37 +0200 Subject: [PATCH] fix intervention dto in creation when location type changed --- .../interventions/form/component.tsx | 167 +----------------- .../interventions/form/schema-validation.ts | 165 +++++++++++++++++ .../containers/interventions/form/types.ts | 4 + .../[scenarioId]/interventions/new.tsx | 10 +- 4 files changed, 181 insertions(+), 165 deletions(-) create mode 100644 client/src/containers/interventions/form/schema-validation.ts create mode 100644 client/src/containers/interventions/form/types.ts diff --git a/client/src/containers/interventions/form/component.tsx b/client/src/containers/interventions/form/component.tsx index 4f6ecae13..72928bfc1 100644 --- a/client/src/containers/interventions/form/component.tsx +++ b/client/src/containers/interventions/form/component.tsx @@ -4,7 +4,6 @@ import { useForm, Controller } from 'react-hook-form'; import { RadioGroup, Disclosure } from '@headlessui/react'; import { yupResolver } from '@hookform/resolvers/yup'; import { PlusIcon, MinusIcon } from '@heroicons/react/solid'; -import * as yup from 'yup'; import classNames from 'classnames'; import { sortBy, omit } from 'lodash-es'; import toast from 'react-hot-toast'; @@ -12,6 +11,7 @@ import toast from 'react-hot-toast'; import { InterventionTypes, LocationTypes, InfoTooltip } from '../enums'; import InterventionTypeIcon from './intervention-type-icon'; +import schemaValidation from './schema-validation'; import { useIndicators } from 'hooks/indicators'; import { useSuppliersTypes, useUnknowSupplier } from 'hooks/suppliers'; @@ -33,6 +33,7 @@ import { recursiveMap, recursiveSort } from 'components/tree-select/utils'; import type { Option } from 'components/forms/select'; import type { Intervention, InterventionFormData } from '../types'; +import type { SubSchema } from './types'; const DISABLED_LOCATION_TYPES = [LocationTypes.unknown, LocationTypes.countryOfDelivery]; @@ -43,164 +44,6 @@ type InterventionFormProps = { onSubmit?: (interventionFormData: InterventionFormData) => void; }; -const optionSchema = yup - .object({ - label: yup.string(), - value: yup.string(), - }) - .default(undefined); - -const locationTypeSchema = yup - .object({ - label: yup.string().nullable(), - value: yup.mixed(), - }) - .default(undefined); - -const schemaValidation = yup.object({ - title: yup.string().label('Title').max(60).required(), - volume: yup.number().optional().typeError('Volume should be a number'), - interventionType: yup - .string() - .label('Intervention type') - .required('Type of intervention is required'), - startYear: yup - .object({ - label: yup.string(), - value: yup.number(), - }) - .label('Start year') - .required() - .typeError('Start should be a number'), - endYear: yup - .object({ - label: yup.string(), - value: yup.number(), - }) - .label('End year') - .optional() - .typeError('Start should be a number'), - percentage: yup - .number() - .label('Percentage') - .moreThan(0) - .max(100) - .required() - .typeError('Percentage should be a number greater than 0 and less or equal than 100'), - scenarioId: yup.string().label('Scenario ID').required(), - - // Filters - materialIds: yup.array().label('Material IDs').of(optionSchema).required(), - businessUnitIds: yup.array().label('Business Unit IDs').of(optionSchema), - t1SupplierIds: yup.array().label('T1 Supplier IDs').of(optionSchema), - producerIds: yup.array().label('Producer IDs').of(optionSchema), - adminRegionIds: yup.array().label('Admin region IDs').of(optionSchema), - - // Supplier - newT1SupplierId: optionSchema.label('New T1 Supplier ID').required(), - newProducerId: optionSchema.label('New producer ID').required(), - - // Location - newLocationType: locationTypeSchema.label('New location type').when('interventionType', { - is: (interventionType: InterventionTypes) => { - return [InterventionTypes.Material, InterventionTypes.SupplierLocation].includes( - interventionType, - ); - }, - then: locationTypeSchema.required(), - otherwise: locationTypeSchema.notRequired(), - }), - newLocationCountryInput: optionSchema.label('New location Country').when('interventionType', { - is: (interventionType: InterventionTypes) => - [InterventionTypes.Material, InterventionTypes.SupplierLocation].includes(interventionType), - then: (schema) => schema.required('Country field is required'), - otherwise: (schema) => schema.nullable(), - }), - - cityAddressCoordinates: yup - .string() - .label('City, addres or coordinates') - .when('newLocationType', { - is: (newLocationType) => - [LocationTypes.aggregationPoint, LocationTypes.pointOfProduction].includes( - newLocationType?.value, - ), - then: (schema) => - schema - .test('is-coordinates', 'Coordinates should be valid (-90/90, -180/180)', (value) => { - if (!isCoordinates(value)) { - return true; - } - const [lat, lng] = value.split(',').map((coordinate) => parseFloat(coordinate)); - return lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180; - }) - .required('City, address or coordinates is required'), - otherwise: (schema) => schema.nullable(), - }), - - // location region - newLocationAdminRegionInput: optionSchema.when('newLocationType', { - is: (newLocationType) => - [LocationTypes.administrativeRegionOfProduction].includes(newLocationType?.value), - then: (schema) => schema.required('Country region is required').nullable(), - otherwise: (schema) => schema.nullable(), - }), - - // New material - newMaterialId: yup - .array() - .label('New material') - .of(optionSchema) - .when('interventionType', (interventionType) => { - if (InterventionTypes.Material === interventionType) { - return yup.array().of(optionSchema).required('New material field is required'); - } - - return yup.array().of(optionSchema).nullable(); - }), - newLocationAddressInput: yup.string().label('Address').nullable(), - newLocationLongitude: yup - .number() - .label('Longitude') - .when('newLocationType', { - is: (newLocationType) => - [LocationTypes.aggregationPoint, LocationTypes.pointOfProduction].includes( - newLocationType?.value, - ), - then: (schema) => schema.min(-180).max(180).required('Longitude field is required'), - otherwise: (schema) => schema.nullable(), - }) - .typeError('Longitude should be a number'), - newLocationLatitude: yup - .number() - .label('Latitude') - .when('newLocationType', { - is: (newLocationType) => - [LocationTypes.aggregationPoint, LocationTypes.pointOfProduction].includes( - newLocationType?.value, - ), - then: (schema) => schema.min(-90).max(90).required('Latitude field is required'), - otherwise: (schema) => schema.nullable(), - }) - .typeError('Latitude should be a number'), - - // Coefficients - coefficients: yup.lazy((coefficientObject = {}) => { - const schema = Object.keys(coefficientObject).reduce( - (prevValue, currentValue) => ({ - ...prevValue, - [currentValue]: yup.lazy((v) => { - if (v === '') return yup.string().required('This coefficient is required.'); - - return yup.number().typeError('Coefficient should be a number'); - }), - }), - {}, - ); - return yup.object(schema); - }), -}); - const LABEL_CLASSNAMES = 'text-sm'; const TYPES_OF_INTERVENTIONS = Object.values(InterventionTypes).map((interventionType) => ({ @@ -208,8 +51,6 @@ const TYPES_OF_INTERVENTIONS = Object.values(InterventionTypes).map((interventio label: interventionType, })); -type SubSchema = yup.InferType; - const InterventionForm: React.FC = ({ intervention, isSubmitting, @@ -524,8 +365,8 @@ const InterventionForm: React.FC = ({ ].includes(locationType?.value) ) { resetField('cityAddressCoordinates', { defaultValue: null }); - resetField('newLocationLatitude', { defaultValue: 0 }); - resetField('newLocationLongitude', { defaultValue: 0 }); + resetField('newLocationLatitude', { defaultValue: null }); + resetField('newLocationLongitude', { defaultValue: null }); } }, [locationType, resetField, setValue]); diff --git a/client/src/containers/interventions/form/schema-validation.ts b/client/src/containers/interventions/form/schema-validation.ts new file mode 100644 index 000000000..11b035843 --- /dev/null +++ b/client/src/containers/interventions/form/schema-validation.ts @@ -0,0 +1,165 @@ +import * as yup from 'yup'; + +import { InterventionTypes, LocationTypes } from '../enums'; + +import { isCoordinates } from 'utils/coordinates'; + +const optionSchema = yup + .object({ + label: yup.string(), + value: yup.string(), + }) + .default(undefined); + +const locationTypeSchema = yup + .object({ + label: yup.string().nullable(), + value: yup.mixed(), + }) + .default(undefined); + +const schemaValidation = yup.object({ + title: yup.string().label('Title').max(60).required(), + volume: yup.number().optional().typeError('Volume should be a number'), + interventionType: yup + .string() + .label('Intervention type') + .required('Type of intervention is required'), + startYear: yup + .object({ + label: yup.string(), + value: yup.number(), + }) + .label('Start year') + .required() + .typeError('Start should be a number'), + endYear: yup + .object({ + label: yup.string(), + value: yup.number(), + }) + .label('End year') + .optional() + .typeError('Start should be a number'), + percentage: yup + .number() + .label('Percentage') + .moreThan(0) + .max(100) + .required() + .typeError('Percentage should be a number greater than 0 and less or equal than 100'), + scenarioId: yup.string().label('Scenario ID').required(), + + // Filters + materialIds: yup.array().label('Material IDs').of(optionSchema).required(), + businessUnitIds: yup.array().label('Business Unit IDs').of(optionSchema), + t1SupplierIds: yup.array().label('T1 Supplier IDs').of(optionSchema), + producerIds: yup.array().label('Producer IDs').of(optionSchema), + adminRegionIds: yup.array().label('Admin region IDs').of(optionSchema), + + // Supplier + newT1SupplierId: optionSchema.label('New T1 Supplier ID').required(), + newProducerId: optionSchema.label('New producer ID').required(), + + // Location + newLocationType: locationTypeSchema.label('New location type').when('interventionType', { + is: (interventionType: InterventionTypes) => { + return [InterventionTypes.Material, InterventionTypes.SupplierLocation].includes( + interventionType, + ); + }, + then: locationTypeSchema.required(), + otherwise: locationTypeSchema.notRequired(), + }), + newLocationCountryInput: optionSchema.label('New location Country').when('interventionType', { + is: (interventionType: InterventionTypes) => + [InterventionTypes.Material, InterventionTypes.SupplierLocation].includes(interventionType), + then: (schema) => schema.required('Country field is required'), + otherwise: (schema) => schema.nullable(), + }), + + cityAddressCoordinates: yup + .string() + .label('City, addres or coordinates') + .when('newLocationType', { + is: (newLocationType) => + [LocationTypes.aggregationPoint, LocationTypes.pointOfProduction].includes( + newLocationType?.value, + ), + then: (schema) => + schema + .test('is-coordinates', 'Coordinates should be valid (-90/90, -180/180)', (value) => { + if (!isCoordinates(value)) { + return true; + } + const [lat, lng] = value.split(',').map((coordinate) => parseFloat(coordinate)); + return lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180; + }) + .required('City, address or coordinates is required'), + otherwise: (schema) => schema.nullable(), + }), + + // location region + newLocationAdminRegionInput: optionSchema.when('newLocationType', { + is: (newLocationType) => + [LocationTypes.administrativeRegionOfProduction].includes(newLocationType?.value), + then: (schema) => schema.required('Country region is required').nullable(), + otherwise: (schema) => schema.nullable(), + }), + + // New material + newMaterialId: yup + .array() + .label('New material') + .of(optionSchema) + .when('interventionType', (interventionType) => { + if (InterventionTypes.Material === interventionType) { + return yup.array().of(optionSchema).required('New material field is required'); + } + + return yup.array().of(optionSchema).nullable(); + }), + newLocationAddressInput: yup.string().label('Address').nullable(), + newLocationLongitude: yup + .number() + .label('Longitude') + .when('newLocationType', { + is: (newLocationType) => + [LocationTypes.aggregationPoint, LocationTypes.pointOfProduction].includes( + newLocationType?.value, + ), + then: (schema) => schema.min(-180).max(180).required('Longitude field is required'), + otherwise: (schema) => schema.nullable(), + }) + .typeError('Longitude should be a number'), + newLocationLatitude: yup + .number() + .label('Latitude') + .when('newLocationType', { + is: (newLocationType) => + [LocationTypes.aggregationPoint, LocationTypes.pointOfProduction].includes( + newLocationType?.value, + ), + then: (schema) => schema.min(-90).max(90).required('Latitude field is required'), + otherwise: (schema) => schema.nullable(), + }) + .typeError('Latitude should be a number'), + + // Coefficients + coefficients: yup.lazy((coefficientObject = {}) => { + const schema = Object.keys(coefficientObject).reduce( + (prevValue, currentValue) => ({ + ...prevValue, + [currentValue]: yup.lazy((v) => { + if (v === '') return yup.string().required('This coefficient is required.'); + + return yup.number().typeError('Coefficient should be a number'); + }), + }), + {}, + ); + return yup.object(schema); + }), +}); + +export default schemaValidation; diff --git a/client/src/containers/interventions/form/types.ts b/client/src/containers/interventions/form/types.ts new file mode 100644 index 000000000..0ff1cd669 --- /dev/null +++ b/client/src/containers/interventions/form/types.ts @@ -0,0 +1,4 @@ +import type * as yup from 'yup'; +import type schemaValidation from './schema-validation'; + +export type SubSchema = yup.InferType; diff --git a/client/src/pages/data/scenarios/[scenarioId]/interventions/new.tsx b/client/src/pages/data/scenarios/[scenarioId]/interventions/new.tsx index 3a105d540..a0a286c46 100644 --- a/client/src/pages/data/scenarios/[scenarioId]/interventions/new.tsx +++ b/client/src/pages/data/scenarios/[scenarioId]/interventions/new.tsx @@ -2,6 +2,7 @@ import { useCallback } from 'react'; import Head from 'next/head'; import router, { useRouter } from 'next/router'; import toast from 'react-hot-toast'; +import omitBy from 'lodash-es/omitBy'; import { useCreateNewIntervention } from 'hooks/interventions'; import CleanLayout from 'layouts/clean'; @@ -10,7 +11,7 @@ import { parseInterventionFormDataToDto } from 'containers/interventions/utils'; import BackLink from 'components/back-link'; import { handleResponseError } from 'services/api'; -import type { InterventionFormData } from 'containers/interventions/types'; +import type { InterventionDto, InterventionFormData } from 'containers/interventions/types'; const CreateInterventionPage: React.FC = () => { const { query } = useRouter(); @@ -28,7 +29,12 @@ const CreateInterventionPage: React.FC = () => { const handleSubmit = useCallback( (interventionFormData: InterventionFormData) => { const interventionDto = parseInterventionFormDataToDto(interventionFormData); - createIntervention.mutate(interventionDto); + // for the creation remove null or undefined values + const interventionDtoCleaned = omitBy( + interventionDto, + (value) => value === null || value === undefined, + ); + createIntervention.mutate(interventionDtoCleaned as InterventionDto); }, [createIntervention], );