diff --git a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go index 79c46d780fd..ab76de035db 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload.go @@ -2294,6 +2294,8 @@ func SearchMoves(appCtx appcontext.AppContext, moves models.Moves) *ghcmessages. if err != nil { destinationGBLOC = *ghcmessages.NewGBLOC("") + } else if customer.Affiliation.String() == "MARINES" { + destinationGBLOC = ghcmessages.GBLOC("USMC/" + PostalCodeToGBLOC.GBLOC) } else { destinationGBLOC = ghcmessages.GBLOC(PostalCodeToGBLOC.GBLOC) } diff --git a/pkg/handlers/ghcapi/internal/payloads/model_to_payload_test.go b/pkg/handlers/ghcapi/internal/payloads/model_to_payload_test.go index 071e410967f..5bb3db594b5 100644 --- a/pkg/handlers/ghcapi/internal/payloads/model_to_payload_test.go +++ b/pkg/handlers/ghcapi/internal/payloads/model_to_payload_test.go @@ -6,6 +6,7 @@ import ( "github.com/gofrs/uuid" + "github.com/transcom/mymove/pkg/factory" "github.com/transcom/mymove/pkg/gen/ghcmessages" "github.com/transcom/mymove/pkg/handlers" "github.com/transcom/mymove/pkg/models" @@ -350,3 +351,24 @@ func (suite *PayloadsSuite) TestCreateCustomer() { suite.IsType(returnedShipmentAddressUpdate, &ghcmessages.CreatedCustomer{}) }) } + +func (suite *PayloadsSuite) TestSearchMoves() { + appCtx := suite.AppContextForTest() + + marines := models.AffiliationMARINES + moveUSMC := factory.BuildMove(suite.DB(), []factory.Customization{ + { + Model: models.ServiceMember{ + Affiliation: &marines, + }, + }, + }, nil) + + moves := models.Moves{moveUSMC} + suite.Run("Success - Returns a ghcmessages Upload payload from Upload Struct", func() { + payload := SearchMoves(appCtx, moves) + + suite.IsType(payload, &ghcmessages.SearchMoves{}) + suite.NotNil(payload) + }) +} diff --git a/src/components/Office/RequestedShipments/ApprovedRequestedShipments.jsx b/src/components/Office/RequestedShipments/ApprovedRequestedShipments.jsx index e181bcc952e..635932cf4f1 100644 --- a/src/components/Office/RequestedShipments/ApprovedRequestedShipments.jsx +++ b/src/components/Office/RequestedShipments/ApprovedRequestedShipments.jsx @@ -1,6 +1,6 @@ import React from 'react'; import * as PropTypes from 'prop-types'; -import { generatePath } from 'react-router-dom'; +import { generatePath, useParams, useNavigate } from 'react-router-dom'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import styles from './RequestedShipments.module.scss'; @@ -14,6 +14,10 @@ import shipmentCardsStyles from 'styles/shipmentCards.module.scss'; import { MTOServiceItemShape, OrdersInfoShape } from 'types/order'; import { ShipmentShape } from 'types/shipment'; import { formatDateFromIso } from 'utils/formatters'; +import ButtonDropdown from 'components/ButtonDropdown/ButtonDropdown'; +import { SHIPMENT_OPTIONS_URL } from 'shared/constants'; +import Restricted from 'components/Restricted/Restricted'; +import { permissionTypes } from 'constants/permissions'; // nts defaults show preferred pickup date and pickup address, flagged items when collapsed // ntsr defaults shows preferred delivery date, storage facility address, destination address, flagged items when collapsed @@ -66,11 +70,54 @@ const ApprovedRequestedShipments = ({ }; }; + const { moveCode } = useParams(); + const navigate = useNavigate(); + const handleButtonDropdownChange = (e) => { + const selectedOption = e.target.value; + + const addShipmentPath = `${generatePath(tooRoutes.SHIPMENT_ADD_PATH, { + moveCode, + shipmentType: selectedOption, + })}`; + + navigate(addShipmentPath); + }; + const dutyLocationPostal = { postalCode: ordersInfo.newDutyLocation?.address?.postalCode }; return (
-

Approved Shipments

+
+

Approved Shipments

+
+ {!isMoveLocked && ( + + + + + + + + + + + + )} +
+
+
{mtoShipments && mtoShipments.map((shipment) => { diff --git a/src/components/Office/RequestedShipments/RequestedShipments.module.scss b/src/components/Office/RequestedShipments/RequestedShipments.module.scss index 0c6ae8d3f66..0e488e96bf3 100644 --- a/src/components/Office/RequestedShipments/RequestedShipments.module.scss +++ b/src/components/Office/RequestedShipments/RequestedShipments.module.scss @@ -7,6 +7,19 @@ @include u-padding-x(4); @include u-padding-y(2); + .sectionHeader { + display: flex; + justify-content: center; + align-items: center; + h2 { + margin-right: auto; + } + + .buttonDropdown { + margin-left: auto + } + } + h4 { @include u-margin(0); font-weight: bold; diff --git a/src/components/Office/RequestedShipments/RequestedShipments.test.jsx b/src/components/Office/RequestedShipments/RequestedShipments.test.jsx index 2dd12b87ee9..4b6230ec3d4 100644 --- a/src/components/Office/RequestedShipments/RequestedShipments.test.jsx +++ b/src/components/Office/RequestedShipments/RequestedShipments.test.jsx @@ -2,6 +2,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { render, screen, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { generatePath } from 'react-router-dom'; import { shipments, @@ -19,12 +20,15 @@ import { import ApprovedRequestedShipments from './ApprovedRequestedShipments'; import SubmittedRequestedShipments from './SubmittedRequestedShipments'; +import { SHIPMENT_OPTIONS_URL } from 'shared/constants'; +import { tooRoutes } from 'constants/routes'; import { MockProviders } from 'testUtils'; import { permissionTypes } from 'constants/permissions'; +const mockNavigate = jest.fn(); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), - useNavigate: () => jest.fn(), + useNavigate: () => mockNavigate, })); const moveTaskOrder = { @@ -115,7 +119,7 @@ const submittedRequestedShipmentsComponentServicesCounselingCompleted = ( ); const submittedRequestedShipmentsComponentMissingRequiredInfo = ( - + ); +const submittedRequestedShipmentsCanCreateNewShipment = ( + + + +); + +const testProps = { + ordersInfo, + allowancesInfo, + customerInfo, + mtoShipments: shipments, + approveMTO, + mtoServiceItems: [], + moveCode: 'TE5TC0DE', +}; + describe('RequestedShipments', () => { describe('Prime-handled shipments', () => { it('renders the container successfully without services counseling completed', () => { @@ -215,6 +244,12 @@ describe('RequestedShipments', () => { expect(container.querySelector('#approvalConfirmationModal')).toHaveStyle('display: block'); }); + it('renders Add a new shjipment Button', async () => { + render(submittedRequestedShipmentsCanCreateNewShipment); + + expect(await screen.getByRole('combobox', { name: 'Add a new shipment' })).toBeInTheDocument(); + }); + it('disables the Approve selected button when there is missing required information', async () => { const { container } = render(submittedRequestedShipmentsComponentMissingRequiredInfo); @@ -226,6 +261,8 @@ describe('RequestedShipments', () => { ); }); + expect(await screen.getByRole('combobox', { name: 'Add a new shipment' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Approve selected' })).toBeDisabled(); await act(async () => { @@ -417,15 +454,6 @@ describe('RequestedShipments', () => { }); describe('Permission dependent rendering', () => { - const testProps = { - ordersInfo, - allowancesInfo, - customerInfo, - mtoShipments: shipments, - approveMTO, - mtoServiceItems: [], - moveCode: 'TE5TC0DE', - }; it('renders the "Add service items to move" section when user has permission', () => { render( @@ -449,6 +477,76 @@ describe('RequestedShipments', () => { }); }); + describe('shows the dropdown and navigates to each option when mtoshipments are submitted', () => { + it.each([ + [ + SHIPMENT_OPTIONS_URL.HHG, + SHIPMENT_OPTIONS_URL.NTS, + SHIPMENT_OPTIONS_URL.NTSrelease, + SHIPMENT_OPTIONS_URL.MOBILE_HOME, + SHIPMENT_OPTIONS_URL.BOAT, + ], + ])('selects the %s option and navigates to the matching form for that shipment type', async (shipmentType) => { + render( + + , + , + ); + + const path = `${generatePath(tooRoutes.SHIPMENT_ADD_PATH, { + moveCode: 'TE5TC0DE', + shipmentType, + })}`; + + const buttonDropdown = await screen.findByRole('combobox'); + + expect(buttonDropdown).toBeInTheDocument(); + + await userEvent.selectOptions(buttonDropdown, shipmentType); + + expect(mockNavigate).toHaveBeenCalledWith(path); + }); + }); + + describe('shows the dropdown and navigates to each option when mtoshipments are approved', () => { + it.each([ + [ + SHIPMENT_OPTIONS_URL.HHG, + SHIPMENT_OPTIONS_URL.NTS, + SHIPMENT_OPTIONS_URL.NTSrelease, + SHIPMENT_OPTIONS_URL.MOBILE_HOME, + SHIPMENT_OPTIONS_URL.BOAT, + ], + ])('selects the %s option and navigates to the matching form for that shipment type', async (shipmentType) => { + render( + + , + , + ); + + const path = `${generatePath(tooRoutes.SHIPMENT_ADD_PATH, { + moveCode: 'TE5TC0DE', + shipmentType, + })}`; + + const buttonDropdown = await screen.findByRole('combobox'); + + expect(buttonDropdown).toBeInTheDocument(); + + await userEvent.selectOptions(buttonDropdown, shipmentType); + + expect(mockNavigate).toHaveBeenCalledWith(path); + }); + }); + describe('Conditional form display', () => { const renderComponent = (props) => { render( @@ -465,36 +563,36 @@ describe('RequestedShipments', () => { moveCode: 'TE5TC0DE', }; it('does not render the "Add service items to move" section when both service items are present', () => { - const testProps = { + const testPropsMsCs = { mtoServiceItems: serviceItemsMSandCS, mtoShipments: shipments, ...conditionalFormTestProps, }; - renderComponent(testProps); + renderComponent(testPropsMsCs); expect(screen.queryByText('Add service items to this move')).not.toBeInTheDocument(); expect(screen.getByText('Approve selected')).toBeInTheDocument(); }); it('does not render the "Add service items to move" section when counseling is present and all shipments are PPM', () => { - const testProps = { + const testPropsCS = { mtoServiceItems: serviceItemsCS, mtoShipments: ppmOnlyShipments, ...conditionalFormTestProps, }; - renderComponent(testProps); + renderComponent(testPropsCS); expect(screen.queryByText('Add service items to this move')).not.toBeInTheDocument(); expect(screen.getByText('Approve selected')).toBeInTheDocument(); }); it('renders the "Add service items to move" section with only counseling when only move management is present in service items', () => { - const testProps = { + const testPropsMS = { mtoServiceItems: serviceItemsMS, mtoShipments: shipments, ...conditionalFormTestProps, }; - renderComponent(testProps); + renderComponent(testPropsMS); expect(screen.getByText('Add service items to this move')).toBeInTheDocument(); expect(screen.getByText('Approve selected')).toBeInTheDocument(); @@ -503,12 +601,12 @@ describe('RequestedShipments', () => { }); it('renders the "Add service items to move" section with only move management when only counseling is present in service items', () => { - const testProps = { + const testPropsCS = { mtoServiceItems: serviceItemsCS, mtoShipments: shipments, ...conditionalFormTestProps, }; - renderComponent(testProps); + renderComponent(testPropsCS); expect(screen.getByText('Add service items to this move')).toBeInTheDocument(); expect(screen.getByText('Approve selected')).toBeInTheDocument(); @@ -517,12 +615,12 @@ describe('RequestedShipments', () => { }); it('renders the "Add service items to move" section with all fields when neither counseling nor move management is present in service items', () => { - const testProps = { + const testPropsServiceItemsEmpty = { mtoServiceItems: serviceItemsEmpty, mtoShipments: shipments, ...conditionalFormTestProps, }; - renderComponent(testProps); + renderComponent(testPropsServiceItemsEmpty); expect(screen.getByText('Add service items to this move')).toBeInTheDocument(); expect(screen.getByText('Approve selected')).toBeInTheDocument(); @@ -531,12 +629,12 @@ describe('RequestedShipments', () => { }); it('renders the "Add service items to move" section with only counseling when all shipments are PPM', () => { - const testProps = { + const testPropsServiceItemsEmpty = { mtoServiceItems: serviceItemsEmpty, mtoShipments: ppmOnlyShipments, ...conditionalFormTestProps, }; - renderComponent(testProps); + renderComponent(testPropsServiceItemsEmpty); expect(screen.getByText('Add service items to this move')).toBeInTheDocument(); expect(screen.getByText('Approve selected')).toBeInTheDocument(); diff --git a/src/components/Office/RequestedShipments/SubmittedRequestedShipments.jsx b/src/components/Office/RequestedShipments/SubmittedRequestedShipments.jsx index bf847cb57cb..ecac5105be9 100644 --- a/src/components/Office/RequestedShipments/SubmittedRequestedShipments.jsx +++ b/src/components/Office/RequestedShipments/SubmittedRequestedShipments.jsx @@ -2,7 +2,7 @@ import React, { useCallback, useState } from 'react'; import { useFormik } from 'formik'; import * as PropTypes from 'prop-types'; import { Button, Checkbox, Fieldset } from '@trussworks/react-uswds'; -import { generatePath } from 'react-router-dom'; +import { generatePath, useParams, useNavigate } from 'react-router-dom'; import { debounce } from 'lodash'; import styles from './RequestedShipments.module.scss'; @@ -21,6 +21,8 @@ import shipmentCardsStyles from 'styles/shipmentCards.module.scss'; import { MoveTaskOrderShape, MTOServiceItemShape, OrdersInfoShape } from 'types/order'; import { ShipmentShape } from 'types/shipment'; import { fieldValidationShape } from 'utils/displayFlags'; +import ButtonDropdown from 'components/ButtonDropdown/ButtonDropdown'; +import { SHIPMENT_OPTIONS_URL } from 'shared/constants'; // nts defaults show preferred pickup date and pickup address, flagged items when collapsed // ntsr defaults shows preferred delivery date, storage facility address, destination address, flagged items when collapsed @@ -67,6 +69,19 @@ const SubmittedRequestedShipments = ({ ntsSac: ordersInfo.NTSsac, }; + const { moveCode } = useParams(); + const navigate = useNavigate(); + const handleButtonDropdownChange = (e) => { + const selectedOption = e.target.value; + + const addShipmentPath = `${generatePath(tooRoutes.SHIPMENT_ADD_PATH, { + moveCode, + shipmentType: selectedOption, + })}`; + + navigate(addShipmentPath); + }; + const shipmentDisplayInfo = (shipment, dutyLocationPostal) => { const destType = displayDestinationType ? shipmentDestinationTypes[shipment.destinationType] : null; @@ -203,7 +218,36 @@ const SubmittedRequestedShipments = ({
-

Requested shipments

+
+

Requested shipments

+
+ {!isMoveLocked && ( + + + + + + + + + + + + )} +
+
{mtoShipments && mtoShipments.map((shipment) => { diff --git a/src/pages/Office/MoveDetails/MoveDetails.jsx b/src/pages/Office/MoveDetails/MoveDetails.jsx index 08a0a63d898..9181eee8036 100644 --- a/src/pages/Office/MoveDetails/MoveDetails.jsx +++ b/src/pages/Office/MoveDetails/MoveDetails.jsx @@ -1,12 +1,11 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { generatePath, Link, useNavigate, useParams } from 'react-router-dom'; +import { Link, useNavigate, useParams } from 'react-router-dom'; import { Alert, Grid, GridContainer } from '@trussworks/react-uswds'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useQueryClient, useMutation } from '@tanstack/react-query'; import { func } from 'prop-types'; import styles from '../TXOMoveInfo/TXOTab.module.scss'; -import 'styles/office.scss'; import hasRiskOfExcess from 'utils/hasRiskOfExcess'; import { MOVES, MTO_SERVICE_ITEMS, MTO_SHIPMENTS } from 'constants/queryKeys'; @@ -15,7 +14,6 @@ import SERVICE_ITEM_STATUSES from 'constants/serviceItems'; import { ADDRESS_UPDATE_STATUS, shipmentStatuses } from 'constants/shipments'; import AllowancesList from 'components/Office/DefinitionLists/AllowancesList'; import CustomerInfoList from 'components/Office/DefinitionLists/CustomerInfoList'; -import ButtonDropdown from 'components/ButtonDropdown/ButtonDropdown'; import OrdersList from 'components/Office/DefinitionLists/OrdersList'; import DetailsPanel from 'components/Office/DetailsPanel/DetailsPanel'; import FinancialReviewButton from 'components/Office/FinancialReviewButton/FinancialReviewButton'; @@ -29,7 +27,6 @@ import LeftNavTag from 'components/LeftNavTag/LeftNavTag'; import Restricted from 'components/Restricted/Restricted'; import LoadingPlaceholder from 'shared/LoadingPlaceholder'; import SomethingWentWrong from 'shared/SomethingWentWrong'; -import { SHIPMENT_OPTIONS_URL } from 'shared/constants'; import { SIT_EXTENSION_STATUS } from 'constants/sitExtensions'; import { ORDERS_TYPE } from 'constants/orders'; import { permissionTypes } from 'constants/permissions'; @@ -176,17 +173,6 @@ const MoveDetails = ({ (shipment) => shipment?.deliveryAddressUpdate?.status === ADDRESS_UPDATE_STATUS.REQUESTED && !shipment.deletedAt, ); - const handleButtonDropdownChange = (e) => { - const selectedOption = e.target.value; - - const addShipmentPath = `${generatePath(tooRoutes.SHIPMENT_ADD_PATH, { - moveCode, - shipmentType: selectedOption, - })}`; - - navigate(addShipmentPath); - }; - useEffect(() => { const shipmentCount = shipmentWithDestinationAddressChangeRequest?.length || 0; if (setShipmentsWithDeliveryAddressUpdateRequestedCount) @@ -408,34 +394,12 @@ const MoveDetails = ({ initialSelection={move?.financialReviewFlag} /> )} - - {alertMessage && ( - - - {alertMessage} - - - )} - - {!isMoveLocked && ( - - - - - - - - - - + {alertMessage && ( + + + {alertMessage} + + )} {submittedShipments?.length > 0 && (
diff --git a/src/pages/Office/MoveDetails/MoveDetails.test.jsx b/src/pages/Office/MoveDetails/MoveDetails.test.jsx index d1c3e45105e..859ddea30b0 100644 --- a/src/pages/Office/MoveDetails/MoveDetails.test.jsx +++ b/src/pages/Office/MoveDetails/MoveDetails.test.jsx @@ -22,10 +22,12 @@ const setUnapprovedSITExtensionCount = jest.fn(); const setMissingOrdersInfoCount = jest.fn(); const setShipmentErrorConcernCount = jest.fn(); +const mockNavigate = jest.fn(); +const mockRequestedMoveCode = 'TE5TC0DE'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), - useParams: () => ({ moveCode: 'TE5TC0DE' }), - useLocation: jest.fn(), + useParams: () => ({ moveCode: mockRequestedMoveCode }), + useNavigate: () => mockNavigate, })); const requestedMoveDetailsQuery = { @@ -1108,6 +1110,16 @@ describe('MoveDetails page', () => { expect(screen.queryByRole('link', { name: 'View orders' })).not.toBeInTheDocument(); }); + it('renders add new shipment button when user has permission', async () => { + render( + + + , + ); + + expect(await screen.getByRole('combobox', { name: 'Add a new shipment' })).toBeInTheDocument(); + }); + it('renders view orders button if user does not have permission to update', async () => { render( diff --git a/src/pages/Office/TXOMoveInfo/TXOTab.module.scss b/src/pages/Office/TXOMoveInfo/TXOTab.module.scss index 5b3f7f1fe11..50465a7f999 100644 --- a/src/pages/Office/TXOMoveInfo/TXOTab.module.scss +++ b/src/pages/Office/TXOMoveInfo/TXOTab.module.scss @@ -27,11 +27,22 @@ @include u-margin-bottom(4); } + .div { + justify-content: flex-end; + float: right; + padding-right: 15px; + } + + .buttonDropdown { + margin-left: auto; + } + .tooMoveDetailsHeadingFlexbox { display: flex; flex-direction: row; justify-content: space-between; align-items: baseline; + padding-bottom: 30px; } .tooMoveDetailsH1 { @@ -122,6 +133,12 @@ border: 1px solid #dfe1e2; } +.buttonDropdown { + display: flex; + justify-content: flex-end; + padding: 0; +} + .section + .section, .shipmentCard + .shipmentCard { @include u-margin-top(4);