diff --git a/pkg/gen/ghcapi/embedded_spec.go b/pkg/gen/ghcapi/embedded_spec.go
index 740d92c587f..842883ee673 100644
--- a/pkg/gen/ghcapi/embedded_spec.go
+++ b/pkg/gen/ghcapi/embedded_spec.go
@@ -1993,7 +1993,8 @@ func init() {
"APPROVALS REQUESTED",
"APPROVED",
"NEEDS SERVICE COUNSELING",
- "SERVICE COUNSELING COMPLETED"
+ "SERVICE COUNSELING COMPLETED",
+ "CANCELED"
]
}
}
@@ -2351,7 +2352,7 @@ func init() {
"operationId": "moveCanceler",
"responses": {
"200": {
- "description": "Successfully cancelled move",
+ "description": "Successfully canceled move",
"schema": {
"$ref": "#/definitions/Move"
}
@@ -2374,7 +2375,10 @@ func init() {
"500": {
"$ref": "#/responses/ServerError"
}
- }
+ },
+ "x-permissions": [
+ "update.cancelMoveFlag"
+ ]
},
"parameters": [
{
@@ -11286,7 +11290,7 @@ func init() {
"PPMStatus": {
"type": "string",
"enum": [
- "CANCELLED",
+ "CANCELED",
"DRAFT",
"SUBMITTED",
"WAITING_ON_CUSTOMER",
@@ -16763,7 +16767,8 @@ func init() {
"APPROVALS REQUESTED",
"APPROVED",
"NEEDS SERVICE COUNSELING",
- "SERVICE COUNSELING COMPLETED"
+ "SERVICE COUNSELING COMPLETED",
+ "CANCELED"
]
}
}
@@ -17202,7 +17207,7 @@ func init() {
"operationId": "moveCanceler",
"responses": {
"200": {
- "description": "Successfully cancelled move",
+ "description": "Successfully canceled move",
"schema": {
"$ref": "#/definitions/Move"
}
@@ -17243,7 +17248,10 @@ func init() {
"$ref": "#/definitions/Error"
}
}
- }
+ },
+ "x-permissions": [
+ "update.cancelMoveFlag"
+ ]
},
"parameters": [
{
@@ -27091,7 +27099,7 @@ func init() {
"PPMStatus": {
"type": "string",
"enum": [
- "CANCELLED",
+ "CANCELED",
"DRAFT",
"SUBMITTED",
"WAITING_ON_CUSTOMER",
diff --git a/pkg/gen/ghcapi/ghcoperations/move/move_canceler_responses.go b/pkg/gen/ghcapi/ghcoperations/move/move_canceler_responses.go
index 828a8086069..d96b78fc5af 100644
--- a/pkg/gen/ghcapi/ghcoperations/move/move_canceler_responses.go
+++ b/pkg/gen/ghcapi/ghcoperations/move/move_canceler_responses.go
@@ -17,7 +17,7 @@ import (
const MoveCancelerOKCode int = 200
/*
-MoveCancelerOK Successfully cancelled move
+MoveCancelerOK Successfully canceled move
swagger:response moveCancelerOK
*/
diff --git a/pkg/gen/ghcapi/ghcoperations/move/search_moves.go b/pkg/gen/ghcapi/ghcoperations/move/search_moves.go
index 9ca46adcc1f..f22f0fbf7cf 100644
--- a/pkg/gen/ghcapi/ghcoperations/move/search_moves.go
+++ b/pkg/gen/ghcapi/ghcoperations/move/search_moves.go
@@ -370,7 +370,7 @@ var searchMovesBodyStatusItemsEnum []interface{}
func init() {
var res []string
- if err := json.Unmarshal([]byte(`["DRAFT","SUBMITTED","APPROVALS REQUESTED","APPROVED","NEEDS SERVICE COUNSELING","SERVICE COUNSELING COMPLETED"]`), &res); err != nil {
+ if err := json.Unmarshal([]byte(`["DRAFT","SUBMITTED","APPROVALS REQUESTED","APPROVED","NEEDS SERVICE COUNSELING","SERVICE COUNSELING COMPLETED","CANCELED"]`), &res); err != nil {
panic(err)
}
for _, v := range res {
diff --git a/pkg/gen/ghcmessages/p_p_m_status.go b/pkg/gen/ghcmessages/p_p_m_status.go
index a581261623a..720b370a8d3 100644
--- a/pkg/gen/ghcmessages/p_p_m_status.go
+++ b/pkg/gen/ghcmessages/p_p_m_status.go
@@ -30,8 +30,8 @@ func (m PPMStatus) Pointer() *PPMStatus {
const (
- // PPMStatusCANCELLED captures enum value "CANCELLED"
- PPMStatusCANCELLED PPMStatus = "CANCELLED"
+ // PPMStatusCANCELED captures enum value "CANCELED"
+ PPMStatusCANCELED PPMStatus = "CANCELED"
// PPMStatusDRAFT captures enum value "DRAFT"
PPMStatusDRAFT PPMStatus = "DRAFT"
@@ -60,7 +60,7 @@ var pPMStatusEnum []interface{}
func init() {
var res []PPMStatus
- if err := json.Unmarshal([]byte(`["CANCELLED","DRAFT","SUBMITTED","WAITING_ON_CUSTOMER","NEEDS_ADVANCE_APPROVAL","NEEDS_CLOSEOUT","CLOSEOUT_COMPLETE","COMPLETED"]`), &res); err != nil {
+ if err := json.Unmarshal([]byte(`["CANCELED","DRAFT","SUBMITTED","WAITING_ON_CUSTOMER","NEEDS_ADVANCE_APPROVAL","NEEDS_CLOSEOUT","CLOSEOUT_COMPLETE","COMPLETED"]`), &res); err != nil {
panic(err)
}
for _, v := range res {
diff --git a/pkg/handlers/authentication/permissions.go b/pkg/handlers/authentication/permissions.go
index 75efbb75cd0..ca2c9045bcb 100644
--- a/pkg/handlers/authentication/permissions.go
+++ b/pkg/handlers/authentication/permissions.go
@@ -40,6 +40,7 @@ var TOO = RolePermissions{
"update.closeoutOffice",
"update.MTOPage",
"create.TXOShipment",
+ "update.cancelMoveFlag",
},
}
@@ -86,6 +87,7 @@ var ServicesCounselor = RolePermissions{
"update.customer",
"update.closeoutOffice",
"view.closeoutOffice",
+ "update.cancelMoveFlag",
},
}
diff --git a/pkg/services/move/move_canceler.go b/pkg/services/move/move_canceler.go
index 26a0c2e04bb..64ec885b93d 100644
--- a/pkg/services/move/move_canceler.go
+++ b/pkg/services/move/move_canceler.go
@@ -39,7 +39,7 @@ func (f *moveCanceler) CancelMove(appCtx appcontext.AppContext, moveID uuid.UUID
if shipment.PPMShipment != nil {
if shipment.PPMShipment.Status == models.PPMShipmentStatusCloseoutComplete {
- return apperror.NewConflictError(move.ID, " cannot cancel move with approved shipment.")
+ return apperror.NewConflictError(move.ID, " cannot cancel move with a closeout complete shipment.")
}
var ppmshipment models.PPMShipment
qerr := appCtx.DB().Where("id = ?", shipment.PPMShipment.ID).First(&ppmshipment)
@@ -55,9 +55,7 @@ func (f *moveCanceler) CancelMove(appCtx appcontext.AppContext, moveID uuid.UUID
} else if err != nil {
return apperror.NewQueryError("PPM Shipment", err, "Failed to update status for ppm shipment")
}
- }
-
- if shipment.Status != models.MTOShipmentStatusApproved {
+ } else if shipment.Status != models.MTOShipmentStatusApproved {
verrs, err := txnAppCtx.DB().ValidateAndUpdate(&shipmentDelta)
if verrs != nil && verrs.HasAny() {
return apperror.NewInvalidInputError(shipment.ID, err, verrs, "Validation errors found while setting shipment status")
diff --git a/pkg/services/move/move_canceler_test.go b/pkg/services/move/move_canceler_test.go
index 314b0719075..0cad20c55eb 100644
--- a/pkg/services/move/move_canceler_test.go
+++ b/pkg/services/move/move_canceler_test.go
@@ -30,4 +30,23 @@ func (suite *MoveServiceSuite) TestMoveCanceler() {
_, err := moveCanceler.CancelMove(suite.AppContextForTest(), move.ID)
suite.Error(err)
})
+
+ suite.Run("fails to cancel move with close complete ppm shipment", func() {
+ move := factory.BuildMove(suite.DB(), nil, nil)
+
+ factory.BuildPPMShipment(suite.DB(), []factory.Customization{
+ {
+ Model: move,
+ LinkOnly: true,
+ },
+ {
+ Model: models.PPMShipment{
+ Status: models.PPMShipmentStatusCloseoutComplete,
+ },
+ },
+ }, nil)
+
+ _, err := moveCanceler.CancelMove(suite.AppContextForTest(), move.ID)
+ suite.Error(err)
+ })
}
diff --git a/src/components/ConfirmationModals/CancelMoveConfirmationModal.jsx b/src/components/ConfirmationModals/CancelMoveConfirmationModal.jsx
new file mode 100644
index 00000000000..3a705d1ab6a
--- /dev/null
+++ b/src/components/ConfirmationModals/CancelMoveConfirmationModal.jsx
@@ -0,0 +1,45 @@
+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 CancelMoveConfirmationModal = ({ onClose, onSubmit, moveID, title, content, submitText, closeText }) => (
+
+ onClose()} />
+
+ {title}
+
+ {content}
+
+
+
+
+
+);
+
+CancelMoveConfirmationModal.propTypes = {
+ onClose: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+
+ title: PropTypes.string,
+ content: PropTypes.string,
+ submitText: PropTypes.string,
+ closeText: PropTypes.string,
+};
+
+CancelMoveConfirmationModal.defaultProps = {
+ title: 'Are you sure?',
+ content:
+ 'You’ll lose all the information in this move. If you want it back later, you’ll have to request a new move.',
+ submitText: 'Cancel move',
+ closeText: 'Keep move',
+};
+
+CancelMoveConfirmationModal.displayName = 'CancelMoveConfirmationModal';
+
+export default connectModal(CancelMoveConfirmationModal);
diff --git a/src/components/ConfirmationModals/CancelMoveConfirmationModal.stories.jsx b/src/components/ConfirmationModals/CancelMoveConfirmationModal.stories.jsx
new file mode 100644
index 00000000000..15205131faa
--- /dev/null
+++ b/src/components/ConfirmationModals/CancelMoveConfirmationModal.stories.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+
+import CancelMoveConfirmationModal from './CancelMoveConfirmationModal';
+
+export default {
+ title: 'Components/CancelMoveConfirmationModal',
+ component: CancelMoveConfirmationModal,
+ args: {
+ moveID: '111',
+ },
+ argTypes: {
+ onClose: { action: 'close button clicked' },
+ onSubmit: { action: 'submit button clicked' },
+ },
+};
+
+const Template = (args) => ;
+
+export const Basic = Template.bind({});
+
+export const WithOverrides = Template.bind({});
+WithOverrides.args = {
+ title: 'This is a sample title',
+ content: 'Some sample description',
+ submitText: 'YES!',
+ closeText: 'NO',
+};
+
+const ConnectedTemplate = (args) => ;
+export const ConnectedModal = ConnectedTemplate.bind({});
+ConnectedModal.args = {
+ isOpen: true,
+};
diff --git a/src/components/ConfirmationModals/CancelMoveConfirmationModal.test.jsx b/src/components/ConfirmationModals/CancelMoveConfirmationModal.test.jsx
new file mode 100644
index 00000000000..76c5b4b885c
--- /dev/null
+++ b/src/components/ConfirmationModals/CancelMoveConfirmationModal.test.jsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { CancelMoveConfirmationModal } from 'components/ConfirmationModals/CancelMoveConfirmationModal';
+
+let onClose;
+let onSubmit;
+beforeEach(() => {
+ onClose = jest.fn();
+ onSubmit = jest.fn();
+});
+
+describe('CancelMoveConfirmationModal', () => {
+ const moveID = '123456';
+
+ it('renders the component', async () => {
+ render();
+
+ expect(await screen.findByRole('heading', { level: 3, name: 'Are you sure?' })).toBeInTheDocument();
+ });
+
+ it('closes the modal when close icon is clicked', async () => {
+ render();
+
+ const closeButton = await screen.findByTestId('modalCloseButton');
+
+ await userEvent.click(closeButton);
+
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+
+ it('closes the modal when the keep button is clicked', async () => {
+ render();
+
+ const keepButton = await screen.findByRole('button', { name: 'Keep move' });
+
+ await userEvent.click(keepButton);
+
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls the submit function when cancel button is clicked', async () => {
+ render();
+
+ const cancelButton = await screen.findByRole('button', { name: 'Cancel move' });
+
+ await userEvent.click(cancelButton);
+
+ expect(onSubmit).toHaveBeenCalledWith(moveID);
+ expect(onSubmit).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/components/Office/EvaluationReportShipmentDisplay/EvaluationReportShipmentDisplay.jsx b/src/components/Office/EvaluationReportShipmentDisplay/EvaluationReportShipmentDisplay.jsx
index b13f4b0f6c6..e562a3d8521 100644
--- a/src/components/Office/EvaluationReportShipmentDisplay/EvaluationReportShipmentDisplay.jsx
+++ b/src/components/Office/EvaluationReportShipmentDisplay/EvaluationReportShipmentDisplay.jsx
@@ -54,7 +54,7 @@ const EvaluationReportShipmentDisplay = ({
{displayInfo.isDiversion && diversion}
- {displayInfo.shipmentStatus === shipmentStatuses.CANCELED && cancelled}
+ {displayInfo.shipmentStatus === shipmentStatuses.CANCELED && canceled}
{displayInfo.shipmentStatus === shipmentStatuses.DIVERSION_REQUESTED && diversion requested}
{displayInfo.shipmentStatus === shipmentStatuses.CANCELLATION_REQUESTED && (
cancellation requested
diff --git a/src/components/Office/ShipmentDisplay/ShipmentDisplay.jsx b/src/components/Office/ShipmentDisplay/ShipmentDisplay.jsx
index d4827aefa74..c04154c1c8e 100644
--- a/src/components/Office/ShipmentDisplay/ShipmentDisplay.jsx
+++ b/src/components/Office/ShipmentDisplay/ShipmentDisplay.jsx
@@ -94,8 +94,10 @@ const ShipmentDisplay = ({
{displayInfo.isDiversion && diversion}
- {displayInfo.shipmentStatus === shipmentStatuses.CANCELED && (
- cancelled
+ {(displayInfo.shipmentStatus === shipmentStatuses.CANCELED ||
+ displayInfo.status === shipmentStatuses.CANCELED ||
+ displayInfo.ppmShipment?.status === ppmShipmentStatuses.CANCELED) && (
+ canceled
)}
{displayInfo.shipmentStatus === shipmentStatuses.DIVERSION_REQUESTED && diversion requested}
{displayInfo.shipmentStatus === shipmentStatuses.CANCELLATION_REQUESTED && (
diff --git a/src/components/Office/ShipmentDisplay/ShipmentDisplay.stories.jsx b/src/components/Office/ShipmentDisplay/ShipmentDisplay.stories.jsx
index 2f4c4e3ef12..f9bbbb6ba14 100644
--- a/src/components/Office/ShipmentDisplay/ShipmentDisplay.stories.jsx
+++ b/src/components/Office/ShipmentDisplay/ShipmentDisplay.stories.jsx
@@ -9,7 +9,7 @@ import {
ntsReleaseMissingInfo,
postalOnlyInfo,
diversionInfo,
- cancelledInfo,
+ canceledInfo,
ppmInfo,
ppmInfoApprovedOrExcluded,
ppmInfoRejected,
@@ -250,11 +250,11 @@ export const DivertedShipment = () => (
);
-export const CancelledShipment = () => (
+export const CanceledShipment = () => (
(
);
-export const CancelledShipmentReadOnly = () => (
+export const CanceledShipmentReadOnly = () => (
{
render();
expect(screen.getByText('diversion')).toBeInTheDocument();
});
- it('renders with cancelled tag', () => {
- render();
- expect(screen.getByText('cancelled')).toBeInTheDocument();
+ it('renders with canceled tag', () => {
+ render();
+ expect(screen.getByText('canceled')).toBeInTheDocument();
});
it('renders a disabled button when move is locked', () => {
render(
@@ -140,6 +140,10 @@ describe('Shipment Container', () => {
);
expect(screen.getByTestId('shipment-display-checkbox')).toBeDisabled();
});
+ it('renders with canceled tag', () => {
+ render();
+ expect(screen.getByText('canceled')).toBeInTheDocument();
+ });
});
describe('NTS-release shipment', () => {
@@ -177,6 +181,10 @@ describe('Shipment Container', () => {
expect(screen.getByText('external vendor')).toBeInTheDocument();
});
+ it('renders with canceled tag', () => {
+ render();
+ expect(screen.getByText('canceled')).toBeInTheDocument();
+ });
it('renders with external vendor tag', () => {
render(
{
);
expect(screen.getByTestId('tag', { name: 'packet ready for download' })).toBeInTheDocument();
});
+ it('renders with canceled tag', () => {
+ render();
+ expect(screen.getByText('canceled')).toBeInTheDocument();
+ });
it('excluded', () => {
render(
diff --git a/src/components/Office/ShipmentDisplay/ShipmentDisplayTestData.js b/src/components/Office/ShipmentDisplay/ShipmentDisplayTestData.js
index 5bf1b4e5a11..b3c584fc751 100644
--- a/src/components/Office/ShipmentDisplay/ShipmentDisplayTestData.js
+++ b/src/components/Office/ShipmentDisplay/ShipmentDisplayTestData.js
@@ -127,7 +127,7 @@ export const diversionInfo = {
counselorRemarks: 'counselor approved',
};
-export const cancelledInfo = {
+export const canceledInfo = {
heading: 'HHG',
shipmentId: 'testShipmentId394',
isDiversion: false,
diff --git a/src/components/Office/ShipmentHeading/ShipmentHeading.jsx b/src/components/Office/ShipmentHeading/ShipmentHeading.jsx
index cf01b46dd85..27ebdd71dc3 100644
--- a/src/components/Office/ShipmentHeading/ShipmentHeading.jsx
+++ b/src/components/Office/ShipmentHeading/ShipmentHeading.jsx
@@ -22,7 +22,7 @@ function ShipmentHeading({ shipmentInfo, handleShowCancellationModal, isMoveLock
{shipmentInfo.shipmentType}
- {shipmentStatus === shipmentStatuses.CANCELED &&
cancelled}
+ {shipmentStatus === shipmentStatuses.CANCELED &&
canceled}
{shipmentInfo.isDiversion &&
diversion}
{!shipmentInfo.isDiversion && shipmentStatus === shipmentStatuses.DIVERSION_REQUESTED && (
diversion requested
diff --git a/src/components/Office/ShipmentHeading/ShipmentHeading.test.jsx b/src/components/Office/ShipmentHeading/ShipmentHeading.test.jsx
index 910024714c5..27cacf3aa5d 100644
--- a/src/components/Office/ShipmentHeading/ShipmentHeading.test.jsx
+++ b/src/components/Office/ShipmentHeading/ShipmentHeading.test.jsx
@@ -41,7 +41,7 @@ describe('Shipment Heading with diversion requested shipment', () => {
});
});
-describe('Shipment Heading with cancelled shipment', () => {
+describe('Shipment Heading with canceled shipment', () => {
const wrapper = mount(
{
,
);
- it('renders the cancelled tag next to the shipment type', () => {
- expect(wrapper.find({ 'data-testid': 'tag' }).text()).toEqual('cancelled');
+ it('renders the canceled tag next to the shipment type', () => {
+ expect(wrapper.find({ 'data-testid': 'tag' }).text()).toEqual('canceled');
});
it('hides the request cancellation button', () => {
diff --git a/src/components/Office/ShipmentHeading/shipmentheading.stories.jsx b/src/components/Office/ShipmentHeading/shipmentheading.stories.jsx
index 9bc7911df34..e46d51cebee 100644
--- a/src/components/Office/ShipmentHeading/shipmentheading.stories.jsx
+++ b/src/components/Office/ShipmentHeading/shipmentheading.stories.jsx
@@ -49,7 +49,7 @@ export const shipmentHeadingDiversion = () => (
);
-export const shipmentHeadingCancelled = () => (
+export const shipmentHeadingCanceled = () => (
{
const { moveCode } = useParams();
const [isFinancialModalVisible, setIsFinancialModalVisible] = useState(false);
+ const [isCancelMoveModalVisible, setIsCancelMoveModalVisible] = useState(false);
const [alertMessage, setAlertMessage] = useState(null);
const [alertType, setAlertType] = useState('success');
@@ -88,6 +91,7 @@ const MoveDetails = ({
// for now we are only showing dest type on retiree and separatee orders
let isRetirementOrSeparation = false;
+ let numberOfShipmentsNotAllowedForCancel = 0;
isRetirementOrSeparation =
order?.order_type === ORDERS_TYPE.RETIREMENT || order?.order_type === ORDERS_TYPE.SEPARATION;
@@ -121,6 +125,20 @@ const MoveDetails = ({
},
});
+ const { mutate: mutateCancelMove } = useMutation(cancelMove, {
+ onSuccess: (data) => {
+ queryClient.setQueryData([MOVES, data.locator], data);
+ queryClient.invalidateQueries([MOVES, data.locator]);
+ queryClient.invalidateQueries({ queryKey: [MTO_SHIPMENTS] });
+ setAlertMessage('Move canceled.');
+ setAlertType('success');
+ },
+ onError: () => {
+ setAlertMessage('There was a problem cancelling the move. Please try again later.');
+ setAlertType('error');
+ },
+ });
+
const { mutate: mutateFinancialReview } = useMutation(updateFinancialFlag, {
onSuccess: (data) => {
queryClient.setQueryData([MOVES, data.locator], data);
@@ -157,6 +175,23 @@ const MoveDetails = ({
const handleCancelFinancialReviewModal = () => {
setIsFinancialModalVisible(false);
};
+
+ const handleShowCancelMoveModal = () => {
+ setIsCancelMoveModalVisible(true);
+ };
+
+ const handleCancelMove = () => {
+ mutateCancelMove({
+ moveID: move.id,
+ ifMatchETag: move.eTag,
+ });
+ setIsCancelMoveModalVisible(false);
+ };
+
+ const handleCloseCancelMoveModal = () => {
+ setIsCancelMoveModalVisible(false);
+ };
+
const submittedShipments = mtoShipments?.filter(
(shipment) => shipment.status === shipmentStatuses.SUBMITTED && !shipment.deletedAt,
);
@@ -180,6 +215,18 @@ const MoveDetails = ({
}, [shipmentWithDestinationAddressChangeRequest?.length, setShipmentsWithDeliveryAddressUpdateRequestedCount]);
const shipmentsInfoNonPPM = mtoShipments?.filter((shipment) => shipment.shipmentType !== 'PPM');
+ if (mtoShipments) {
+ const nonDeletedShipments = mtoShipments?.filter((shipment) => !shipment.deletedAt);
+ const nonPpmShipments = nonDeletedShipments.filter((shipment) => shipment.shipmentType !== 'PPM');
+ const nonPpmApprovedShipments = nonPpmShipments.filter(
+ (shipment) => shipment?.status === shipmentStatuses.APPROVED,
+ );
+ const onlyPpmShipments = nonDeletedShipments.filter((shipment) => shipment.shipmentType === 'PPM');
+ const ppmCloseoutCompleteShipments = onlyPpmShipments.filter(
+ (shipment) => shipment.ppmShipment?.status === ppmShipmentStatuses.CLOSEOUT_COMPLETE,
+ );
+ numberOfShipmentsNotAllowedForCancel = nonPpmApprovedShipments.length + ppmCloseoutCompleteShipments.length;
+ }
useEffect(() => {
const shipmentCount = submittedShipments?.length || 0;
@@ -329,6 +376,7 @@ const MoveDetails = ({
const hasAmendedOrders = ordersInfo.uploadedAmendedOrderID && !ordersInfo.amendedOrdersAcknowledgedAt;
const hasDestinationAddressUpdate =
shipmentWithDestinationAddressChangeRequest && shipmentWithDestinationAddressChangeRequest.length > 0;
+ const tooCanCancelMove = move.status !== MOVE_STATUSES.CANCELED && numberOfShipmentsNotAllowedForCancel === 0;
return (
@@ -374,17 +422,28 @@ const MoveDetails = ({
-
-
Move details
-
-
-
-
-
+
+
+ {alertMessage && (
+
+
+ {alertMessage}
+
+
+ )}
+
+
+ Move details
+
+
+
+
+
+
{isFinancialModalVisible && (
)}
- {alertMessage && (
-
-
- {alertMessage}
-
-
- )}
+
+
+
+ {tooCanCancelMove && !isMoveLocked && (
+
+ )}
+
+
+
+
{submittedShipments?.length > 0 && (
{
expect(screen.queryByRole('link', { name: 'Edit allowances' })).not.toBeInTheDocument();
expect(screen.queryByRole('link', { name: 'Edit customer info' })).not.toBeInTheDocument();
});
+
+ it('renders the cancel move button when user has permission', async () => {
+ render(
+
+
+ ,
+ );
+
+ expect(await screen.getByText('Cancel move')).toBeInTheDocument();
+ });
+
+ it('does not show the cancel move button if user does not have permission', () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.queryByText('Cancel move')).not.toBeInTheDocument();
+ });
});
describe('when MTO shipments are not yet defined', () => {
diff --git a/src/pages/Office/MoveTaskOrder/MoveTaskOrder.jsx b/src/pages/Office/MoveTaskOrder/MoveTaskOrder.jsx
index 97c119a8825..3b0ff2efc62 100644
--- a/src/pages/Office/MoveTaskOrder/MoveTaskOrder.jsx
+++ b/src/pages/Office/MoveTaskOrder/MoveTaskOrder.jsx
@@ -1219,7 +1219,7 @@ export const MoveTaskOrder = (props) => {
originPostalCode: pickupAddress?.postalCode || '',
destinationAddress: destinationAddress || dutyLocationPostal,
scheduledPickupDate: formattedScheduledPickup,
- shipmentStatus: mtoShipment.status,
+ shipmentStatus: mtoShipment.ppmShipment?.status || mtoShipment.status,
ifMatchEtag: mtoShipment.eTag,
moveTaskOrderID: mtoShipment.moveTaskOrderID,
shipmentLocator: mtoShipment.shipmentLocator,
diff --git a/src/pages/Office/MoveTaskOrder/MoveTaskOrder.test.jsx b/src/pages/Office/MoveTaskOrder/MoveTaskOrder.test.jsx
index 39736569bfa..0f114276dc1 100644
--- a/src/pages/Office/MoveTaskOrder/MoveTaskOrder.test.jsx
+++ b/src/pages/Office/MoveTaskOrder/MoveTaskOrder.test.jsx
@@ -4,7 +4,7 @@ import { render, screen } from '@testing-library/react';
import {
unapprovedMTOQuery,
- approvedMTOWithCancelledShipmentQuery,
+ approvedMTOWithCanceledShipmentQuery,
missingWeightQuery,
someShipmentsApprovedMTOQuery,
someWeightNotReturned,
@@ -685,8 +685,8 @@ describe('MoveTaskOrder', () => {
});
});
- describe('approved mto with cancelled shipment', () => {
- useMoveTaskOrderQueries.mockReturnValue(approvedMTOWithCancelledShipmentQuery);
+ describe('approved mto with canceled shipment', () => {
+ useMoveTaskOrderQueries.mockReturnValue(approvedMTOWithCanceledShipmentQuery);
const wrapper = mount(
{
it('renders the ShipmentHeading', () => {
expect(wrapper.find('ShipmentHeading').exists()).toBe(true);
expect(wrapper.find('h2').at(0).text()).toEqual('Household goods');
- expect(wrapper.find('span[data-testid="tag"]').at(0).text()).toEqual('cancelled');
+ expect(wrapper.find('span[data-testid="tag"]').at(0).text()).toEqual('canceled');
});
it('renders the ImportantShipmentDates', () => {
diff --git a/src/pages/Office/MoveTaskOrder/moveTaskOrderUnitTestData.js b/src/pages/Office/MoveTaskOrder/moveTaskOrderUnitTestData.js
index ffd30aea60a..ea37799f7b9 100644
--- a/src/pages/Office/MoveTaskOrder/moveTaskOrderUnitTestData.js
+++ b/src/pages/Office/MoveTaskOrder/moveTaskOrderUnitTestData.js
@@ -1268,7 +1268,7 @@ export const riskOfExcessWeightQueryExternalShipment = {
],
};
-export const approvedMTOWithCancelledShipmentQuery = {
+export const approvedMTOWithCanceledShipmentQuery = {
orders: {
1: {
id: '1',
diff --git a/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.jsx b/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.jsx
index c5be3bf99f0..5db21407df5 100644
--- a/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.jsx
+++ b/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.jsx
@@ -10,7 +10,7 @@ import styles from '../ServicesCounselingMoveInfo/ServicesCounselingTab.module.s
import scMoveDetailsStyles from './ServicesCounselingMoveDetails.module.scss';
-import { MOVES } from 'constants/queryKeys';
+import { MOVES, MTO_SHIPMENTS } from 'constants/queryKeys';
import { ORDERS_TYPE } from 'constants/orders';
import { servicesCounselingRoutes } from 'constants/routes';
import AllowancesList from 'components/Office/DefinitionLists/AllowancesList';
@@ -19,12 +19,13 @@ import OrdersList from 'components/Office/DefinitionLists/OrdersList';
import DetailsPanel from 'components/Office/DetailsPanel/DetailsPanel';
import FinancialReviewButton from 'components/Office/FinancialReviewButton/FinancialReviewButton';
import FinancialReviewModal from 'components/Office/FinancialReviewModal/FinancialReviewModal';
+import CancelMoveConfirmationModal from 'components/ConfirmationModals/CancelMoveConfirmationModal';
import ShipmentDisplay from 'components/Office/ShipmentDisplay/ShipmentDisplay';
import { SubmitMoveConfirmationModal } from 'components/Office/SubmitMoveConfirmationModal/SubmitMoveConfirmationModal';
import { useMoveDetailsQueries, useOrdersDocumentQueries } from 'hooks/queries';
-import { updateMoveStatusServiceCounselingCompleted, updateFinancialFlag } from 'services/ghcApi';
+import { updateMoveStatusServiceCounselingCompleted, cancelMove, updateFinancialFlag } from 'services/ghcApi';
import { MOVE_STATUSES, SHIPMENT_OPTIONS_URL, SHIPMENT_OPTIONS } from 'shared/constants';
-import { ppmShipmentStatuses } from 'constants/shipments';
+import { ppmShipmentStatuses, shipmentStatuses } from 'constants/shipments';
import shipmentCardsStyles from 'styles/shipmentCards.module.scss';
import LeftNav from 'components/LeftNav/LeftNav';
import LeftNavTag from 'components/LeftNavTag/LeftNavTag';
@@ -59,6 +60,7 @@ const ServicesCounselingMoveDetails = ({
const [moveHasExcessWeight, setMoveHasExcessWeight] = useState(false);
const [isSubmitModalVisible, setIsSubmitModalVisible] = useState(false);
const [isFinancialModalVisible, setIsFinancialModalVisible] = useState(false);
+ const [isCancelMoveModalVisible, setIsCancelMoveModalVisible] = useState(false);
const { upload, amendedUpload } = useOrdersDocumentQueries(moveCode);
const documentsForViewer = Object.values(upload || {})
.concat(Object.values(amendedUpload || {}))
@@ -76,6 +78,7 @@ const ServicesCounselingMoveDetails = ({
let counselorCanReview;
let reviewWeightsURL;
let counselorCanEdit;
+ let counselorCanCancelMove;
let counselorCanEditNonPPM;
const sections = useMemo(() => {
@@ -116,6 +119,7 @@ const ServicesCounselingMoveDetails = ({
let shipmentsInfo = [];
let ppmShipmentsInfoNeedsApproval = [];
let ppmShipmentsOtherStatuses = [];
+ let numberOfShipmentsNotAllowedForCancel = 0;
let disableSubmit = false;
let disableSubmitDueToMissingOrderInfo = false;
let numberOfErrorIfMissingForAllShipments = 0;
@@ -168,6 +172,15 @@ const ServicesCounselingMoveDetails = ({
(shipment) => shipment.ppmShipment?.status !== ppmShipmentStatuses.NEEDS_CLOSEOUT,
);
+ const nonPpmShipments = submittedShipments.filter((shipment) => shipment.shipmentType !== 'PPM');
+ const nonPpmApprovedShipments = nonPpmShipments.filter(
+ (shipment) => shipment?.status === shipmentStatuses.APPROVED,
+ );
+ const ppmCloseoutCompleteShipments = onlyPpmShipments.filter(
+ (shipment) => shipment.ppmShipment?.status === ppmShipmentStatuses.CLOSEOUT_COMPLETE,
+ );
+ numberOfShipmentsNotAllowedForCancel = nonPpmApprovedShipments.length + ppmCloseoutCompleteShipments.length;
+
ppmShipmentsInfoNeedsApproval = ppmNeedsApprovalShipments.map((shipment) => {
const reviewURL = `../${generatePath(servicesCounselingRoutes.SHIPMENT_REVIEW_PATH, {
moveCode,
@@ -229,6 +242,7 @@ const ServicesCounselingMoveDetails = ({
counselorCanReview = ppmShipmentsInfoNeedsApproval.length > 0;
reviewWeightsURL = generatePath(servicesCounselingRoutes.BASE_REVIEW_SHIPMENT_WEIGHTS_PATH, { moveCode });
counselorCanEdit = move.status === MOVE_STATUSES.NEEDS_SERVICE_COUNSELING && ppmShipmentsOtherStatuses.length > 0;
+ counselorCanCancelMove = move.status !== MOVE_STATUSES.CANCELED && numberOfShipmentsNotAllowedForCancel === 0;
counselorCanEditNonPPM =
move.status === MOVE_STATUSES.NEEDS_SERVICE_COUNSELING && shipmentsInfo.shipmentType !== 'PPM';
@@ -395,6 +409,20 @@ const ServicesCounselingMoveDetails = ({
},
});
+ const { mutate: mutateCancelMove } = useMutation(cancelMove, {
+ onSuccess: (data) => {
+ queryClient.setQueryData([MOVES, data.locator], data);
+ queryClient.invalidateQueries([MOVES, data.locator]);
+ queryClient.invalidateQueries({ queryKey: [MTO_SHIPMENTS] });
+ setAlertMessage('Move canceled.');
+ setAlertType('success');
+ },
+ onError: () => {
+ setAlertMessage('There was a problem cancelling the move. Please try again later.');
+ setAlertType('error');
+ },
+ });
+
const { mutate: mutateFinancialReview } = useMutation(updateFinancialFlag, {
onSuccess: (data) => {
queryClient.setQueryData([MOVES, data.locator], data);
@@ -447,7 +475,7 @@ const ServicesCounselingMoveDetails = ({
if (isLoading) return ;
if (isError) return ;
- const handleShowCancellationModal = () => {
+ const handleShowSubmitMoveModal = () => {
setIsSubmitModalVisible(true);
};
@@ -474,6 +502,21 @@ const ServicesCounselingMoveDetails = ({
setIsFinancialModalVisible(false);
};
+ const handleShowCancelMoveModal = () => {
+ setIsCancelMoveModalVisible(true);
+ };
+
+ const handleCancelMove = () => {
+ mutateCancelMove({
+ moveID: move.id,
+ });
+ setIsCancelMoveModalVisible(false);
+ };
+
+ const handleCloseCancelMoveModal = () => {
+ setIsCancelMoveModalVisible(false);
+ };
+
const counselorCanEditOrdersAndAllowances = () => {
if (counselorCanEdit || counselorCanEditNonPPM) return true;
if (
@@ -538,6 +581,11 @@ const ServicesCounselingMoveDetails = ({
initialSelection={move?.financialReviewFlag}
/>
)}
+
@@ -577,7 +625,7 @@ const ServicesCounselingMoveDetails = ({
isMoveLocked
}
type="button"
- onClick={handleShowCancellationModal}
+ onClick={handleShowSubmitMoveModal}
>
Submit move details
@@ -585,6 +633,17 @@ const ServicesCounselingMoveDetails = ({
)}
+
+
+
+ {counselorCanCancelMove && !isMoveLocked && (
+
+ )}
+
+
+
{hasInvalidProGearAllowances ? (
diff --git a/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.module.scss b/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.module.scss
index 5e7a5add39d..70809786fa5 100644
--- a/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.module.scss
+++ b/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.module.scss
@@ -28,6 +28,11 @@
@include u-margin-bottom(3);
}
+ .scCancelMoveContainer {
+ @include u-margin-top(1);
+ padding-left: 810px;
+ }
+
.alertContainer {
@include u-margin-bottom(4);
}
diff --git a/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.test.jsx b/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.test.jsx
index 2e3c7a329a8..d3484679fe8 100644
--- a/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.test.jsx
+++ b/src/pages/Office/ServicesCounselingMoveDetails/ServicesCounselingMoveDetails.test.jsx
@@ -1144,6 +1144,31 @@ describe('MoveDetails page', () => {
expect(screen.queryByText('Edit customer info')).not.toBeInTheDocument();
});
+
+ it('renders the cancel move button when user has permission', async () => {
+ render(
+
+
+ ,
+ );
+
+ expect(await screen.getByText('Cancel move')).toBeInTheDocument();
+ });
+
+ it('does not show the cancel move button if user does not have permission', () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.queryByText('Cancel move')).not.toBeInTheDocument();
+ });
});
});
});
diff --git a/src/pages/Office/TXOMoveInfo/TXOTab.module.scss b/src/pages/Office/TXOMoveInfo/TXOTab.module.scss
index 50465a7f999..86ae35a376f 100644
--- a/src/pages/Office/TXOMoveInfo/TXOTab.module.scss
+++ b/src/pages/Office/TXOMoveInfo/TXOTab.module.scss
@@ -42,11 +42,16 @@
flex-direction: row;
justify-content: space-between;
align-items: baseline;
- padding-bottom: 30px;
}
.tooMoveDetailsH1 {
@include u-margin-bottom(0);
+ align-self: center;
+ }
+
+ .tooCancelMoveContainer {
+ @include u-margin-bottom(3);
+ padding-left: 810px;
}
.gridContainer {
diff --git a/src/services/ghcApi.js b/src/services/ghcApi.js
index f3476b7c253..b99dc5c91c9 100644
--- a/src/services/ghcApi.js
+++ b/src/services/ghcApi.js
@@ -432,6 +432,17 @@ export function updateMoveStatusServiceCounselingCompleted({ moveTaskOrderID, if
);
}
+export function cancelMove({ moveID, normalize = false }) {
+ const operationPath = 'move.moveCanceler';
+ return makeGHCRequest(
+ operationPath,
+ {
+ moveID,
+ },
+ { normalize },
+ );
+}
+
export function updateMTOShipmentStatus({
shipmentID,
diversionReason,
diff --git a/swagger-def/ghc.yaml b/swagger-def/ghc.yaml
index 5dddb4f87bc..1aa19cd96c3 100644
--- a/swagger-def/ghc.yaml
+++ b/swagger-def/ghc.yaml
@@ -384,7 +384,7 @@ paths:
parameters: []
responses:
'200':
- description: Successfully cancelled move
+ description: Successfully canceled move
schema:
$ref: '#/definitions/Move'
'403':
@@ -404,6 +404,8 @@ paths:
description: cancels a move
operationId: moveCanceler
summary: Cancels a move
+ x-permissions:
+ - update.cancelMoveFlag
'/counseling/orders/{orderID}':
parameters:
- description: ID of order to update
@@ -3716,6 +3718,7 @@ paths:
- APPROVED
- NEEDS SERVICE COUNSELING
- SERVICE COUNSELING COMPLETED
+ - CANCELED
originPostalCode:
type: string
x-nullable: true
@@ -5148,7 +5151,7 @@ definitions:
PPMStatus:
type: string
enum:
- - CANCELLED
+ - CANCELED
- DRAFT
- SUBMITTED
- WAITING_ON_CUSTOMER
diff --git a/swagger/ghc.yaml b/swagger/ghc.yaml
index 37d14b07416..6b89514f604 100644
--- a/swagger/ghc.yaml
+++ b/swagger/ghc.yaml
@@ -422,7 +422,7 @@ paths:
parameters: []
responses:
'200':
- description: Successfully cancelled move
+ description: Successfully canceled move
schema:
$ref: '#/definitions/Move'
'403':
@@ -442,6 +442,8 @@ paths:
description: cancels a move
operationId: moveCanceler
summary: Cancels a move
+ x-permissions:
+ - update.cancelMoveFlag
/counseling/orders/{orderID}:
parameters:
- description: ID of order to update
@@ -3887,6 +3889,7 @@ paths:
- APPROVED
- NEEDS SERVICE COUNSELING
- SERVICE COUNSELING COMPLETED
+ - CANCELED
originPostalCode:
type: string
x-nullable: true
@@ -5369,7 +5372,7 @@ definitions:
PPMStatus:
type: string
enum:
- - CANCELLED
+ - CANCELED
- DRAFT
- SUBMITTED
- WAITING_ON_CUSTOMER