diff --git a/pkg/services/move/move_router.go b/pkg/services/move/move_router.go
index c510023e5a7..9894a841df4 100644
--- a/pkg/services/move/move_router.go
+++ b/pkg/services/move/move_router.go
@@ -213,6 +213,16 @@ func (router moveRouter) sendToServiceCounselor(appCtx appcontext.AppContext, mo
move.MTOShipments[i].ShipmentType == models.MTOShipmentTypeMobileHome {
move.MTOShipments[i].Status = models.MTOShipmentStatusSubmitted
+ if verrs, err := appCtx.DB().ValidateAndUpdate(&move.MTOShipments[i]); verrs.HasAny() || err != nil {
+ msg := "failure saving shipment when routing move submission"
+ appCtx.Logger().Error(msg, zap.Error(err))
+ return apperror.NewInvalidInputError(move.MTOShipments[i].ID, err, verrs, msg)
+ }
+ }
+ // update status for mobile home shipment
+ if move.MTOShipments[i].ShipmentType == models.MTOShipmentTypeMobileHome {
+ move.MTOShipments[i].Status = models.MTOShipmentStatusSubmitted
+
if verrs, err := appCtx.DB().ValidateAndUpdate(&move.MTOShipments[i]); verrs.HasAny() || err != nil {
msg := "failure saving shipment when routing move submission"
appCtx.Logger().Error(msg, zap.Error(err))
diff --git a/playwright/tests/office/servicescounseling/servicesCounselingMobileHome.spec.js b/playwright/tests/office/servicescounseling/servicesCounselingMobileHome.spec.js
new file mode 100644
index 00000000000..0588fe6cd0d
--- /dev/null
+++ b/playwright/tests/office/servicescounseling/servicesCounselingMobileHome.spec.js
@@ -0,0 +1,47 @@
+// @ts-check
+import { test, expect } from './servicesCounselingTestFixture';
+
+test.describe('Services counselor user', () => {
+ test.beforeEach(async ({ scPage }) => {
+ const move = await scPage.testHarness.buildHHGMoveWithNTSAndNeedsSC();
+ await scPage.navigateToMove(move.locator);
+ });
+
+ test('Services Counselor can create a mobile home shipment and view shipment card info', async ({ page, scPage }) => {
+ const deliveryDate = new Date().toLocaleDateString('en-US');
+ await page.getByTestId('dropdown').selectOption({ label: 'Mobile Home' });
+
+ await expect(page.getByRole('heading', { level: 1 })).toHaveText('Add shipment details');
+ await expect(page.getByTestId('tag')).toHaveText('Mobile Home');
+
+ await page.getByLabel('Year').fill('2022');
+ await page.getByLabel('Make').fill('make');
+ await page.getByLabel('Model').fill('model');
+ await page.getByTestId('lengthFeet').fill('22');
+ await page.getByTestId('widthFeet').fill('22');
+ await page.getByTestId('heightFeet').fill('22');
+
+ await page.locator('#requestedPickupDate').fill(deliveryDate);
+ await page.locator('#requestedPickupDate').blur();
+ await page.getByText('Use current address').click();
+ await page.locator('#requestedDeliveryDate').fill('16 Mar 2022');
+ await page.locator('#requestedDeliveryDate').blur();
+
+ await page.getByLabel('Counselor remarks').fill('Sample counselor remarks');
+
+ // Save the shipment
+ await page.getByRole('button', { name: 'Save' }).click();
+ await scPage.waitForPage.moveDetails();
+
+ await expect(page.getByTestId('ShipmentContainer')).toHaveCount(2);
+
+ await expect(page.getByText('Mobile home year')).toBeVisible();
+ await expect(page.getByTestId('year')).toHaveText('2022');
+ await expect(page.getByText('Mobile home make')).toBeVisible();
+ await expect(page.getByTestId('make')).toHaveText('make');
+ await expect(page.getByText('Mobile home model')).toBeVisible();
+ await expect(page.getByTestId('model')).toHaveText('model');
+ await expect(page.getByText('Dimensions')).toBeVisible();
+ await expect(page.getByTestId('dimensions')).toHaveText("22' L x 22' W x 22' H");
+ });
+});
diff --git a/playwright/tests/office/txo/tooFlowsMobileHome.spec.js b/playwright/tests/office/txo/tooFlowsMobileHome.spec.js
new file mode 100644
index 00000000000..0d98f4c5ee7
--- /dev/null
+++ b/playwright/tests/office/txo/tooFlowsMobileHome.spec.js
@@ -0,0 +1,58 @@
+/**
+ * Semi-automated converted from a cypress test, and thus may contain
+ * non best-practices, in particular: heavy use of `page.locator`
+ * instead of `page.getBy*`.
+ */
+
+// @ts-check
+import { test, expect } from '../../utils/office/officeTest';
+
+import { TooFlowPage } from './tooTestFixture';
+
+test.describe('TOO user', () => {
+ /** @type {TooFlowPage} */
+ let tooFlowPage;
+
+ test.beforeEach(async ({ officePage }) => {
+ const move = await officePage.testHarness.buildHHGMoveWithServiceItemsAndPaymentRequestsAndFilesForTOO();
+ await officePage.signInAsNewTOOUser();
+ tooFlowPage = new TooFlowPage(officePage, move);
+ await tooFlowPage.waitForLoading();
+ await officePage.tooNavigateToMove(tooFlowPage.moveLocator);
+ });
+
+ test('TOO can create a mobile home shipment and view shipment card info', async ({ page }) => {
+ const deliveryDate = new Date().toLocaleDateString('en-US');
+ await page.getByTestId('dropdown').selectOption({ label: 'Mobile Home' });
+
+ await expect(page.getByRole('heading', { level: 1 })).toHaveText('Add shipment details');
+ await expect(page.getByTestId('tag')).toHaveText('Mobile Home');
+
+ await page.getByLabel('Year').fill('2022');
+ await page.getByLabel('Make').fill('make');
+ await page.getByLabel('Model').fill('model');
+ await page.getByTestId('lengthFeet').fill('22');
+ await page.getByTestId('widthFeet').fill('22');
+ await page.getByTestId('heightFeet').fill('22');
+
+ await page.locator('#requestedPickupDate').fill(deliveryDate);
+ await page.locator('#requestedPickupDate').blur();
+ await page.getByText('Use current address').click();
+ await page.locator('#requestedDeliveryDate').fill('16 Mar 2022');
+ await page.locator('#requestedDeliveryDate').blur();
+
+ // Save the shipment
+ await page.getByRole('button', { name: 'Save' }).click();
+
+ await expect(page.getByTestId('ShipmentContainer')).toHaveCount(2);
+
+ await expect(page.getByText('Mobile home year')).toBeVisible();
+ await expect(page.getByTestId('year')).toHaveText('2022');
+ await expect(page.getByText('Mobile home make')).toBeVisible();
+ await expect(page.getByTestId('make')).toHaveText('make');
+ await expect(page.getByText('Mobile home model')).toBeVisible();
+ await expect(page.getByTestId('model')).toHaveText('model');
+ await expect(page.getByText('Dimensions')).toBeVisible();
+ await expect(page.getByTestId('dimensions')).toHaveText("22' L x 22' W x 22' H");
+ });
+});
diff --git a/src/components/Office/DefinitionLists/MobileHomeShipmentInfoList.jsx b/src/components/Office/DefinitionLists/MobileHomeShipmentInfoList.jsx
new file mode 100644
index 00000000000..8854c983d75
--- /dev/null
+++ b/src/components/Office/DefinitionLists/MobileHomeShipmentInfoList.jsx
@@ -0,0 +1,394 @@
+import React, { useEffect, useState } from 'react';
+import * as PropTypes from 'prop-types';
+import classNames from 'classnames';
+
+import shipmentDefinitionListsStyles from './ShipmentDefinitionLists.module.scss';
+
+import styles from 'styles/descriptionList.module.scss';
+import { formatDateWithUTC } from 'shared/dates';
+import { ShipmentShape } from 'types/shipment';
+import { formatAddress, formatAgent } from 'utils/shipmentDisplay';
+import { convertInchesToFeetAndInches } from 'utils/formatMtoShipment';
+import {
+ setFlagStyles,
+ setDisplayFlags,
+ getDisplayFlags,
+ getMissingOrDash,
+ fieldValidationShape,
+} from 'utils/displayFlags';
+import { ADDRESS_UPDATE_STATUS } from 'constants/shipments';
+import { isBooleanFlagEnabled } from 'utils/featureFlags';
+
+const ShipmentInfoList = ({
+ className,
+ shipment,
+ warnIfMissing,
+ errorIfMissing,
+ showWhenCollapsed,
+ isExpanded,
+ isForEvaluationReport,
+}) => {
+ const {
+ actualPickupDate,
+ requestedPickupDate,
+ requiredDeliveryDate,
+ scheduledDeliveryDate,
+ requestedDeliveryDate,
+ scheduledPickupDate,
+ actualDeliveryDate,
+ pickupAddress,
+ secondaryPickupAddress,
+ tertiaryPickupAddress,
+ destinationAddress,
+ destinationType,
+ displayDestinationType,
+ secondaryDeliveryAddress,
+ tertiaryDeliveryAddress,
+ mtoAgents,
+ counselorRemarks,
+ customerRemarks,
+ deliveryAddressUpdate,
+ } = shipment;
+
+ const { year, make, model, lengthInInches, widthInInches, heightInInches } = shipment?.mobileHomeShipment || {};
+
+ setFlagStyles({
+ row: styles.row,
+ warning: shipmentDefinitionListsStyles.warning,
+ missingInfoError: shipmentDefinitionListsStyles.missingInfoError,
+ });
+ setDisplayFlags(errorIfMissing, warnIfMissing, showWhenCollapsed, null, shipment);
+
+ const [isTertiaryAddressEnabled, setIsTertiaryAddressEnabled] = useState(false);
+ useEffect(() => {
+ const fetchData = async () => {
+ setIsTertiaryAddressEnabled(await isBooleanFlagEnabled('third_address_available'));
+ };
+ if (!isForEvaluationReport) fetchData();
+ }, [isForEvaluationReport]);
+
+ const showElement = (elementFlags) => {
+ return (isExpanded || elementFlags.alwaysShow) && !elementFlags.hideRow;
+ };
+
+ 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`;
+
+ const releasingAgent = mtoAgents ? mtoAgents.find((agent) => agent.agentType === 'RELEASING_AGENT') : false;
+ const receivingAgent = mtoAgents ? mtoAgents.find((agent) => agent.agentType === 'RECEIVING_AGENT') : false;
+
+ const agentsElementFlags = getDisplayFlags('mtoAgents');
+ const releasingAgentElement = !releasingAgent ? (
+
+
Releasing agent
+ —
+
+ ) : (
+
+
Releasing agent
+ {formatAgent(releasingAgent)}
+
+ );
+
+ const receivingAgentElement = !receivingAgent ? (
+
+
Receiving agent
+ —
+
+ ) : (
+
+
Receiving agent
+ {formatAgent(receivingAgent)}
+
+ );
+
+ const scheduledPickupDateElementFlags = getDisplayFlags('scheduledPickupDate');
+ const scheduledPickupDateElement = (
+
+
Scheduled pickup date
+
+ {(scheduledPickupDate && formatDateWithUTC(scheduledPickupDate, 'DD MMM YYYY')) ||
+ getMissingOrDash('scheduledPickupDate')}
+
+
+ );
+
+ const requestedPickupDateElementFlags = getDisplayFlags('scheduledPickupDate');
+ const requestedPickupDateElement = (
+
+
Requested pickup date
+
+ {(requestedPickupDate && formatDateWithUTC(requestedPickupDate, 'DD MMM YYYY')) ||
+ getMissingOrDash('requestedPickupDate')}
+
+
+ );
+
+ const actualPickupDateElementFlags = getDisplayFlags('actualPickupDate');
+ const actualPickupDateElement = (
+
+
Actual pickup date
+
+ {(actualPickupDate && formatDateWithUTC(actualPickupDate, 'DD MMM YYYY')) ||
+ getMissingOrDash('actualPickupDate')}
+
+
+ );
+
+ const requestedDeliveryDateElementFlags = getDisplayFlags('requestedDeliveryDate');
+ const requestedDeliveryDateElement = (
+
+
Requested delivery date
+
+ {(requestedDeliveryDate && formatDateWithUTC(requestedDeliveryDate, 'DD MMM YYYY')) ||
+ getMissingOrDash('requestedDeliveryDate')}
+
+
+ );
+
+ const scheduledDeliveryDateElementFlags = getDisplayFlags('scheduledDeliveryDate');
+ const scheduledDeliveryDateElement = (
+
+
Scheduled delivery date
+
+ {(scheduledDeliveryDate && formatDateWithUTC(scheduledDeliveryDate, 'DD MMM YYYY')) ||
+ getMissingOrDash('scheduledDeliveryDate')}
+
+
+ );
+
+ const requiredDeliveryDateElementFlags = getDisplayFlags('requiredDeliveryDate');
+ const requiredDeliveryDateElement = (
+
+
Required delivery date
+
+ {(requiredDeliveryDate && formatDateWithUTC(requiredDeliveryDate, 'DD MMM YYYY')) ||
+ getMissingOrDash('requiredDeliveryDate')}
+
+
+ );
+ const actualDeliveryDateElementFlags = getDisplayFlags('actualDeliveryDate');
+ const actualDeliveryDateElement = (
+
+
Actual delivery date
+
+ {(actualDeliveryDate && formatDateWithUTC(actualDeliveryDate, 'DD MMM YYYY')) ||
+ getMissingOrDash('actualDeliverDate')}
+
+
+ );
+
+ const pickupAddressElementFlags = getDisplayFlags('pickupAddress');
+ const pickupAddressElement = (
+
+
Origin address
+
+ {(pickupAddress && formatAddress(pickupAddress)) || getMissingOrDash('pickupAddress')}
+
+
+ );
+
+ const secondaryPickupAddressElementFlags = getDisplayFlags('secondaryPickupAddress');
+ const secondaryPickupAddressElement = (
+
+
Second pickup address
+
+ {secondaryPickupAddress ? formatAddress(secondaryPickupAddress) : '—'}
+
+
+ );
+
+ const tertiaryPickupAddressElementFlags = getDisplayFlags('tertiaryPickupAddress');
+ const tertiaryPickupAddressElement = (
+
+
Third pickup address
+ {tertiaryPickupAddress ? formatAddress(tertiaryPickupAddress) : '—'}
+
+ );
+
+ const destinationTypeFlags = getDisplayFlags('destinationType');
+ const destinationTypeElement = (
+
+
Destination type
+ {destinationType || getMissingOrDash('destinationType')}
+
+ );
+
+ const destinationAddressElementFlags = getDisplayFlags('destinationAddress');
+ const destinationAddressElement = (
+
+
Destination address
+
+ {deliveryAddressUpdate?.status === ADDRESS_UPDATE_STATUS.REQUESTED
+ ? 'Review required'
+ : (destinationAddress && formatAddress(destinationAddress)) || '—'}
+
+
+ );
+
+ const secondaryDeliveryAddressElementFlags = getDisplayFlags('secondaryDeliveryAddress');
+ const secondaryDeliveryAddressElement = (
+
+
Second destination address
+
+ {secondaryDeliveryAddress ? formatAddress(secondaryDeliveryAddress) : '—'}
+
+
+ );
+
+ const tertiaryDeliveryAddressElementFlags = getDisplayFlags('tertiaryDeliveryAddress');
+ const tertiaryDeliveryAddressElement = (
+
+
Third destination address
+
+ {tertiaryDeliveryAddress ? formatAddress(tertiaryDeliveryAddress) : '—'}
+
+
+ );
+
+ const yearElementFlags = getDisplayFlags('year');
+ const yearElement = (
+
+
Mobile home year
+ {year}
+
+ );
+
+ const makeElementFlags = getDisplayFlags('make');
+ const makeElement = (
+
+
Mobile home make
+ {make}
+
+ );
+
+ const modelElementFlags = getDisplayFlags('model');
+ const modelElement = (
+
+
Mobile home model
+ {model}
+
+ );
+
+ const dimensionsElementFlags = getDisplayFlags('dimensions');
+ const dimensionsElement = (
+
+
Dimensions
+ {formattedDimensions}
+
+ );
+
+ const counselorRemarksElementFlags = getDisplayFlags('counselorRemarks');
+ const counselorRemarksElement = (
+
+
Counselor remarks
+ {counselorRemarks || '—'}
+
+ );
+
+ const customerRemarksElementFlags = getDisplayFlags('customerRemarks');
+ const customerRemarksElement = (
+
+
Customer remarks
+ {customerRemarks || '—'}
+
+ );
+
+ const defaultDetails = (
+
+ {requestedPickupDateElement}
+ {pickupAddressElement}
+ {secondaryPickupAddressElement}
+ {isTertiaryAddressEnabled ? tertiaryPickupAddressElement : null}
+ {showElement(agentsElementFlags) && releasingAgentElement}
+ {showElement(requestedDeliveryDateElementFlags) && requestedDeliveryDateElement}
+ {requestedDeliveryDateElement}
+ {destinationAddressElement}
+ {showElement(destinationTypeFlags) && displayDestinationType && destinationTypeElement}
+ {secondaryDeliveryAddressElement}
+ {isTertiaryAddressEnabled ? tertiaryDeliveryAddressElement : null}
+ {showElement(agentsElementFlags) && receivingAgentElement}
+ {yearElement}
+ {makeElement}
+ {modelElement}
+ {dimensionsElement}
+ {counselorRemarksElement}
+ {customerRemarksElement}
+
+ );
+
+ const evaluationReportDetails = (
+
+
+
+ {showElement(scheduledPickupDateElement) && scheduledPickupDateElement}
+ {showElement(actualPickupDateElementFlags) && actualPickupDateElement}
+ {showElement(requestedDeliveryDateElementFlags) && requestedDeliveryDateElement}
+ {showElement(agentsElementFlags) && releasingAgentElement}
+
+
+
+
+ {showElement(scheduledDeliveryDateElementFlags) && scheduledDeliveryDateElement}
+ {showElement(requiredDeliveryDateElementFlags) && requiredDeliveryDateElement}
+ {showElement(actualDeliveryDateElementFlags) && actualDeliveryDateElement}
+ {showElement(agentsElementFlags) && receivingAgentElement}
+
+
+
+ );
+
+ return {isForEvaluationReport ? evaluationReportDetails : defaultDetails}
;
+};
+
+ShipmentInfoList.propTypes = {
+ className: PropTypes.string,
+ shipment: ShipmentShape.isRequired,
+ warnIfMissing: PropTypes.arrayOf(fieldValidationShape),
+ errorIfMissing: PropTypes.arrayOf(fieldValidationShape),
+ showWhenCollapsed: PropTypes.arrayOf(PropTypes.string),
+ isExpanded: PropTypes.bool,
+};
+
+ShipmentInfoList.defaultProps = {
+ className: '',
+ warnIfMissing: [],
+ errorIfMissing: [],
+ showWhenCollapsed: [],
+ isExpanded: false,
+};
+
+export default ShipmentInfoList;
diff --git a/src/components/Office/DefinitionLists/MobileHomeShipmentInfoList.stories.jsx b/src/components/Office/DefinitionLists/MobileHomeShipmentInfoList.stories.jsx
new file mode 100644
index 00000000000..735c38d3ff4
--- /dev/null
+++ b/src/components/Office/DefinitionLists/MobileHomeShipmentInfoList.stories.jsx
@@ -0,0 +1,99 @@
+import React from 'react';
+import { object, text } from '@storybook/addon-knobs';
+
+import MobileHomeShipmentInfoList from './MobileHomeShipmentInfoList';
+
+export default {
+ title: 'Office Components/Mobile Home Shipment Info List',
+ component: MobileHomeShipmentInfoList,
+};
+
+const mobileHomeShipment = {
+ mobileHomeShipment: {
+ year: 2020,
+ make: 'Yamaha',
+ model: '242X E-Series',
+ lengthInInches: 276,
+ widthInInches: 102,
+ heightInInches: 120,
+ },
+ pickupAddress: {
+ streetAddress1: '123 Harbor Dr',
+ city: 'Miami',
+ state: 'FL',
+ postalCode: '33101',
+ },
+ destinationAddress: {
+ streetAddress1: '456 Marina Blvd',
+ city: 'Key West',
+ state: 'FL',
+ postalCode: '33040',
+ },
+ secondaryPickupAddress: {
+ streetAddress1: '789 Seaport Ln',
+ city: 'Fort Lauderdale',
+ state: 'FL',
+ postalCode: '33316',
+ },
+ tertiaryPickupAddress: {
+ streetAddress1: '101 Yacht Club Rd',
+ city: 'Naples',
+ state: 'FL',
+ postalCode: '34102',
+ },
+ secondaryDeliveryAddress: {
+ streetAddress1: '111 Ocean Dr',
+ city: 'Palm Beach',
+ state: 'FL',
+ postalCode: '33480',
+ },
+ tertiaryDeliveryAddress: {
+ streetAddress1: '222 Shoreline Dr',
+ city: 'Clearwater',
+ state: 'FL',
+ postalCode: '33767',
+ },
+ mtoAgents: [
+ {
+ agentType: 'RELEASING_AGENT',
+ firstName: 'John',
+ lastName: 'Doe',
+ phone: '123-456-7890',
+ email: 'john.doe@example.com',
+ },
+ {
+ agentType: 'RECEIVING_AGENT',
+ firstName: 'Jane',
+ lastName: 'Doe',
+ phone: '987-654-3210',
+ email: 'jane.doe@example.com',
+ },
+ ],
+ counselorRemarks: 'Please be cautious with the mobile home.',
+ customerRemarks: 'Handle with care.',
+};
+
+export const Basic = () => (
+
+);
+
+export const DefaultView = () => (
+
+);
diff --git a/src/components/Office/DefinitionLists/MobileHomeShipmentInfoList.test.jsx b/src/components/Office/DefinitionLists/MobileHomeShipmentInfoList.test.jsx
new file mode 100644
index 00000000000..3d50715bc35
--- /dev/null
+++ b/src/components/Office/DefinitionLists/MobileHomeShipmentInfoList.test.jsx
@@ -0,0 +1,123 @@
+import React from 'react';
+import { render, screen, within, act } from '@testing-library/react';
+
+import MobileHomeShipmentInfoList from './MobileHomeShipmentInfoList';
+
+import { isBooleanFlagEnabled } from 'utils/featureFlags';
+
+jest.mock('utils/featureFlags', () => ({
+ ...jest.requireActual('utils/featureFlags'),
+ isBooleanFlagEnabled: jest.fn().mockImplementation(() => Promise.resolve(false)),
+}));
+
+const shipment = {
+ mobileHomeShipment: {
+ year: 2022,
+ make: 'Fleetwood',
+ model: 'Southwind',
+ lengthInInches: 3600,
+ widthInInches: 1020,
+ heightInInches: 1440,
+ },
+ requestedPickupDate: '2020-03-26',
+ pickupAddress: {
+ streetAddress1: '123 Harbor Dr',
+ city: 'Miami',
+ state: 'FL',
+ postalCode: '33101',
+ },
+ destinationAddress: {
+ streetAddress1: '456 Marina Blvd',
+ city: 'Key West',
+ state: 'FL',
+ postalCode: '33040',
+ },
+ mtoAgents: [
+ {
+ agentType: 'RELEASING_AGENT',
+ firstName: 'John',
+ lastName: 'Doe',
+ phone: '123-456-7890',
+ email: 'john.doe@example.com',
+ },
+ {
+ agentType: 'RECEIVING_AGENT',
+ firstName: 'Jane',
+ lastName: 'Smith',
+ phone: '987-654-3210',
+ email: 'jane.smith@example.com',
+ },
+ ],
+ counselorRemarks: 'Handle with care.',
+ customerRemarks: 'Please avoid scratches.',
+};
+
+const labels = {
+ requestedPickupDate: 'Requested pickup date',
+ pickupAddress: 'Origin address',
+ destinationAddress: 'Destination address',
+ mtoAgents: ['Releasing agent', 'Receiving agent'],
+ counselorRemarks: 'Counselor remarks',
+ customerRemarks: 'Customer remarks',
+ dimensions: 'Dimensions',
+};
+
+describe('Shipment Info List - Mobile Home Shipment', () => {
+ it('renders all mobile home shipment fields when provided and expanded', async () => {
+ isBooleanFlagEnabled.mockImplementation(() => Promise.resolve(true));
+
+ await act(async () => {
+ render();
+ });
+
+ const requestedPickupDate = screen.getByText(labels.requestedPickupDate);
+ expect(within(requestedPickupDate.parentElement).getByText('26 Mar 2020')).toBeInTheDocument();
+
+ const pickupAddress = screen.getByText(labels.pickupAddress);
+ expect(
+ within(pickupAddress.parentElement).getByText(shipment.pickupAddress.streetAddress1, { exact: false }),
+ ).toBeInTheDocument();
+
+ const destinationAddress = screen.getByText(labels.destinationAddress);
+ expect(
+ within(destinationAddress.parentElement).getByText(shipment.destinationAddress.streetAddress1, {
+ exact: false,
+ }),
+ ).toBeInTheDocument();
+
+ const releasingAgent = screen.getByText(labels.mtoAgents[0]);
+ expect(
+ within(releasingAgent.parentElement).getByText(shipment.mtoAgents[0].email, { exact: false }),
+ ).toBeInTheDocument();
+
+ const receivingAgent = screen.getByText(labels.mtoAgents[1]);
+ expect(
+ within(receivingAgent.parentElement).getByText(shipment.mtoAgents[1].email, { exact: false }),
+ ).toBeInTheDocument();
+
+ const counselorRemarks = screen.getByText(labels.counselorRemarks);
+ expect(within(counselorRemarks.parentElement).getByText(shipment.counselorRemarks)).toBeInTheDocument();
+
+ const customerRemarks = screen.getByText(labels.customerRemarks);
+ expect(within(customerRemarks.parentElement).getByText(shipment.customerRemarks)).toBeInTheDocument();
+
+ const dimensions = screen.getByText(labels.dimensions);
+ expect(within(dimensions.parentElement).getByText("300' L x 85' W x 120' H", { exact: false })).toBeInTheDocument();
+ });
+
+ it('does not render mtoAgents when not provided', async () => {
+ await act(async () => {
+ render(
+ ,
+ );
+ });
+
+ expect(screen.queryByText(labels.mtoAgents[0])).not.toBeInTheDocument();
+ expect(screen.queryByText(labels.mtoAgents[1])).not.toBeInTheDocument();
+ });
+});
diff --git a/src/components/Office/DefinitionLists/ShipmentInfoListSelector.jsx b/src/components/Office/DefinitionLists/ShipmentInfoListSelector.jsx
index ce2441f7d77..fa152b443d9 100644
--- a/src/components/Office/DefinitionLists/ShipmentInfoListSelector.jsx
+++ b/src/components/Office/DefinitionLists/ShipmentInfoListSelector.jsx
@@ -6,6 +6,7 @@ import ShipmentInfoList from 'components/Office/DefinitionLists/ShipmentInfoList
import PPMShipmentInfoList from 'components/Office/DefinitionLists/PPMShipmentInfoList';
import NTSRShipmentInfoList from 'components/Office/DefinitionLists/NTSRShipmentInfoList';
import NTSShipmentInfoList from 'components/Office/DefinitionLists/NTSShipmentInfoList';
+import MobileHomeShipmentInfoList from 'components/Office/DefinitionLists/MobileHomeShipmentInfoList';
import BoatShipmentInfoList from 'components/Office/DefinitionLists/BoatShipmentInfoList';
import { SHIPMENT_OPTIONS, SHIPMENT_TYPES } from 'shared/constants';
import { fieldValidationShape } from 'utils/displayFlags';
@@ -79,6 +80,20 @@ const ShipmentInfoListSelector = ({
isForEvaluationReport={isForEvaluationReport}
/>
);
+ case SHIPMENT_OPTIONS.MOBILE_HOME:
+ return (
+
+ );
case SHIPMENT_OPTIONS.BOAT:
case SHIPMENT_TYPES.BOAT_HAUL_AWAY:
case SHIPMENT_TYPES.BOAT_TOW_AWAY:
diff --git a/src/components/Office/ShipmentForm/MobileHomeShipmentForm/MobileHomeShipmentForm.jsx b/src/components/Office/ShipmentForm/MobileHomeShipmentForm/MobileHomeShipmentForm.jsx
new file mode 100644
index 00000000000..b5b7f2dd1c1
--- /dev/null
+++ b/src/components/Office/ShipmentForm/MobileHomeShipmentForm/MobileHomeShipmentForm.jsx
@@ -0,0 +1,207 @@
+import React from 'react';
+import { Fieldset } from '@trussworks/react-uswds';
+import { func } from 'prop-types';
+import classnames from 'classnames';
+
+import styles from './MobileHomeShipmentForm.module.scss';
+
+import formStyles from 'styles/form.module.scss';
+import SectionWrapper from 'components/Customer/SectionWrapper';
+import MaskedTextField from 'components/form/fields/MaskedTextField/MaskedTextField';
+import TextField from 'components/form/fields/TextField/TextField';
+import { ErrorMessage } from 'components/form/index';
+
+const MobileHomeShipmentForm = ({
+ lengthHasError,
+ widthHasError,
+ heightHasError,
+ setFieldTouched,
+ setFieldError,
+ validateForm,
+ dimensionError,
+}) => {
+ return (
+
+
+ Mobile Home Information
+
+
+ {
+ setFieldError('year', null);
+ }}
+ onBlur={() => {
+ setFieldTouched('year', true);
+ setFieldError('year', null);
+ validateForm();
+ }}
+ required
+ />
+
+
+
+
+
+
+
+
+ Mobile Home Dimensions
+ Enter the total outside dimensions of the mobile home.
+
+
+
+ The dimensions do not meet the requirements for a mobile home shipment. Please cancel and select a
+ different shipment type.
+
+
+
+
+
+
+
+
+ );
+};
+
+export default MobileHomeShipmentForm;
+
+MobileHomeShipmentForm.propTypes = {
+ lengthHasError: func.isRequired,
+ widthHasError: func.isRequired,
+ heightHasError: func.isRequired,
+ setFieldTouched: func.isRequired,
+ setFieldError: func.isRequired,
+ validateForm: func.isRequired,
+};
diff --git a/src/components/Office/ShipmentForm/MobileHomeShipmentForm/MobileHomeShipmentForm.module.scss b/src/components/Office/ShipmentForm/MobileHomeShipmentForm/MobileHomeShipmentForm.module.scss
new file mode 100644
index 00000000000..093d03a6cd4
--- /dev/null
+++ b/src/components/Office/ShipmentForm/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/Office/ShipmentForm/MobileHomeShipmentForm/mobileHomeShipmentSchema.js b/src/components/Office/ShipmentForm/MobileHomeShipmentForm/mobileHomeShipmentSchema.js
new file mode 100644
index 00000000000..bc64db9ea22
--- /dev/null
+++ b/src/components/Office/ShipmentForm/MobileHomeShipmentForm/mobileHomeShipmentSchema.js
@@ -0,0 +1,108 @@
+import * as Yup from 'yup';
+
+import {
+ AdditionalAddressSchema,
+ RequiredPlaceSchema,
+ OptionalPlaceSchema,
+} from 'components/Customer/MtoShipmentForm/validationSchemas';
+import { toTotalInches } from 'utils/formatMtoShipment';
+
+const currentYear = new Date().getFullYear();
+const maxYear = currentYear + 2;
+
+const mobileHomeShipmentSchema = () => {
+ const formSchema = Yup.object()
+ .shape({
+ 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().min(0).nullable(),
+
+ 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).nullable(),
+
+ 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).nullable(),
+
+ pickup: RequiredPlaceSchema,
+ delivery: OptionalPlaceSchema,
+ secondaryPickup: AdditionalAddressSchema,
+ secondaryDelivery: AdditionalAddressSchema,
+ tertiaryPickup: AdditionalAddressSchema,
+ tertiaryDelivery: AdditionalAddressSchema,
+ counselorRemarks: Yup.string(),
+ customerRemarks: Yup.string(),
+ })
+ .test('dimension-check', 'Dimensions requirements.', function dimensionTest(values) {
+ const { lengthFeet, lengthInches, widthFeet, widthInches, heightFeet, heightInches } = values;
+ const hasLength = lengthFeet !== undefined || lengthInches !== undefined;
+ const hasWidth = widthFeet !== undefined || widthInches !== undefined;
+ const hasHeight = heightFeet !== undefined || heightInches !== undefined;
+
+ if (hasLength && hasWidth && hasHeight) {
+ const lengthInInches = toTotalInches(lengthFeet, lengthInches);
+ const widthInInches = toTotalInches(widthFeet, widthInches);
+ const heightInInches = toTotalInches(heightFeet, heightInches);
+
+ if (lengthInInches < 1 && widthInInches < 1 && heightInInches < 1) {
+ const errors = [];
+ errors.push(
+ this.createError({
+ path: 'lengthFeet',
+ message: 'Dimensions do not meet the requirement.',
+ }),
+ );
+
+ errors.push(
+ this.createError({
+ path: 'widthFeet',
+ message: 'Dimensions do not meet the requirement.',
+ }),
+ );
+
+ errors.push(
+ this.createError({
+ path: 'heightFeet',
+ message: 'Dimensions do not meet the requirement.',
+ }),
+ );
+
+ if (errors.length) {
+ throw new Yup.ValidationError(errors);
+ }
+ }
+ }
+ return true;
+ });
+ return formSchema;
+};
+
+export default mobileHomeShipmentSchema;
diff --git a/src/components/Office/ShipmentForm/ShipmentForm.jsx b/src/components/Office/ShipmentForm/ShipmentForm.jsx
index 72ae2a3806d..5a3b1d9e9aa 100644
--- a/src/components/Office/ShipmentForm/ShipmentForm.jsx
+++ b/src/components/Office/ShipmentForm/ShipmentForm.jsx
@@ -12,6 +12,8 @@ import { CloseoutOfficeInput } from '../../form/fields/CloseoutOfficeInput';
import ppmShipmentSchema from './ppmShipmentSchema';
import styles from './ShipmentForm.module.scss';
+import MobileHomeShipmentForm from './MobileHomeShipmentForm/MobileHomeShipmentForm';
+import mobileHomeShipmentSchema from './MobileHomeShipmentForm/mobileHomeShipmentSchema';
import BoatShipmentForm from './BoatShipmentForm/BoatShipmentForm';
import boatShipmentSchema from './BoatShipmentForm/boatShipmentSchema';
@@ -58,6 +60,8 @@ import {
formatMtoShipmentForDisplay,
formatPpmShipmentForAPI,
formatPpmShipmentForDisplay,
+ formatMobileHomeShipmentForDisplay,
+ formatMobileHomeShipmentForAPI,
formatBoatShipmentForDisplay,
formatBoatShipmentForAPI,
} from 'utils/formatMtoShipment';
@@ -249,6 +253,7 @@ const ShipmentForm = (props) => {
const isNTS = shipmentType === SHIPMENT_OPTIONS.NTS;
const isNTSR = shipmentType === SHIPMENT_OPTIONS.NTSR;
const isPPM = shipmentType === SHIPMENT_OPTIONS.PPM;
+ const isMobileHome = shipmentType === SHIPMENT_OPTIONS.MOBILE_HOME;
const isBoat =
shipmentType === SHIPMENT_OPTIONS.BOAT ||
shipmentType === SHIPMENT_OPTIONS.BOAT_HAUL_AWAY ||
@@ -279,6 +284,11 @@ const ShipmentForm = (props) => {
closeoutOffice: move.closeoutOffice,
},
);
+ } else if (isMobileHome) {
+ const hhgInitialValues = formatMtoShipmentForDisplay(
+ isCreatePage ? { userRole } : { userRole, shipmentType, agents: mtoShipment.mtoAgents, ...mtoShipment },
+ );
+ initialValues = formatMobileHomeShipmentForDisplay(mtoShipment?.mobileHomeShipment, hhgInitialValues);
} else if (isBoat) {
const hhgInitialValues = formatMtoShipmentForDisplay(
isCreatePage ? { userRole } : { userRole, shipmentType, agents: mtoShipment.mtoAgents, ...mtoShipment },
@@ -306,6 +316,10 @@ const ShipmentForm = (props) => {
showCloseoutOffice,
sitEstimatedWeightMax: estimatedWeightValue || 0,
});
+ } else if (isMobileHome) {
+ schema = mobileHomeShipmentSchema();
+ showDeliveryFields = true;
+ showPickupFields = true;
} else if (isBoat) {
schema = boatShipmentSchema();
showDeliveryFields = true;
@@ -541,6 +555,15 @@ const ShipmentForm = (props) => {
tertiaryDelivery: hasTertiaryDelivery === 'yes' ? tertiaryDelivery : {},
});
+ // Mobile Home Shipment
+ if (isMobileHome) {
+ const mobileHomeShipmentBody = formatMobileHomeShipmentForAPI(formValues);
+ pendingMtoShipment = {
+ ...pendingMtoShipment,
+ ...mobileHomeShipmentBody,
+ };
+ }
+
// Boat Shipment
if (isBoat) {
const boatShipmentBody = formatBoatShipmentForAPI(formValues);
@@ -612,13 +635,13 @@ const ShipmentForm = (props) => {
values,
isValid,
isSubmitting,
- touched,
setValues,
handleSubmit,
+ errors,
+ touched,
setFieldTouched,
setFieldError,
validateForm,
- errors,
}) => {
const {
hasSecondaryDestination,
@@ -829,10 +852,23 @@ const ShipmentForm = (props) => {