From f91d4194b8ac44a79408c17b742ed0fed6e02f80 Mon Sep 17 00:00:00 2001 From: KonstanceH Date: Mon, 26 Aug 2024 10:18:49 +0000 Subject: [PATCH 01/18] adding in integration changes --- pkg/handlers/internalapi/moves.go | 29 ++ .../shipment/shipment_updater.go | 2 +- .../MobileHomeShipmentForm.jsx | 324 ++++++++++++++++++ .../MobileHomeShipmentForm.module.scss | 121 +++++++ .../MobileHomeShipmentForm.stories.jsx | 83 +++++ .../MobileHomeShipmentForm.test.jsx | 116 +++++++ .../MtoShipmentForm/MtoShipmentForm.jsx | 4 +- .../MtoShipmentForm/getShipmentOptions.js | 16 + .../MobileHomeShipmentCard.jsx | 151 ++++++++ .../Customer/Review/Summary/Summary.jsx | 39 +++ .../MobileHomeInfoModal.jsx | 35 ++ .../ShipmentContainer/ShipmentContainer.jsx | 1 + src/components/ShipmentList/ShipmentList.jsx | 8 +- .../ShipmentList/ShipmentList.module.scss | 4 + .../ShipmentTag/ShipmentTag.module.scss | 3 + src/constants/routes.js | 1 + src/constants/shipments.js | 1 + src/content/shipments.js | 3 + src/pages/MyMove/CreateOrEditMtoShipment.jsx | 12 + src/pages/MyMove/Home/MoveHome.jsx | 3 +- .../MobileHomeShipmentCreate.jsx | 241 +++++++++++++ .../MobileHomeShipmentCreate.test.jsx | 263 ++++++++++++++ .../MobileHomeShipmentLocationInfo.jsx | 47 +++ .../MobileHomeShipmentLocationInfo.test.jsx | 247 +++++++++++++ src/pages/MyMove/SelectShipmentType.jsx | 35 ++ src/scenes/MyMove/index.jsx | 7 + src/shared/constants.js | 3 + src/shared/styles/_custom.scss | 12 +- src/shared/styles/colors.scss | 1 + src/types/shipment.js | 14 + src/utils/shipmentInfo.js | 1 + src/utils/shipments.js | 4 + 32 files changed, 1822 insertions(+), 9 deletions(-) create mode 100644 src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.jsx create mode 100644 src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.module.scss create mode 100644 src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.stories.jsx create mode 100644 src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.test.jsx create mode 100644 src/components/Customer/Review/ShipmentCard/MobileHomeShipmentCard/MobileHomeShipmentCard.jsx create mode 100644 src/components/Customer/modals/MobileHomeInfoModal/MobileHomeInfoModal.jsx create mode 100644 src/pages/MyMove/MobileHome/MobileHomeShipmentCreate/MobileHomeShipmentCreate.jsx create mode 100644 src/pages/MyMove/MobileHome/MobileHomeShipmentCreate/MobileHomeShipmentCreate.test.jsx create mode 100644 src/pages/MyMove/MobileHome/MobileHomeShipmentLocationInfo/MobileHomeShipmentLocationInfo.jsx create mode 100644 src/pages/MyMove/MobileHome/MobileHomeShipmentLocationInfo/MobileHomeShipmentLocationInfo.test.jsx diff --git a/pkg/handlers/internalapi/moves.go b/pkg/handlers/internalapi/moves.go index 5deb85a9d97..6188b30153f 100644 --- a/pkg/handlers/internalapi/moves.go +++ b/pkg/handlers/internalapi/moves.go @@ -368,6 +368,18 @@ func (h GetAllMovesHandler) Handle(params moveop.GetAllMovesParams) middleware.R } /** End of Feature Flag Block **/ + /** Feature Flag - Mobile Home Shipment **/ + featureFlagNameMH := "mobileHome" + isMobileHomeFeatureOn := false + flagMH, err := h.FeatureFlagFetcher().GetBooleanFlagForUser(params.HTTPRequest.Context(), appCtx, featureFlagNameMH, map[string]string{}) + if err != nil { + appCtx.Logger().Error("Error fetching feature flag", zap.String("featureFlagKey", featureFlagName), zap.Error(err)) + isMobileHomeFeatureOn = false + } else { + isMobileHomeFeatureOn = flagMH.Match + } + /** End of Feature Flag Block **/ + for _, move := range movesList { /** Feature Flag - Boat Shipment **/ @@ -448,6 +460,23 @@ func (h GetAllMovesHandler) Handle(params moveop.GetAllMovesParams) middleware.R } /** End of Feature Flag Block **/ + /** Feature Flag - Mobile Home Shipment **/ + if !isMobileHomeFeatureOn { + var filteredShipments models.MTOShipments + if move.MTOShipments != nil { + filteredShipments = models.MTOShipments{} + } + for i, shipment := range move.MTOShipments { + if shipment.ShipmentType == models.MTOShipmentTypeMobileHome { + continue + } + + filteredShipments = append(filteredShipments, move.MTOShipments[i]) + } + move.MTOShipments = filteredShipments + } + /** End of Feature Flag Block **/ + previousMovesList = append(previousMovesList, move) } } diff --git a/pkg/services/orchestrators/shipment/shipment_updater.go b/pkg/services/orchestrators/shipment/shipment_updater.go index b8f3d9d30d2..574bad183f0 100644 --- a/pkg/services/orchestrators/shipment/shipment_updater.go +++ b/pkg/services/orchestrators/shipment/shipment_updater.go @@ -80,7 +80,7 @@ func (s *shipmentUpdater) UpdateShipment(appCtx appcontext.AppContext, shipment mtoShipment.BoatShipment = boatShipment return nil - } else if shipment.ShipmentType == models.MTOShipmentTypeMobileHome { + } else if shipment.ShipmentType == models.MTOShipmentTypeMobileHome && shipment.MobileHome != nil { shipment.MobileHome.ShipmentID = mtoShipment.ID shipment.MobileHome.Shipment = *mtoShipment diff --git a/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.jsx b/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.jsx new file mode 100644 index 00000000000..8c4d45885bf --- /dev/null +++ b/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.jsx @@ -0,0 +1,324 @@ +import React from 'react'; +import { func } from 'prop-types'; +import * as Yup from 'yup'; +import { Formik, Field } from 'formik'; +import { Button, Form, Label, Textarea } from '@trussworks/react-uswds'; +import classnames from 'classnames'; + +import styles from './MobileHomeShipmentForm.module.scss'; + +import SectionWrapper from 'components/Customer/SectionWrapper'; +import Hint from 'components/Hint'; +import Fieldset from 'shared/Fieldset'; +import formStyles from 'styles/form.module.scss'; +import { ShipmentShape } from 'types/shipment'; +import TextField from 'components/form/fields/TextField/TextField'; +import MaskedTextField from 'components/form/fields/MaskedTextField/MaskedTextField'; +import Callout from 'components/Callout'; +import { ErrorMessage } from 'components/form/index'; +import { convertInchesToFeetAndInches } from 'utils/formatMtoShipment'; + +const currentYear = new Date().getFullYear(); +const maxYear = currentYear + 2; + +const validationShape = { + year: Yup.number().required('Required').min(1700, 'Invalid year').max(maxYear, 'Invalid year'), + make: Yup.string().required('Required'), + model: Yup.string().required('Required'), + lengthFeet: Yup.number() + .min(0) + .nullable() + .when('lengthInches', { + is: (lengthInches) => !lengthInches, + then: (schema) => schema.required('Required'), + otherwise: (schema) => schema.notRequired(), + }), + lengthInches: Yup.number(), + widthFeet: Yup.number() + .min(0) + .nullable() + .when('widthInches', { + is: (widthInches) => !widthInches, + then: (schema) => schema.required('Required'), + otherwise: (schema) => schema.notRequired(), + }), + widthInches: Yup.number().min(0), + heightFeet: Yup.number() + .min(0) + .nullable() + .when('heightInches', { + is: (heightInches) => !heightInches, + then: (schema) => schema.required('Required'), + otherwise: (schema) => schema.notRequired(), + }), + heightInches: Yup.number().min(0), + customerRemarks: Yup.string(), +}; + +const MobileHomeShipmentForm = ({ mtoShipment, onBack, onSubmit }) => { + const { year, make, model, lengthInInches, widthInInches, heightInInches } = mtoShipment?.mobileHomeShipment || {}; + + const length = convertInchesToFeetAndInches(lengthInInches); + const width = convertInchesToFeetAndInches(widthInInches); + const height = convertInchesToFeetAndInches(heightInInches); + + const initialValues = { + year: year?.toString() || null, + make: make || '', + model: model || '', + lengthFeet: length.feet, + lengthInches: length.inches, + widthFeet: width.feet, + widthInches: width.inches, + heightFeet: height.feet, + heightInches: height.inches, + customerRemarks: mtoShipment?.customerRemarks, + }; + + return ( + + {({ isValid, handleSubmit, values, errors, touched, setFieldTouched, setFieldError, validateForm }) => { + const lengthHasError = !!( + (touched.lengthFeet && errors.lengthFeet) || + (touched.lengthInches && errors.lengthFeet) + ); + const widthHasError = !!((touched.widthFeet && errors.widthFeet) || (touched.widthInches && errors.widthFeet)); + const heightHasError = !!( + (touched.heightFeet && errors.heightFeet) || + (touched.heightInches && errors.heightFeet) + ); + if (touched.lengthInches && !touched.lengthFeet) { + setFieldTouched('lengthFeet', true); + } + if (touched.widthInches && !touched.widthFeet) { + setFieldTouched('widthFeet', true); + } + if (touched.heightInches && !touched.heightFeet) { + setFieldTouched('heightFeet', true); + } + // manually turn off 'required' error when page loads if field is empty. + if (values.year === null && !touched.year && errors.year === 'Required') { + setFieldError('year', null); + } + return ( +
+
+ +

Mobile home Information

+
+
+ { + setFieldError('year', null); + }} + onBlur={() => { + setFieldTouched('year', true); + setFieldError('year', null); + validateForm(); + }} + required + /> +
+
+
+ + +
+
+ +

Mobile Home Dimensions

+

Enter all of the dimensions of the mobile home.

+
+
+
+ Length + Required +
+
+
+ +
+
+ +
+
+
+
+
+ Width + Required +
+
+
+ +
+
+ +
+
+
+
+
+ Height + Required +
+
+
+ +
+
+ +
+
+
+
+
+ +
+ Remarks Optional +
+ } + > + + + + Examples +
    +
  • + Dimensions of the mobile home on the trailer are signigicantly different than one would expect + given their individual dimensions +
  • + +
  • Access info for your origin or destination address/marina
  • +
+
+ + + +

250 characters

+
+ + +
+ + +
+ + + ); + }} +
+ ); +}; + +MobileHomeShipmentForm.propTypes = { + mtoShipment: ShipmentShape, + onBack: func.isRequired, + onSubmit: func.isRequired, +}; + +MobileHomeShipmentForm.defaultProps = { + mtoShipment: undefined, +}; + +export default MobileHomeShipmentForm; diff --git a/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.module.scss b/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.module.scss new file mode 100644 index 00000000000..09d5efe98f8 --- /dev/null +++ b/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.module.scss @@ -0,0 +1,121 @@ +@import 'shared/styles/_basics'; +@import 'shared/styles/_variables'; +@import 'shared/styles/colors'; + +.formContainer { + :global(.usa-legend) { + max-width: none; + } + .formTextFieldWrapper { + :global(.usa-form-group) { + margin-top: 0; + } + .hide { + display: none; + } + } + .formFieldContainer { + margin-top: 1.6rem; + margin-bottom: 0; + padding-bottom: 0; + border: none; + } + .form { + max-width: none; + + :global(.usa-input) { + @include u-display('inline-block'); + width: unset; + } + + :global(.usa-form-group--error), + :global(.usa-form-group.warning) { + margin-top: 1.6rem; + } + + :global(.usa-form-group:first-of-type .usa-label) { + margin-top: 0; + } + + // last section wrapper on mobile shouldn't render divider + @include at-media-max('tablet') { + .sectionWrapper:nth-last-child(2) { + border-bottom: none; + @include u-padding-bottom(0); + @include u-margin-bottom(3); + } + } + } + + h2 { + @include u-margin-bottom(2.5); + } + + // fixes collapsing margins cross-browser for Storage section + h2 + fieldset legend { + @include u-padding-top(1.5); + } + + .sectionWrapper { + border-bottom: 1px solid $base-lighter; + + @include at-media-max('tablet') { + @include u-padding-bottom(4); + @include u-margin-top(4); + } + } + + .sectionWrapper:last-of-type { + border-bottom: none; + } + + fieldset { + @include u-margin-top(2); + @include u-padding-top(0); + + legend:global(.usa-label) { + @include u-margin-top(0); + } + } + + :global(.usa-label), + :global(.usa-checkbox__label) { + @include u-margin-top(2); + } + + :global(.usa-label.usa-label--error) { + @include u-margin-top(0); + } + + :global(.usa-legend) { + @include u-margin-top(0); + } + + .innerHint { + @include u-margin-top(1); + } + + .hint { + @include u-margin-top(2); + } +} + +.buttonContainer { + @include u-display(flex); + flex-wrap: wrap; + + button:global(.usa-button) { + @include u-margin-top(2); + @include u-margin-bottom(0); + } + + @include at-media-max(mobile-lg) { + .backButton { + order: 2; + } + + .saveButton { + order: 1; + } + } +} diff --git a/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.stories.jsx b/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.stories.jsx new file mode 100644 index 00000000000..9cbee05fd9a --- /dev/null +++ b/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.stories.jsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { expect } from '@storybook/jest'; +import { action } from '@storybook/addon-actions'; +import { Grid, GridContainer } from '@trussworks/react-uswds'; +import { within, userEvent } from '@storybook/testing-library'; + +import MobileHomeShipmentForm from './MobileHomeShipmentForm'; + +export default { + title: 'Customer Components / Mobile Home Shipment / Mobile Home Shipment Form', + component: MobileHomeShipmentForm, + decorators: [ + (Story) => ( + + + + + + + + ), + ], +}; + +const Template = (args) => ; + +export const BlankMobileHomeShipmentForm = Template.bind({}); +BlankMobileHomeShipmentForm.args = { + onSubmit: action('submit button clicked'), + onBack: action('back button clicked'), + mtoShipment: { + mobileHomeShipment: { + year: '', + make: '', + model: '', + lengthInInches: null, + widthInInches: null, + heightInInches: null, + }, + }, +}; + +export const FilledMobileHomeShipmentForm = Template.bind({}); +FilledMobileHomeShipmentForm.args = { + onSubmit: action('submit button clicked'), + onBack: action('back button clicked'), + mtoShipment: { + mobileHomeShipment: { + year: 2022, + make: 'Yamaha', + model: '242X', + lengthInInches: 288, // 24 feet + widthInInches: 102, // 8 feet 6 inches + heightInInches: 84, // 7 feet + }, + }, +}; + +export const ErrorMobileHomeShipmentForm = Template.bind({}); +ErrorMobileHomeShipmentForm.args = { + onSubmit: action('submit button clicked'), + onBack: action('back button clicked'), + mtoShipment: { + mobileHomeShipment: { + year: '', + make: '', + model: '', + lengthInInches: null, + widthInInches: null, + heightInInches: null, + }, + }, +}; +ErrorMobileHomeShipmentForm.play = async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await expect(canvas.getByRole('button', { name: 'Continue' })).toBeEnabled(); + + await userEvent.click(canvas.getByRole('button', { name: 'Continue' })); +}; +ErrorMobileHomeShipmentForm.parameters = { + happo: false, +}; diff --git a/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.test.jsx b/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.test.jsx new file mode 100644 index 00000000000..21ce54b1829 --- /dev/null +++ b/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.test.jsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { render, screen, act, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import MobileHomeShipmentForm from './MobileHomeShipmentForm'; + +const mtoShipment = { + mobileHomeShipment: { + year: '2022', + make: 'Yamaha', + model: '242X', + lengthInInches: 288, // 24 feet + widthInInches: 102, // 8 feet 6 inches + heightInInches: 84, // 7 feet + }, +}; +const emptyMobileHomeInfo = { + mobileHomeShipment: { + year: '', + make: '', + model: '', + lengthInInches: 0, // 24 feet + widthInInches: 0, // 8 feet 6 inches + heightInInches: 0, // 7 feet + }, +}; + +const defaultProps = { + onSubmit: jest.fn(), + onBack: jest.fn(), + mtoShipment, +}; + +const emptyInfoProps = { + onSubmit: jest.fn(), + onBack: jest.fn(), + emptyMobileHomeInfo, +}; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('MobileHomeShipmentForm component', () => { + describe('displays form', () => { + it('renders filled form on load', async () => { + render(); + expect(await screen.getByTestId('year')).toHaveValue(mtoShipment.mobileHomeShipment.year); + expect(screen.getByTestId('make')).toHaveValue(mtoShipment.mobileHomeShipment.make); + expect(screen.getByTestId('model')).toHaveValue(mtoShipment.mobileHomeShipment.model); + expect(screen.getByTestId('lengthFeet')).toHaveValue('24'); + expect(screen.getByTestId('lengthInches')).toHaveValue('0'); + expect(screen.getByTestId('widthFeet')).toHaveValue('8'); + expect(screen.getByTestId('widthInches')).toHaveValue('6'); + expect(screen.getByTestId('heightFeet')).toHaveValue('7'); + expect(screen.getByTestId('heightInches')).toHaveValue('0'); + expect( + screen.getByLabelText( + 'Are there things about this mobile home shipment that your counselor or movers should know or discuss with you?', + ), + ).toBeVisible(); + }); + }); + + describe('validates form fields and displays error messages', () => { + it('marks required inputs when left empty', async () => { + render(); + + const requiredFields = [ + 'year', + 'make', + 'model', + 'lengthFeet', + 'lengthInches', + 'widthFeet', + 'widthInches', + 'heightFeet', + 'heightInches', + ]; + + await act(async () => { + requiredFields.forEach(async (field) => { + const input = screen.getByTestId(field); + await userEvent.clear(input); + // await userEvent.click(input); + fireEvent.blur(input); + }); + }); + + expect(screen.getAllByText('Required').length).toBe(requiredFields.length); + }); + }); + + describe('form submission', () => { + it('submits the form with valid data', async () => { + render(); + + await act(async () => { + await userEvent.click(screen.getByRole('button', { name: 'Continue' })); + }); + + expect(defaultProps.onSubmit).toHaveBeenCalled(); + }); + + it('does not submit the form with invalid data', async () => { + render(); + + await act(async () => { + await userEvent.clear(screen.getByTestId('year')); + await userEvent.click(screen.getByRole('button', { name: 'Continue' })); + }); + + expect(defaultProps.onSubmit).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/components/Customer/MtoShipmentForm/MtoShipmentForm.jsx b/src/components/Customer/MtoShipmentForm/MtoShipmentForm.jsx index 95558f26db2..616d23cebd4 100644 --- a/src/components/Customer/MtoShipmentForm/MtoShipmentForm.jsx +++ b/src/components/Customer/MtoShipmentForm/MtoShipmentForm.jsx @@ -181,7 +181,9 @@ class MtoShipmentForm extends Component { const isNTS = shipmentType === SHIPMENT_OPTIONS.NTS; const isNTSR = shipmentType === SHIPMENT_OPTIONS.NTSR; const isBoat = shipmentType === SHIPMENT_TYPES.BOAT_HAUL_AWAY || shipmentType === SHIPMENT_TYPES.BOAT_TOW_AWAY; - const shipmentNumber = shipmentType === SHIPMENT_OPTIONS.HHG || isBoat ? this.getShipmentNumber() : null; + const isMobileHome = shipmentType === SHIPMENT_TYPES.MOBILE_HOME; + const shipmentNumber = + shipmentType === SHIPMENT_OPTIONS.HHG || isBoat || isMobileHome ? this.getShipmentNumber() : null; const isRetireeSeparatee = orders.orders_type === ORDERS_TYPE.RETIREMENT || orders.orders_type === ORDERS_TYPE.SEPARATION; diff --git a/src/components/Customer/MtoShipmentForm/getShipmentOptions.js b/src/components/Customer/MtoShipmentForm/getShipmentOptions.js index 9ec932f2b93..c4bfbce1b4b 100644 --- a/src/components/Customer/MtoShipmentForm/getShipmentOptions.js +++ b/src/components/Customer/MtoShipmentForm/getShipmentOptions.js @@ -21,6 +21,15 @@ const hhgShipmentSchema = Yup.object().shape({ counselorRemarks: Yup.string(), }); +const mobileHomeShipmentLocationSchema = Yup.object().shape({ + pickup: RequiredPlaceSchema, + delivery: OptionalPlaceSchema, + secondaryPickup: AdditionalAddressSchema, + secondaryDelivery: AdditionalAddressSchema, + customerRemarks: Yup.string(), + counselorRemarks: Yup.string(), +}); + const boatShipmentLocationInfoSchema = Yup.object().shape({ pickup: RequiredPlaceSchema, delivery: OptionalPlaceSchema, @@ -84,6 +93,13 @@ function getShipmentOptions(shipmentType, userRole) { showDeliveryFields: true, }; + case SHIPMENT_OPTIONS.MOBILE_HOME: + return { + schema: mobileHomeShipmentLocationSchema, + showPickupFields: true, + showDeliveryFields: true, + }; + case SHIPMENT_TYPES.BOAT_HAUL_AWAY: case SHIPMENT_TYPES.BOAT_TOW_AWAY: return { diff --git a/src/components/Customer/Review/ShipmentCard/MobileHomeShipmentCard/MobileHomeShipmentCard.jsx b/src/components/Customer/Review/ShipmentCard/MobileHomeShipmentCard/MobileHomeShipmentCard.jsx new file mode 100644 index 00000000000..a139f557a5e --- /dev/null +++ b/src/components/Customer/Review/ShipmentCard/MobileHomeShipmentCard/MobileHomeShipmentCard.jsx @@ -0,0 +1,151 @@ +import { React } from 'react'; +import { bool, func, number } from 'prop-types'; +import { Button } from '@trussworks/react-uswds'; +import { generatePath } from 'react-router-dom'; + +import PickupDisplay from '../PickupDisplay'; +import DeliveryDisplay from '../DeliveryDisplay'; + +import styles from 'components/Customer/Review/ShipmentCard/ShipmentCard.module.scss'; +import ShipmentContainer from 'components/Office/ShipmentContainer/ShipmentContainer'; +import IncompleteShipmentToolTip from 'components/Customer/Review/IncompleteShipmentToolTip/IncompleteShipmentToolTip'; +import { customerRoutes } from 'constants/routes'; +import { SHIPMENT_OPTIONS } from 'shared/constants'; +import { ShipmentShape } from 'types/shipment'; +import { convertInchesToFeetAndInches } from 'utils/formatMtoShipment'; +import { getShipmentTypeLabel } from 'utils/shipmentDisplay'; +import { isMobileHomeShipmentComplete } from 'utils/shipments'; + +const MobileHomeShipmentCard = ({ + shipment, + shipmentNumber, + showEditAndDeleteBtn, + onEditClick, + onDeleteClick, + onIncompleteClick, + destinationLocation, + destinationZIP, + secondaryDeliveryAddress, + tertiaryDeliveryAddress, + pickupLocation, + secondaryPickupAddress, + tertiaryPickupAddress, + receivingAgent, + releasingAgent, + remarks, + requestedDeliveryDate, + requestedPickupDate, + shipmentId, +}) => { + const { moveTaskOrderID, id, shipmentType, shipmentLocator } = shipment; + const { year, make, model, lengthInInches, widthInInches, heightInInches } = shipment?.mobileHomeShipment || {}; + + const editPath = `${generatePath(customerRoutes.SHIPMENT_EDIT_PATH, { + moveId: moveTaskOrderID, + mtoShipmentId: id, + })}?shipmentNumber=${shipmentNumber}`; + + const shipmentLabel = `${getShipmentTypeLabel(shipmentType)} ${shipmentNumber}`; + const moveCodeLabel = `${shipmentLocator}`; + const shipmentIsIncomplete = !isMobileHomeShipmentComplete(shipment); + const length = convertInchesToFeetAndInches(lengthInInches); + const width = convertInchesToFeetAndInches(widthInInches); + const height = convertInchesToFeetAndInches(heightInInches); + const formattedDimensions = `${length?.feet}'${length?.inches > 0 ? ` ${length.inches}"` : ''} L x ${width?.feet}'${ + width?.inches > 0 ? ` ${width.inches}"` : '' + } W x ${height?.feet}'${height?.inches > 0 ? ` ${height.inches}"` : ''} H`; + + return ( +
+ + {shipmentIsIncomplete && ( + + )} +
+
+

{shipmentLabel}

+

#{moveCodeLabel}

+
+ {showEditAndDeleteBtn && ( +
+ + | + +
+ )} +
+ +
+ + +
+
Mobile Home year
+
{year}
+
+
+
Mobile Home make
+
{make}
+
+
+
Mobile Home model
+
{model}
+
+
+
Dimensions
+
{formattedDimensions}
+
+ {remarks && ( +
+
Remarks
+
{remarks}
+
+ )} +
+
+
+ ); +}; + +MobileHomeShipmentCard.propTypes = { + shipment: ShipmentShape.isRequired, + shipmentNumber: number, + showEditAndDeleteBtn: bool.isRequired, + onEditClick: func, + onDeleteClick: func, + onIncompleteClick: func, +}; + +MobileHomeShipmentCard.defaultProps = { + shipmentNumber: undefined, + onEditClick: undefined, + onDeleteClick: undefined, + onIncompleteClick: undefined, +}; + +export default MobileHomeShipmentCard; diff --git a/src/components/Customer/Review/Summary/Summary.jsx b/src/components/Customer/Review/Summary/Summary.jsx index 3f89f04caac..2bafeb88b1a 100644 --- a/src/components/Customer/Review/Summary/Summary.jsx +++ b/src/components/Customer/Review/Summary/Summary.jsx @@ -21,6 +21,7 @@ import NTSRShipmentCard from 'components/Customer/Review/ShipmentCard/NTSRShipme import NTSShipmentCard from 'components/Customer/Review/ShipmentCard/NTSShipmentCard/NTSShipmentCard'; import PPMShipmentCard from 'components/Customer/Review/ShipmentCard/PPMShipmentCard/PPMShipmentCard'; import BoatShipmentCard from 'components/Customer/Review/ShipmentCard/BoatShipmentCard/BoatShipmentCard'; +import MobileHomeShipmentCard from 'components/Customer/Review/ShipmentCard/MobileHomeShipmentCard/MobileHomeShipmentCard'; import SectionWrapper from 'components/Customer/SectionWrapper'; import { ORDERS_BRANCH_OPTIONS, ORDERS_PAY_GRADE_OPTIONS } from 'constants/orders'; import { customerRoutes } from 'constants/routes'; @@ -56,6 +57,7 @@ export class Summary extends Component { enableNTS: true, enableNTSR: true, enableBoat: true, + enableMobileHome: true, }; } @@ -90,6 +92,11 @@ export class Summary extends Component { enableBoat: enabled, }); }); + isBooleanFlagEnabled(FEATURE_FLAG_KEYS.MOBILE_HOME).then((enabled) => { + this.setState({ + enableMobileHome: enabled, + }); + }); } handleEditClick = (path) => { @@ -164,6 +171,7 @@ export class Summary extends Component { let hhgShipmentNumber = 0; let ppmShipmentNumber = 0; let boatShipmentNumber = 0; + let mobileHomeShipmentNumber = 0; return sortedShipments.map((shipment) => { let receivingAgent; let releasingAgent; @@ -265,6 +273,35 @@ export class Summary extends Component { /> ); } + if (shipment.shipmentType === SHIPMENT_TYPES.MOBILE_HOME) { + mobileHomeShipmentNumber += 1; + return ( + + ); + } hhgShipmentNumber += 1; return ( ); diff --git a/src/components/Customer/modals/MobileHomeInfoModal/MobileHomeInfoModal.jsx b/src/components/Customer/modals/MobileHomeInfoModal/MobileHomeInfoModal.jsx new file mode 100644 index 00000000000..ed74fa7f832 --- /dev/null +++ b/src/components/Customer/modals/MobileHomeInfoModal/MobileHomeInfoModal.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button } from '@trussworks/react-uswds'; + +import Modal, { ModalTitle, ModalClose, ModalActions, connectModal } from 'components/Modal/Modal'; + +export const MobileHomeInfoModal = ({ closeModal }) => ( + + + +

Boat & Mobile homes info

+
+

+ Mobile Home shipment +

+

This option is for privately owned mobile homes.

+ + + +
+); + +MobileHomeInfoModal.propTypes = { + closeModal: PropTypes.func, +}; + +MobileHomeInfoModal.defaultProps = { + closeModal: () => {}, +}; + +MobileHomeInfoModal.displayName = 'MobileHomeInfoModal'; + +export default connectModal(MobileHomeInfoModal); diff --git a/src/components/Office/ShipmentContainer/ShipmentContainer.jsx b/src/components/Office/ShipmentContainer/ShipmentContainer.jsx index fad22021077..3231860de51 100644 --- a/src/components/Office/ShipmentContainer/ShipmentContainer.jsx +++ b/src/components/Office/ShipmentContainer/ShipmentContainer.jsx @@ -20,6 +20,7 @@ const ShipmentContainer = ({ id, className, children, shipmentType }) => { 'container--accent--ntsr': shipmentType === SHIPMENT_OPTIONS.NTSR, 'container--accent--ppm': shipmentType === SHIPMENT_OPTIONS.PPM, 'container--accent--boat': shipmentType === SHIPMENT_OPTIONS.BOAT, + 'container--accent--mobilehome': shipmentType === SHIPMENT_OPTIONS.MOBILE_HOME, }, className, ); diff --git a/src/components/ShipmentList/ShipmentList.jsx b/src/components/ShipmentList/ShipmentList.jsx index fcfdf552b84..67a52c92c5b 100644 --- a/src/components/ShipmentList/ShipmentList.jsx +++ b/src/components/ShipmentList/ShipmentList.jsx @@ -10,7 +10,7 @@ import { shipmentTypes, WEIGHT_ADJUSTMENT } from 'constants/shipments'; import { SHIPMENT_OPTIONS, SHIPMENT_TYPES } from 'shared/constants'; import { ShipmentShape } from 'types/shipment'; import { formatWeight } from 'utils/formatters'; -import { isPPMShipmentComplete, isBoatShipmentComplete } from 'utils/shipments'; +import { isPPMShipmentComplete, isBoatShipmentComplete, isMobileHomeShipmentComplete } from 'utils/shipments'; import { shipmentIsOverweight } from 'utils/shipmentWeights'; import ToolTip from 'shared/ToolTip/ToolTip'; @@ -26,6 +26,7 @@ export const ShipmentListItem = ({ isOverweight, isMissingWeight, }) => { + const isMobileHome = shipment.shipmentType === SHIPMENT_OPTIONS.MOBILE_HOME; const isPPM = shipment.shipmentType === SHIPMENT_OPTIONS.PPM; const isBoat = shipment.shipmentType === SHIPMENT_TYPES.BOAT_TOW_AWAY || shipment.shipmentType === SHIPMENT_TYPES.BOAT_HAUL_AWAY; @@ -36,6 +37,7 @@ export const ShipmentListItem = ({ [styles[`shipment-list-item-HHG`]]: shipment.shipmentType === SHIPMENT_OPTIONS.HHG, [styles[`shipment-list-item-PPM`]]: isPPM, [styles[`shipment-list-item-Boat`]]: isBoat, + [styles[`shipment-list-item-MobileHome`]]: isMobileHome, }); const estimated = 'Estimated'; const actual = 'Actual'; @@ -191,6 +193,10 @@ const ShipmentList = ({ shipments, onShipmentClick, onDeleteClick, moveSubmitted isIncomplete = !isBoatShipmentComplete(shipment); break; + case SHIPMENT_OPTIONS.MOBILE_HOME: + isIncomplete = !isMobileHomeShipmentComplete(shipment); + break; + default: break; } diff --git a/src/components/ShipmentList/ShipmentList.module.scss b/src/components/ShipmentList/ShipmentList.module.scss index d3b872331dd..2e2e1cd6cc3 100644 --- a/src/components/ShipmentList/ShipmentList.module.scss +++ b/src/components/ShipmentList/ShipmentList.module.scss @@ -99,6 +99,10 @@ border-left: 5px solid $accent-boat; } +.shipment-list-item-MobileHome { + border-left: 5px solid $accent-mobile-home; +} + .spaceBetween { display: flex; justify-content: center; diff --git a/src/components/ShipmentTag/ShipmentTag.module.scss b/src/components/ShipmentTag/ShipmentTag.module.scss index 685bcc62e65..85d03da1b77 100644 --- a/src/components/ShipmentTag/ShipmentTag.module.scss +++ b/src/components/ShipmentTag/ShipmentTag.module.scss @@ -31,4 +31,7 @@ &.Boat { background-color: $accent-boat; } + &.MobileHome { + background-color: $accent-mobile-home; + } } diff --git a/src/constants/routes.js b/src/constants/routes.js index 03236986fa6..33b5afd2842 100644 --- a/src/constants/routes.js +++ b/src/constants/routes.js @@ -29,6 +29,7 @@ export const customerRoutes = { SHIPMENT_CREATE_PATH: '/moves/:moveId/new-shipment', SHIPMENT_EDIT_PATH: '/moves/:moveId/shipments/:mtoShipmentId/edit', SHIPMENT_BOAT_LOCATION_INFO: '/moves/:moveId/shipments/:mtoShipmentId/location-info', + SHIPMENT_MOBILE_HOME_LOCATION_INFO: '/moves/:moveId/shipments/:mtoShipmentId/location-info', SHIPMENT_PPM_ESTIMATED_WEIGHT_PATH: '/moves/:moveId/shipments/:mtoShipmentId/estimated-weight', SHIPMENT_PPM_ESTIMATED_INCENTIVE_PATH: '/moves/:moveId/shipments/:mtoShipmentId/estimated-incentive', SHIPMENT_PPM_ADVANCES_PATH: '/moves/:moveId/shipments/:mtoShipmentId/advances', diff --git a/src/constants/shipments.js b/src/constants/shipments.js index 5de4dc218ea..4d4b40a8660 100644 --- a/src/constants/shipments.js +++ b/src/constants/shipments.js @@ -9,6 +9,7 @@ export const shipmentTypes = { [SHIPMENT_OPTIONS.BOAT]: 'Boat', [SHIPMENT_TYPES.BOAT_HAUL_AWAY]: 'Boat', [SHIPMENT_TYPES.BOAT_TOW_AWAY]: 'Boat', + [SHIPMENT_TYPES.MOBILE_HOME]: 'MobileHome', }; export const shipmentModificationTypes = { diff --git a/src/content/shipments.js b/src/content/shipments.js index 02e24138ef7..2d7ecae0329 100644 --- a/src/content/shipments.js +++ b/src/content/shipments.js @@ -9,6 +9,7 @@ export const shipmentTypeLabels = { [SHIPMENT_OPTIONS.BOAT]: 'Boat', [SHIPMENT_TYPES.BOAT_HAUL_AWAY]: 'Boat', [SHIPMENT_TYPES.BOAT_TOW_AWAY]: 'Boat', + [SHIPMENT_TYPES.MOBILE_HOME]: 'Mobile Home', }; export const shipmentForm = { @@ -16,11 +17,13 @@ export const shipmentForm = { [SHIPMENT_OPTIONS.HHG]: 'Movers pack and transport this shipment', [SHIPMENT_OPTIONS.NTS]: 'Where and when should the movers pick up your personal property going into storage?', [SHIPMENT_OPTIONS.NTSR]: 'Where and when should the movers deliver your personal property from storage?', + [SHIPMENT_OPTIONS.MOBILE_HOME]: 'Where and when should the movers deliver your mobile home?', }, }; export const shipmentSectionLabels = { HHG: 'HHG shipment', + MOBILE_HOME: 'MOBILE Home shipment', HHG_INTO_NTS_DOMESTIC: 'NTS shipment', HHG_OUTOF_NTS_DOMESTIC: 'NTS-release shipment', }; diff --git a/src/pages/MyMove/CreateOrEditMtoShipment.jsx b/src/pages/MyMove/CreateOrEditMtoShipment.jsx index 7d01a3cf6f7..27f4b96a848 100644 --- a/src/pages/MyMove/CreateOrEditMtoShipment.jsx +++ b/src/pages/MyMove/CreateOrEditMtoShipment.jsx @@ -3,6 +3,7 @@ import { connect } from 'react-redux'; import { func } from 'prop-types'; import qs from 'query-string'; +import MobileHomeShipmentCreate from 'pages/MyMove/MobileHome/MobileHomeShipmentCreate/MobileHomeShipmentCreate'; import MtoShipmentForm from 'components/Customer/MtoShipmentForm/MtoShipmentForm'; import DateAndLocation from 'pages/MyMove/PPM/Booking/DateAndLocation/DateAndLocation'; import BoatShipmentCreate from 'pages/MyMove/Boat/BoatShipmentCreate/BoatShipmentCreate'; @@ -115,6 +116,17 @@ export class CreateOrEditMtoShipment extends Component { /> ); } + if (type === SHIPMENT_OPTIONS.MOBILE_HOME) { + return ( + + ); + } return ( { + const [errorMessage, setErrorMessage] = useState(null); + const [multiMove, setMultiMove] = useState(false); + // const [mobileHomeShipmentObj, setMobileHomeShipmentObj] = useState(null); + // const [setSubmitValues] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + + const navigate = useNavigate(); + const { moveId } = useParams(); + const dispatch = useDispatch(); + const location = useLocation(); + const searchParams = new URLSearchParams(location.search); + const shipmentNumber = searchParams.get('shipmentNumber'); + const isEditPage = location?.pathname?.includes('/edit'); + + const isNewShipment = !mtoShipment?.id; + + useEffect(() => { + isBooleanFlagEnabled('multi_move').then((enabled) => { + setMultiMove(enabled); + }); + }, []); + + const handleBack = () => { + if (isNewShipment) { + navigate(generatePath(customerRoutes.SHIPMENT_SELECT_TYPE_PATH, { moveId })); + } else if (multiMove) { + navigate(generatePath(customerRoutes.MOVE_HOME_PATH, { moveId })); + } else { + navigate(generalRoutes.HOME_PATH); + } + }; + + const onShipmentSaveSuccess = (response, setSubmitting) => { + // Update submitting state + setSubmitting(false); + const baseMtoShipment = mtoShipment?.id ? mtoShipment : response; + const data = { + ...baseMtoShipment, + mobileHomeShipment: response?.mobileHomeShipment, + shipmentType: response?.shipmentType, + customerRemarks: response?.customerRemarks, + eTag: response?.eTag, + }; + const currentMove = serviceMemberMoves?.currentMove[0]; + + if (currentMove?.mtoShipments?.length) { + currentMove?.mtoShipments?.forEach((element, idx) => { + if (element.id === response.id) { + currentMove.mtoShipments[idx] = data; + } + }); + } + + dispatch(updateMTOShipment(response)); + + // navigate to the next page + navigate( + generatePath(customerRoutes.SHIPMENT_MOBILE_HOME_LOCATION_INFO, { + moveId, + mtoShipmentId: response.id, + }), + ); + }; + + const redirectShipment = () => { + setTimeout(() => { + scrollToTop(); + const createShipmentPath = generatePath(customerRoutes.SHIPMENT_CREATE_PATH, { moveId }); + navigate(`${createShipmentPath}?type=${SHIPMENT_TYPES.HHG}`, { + state: { + mtoShipment, + }, + }); + }, 100); + }; + + const handleConfirmationDeleteAndRedirect = () => { + if (isDeleting || isSubmitting) return; + setIsDeleting(true); + + deleteMTOShipment(mtoShipment?.id) + .then(() => { + getAllMoves(serviceMember.id).then((res) => { + updateAllMoves(res); + }); + redirectShipment(); + }) + .catch(() => { + const errorMsg = 'There was an error attempting to delete your shipment.'; + setErrorMessage(errorMsg); + }) + .finally(() => { + setIsDeleting(false); + }); + }; + + // open confirmation modal to validate mobile home shipment + const handleSubmit = async (values, { setSubmitting }) => { + setErrorMessage(null); + const totalLengthInInches = toTotalInches(values.lengthFeet, values.lengthInches); + const totalWidthInInches = toTotalInches(values.widthFeet, values.widthInches); + const totalHeightInInches = toTotalInches(values.heightFeet, values.heightInches); + + // const mobileHomeShipment = { + // year: Number(values.year), + // make: values.make, + // model: values.model, + // lengthInInches: totalLengthInInches, + // widthInInches: totalWidthInInches, + // heightInInches: totalHeightInInches, + // }; + // setMobileHomeShipmentObj(mobileHomeShipment); + + const createOrUpdateShipment = { + moveTaskOrderID: moveId, + shipmentType: SHIPMENT_TYPES.MOBILE_HOME, + mobileHomeShipment: { + year: Number(values.year), + make: values.make, + model: values.model, + lengthInInches: totalLengthInInches, + widthInInches: totalWidthInInches, + heightInInches: totalHeightInInches, + }, + customerRemarks: values.customerRemarks, + }; + + if (isNewShipment) { + createMTOShipment(createOrUpdateShipment) + .then((shipmentResponse) => { + onShipmentSaveSuccess(shipmentResponse, setSubmitting); + }) + .catch((e) => { + setSubmitting(false); + const { response } = e; + let errorMsg = 'There was an error attempting to create your shipment.'; + if (response?.body?.invalidFields) { + const keys = Object.keys(response?.body?.invalidFields); + const firstError = response?.body?.invalidFields[keys[0]][0]; + errorMsg = firstError; + } + setErrorMessage(errorMsg); + }); + } else { + // console.log('Create output: ', errorMsg); + createOrUpdateShipment.id = mtoShipment.id; + createOrUpdateShipment.mobileHomeShipment.id = mtoShipment.mobileHomeShipment?.id; + patchMTOShipment(mtoShipment.id, createOrUpdateShipment, mtoShipment.eTag) + .then((shipmentResponse) => { + onShipmentSaveSuccess(shipmentResponse, setSubmitting); + }) + .catch((e) => { + setSubmitting(false); + const { response } = e; + let errorMsg = 'There was an error attempting to update your shipment.'; + if (response?.body?.invalidFields) { + const keys = Object.keys(response?.body?.invalidFields); + const firstError = response?.body?.invalidFields[keys[0]][0]; + errorMsg = firstError; + } + setErrorMessage(errorMsg); + setIsSubmitting(false); + }); + } + }; + + return ( +
+ + + + + +

Mobile home details and measurements

+ {errorMessage && ( + + {errorMessage} + + )} + +
+
+
+
+ ); +}; + +MobileHomeShipmentCreate.propTypes = { + mtoShipment: ShipmentShape, + serviceMember: ServiceMemberShape.isRequired, + destinationDutyLocation: DutyLocationShape.isRequired, + move: MoveShape, +}; + +MobileHomeShipmentCreate.defaultProps = { + move: {}, + mtoShipment: {}, +}; + +export default MobileHomeShipmentCreate; diff --git a/src/pages/MyMove/MobileHome/MobileHomeShipmentCreate/MobileHomeShipmentCreate.test.jsx b/src/pages/MyMove/MobileHome/MobileHomeShipmentCreate/MobileHomeShipmentCreate.test.jsx new file mode 100644 index 00000000000..2a9c5bb3692 --- /dev/null +++ b/src/pages/MyMove/MobileHome/MobileHomeShipmentCreate/MobileHomeShipmentCreate.test.jsx @@ -0,0 +1,263 @@ +import React from 'react'; +import { waitFor, screen, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { generatePath } from 'react-router'; + +import MobileHomeShipmentCreate from 'pages/MyMove/MobileHome/MobileHomeShipmentCreate/MobileHomeShipmentCreate'; +import { customerRoutes } from 'constants/routes'; +import { createMTOShipment, patchMTOShipment } from 'services/internalApi'; +import { updateMTOShipment } from 'store/entities/actions'; +import { renderWithRouter } from 'testUtils'; +import { isBooleanFlagEnabled } from 'utils/featureFlags'; + +const mockNavigate = jest.fn(); + +const mockMoveId = 'move123'; +const mockNewShipmentId = 'newShipment123'; + +jest.mock('utils/featureFlags', () => ({ + ...jest.requireActual('utils/featureFlags'), + isBooleanFlagEnabled: jest.fn().mockImplementation(() => Promise.resolve(false)), +})); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, + useParams: () => ({ moveId: mockMoveId }), + useLocation: () => ({ search: '' }), +})); + +jest.mock('services/internalApi', () => ({ + ...jest.requireActual('services/internalApi'), + createMTOShipment: jest.fn(), + patchMTOShipment: jest.fn(), + deleteMTOShipment: jest.fn(), + getAllMoves: jest.fn(), +})); + +jest.mock('utils/validation', () => ({ + ...jest.requireActual('utils/validation'), + validatePostalCode: jest.fn(), +})); + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: () => mockDispatch, +})); + +const serviceMember = { + id: '8', + residential_address: { + streetAddress1: '123 Any St', + streetAddress2: '', + city: 'Norfolk', + state: 'VA', + postalCode: '20001', + }, +}; + +const defaultProps = { + destinationDutyLocation: { + address: { + streetAddress1: '234 Any St', + streetAddress2: '', + city: 'Richmond', + state: 'VA', + postalCode: '10002', + }, + }, + postalCodeValidator: jest.fn(), + serviceMember, +}; + +beforeEach(() => { + jest.clearAllMocks(); +}); + +const renderMobileHomeShipmentCreate = async (props) => { + await act(async () => { + renderWithRouter(, { + path: customerRoutes.SHIPMENT_MOBILE_HOME_PATH, + params: { moveId: 'move123' }, + }); + }); +}; + +describe('MobileHomeShipmentCreate component', () => { + describe('creating a new mobile home shipment', () => { + it('renders the heading and empty form', async () => { + await renderMobileHomeShipmentCreate(); + + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Mobile home details and measurements'); + }); + + it('routes back to the new shipment type screen when back is clicked', async () => { + await renderMobileHomeShipmentCreate(); + const selectShipmentType = generatePath(customerRoutes.SHIPMENT_SELECT_TYPE_PATH, { + moveId: mockMoveId, + }); + + const backButton = await screen.getByRole('button', { name: 'Back' }); + await act(async () => { + await userEvent.click(backButton); + }); + + expect(mockNavigate).toHaveBeenCalledWith(selectShipmentType); + }); + + it('calls create shipment endpoint and formats required payload values', async () => { + isBooleanFlagEnabled.mockImplementation(() => Promise.resolve(true)); + createMTOShipment.mockResolvedValueOnce({ id: mockNewShipmentId }); + + await renderMobileHomeShipmentCreate(); + + await act(async () => { + await userEvent.type(screen.getByTestId('year'), '2022'); + await userEvent.type(screen.getByTestId('make'), 'Skyline Homes'); + await userEvent.type(screen.getByTestId('model'), 'Crown'); + await userEvent.type(screen.getByTestId('lengthFeet'), '21'); + await userEvent.type(screen.getByTestId('widthFeet'), '8'); + await userEvent.type(screen.getByTestId('heightFeet'), '7'); + await userEvent.click(screen.getByRole('button', { name: 'Continue' })); + }); + + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Mobile home details and measurements'); + + await waitFor(() => { + expect(createMTOShipment).toHaveBeenCalledWith({ + moveTaskOrderID: mockMoveId, + shipmentType: 'MOBILE_HOME', + mobileHomeShipment: { + year: 2022, + make: 'Skyline Homes', + model: 'Crown', + lengthInInches: 252, + widthInInches: 96, + heightInInches: 84, + }, + customerRemarks: undefined, + }); + + expect(mockDispatch).toHaveBeenCalledWith( + updateMTOShipment(expect.objectContaining({ id: mockNewShipmentId })), + ); + expect(mockNavigate).toHaveBeenCalledWith( + generatePath(customerRoutes.SHIPMENT_MOBILE_HOME_LOCATION_INFO, { + moveId: mockMoveId, + mtoShipmentId: mockNewShipmentId, + }), + ); + }); + }); + + it('displays an error alert when the create shipment fails', async () => { + createMTOShipment.mockRejectedValueOnce({ + response: { body: { invalidFields: { model: ['Some error message'] } } }, + }); + await renderMobileHomeShipmentCreate(); + + await act(async () => { + await userEvent.type(screen.getByTestId('year'), '2022'); + await userEvent.type(screen.getByTestId('make'), 'Skyline Homes'); + await userEvent.type(screen.getByTestId('model'), 'Crown'); + await userEvent.type(screen.getByTestId('lengthFeet'), '21'); + await userEvent.type(screen.getByTestId('widthFeet'), '8'); + await userEvent.type(screen.getByTestId('heightFeet'), '7'); + await userEvent.click(screen.getByRole('button', { name: 'Continue' })); + }); + + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Mobile home details and measurements'); + + await waitFor(() => { + expect(createMTOShipment).toHaveBeenCalledWith({ + moveTaskOrderID: mockMoveId, + shipmentType: 'MOBILE_HOME', + mobileHomeShipment: { + year: 2022, + make: 'Skyline Homes', + model: 'Crown', + lengthInInches: 252, + widthInInches: 96, + heightInInches: 84, + }, + customerRemarks: undefined, + }); + + expect(screen.getByText('Some error message')).toBeInTheDocument(); + }); + }); + }); + + describe('editing an existing Mobile home shipment', () => { + const existingShipment = { + id: 'existingShipment123', + eTag: 'someETag', + mobileHomeShipment: { + id: 'mobileHome123', + year: 2020, + make: 'Sea Ray', + model: 'Sundancer', + lengthInInches: 240, + widthInInches: 96, + heightInInches: 84, + }, + }; + + it('calls patch shipment endpoint and formats required payload values', async () => { + patchMTOShipment.mockResolvedValueOnce({ id: existingShipment.id }); + + await renderMobileHomeShipmentCreate({ mtoShipment: existingShipment }); + + await act(async () => { + await userEvent.clear(screen.getByTestId('year')); + await userEvent.type(screen.getByTestId('year'), '2021'); + await userEvent.clear(screen.getByTestId('make')); + await userEvent.type(screen.getByTestId('make'), 'Bayliner'); + await userEvent.clear(screen.getByTestId('model')); + await userEvent.type(screen.getByTestId('model'), 'Ciera'); + await userEvent.clear(screen.getByTestId('lengthFeet')); + await userEvent.type(screen.getByTestId('lengthFeet'), '25'); + await userEvent.clear(screen.getByTestId('widthFeet')); + await userEvent.type(screen.getByTestId('widthFeet'), '8'); + await userEvent.clear(screen.getByTestId('heightFeet')); + await userEvent.type(screen.getByTestId('heightFeet'), '7'); + await userEvent.click(screen.getByRole('button', { name: 'Continue' })); + }); + + expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Mobile home details and measurements'); + + await waitFor(() => { + expect(patchMTOShipment).toHaveBeenCalledWith( + existingShipment.id, + { + moveTaskOrderID: mockMoveId, + shipmentType: 'MOBILE_HOME', + mobileHomeShipment: { + id: 'mobileHome123', + year: 2021, + make: 'Bayliner', + model: 'Ciera', + lengthInInches: 300, + widthInInches: 96, + heightInInches: 84, + }, + customerRemarks: undefined, + id: 'existingShipment123', + }, + 'someETag', + ); + + expect(mockDispatch).toHaveBeenCalledWith( + updateMTOShipment(expect.objectContaining({ id: existingShipment.id })), + ); + expect(mockNavigate).toHaveBeenCalledWith( + generatePath(customerRoutes.SHIPMENT_MOBILE_HOME_LOCATION_INFO, { + moveId: mockMoveId, + mtoShipmentId: existingShipment.id, + }), + ); + }); + }); + }); +}); diff --git a/src/pages/MyMove/MobileHome/MobileHomeShipmentLocationInfo/MobileHomeShipmentLocationInfo.jsx b/src/pages/MyMove/MobileHome/MobileHomeShipmentLocationInfo/MobileHomeShipmentLocationInfo.jsx new file mode 100644 index 00000000000..e79b5c4d455 --- /dev/null +++ b/src/pages/MyMove/MobileHome/MobileHomeShipmentLocationInfo/MobileHomeShipmentLocationInfo.jsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { generatePath, useNavigate, useParams } from 'react-router-dom'; + +import { customerRoutes } from 'constants/routes'; +import { updateMTOShipment } from 'store/entities/actions'; +import { + selectCurrentOrders, + selectMTOShipmentById, + selectServiceMemberFromLoggedInUser, +} from 'store/entities/selectors'; +import LoadingPlaceholder from 'shared/LoadingPlaceholder'; +import MtoShipmentForm from 'components/Customer/MtoShipmentForm/MtoShipmentForm'; + +const MobileHomeInfo = () => { + const navigate = useNavigate(); + const { moveId, mtoShipmentId } = useParams(); + const dispatch = useDispatch(); + + const handleBack = () => { + navigate(generatePath(customerRoutes.SHIPMENT_EDIT_PATH, { moveId, mtoShipmentId })); + }; + + const serviceMember = useSelector((state) => selectServiceMemberFromLoggedInUser(state)); + const orders = useSelector((state) => selectCurrentOrders(state)); + const mtoShipment = useSelector((state) => selectMTOShipmentById(state, mtoShipmentId)); + + // Loading placeholder while data loads + if (!serviceMember || !orders || !mtoShipment) { + return ; + } + + return ( + dispatch(updateMTOShipment(shipment))} + serviceMember={serviceMember} + orders={orders} + handleBack={handleBack} + /> + ); +}; + +export default MobileHomeInfo; diff --git a/src/pages/MyMove/MobileHome/MobileHomeShipmentLocationInfo/MobileHomeShipmentLocationInfo.test.jsx b/src/pages/MyMove/MobileHome/MobileHomeShipmentLocationInfo/MobileHomeShipmentLocationInfo.test.jsx new file mode 100644 index 00000000000..e1387dee4dc --- /dev/null +++ b/src/pages/MyMove/MobileHome/MobileHomeShipmentLocationInfo/MobileHomeShipmentLocationInfo.test.jsx @@ -0,0 +1,247 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { generatePath } from 'react-router-dom'; +import { v4 as uuidv4 } from 'uuid'; + +import MobileHomeShipmentLocationInfo from './MobileHomeShipmentLocationInfo'; + +import { customerRoutes } from 'constants/routes'; +import { patchMTOShipment } from 'services/internalApi'; +import { SHIPMENT_OPTIONS, SHIPMENT_TYPES } from 'shared/constants'; +import { selectMTOShipmentById } from 'store/entities/selectors'; +import { MockProviders } from 'testUtils'; + +const mockNavigate = jest.fn(); +const mockMoveId = uuidv4(); +const mockMTOShipmentId = uuidv4(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, + useLocation: () => ({ search: '' }), + useParams: () => ({ moveId: mockMoveId, mtoShipmentId: mockMTOShipmentId }), +})); + +const shipmentEditPath = generatePath(customerRoutes.SHIPMENT_EDIT_PATH, { + moveId: mockMoveId, + mtoShipmentId: mockMTOShipmentId, +}); + +const mockRoutingConfig = { + path: customerRoutes.SHIPMENT_MOBILE_HOME_LOCATION_INFO, + params: { + moveId: mockMoveId, + mtoShipmentId: mockMTOShipmentId, + }, +}; + +const mockOrders = { + has_dependents: false, + authorizedWeight: 5000, + entitlement: { + proGear: 2000, + proGearSpouse: 500, + }, +}; + +const mockServiceMember = { + id: uuidv4(), +}; + +const mockMTOShipment = { + id: mockMTOShipmentId, + moveTaskOrderID: mockMoveId, + shipmentType: SHIPMENT_TYPES.MOBILE_HOME, + mobileHomeShipment: { + id: uuidv4(), + year: 2022, + make: 'Skyline Homes', + model: 'Crown', + lengthInInches: 252, + widthInInches: 96, + heightInInches: 84, + eTag: window.btoa(new Date()), + }, + eTag: window.btoa(new Date()), + createdAt: '2021-06-11T18:12:11.918Z', + customerRemarks: 'mock remarks', + requestedPickupDate: '2021-08-01', + requestedDeliveryDate: '2021-08-11', + pickupAddress: { + id: uuidv4(), + streetAddress1: '812 S 129th St', + city: 'San Antonio', + state: 'TX', + postalCode: '78234', + }, + destinationAddress: { + id: uuidv4(), + streetAddress1: '441 SW Rio de la Plata Drive', + city: 'Tacoma', + state: 'WA', + postalCode: '98421', + }, +}; + +const mockPreExistingShipment = { + ...mockMTOShipment, + mobileHomeShipment: { + ...mockMTOShipment.mobileHomeShipment, + lengthInInches: 352, + widthInInches: 16, + heightInInches: 34, + eTag: window.btoa(new Date()), + }, + eTag: window.btoa(new Date()), +}; + +const mockDispatch = jest.fn(); + +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useDispatch: jest.fn().mockImplementation(() => mockDispatch), +})); + +jest.mock('services/internalApi', () => ({ + ...jest.requireActual('services/internalApi'), + getResponseError: jest.fn(), + patchMTOShipment: jest.fn(), +})); + +jest.mock('store/entities/selectors', () => ({ + ...jest.requireActual('store/entities/selectors'), + selectCurrentOrders: jest.fn().mockImplementation(() => mockOrders), + selectMTOShipmentById: jest.fn().mockImplementation(() => mockMTOShipment), + selectServiceMemberFromLoggedInUser: jest.fn().mockImplementation(() => mockServiceMember), +})); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +const renderMobileHomeShipmentLocationInfo = (props) => { + return render( + + + , + ); +}; + +describe('Pickup info page', () => { + it('renders the MtoShipmentForm component', () => { + renderMobileHomeShipmentLocationInfo(); + + expect(screen.getByRole('heading', { level: 2, name: 'Pickup info' })).toBeInTheDocument(); + }); + + it.each([[mockPreExistingShipment]])( + 'renders the form pre-filled when info has been entered previously', + async (preExistingShipment) => { + selectMTOShipmentById.mockImplementationOnce(() => preExistingShipment); + + renderMobileHomeShipmentLocationInfo(); + + expect(await screen.findByLabelText('Preferred pickup date')).toHaveValue('01 Aug 2021'); + expect(screen.getByLabelText('Use my current address')).not.toBeChecked(); + expect(screen.getAllByLabelText('Address 1')[0]).toHaveValue('812 S 129th St'); + expect(screen.getAllByLabelText(/Address 2/)[0]).toHaveValue(''); + expect(screen.getAllByLabelText('City')[0]).toHaveValue('San Antonio'); + expect(screen.getAllByLabelText('State')[0]).toHaveValue('TX'); + expect(screen.getAllByLabelText('ZIP')[0]).toHaveValue('78234'); + expect(screen.getByLabelText('Preferred delivery date')).toHaveValue('11 Aug 2021'); + expect(screen.getByTitle('Yes, I know my delivery address')).toBeChecked(); + expect(screen.getAllByLabelText('Address 1')[1]).toHaveValue('441 SW Rio de la Plata Drive'); + expect(screen.getAllByLabelText(/Address 2/)[1]).toHaveValue(''); + expect(screen.getAllByLabelText('City')[1]).toHaveValue('Tacoma'); + expect(screen.getAllByLabelText('State')[1]).toHaveValue('WA'); + expect(screen.getAllByLabelText('ZIP')[1]).toHaveValue('98421'); + }, + ); + + it('routes back to the previous page when the back button is clicked', async () => { + renderMobileHomeShipmentLocationInfo(); + + const backButton = screen.getByRole('button', { name: /back/i }); + + await userEvent.click(backButton); + + expect(mockNavigate).toHaveBeenCalledWith(shipmentEditPath); + }); + + it('can submit with pickup information successfully', async () => { + const shipmentInfo = { + pickupAddress: { + streetAddress1: '6622 Airport Way S', + streetAddress2: '#1430', + city: 'San Marcos', + state: 'TX', + postalCode: '78666', + }, + }; + + const expectedPayload = { + moveTaskOrderID: mockMoveId, + shipmentType: SHIPMENT_TYPES.MOBILE_HOME, + pickupAddress: { ...shipmentInfo.pickupAddress }, + customerRemarks: mockMTOShipment.customerRemarks, + requestedPickupDate: mockMTOShipment.requestedPickupDate, + requestedDeliveryDate: mockMTOShipment.requestedDeliveryDate, + destinationAddress: { ...mockMTOShipment.destinationAddress, streetAddress2: '' }, + secondaryDeliveryAddress: undefined, + hasSecondaryDeliveryAddress: false, + secondaryPickupAddress: undefined, + hasSecondaryPickupAddress: false, + tertiaryDeliveryAddress: undefined, + hasTertiaryDeliveryAddress: false, + tertiaryPickupAddress: undefined, + hasTertiaryPickupAddress: false, + agents: [ + { agentType: 'RELEASING_AGENT', email: '', firstName: '', lastName: '', phone: '' }, + { agentType: 'RECEIVING_AGENT', email: '', firstName: '', lastName: '', phone: '' }, + ], + counselorRemarks: undefined, + }; + delete expectedPayload.destinationAddress.id; + + const newUpdatedAt = '2021-06-11T21:20:22.150Z'; + const expectedUpdateResponse = { + ...mockMTOShipment, + pickupAddress: { ...shipmentInfo.pickupAddress }, + shipmentType: SHIPMENT_OPTIONS.HHG, + eTag: window.btoa(newUpdatedAt), + status: 'SUBMITTED', + }; + + patchMTOShipment.mockImplementation(() => Promise.resolve(expectedUpdateResponse)); + + renderMobileHomeShipmentLocationInfo({ isCreatePage: false, mtoShipment: mockMTOShipment }); + + const pickupAddress1Input = screen.getAllByLabelText('Address 1')[0]; + await userEvent.clear(pickupAddress1Input); + await userEvent.type(pickupAddress1Input, shipmentInfo.pickupAddress.streetAddress1); + + const pickupAddress2Input = screen.getAllByLabelText(/Address 2/)[0]; + await userEvent.clear(pickupAddress2Input); + await userEvent.type(pickupAddress2Input, shipmentInfo.pickupAddress.streetAddress2); + + const pickupCityInput = screen.getAllByLabelText('City')[0]; + await userEvent.clear(pickupCityInput); + await userEvent.type(pickupCityInput, shipmentInfo.pickupAddress.city); + + const pickupStateInput = screen.getAllByLabelText('State')[0]; + await userEvent.selectOptions(pickupStateInput, shipmentInfo.pickupAddress.state); + + const pickupPostalCodeInput = screen.getAllByLabelText('ZIP')[0]; + await userEvent.clear(pickupPostalCodeInput); + await userEvent.type(pickupPostalCodeInput, shipmentInfo.pickupAddress.postalCode); + + const saveButton = await screen.findByRole('button', { name: 'Save & Continue' }); + expect(saveButton).not.toBeDisabled(); + await userEvent.click(saveButton); + + await waitFor(() => { + expect(patchMTOShipment).toHaveBeenCalledWith(mockMTOShipment.id, expectedPayload, mockMTOShipment.eTag); + }); + }); +}); diff --git a/src/pages/MyMove/SelectShipmentType.jsx b/src/pages/MyMove/SelectShipmentType.jsx index 48f848d6e0d..b9458efa050 100644 --- a/src/pages/MyMove/SelectShipmentType.jsx +++ b/src/pages/MyMove/SelectShipmentType.jsx @@ -11,6 +11,7 @@ import { FEATURE_FLAG_KEYS, SHIPMENT_OPTIONS } from '../../shared/constants'; import ConnectedMoveInfoModal from 'components/Customer/modals/MoveInfoModal/MoveInfoModal'; import ConnectedStorageInfoModal from 'components/Customer/modals/StorageInfoModal/StorageInfoModal'; import ConnectedBoatInfoModal from 'components/Customer/modals/BoatInfoModal/BoatInfoModal'; +import ConnectedMobileHomeInfoModal from 'components/Customer/modals/MobileHomeInfoModal/MobileHomeInfoModal'; import SelectableCard from 'components/Customer/SelectableCard'; import WizardNavigation from 'components/Customer/WizardNavigation/WizardNavigation'; import NotificationScrollToTop from 'components/NotificationScrollToTop'; @@ -34,11 +35,13 @@ export class SelectShipmentType extends Component { showStorageInfoModal: false, showMoveInfoModal: false, showBoatInfoModal: false, + showMobileHomeInfoModal: false, errorMessage: null, enablePPM: false, enableNTS: false, enableNTSR: false, enableBoat: false, + enableMobileHome: false, }; } @@ -65,6 +68,11 @@ export class SelectShipmentType extends Component { enableBoat: enabled, }); }); + isBooleanFlagEnabled(FEATURE_FLAG_KEYS.MOBILE_HOME).then((enabled) => { + this.setState({ + enableMobileHome: enabled, + }); + }); } setShipmentType = (e) => { @@ -89,6 +97,12 @@ export class SelectShipmentType extends Component { })); }; + toggleMobileHomeInfoModal = () => { + this.setState((state) => ({ + showMobileHomeInfoModal: !state.showMobileHomeInfoModal, + })); + }; + handleSubmit = () => { const { router: { navigate }, @@ -111,10 +125,12 @@ export class SelectShipmentType extends Component { showStorageInfoModal, showMoveInfoModal, showBoatInfoModal, + showMobileHomeInfoModal, enablePPM, enableNTS, enableNTSR, enableBoat, + enableMobileHome, errorMessage, } = this.state; @@ -138,6 +154,8 @@ export class SelectShipmentType extends Component { const boatCardText = 'Provide information about your boat and we will determine how it will ship.'; + const mobileHomeCardText = 'Please provide information about your mobile home.'; + const selectableCardDefaultProps = { onChange: (e) => this.setShipmentType(e), name: 'shipmentType', @@ -266,6 +284,18 @@ export class SelectShipmentType extends Component { /> )} + {enableMobileHome && ( + + )} {!shipmentInfo.hasShipment && (

@@ -302,6 +332,11 @@ export class SelectShipmentType extends Component { enablePPM={enableBoat} closeModal={this.toggleBoatInfoModal} /> + ); } diff --git a/src/scenes/MyMove/index.jsx b/src/scenes/MyMove/index.jsx index e92a6e7f5e0..4416a84bf97 100644 --- a/src/scenes/MyMove/index.jsx +++ b/src/scenes/MyMove/index.jsx @@ -66,6 +66,9 @@ const EditOrders = lazy(() => import('pages/MyMove/EditOrders')); const BoatShipmentLocationInfo = lazy(() => import('pages/MyMove/Boat/BoatShipmentLocationInfo/BoatShipmentLocationInfo'), ); +const MobileHomeShipmentLocationInfo = lazy(() => + import('pages/MyMove/MobileHome/MobileHomeShipmentLocationInfo/MobileHomeShipmentLocationInfo'), +); const EstimatedWeightsProGear = lazy(() => import('pages/MyMove/PPM/Booking/EstimatedWeightsProGear/EstimatedWeightsProGear'), ); @@ -353,6 +356,10 @@ export class CustomerApp extends Component { element={} /> } /> + } + /> } diff --git a/src/shared/constants.js b/src/shared/constants.js index 4fb0ab134a8..999d79f9c55 100644 --- a/src/shared/constants.js +++ b/src/shared/constants.js @@ -99,6 +99,7 @@ export const SHIPMENT_OPTIONS_URL = { NTS: 'NTS', NTSrelease: 'NTSrelease', BOAT: 'Boat', + MOBILE_HOME: 'Mobilehome', }; export const LOA_TYPE = { @@ -119,6 +120,7 @@ export const shipmentOptionLabels = [ { key: SHIPMENT_OPTIONS.BOAT, label: 'Boat' }, { key: SHIPMENT_TYPES.BOAT_HAUL_AWAY, label: 'Boat' }, { key: SHIPMENT_TYPES.BOAT_TOW_AWAY, label: 'Boat' }, + { key: SHIPMENT_TYPES.MOBILE_HOME, label: 'Mobile Home' }, ]; export const SERVICE_ITEM_STATUS = { @@ -191,6 +193,7 @@ export const FEATURE_FLAG_KEYS = { NTS: 'nts', NTSR: 'ntsr', BOAT: 'boat', + MOBILE_HOME: 'mobile_home', }; export const MOVE_DOCUMENT_TYPE = { diff --git a/src/shared/styles/_custom.scss b/src/shared/styles/_custom.scss index 96748b69925..2a4d7069b8e 100644 --- a/src/shared/styles/_custom.scss +++ b/src/shared/styles/_custom.scss @@ -29,6 +29,7 @@ html { &--ppm, &--ub, &--boat, + &--mobilehome, &--nts, &--ntsr { @include u-radius(0); @@ -56,6 +57,9 @@ html { &--boat { @include cont-border-top($accent-boat); } + &--mobilehome { + @include cont-border-top($accent-mobile-home); + } } } @@ -142,8 +146,6 @@ table { @include u-border-bottom(1px); @include u-border('base-darker'); } - td { - } } &.table--stacked { td, @@ -155,10 +157,10 @@ table { } .error { th { - @include u-border-left(0.5); - @include u-border-left('error'); + @include u-border-left(0.5); + @include u-border-left('error'); } - td{ + td { @include u-border-left(0); @include u-text('error'); @include u-text('bold'); diff --git a/src/shared/styles/colors.scss b/src/shared/styles/colors.scss index 4914ff82a5b..9fe6ebdeb10 100644 --- a/src/shared/styles/colors.scss +++ b/src/shared/styles/colors.scss @@ -38,6 +38,7 @@ $accent-ub: #f2938c; $accent-nts: #d85bef; $accent-ntsr: #8168b3; $accent-boat: #5d92ba; +$accent-mobile-home: #1fd819; $accent-pro-gear: #71767A; $accent-default: $base-light; diff --git a/src/types/shipment.js b/src/types/shipment.js index 8ec2b926091..7efcc6fec1d 100644 --- a/src/types/shipment.js +++ b/src/types/shipment.js @@ -102,6 +102,20 @@ export const BoatShipmentShape = shape({ eTag: string, }); +export const MobileHomeShipmentShape = shape({ + id: string, + shipmentId: string, + shipmentLocator: string, + createdAt: string, + year: number, + make: string, + model: string, + lengthInInches: number, + heightInInches: number, + widthInInches: number, + eTag: string, +}); + export const PPMCloseoutShape = shape({ id: string, plannedMoveDate: string, diff --git a/src/utils/shipmentInfo.js b/src/utils/shipmentInfo.js index 6ff6cc95617..3b7dbcc87a5 100644 --- a/src/utils/shipmentInfo.js +++ b/src/utils/shipmentInfo.js @@ -16,6 +16,7 @@ const determineShipmentInfo = (move, mtoShipments) => { isNTSRSelectable: isMoveDraft, isPPMSelectable: ppmCount === 0, isBoatSelectable: isMoveDraft, + isMobileHomeSelectable: isMoveDraft, shipmentNumber: existingShipmentCount + 1, }; }; diff --git a/src/utils/shipments.js b/src/utils/shipments.js index 5107ef2bdb6..52c4cc13011 100644 --- a/src/utils/shipments.js +++ b/src/utils/shipments.js @@ -118,3 +118,7 @@ export function isPPMOnly(mtoShipments) { export function isBoatShipmentComplete(mtoShipment) { return mtoShipment?.requestedPickupDate; } + +export function isMobileHomeShipmentComplete(mtoShipment) { + return mtoShipment?.requestedPickupDate; +} From 1a20ed02dc0ef87f291783293d54ccae454def8f Mon Sep 17 00:00:00 2001 From: KonstanceH Date: Mon, 26 Aug 2024 15:44:24 +0000 Subject: [PATCH 02/18] missed shape --- src/types/shipment.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/shipment.js b/src/types/shipment.js index 7efcc6fec1d..a27961e0c9b 100644 --- a/src/types/shipment.js +++ b/src/types/shipment.js @@ -211,6 +211,7 @@ export const ShipmentShape = shape({ }), ppmShipment: PPMShipmentShape, boatShipment: BoatShipmentShape, + mobileHomeShipment: MobileHomeShipmentShape, deliveryAddressUpdate: ShipmentAddressUpdateShape, actual_pro_gear_weight: number, actual_spouse_pro_gear_weight: number, From 9682d7cc73ce21edbcdaf6ae9151f08e5935fa53 Mon Sep 17 00:00:00 2001 From: KonstanceH Date: Mon, 26 Aug 2024 15:47:14 +0000 Subject: [PATCH 03/18] missing tests --- .../MobileHomeShipmentForm.test.jsx | 8 ++++---- src/components/ShipmentTag/ShipmentTag.module.scss | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.test.jsx b/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.test.jsx index 21ce54b1829..eb65ff30eb4 100644 --- a/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.test.jsx +++ b/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.test.jsx @@ -7,8 +7,8 @@ import MobileHomeShipmentForm from './MobileHomeShipmentForm'; const mtoShipment = { mobileHomeShipment: { year: '2022', - make: 'Yamaha', - model: '242X', + make: 'Skyline Homes', + model: 'Crown', lengthInInches: 288, // 24 feet widthInInches: 102, // 8 feet 6 inches heightInInches: 84, // 7 feet @@ -45,7 +45,7 @@ describe('MobileHomeShipmentForm component', () => { describe('displays form', () => { it('renders filled form on load', async () => { render(); - expect(await screen.getByTestId('year')).toHaveValue(mtoShipment.mobileHomeShipment.year); + expect(screen.getByTestId('year')).toHaveValue(mtoShipment.mobileHomeShipment.year); expect(screen.getByTestId('make')).toHaveValue(mtoShipment.mobileHomeShipment.make); expect(screen.getByTestId('model')).toHaveValue(mtoShipment.mobileHomeShipment.model); expect(screen.getByTestId('lengthFeet')).toHaveValue('24'); @@ -87,7 +87,7 @@ describe('MobileHomeShipmentForm component', () => { }); }); - expect(screen.getAllByText('Required').length).toBe(requiredFields.length); + expect(screen.getAllByTestId('errorMessage').length).toBe(requiredFields.length); }); }); diff --git a/src/components/ShipmentTag/ShipmentTag.module.scss b/src/components/ShipmentTag/ShipmentTag.module.scss index 85d03da1b77..9f3b63924e5 100644 --- a/src/components/ShipmentTag/ShipmentTag.module.scss +++ b/src/components/ShipmentTag/ShipmentTag.module.scss @@ -31,7 +31,7 @@ &.Boat { background-color: $accent-boat; } - &.MobileHome { + &.MOBILE_HOME { background-color: $accent-mobile-home; } } From 92cc5d9a08abb18c2e07f19bd7b1b837f4de93cc Mon Sep 17 00:00:00 2001 From: KonstanceH Date: Mon, 26 Aug 2024 15:49:48 +0000 Subject: [PATCH 04/18] remove extra comments and fix name --- src/constants/shipments.js | 2 +- .../MobileHomeShipmentCreate.jsx | 13 ------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/constants/shipments.js b/src/constants/shipments.js index 4d4b40a8660..815d750120e 100644 --- a/src/constants/shipments.js +++ b/src/constants/shipments.js @@ -9,7 +9,7 @@ export const shipmentTypes = { [SHIPMENT_OPTIONS.BOAT]: 'Boat', [SHIPMENT_TYPES.BOAT_HAUL_AWAY]: 'Boat', [SHIPMENT_TYPES.BOAT_TOW_AWAY]: 'Boat', - [SHIPMENT_TYPES.MOBILE_HOME]: 'MobileHome', + [SHIPMENT_TYPES.MOBILE_HOME]: 'MOBILE_HOME', }; export const shipmentModificationTypes = { diff --git a/src/pages/MyMove/MobileHome/MobileHomeShipmentCreate/MobileHomeShipmentCreate.jsx b/src/pages/MyMove/MobileHome/MobileHomeShipmentCreate/MobileHomeShipmentCreate.jsx index 244c401263e..0a6e0ca8d19 100644 --- a/src/pages/MyMove/MobileHome/MobileHomeShipmentCreate/MobileHomeShipmentCreate.jsx +++ b/src/pages/MyMove/MobileHome/MobileHomeShipmentCreate/MobileHomeShipmentCreate.jsx @@ -29,8 +29,6 @@ const MobileHomeShipmentCreate = ({ }) => { const [errorMessage, setErrorMessage] = useState(null); const [multiMove, setMultiMove] = useState(false); - // const [mobileHomeShipmentObj, setMobileHomeShipmentObj] = useState(null); - // const [setSubmitValues] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [isDeleting, setIsDeleting] = useState(false); @@ -131,16 +129,6 @@ const MobileHomeShipmentCreate = ({ const totalWidthInInches = toTotalInches(values.widthFeet, values.widthInches); const totalHeightInInches = toTotalInches(values.heightFeet, values.heightInches); - // const mobileHomeShipment = { - // year: Number(values.year), - // make: values.make, - // model: values.model, - // lengthInInches: totalLengthInInches, - // widthInInches: totalWidthInInches, - // heightInInches: totalHeightInInches, - // }; - // setMobileHomeShipmentObj(mobileHomeShipment); - const createOrUpdateShipment = { moveTaskOrderID: moveId, shipmentType: SHIPMENT_TYPES.MOBILE_HOME, @@ -172,7 +160,6 @@ const MobileHomeShipmentCreate = ({ setErrorMessage(errorMsg); }); } else { - // console.log('Create output: ', errorMsg); createOrUpdateShipment.id = mtoShipment.id; createOrUpdateShipment.mobileHomeShipment.id = mtoShipment.mobileHomeShipment?.id; patchMTOShipment(mtoShipment.id, createOrUpdateShipment, mtoShipment.eTag) From b09c328442066ef5b93517961e5962f833722ec0 Mon Sep 17 00:00:00 2001 From: KonstanceH Date: Fri, 6 Sep 2024 17:28:23 +0000 Subject: [PATCH 05/18] move mobile and boat info modal togeter --- .../BoatAndMobileInfoModal.jsx} | 14 +++++--- .../MobileHomeInfoModal.jsx | 35 ------------------- src/pages/MyMove/SelectShipmentType.jsx | 25 +++++-------- 3 files changed, 18 insertions(+), 56 deletions(-) rename src/components/Customer/modals/{BoatInfoModal/BoatInfoModal.jsx => BoatAndMobileInfoModal/BoatAndMobileInfoModal.jsx} (75%) delete mode 100644 src/components/Customer/modals/MobileHomeInfoModal/MobileHomeInfoModal.jsx diff --git a/src/components/Customer/modals/BoatInfoModal/BoatInfoModal.jsx b/src/components/Customer/modals/BoatAndMobileInfoModal/BoatAndMobileInfoModal.jsx similarity index 75% rename from src/components/Customer/modals/BoatInfoModal/BoatInfoModal.jsx rename to src/components/Customer/modals/BoatAndMobileInfoModal/BoatAndMobileInfoModal.jsx index 03b4159f142..680aad70501 100644 --- a/src/components/Customer/modals/BoatInfoModal/BoatInfoModal.jsx +++ b/src/components/Customer/modals/BoatAndMobileInfoModal/BoatAndMobileInfoModal.jsx @@ -4,7 +4,7 @@ import { Button } from '@trussworks/react-uswds'; import Modal, { ModalTitle, ModalClose, ModalActions, connectModal } from 'components/Modal/Modal'; -export const BoatInfoModal = ({ closeModal }) => ( +export const BoatAndMobileInfoModal = ({ closeModal }) => ( @@ -20,6 +20,10 @@ export const BoatInfoModal = ({ closeModal }) => ( above dimensions shall be shipped with household goods and not be considered a separate shipment. If your boat is under those dimensions, choose the "HHG" option above.

+

+ Mobile Home shipment +

+

This option is for privately owned mobile homes.

- - -); - -MobileHomeInfoModal.propTypes = { - closeModal: PropTypes.func, -}; - -MobileHomeInfoModal.defaultProps = { - closeModal: () => {}, -}; - -MobileHomeInfoModal.displayName = 'MobileHomeInfoModal'; - -export default connectModal(MobileHomeInfoModal); diff --git a/src/pages/MyMove/SelectShipmentType.jsx b/src/pages/MyMove/SelectShipmentType.jsx index b9458efa050..29719df9fdc 100644 --- a/src/pages/MyMove/SelectShipmentType.jsx +++ b/src/pages/MyMove/SelectShipmentType.jsx @@ -8,10 +8,9 @@ import { generatePath } from 'react-router-dom'; import { isBooleanFlagEnabled } from '../../utils/featureFlags'; import { FEATURE_FLAG_KEYS, SHIPMENT_OPTIONS } from '../../shared/constants'; +import ConnectedBoatAndMobileInfoModal from 'components/Customer/modals/BoatAndMobileInfoModal/BoatAndMobileInfoModal'; import ConnectedMoveInfoModal from 'components/Customer/modals/MoveInfoModal/MoveInfoModal'; import ConnectedStorageInfoModal from 'components/Customer/modals/StorageInfoModal/StorageInfoModal'; -import ConnectedBoatInfoModal from 'components/Customer/modals/BoatInfoModal/BoatInfoModal'; -import ConnectedMobileHomeInfoModal from 'components/Customer/modals/MobileHomeInfoModal/MobileHomeInfoModal'; import SelectableCard from 'components/Customer/SelectableCard'; import WizardNavigation from 'components/Customer/WizardNavigation/WizardNavigation'; import NotificationScrollToTop from 'components/NotificationScrollToTop'; @@ -34,7 +33,7 @@ export class SelectShipmentType extends Component { this.state = { showStorageInfoModal: false, showMoveInfoModal: false, - showBoatInfoModal: false, + showBoatAndMobileInfoModal: false, showMobileHomeInfoModal: false, errorMessage: null, enablePPM: false, @@ -91,9 +90,9 @@ export class SelectShipmentType extends Component { })); }; - toggleBoatInfoModal = () => { + toggleBoatAndMobileInfoModal = () => { this.setState((state) => ({ - showBoatInfoModal: !state.showBoatInfoModal, + showBoatAndMobileInfoModal: !state.showBoatAndMobileInfoModal, })); }; @@ -124,8 +123,7 @@ export class SelectShipmentType extends Component { shipmentType, showStorageInfoModal, showMoveInfoModal, - showBoatInfoModal, - showMobileHomeInfoModal, + showBoatAndMobileInfoModal, enablePPM, enableNTS, enableNTSR, @@ -280,7 +278,7 @@ export class SelectShipmentType extends Component { cardText={boatCardText} checked={shipmentType === SHIPMENT_OPTIONS.BOAT && shipmentInfo.isBoatSelectable} disabled={!shipmentInfo.isBoatSelectable} - onHelpClick={this.toggleBoatInfoModal} + onHelpClick={this.toggleBoatAndMobileInfoModal} /> )} @@ -327,15 +325,10 @@ export class SelectShipmentType extends Component { enableNTSR={enableNTSR} closeModal={this.toggleStorageModal} /> - - ); From 43ae9234bcc0ef9cf0583470643e534c81c1f6ae Mon Sep 17 00:00:00 2001 From: KonstanceH Date: Tue, 10 Sep 2024 19:17:47 +0000 Subject: [PATCH 06/18] reverting to old commit --- go.mod | 2 +- go.sum | 4 +- pkg/notifications/move_submitted.go | 3 +- pkg/notifications/move_submitted_test.go | 24 +++++++++++ .../MobileHomeShipmentForm.jsx | 7 ++++ .../MtoShipmentForm/MtoShipmentForm.jsx | 2 +- .../BoatAndMobileInfoModal.jsx | 2 +- src/components/ShipmentList/ShipmentList.jsx | 2 +- .../ShipmentTag/ShipmentTag.module.scss | 2 +- src/constants/shipments.js | 2 +- src/pages/MyMove/CreateOrEditMtoShipment.jsx | 2 +- .../MobileHomeShipmentCreate.jsx | 42 +------------------ src/pages/MyMove/SelectShipmentType.jsx | 15 +++---- src/shared/constants.js | 2 +- 14 files changed, 50 insertions(+), 61 deletions(-) diff --git a/go.mod b/go.mod index 84a5a1a08c8..00e33fa4d7a 100644 --- a/go.mod +++ b/go.mod @@ -94,7 +94,7 @@ require ( golang.org/x/crypto v0.26.0 golang.org/x/net v0.28.0 golang.org/x/oauth2 v0.22.0 - golang.org/x/text v0.17.0 + golang.org/x/text v0.18.0 golang.org/x/tools v0.24.0 google.golang.org/grpc v1.65.0 gopkg.in/dnaeon/go-vcr.v3 v3.2.0 diff --git a/go.sum b/go.sum index 50dbd52ab9e..c9ccb4c584a 100644 --- a/go.sum +++ b/go.sum @@ -859,8 +859,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/pkg/notifications/move_submitted.go b/pkg/notifications/move_submitted.go index ecdb77923eb..1877cb6164a 100644 --- a/pkg/notifications/move_submitted.go +++ b/pkg/notifications/move_submitted.go @@ -72,7 +72,8 @@ func (m MoveSubmitted) emails(appCtx appcontext.AppContext) ([]emailContent, err if originDSTransportInfo != nil { originDutyLocationName = &originDSTransportInfo.Name originDutyLocationPhoneLine = &originDSTransportInfo.PhoneLine - + } else if originDutyLocation != nil { + originDutyLocationName = &originDutyLocation.Name } totalEntitlement := models.GetWeightAllotment(*orders.Grade) diff --git a/pkg/notifications/move_submitted_test.go b/pkg/notifications/move_submitted_test.go index cdfc856cd56..d9228391245 100644 --- a/pkg/notifications/move_submitted_test.go +++ b/pkg/notifications/move_submitted_test.go @@ -26,6 +26,30 @@ func (suite *NotificationSuite) TestMoveSubmitted() { suite.NotEmpty(email.textBody) } +func (suite *NotificationSuite) TestMoveSubmittedoriginDSTransportInfoIsNil() { + move := factory.BuildMove(suite.DB(), nil, nil) + notification := NewMoveSubmitted(move.ID) + + move.Orders.OriginDutyLocationID = nil + + emails, err := notification.emails(suite.AppContextWithSessionForTest(&auth.Session{ + ServiceMemberID: move.Orders.ServiceMember.ID, + ApplicationName: auth.MilApp, + })) + subject := "Thank you for submitting your move details" + + suite.NoError(err) + suite.Equal(len(emails), 1) + + email := emails[0] + sm := move.Orders.ServiceMember + suite.Equal(email.recipientEmail, *sm.PersonalEmail) + suite.Equal(email.subject, subject) + suite.NotEmpty(email.htmlBody) + suite.NotEmpty(email.textBody) + suite.Contains(email.textBody, move.Orders.OriginDutyLocation.Name) +} + func (suite *NotificationSuite) TestMoveSubmittedHTMLTemplateRenderWithGovCounseling() { approver := factory.BuildUser(nil, nil, nil) move := factory.BuildMove(suite.DB(), nil, nil) diff --git a/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.jsx b/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.jsx index 8c4d45885bf..0cd11239013 100644 --- a/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.jsx +++ b/src/components/Customer/MobileHomeShipment/MobileHomeShipmentForm/MobileHomeShipmentForm.jsx @@ -4,6 +4,7 @@ import * as Yup from 'yup'; import { Formik, Field } from 'formik'; import { Button, Form, Label, Textarea } from '@trussworks/react-uswds'; import classnames from 'classnames'; +// import RequiredTag from 'components/form/RequiredTag'; import styles from './MobileHomeShipmentForm.module.scss'; @@ -161,6 +162,7 @@ const MobileHomeShipmentForm = ({ mtoShipment, onBack, onSubmit }) => { suffix="Feet" errorClassName={styles.hide} title="Length in feet" + optional />
@@ -176,6 +178,7 @@ const MobileHomeShipmentForm = ({ mtoShipment, onBack, onSubmit }) => { max={11} errorClassName={styles.hide} title="Length in inches" + optional />
@@ -198,6 +201,7 @@ const MobileHomeShipmentForm = ({ mtoShipment, onBack, onSubmit }) => { suffix="Feet" errorClassName={styles.hide} title="Width in feet" + optional />
@@ -213,6 +217,7 @@ const MobileHomeShipmentForm = ({ mtoShipment, onBack, onSubmit }) => { max={11} errorClassName={styles.hide} title="Width in inches" + optional />
@@ -235,6 +240,7 @@ const MobileHomeShipmentForm = ({ mtoShipment, onBack, onSubmit }) => { suffix="Feet" errorClassName={styles.hide} title="Height in feet" + optional />
@@ -250,6 +256,7 @@ const MobileHomeShipmentForm = ({ mtoShipment, onBack, onSubmit }) => { max={11} errorClassName={styles.hide} title="Height in inches" + optional />
diff --git a/src/components/Customer/MtoShipmentForm/MtoShipmentForm.jsx b/src/components/Customer/MtoShipmentForm/MtoShipmentForm.jsx index c9b3f7df6b4..354407ad2a6 100644 --- a/src/components/Customer/MtoShipmentForm/MtoShipmentForm.jsx +++ b/src/components/Customer/MtoShipmentForm/MtoShipmentForm.jsx @@ -664,7 +664,7 @@ class MtoShipmentForm extends Component {

You can change details about your move by talking with your counselor or your movers

- {isBoat ? ( + {isBoat || isMobileHome ? (