From cba2315f635b0326d12a4e9dace79b1915e46478 Mon Sep 17 00:00:00 2001 From: Yury Saukou Date: Mon, 23 Dec 2024 19:34:44 +0400 Subject: [PATCH 1/3] UIREC-412 Update 'Send claim' action to use 'pieces.send-claims' interface --- CHANGELOG.md | 1 + package.json | 3 + src/Piece/DelayClaimModal/DelayClaimModal.js | 80 ----------- .../DelayClaimModal/DelayClaimModal.test.js | 73 ---------- src/Piece/DelayClaimModal/index.js | 1 - src/Piece/PieceForm/PieceForm.js | 44 +++--- .../PieceFormActionButtons/constants.js | 51 +++---- .../PieceForm/PieceFormActionButtons/utils.js | 5 +- src/Piece/PieceForm/PieceFormContainer.js | 31 ++++- src/Piece/SendClaimModal/SendClaimModal.js | 103 -------------- .../SendClaimModal/SendClaimModal.test.js | 81 ----------- src/Piece/SendClaimModal/index.js | 1 - src/Piece/hooks/index.js | 1 + src/Piece/hooks/usePieceClaimSend/index.js | 1 + .../usePieceClaimSend/usePieceClaimSend.js | 128 ++++++++++++++++++ src/common/constants/errorCodes.js | 4 +- .../utils/getClaimingIntervalFromDate.js | 7 - .../utils/getClaimingIntervalFromDate.test.js | 12 -- src/common/utils/index.js | 1 - translations/ui-receiving/en.json | 12 +- 20 files changed, 213 insertions(+), 427 deletions(-) delete mode 100644 src/Piece/DelayClaimModal/DelayClaimModal.js delete mode 100644 src/Piece/DelayClaimModal/DelayClaimModal.test.js delete mode 100644 src/Piece/DelayClaimModal/index.js delete mode 100644 src/Piece/SendClaimModal/SendClaimModal.js delete mode 100644 src/Piece/SendClaimModal/SendClaimModal.test.js delete mode 100644 src/Piece/SendClaimModal/index.js create mode 100644 src/Piece/hooks/usePieceClaimSend/index.js create mode 100644 src/Piece/hooks/usePieceClaimSend/usePieceClaimSend.js delete mode 100644 src/common/utils/getClaimingIntervalFromDate.js delete mode 100644 src/common/utils/getClaimingIntervalFromDate.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a62f06e..12aa9c5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Add "Mark late" action to the piece form action menu. Refs UIREC-413. * Hide "Add piece" action when related order has "Pending" status and "Synchronized" workflow. Refs UIREC-362. +* *Breaking* Update "Send claim" action to use `pieces.send-claims` interface. Refs UIREC-412. ## [6.0.5](https://github.com/folio-org/ui-receiving/tree/v6.0.5) (2024-12-13) [Full Changelog](https://github.com/folio-org/ui-receiving/compare/v6.0.4...v6.0.5) diff --git a/package.json b/package.json index eb74ff82..997e1d12 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "orders-storage.settings": "1.0", "organizations.organizations": "1.0", "pieces": "3.1", + "pieces.send-claims": "1.0", "receiving": "2.0", "search": "0.5 0.6 1.0", "tags": "1.0", @@ -158,10 +159,12 @@ "inventory.instances.collection.get", "inventory.instances.item.get", "orders.bind-pieces.collection.post", + "orders.pieces.collection.put", "orders.routing-lists.item.delete", "orders.routing-lists.item.put", "orders.routing-lists.item.post", "orders.titles.item.put", + "pieces.send-claims.collection.post", "search.instances.collection.get", "search.facets.collection.get" ] diff --git a/src/Piece/DelayClaimModal/DelayClaimModal.js b/src/Piece/DelayClaimModal/DelayClaimModal.js deleted file mode 100644 index 0a7688e9..00000000 --- a/src/Piece/DelayClaimModal/DelayClaimModal.js +++ /dev/null @@ -1,80 +0,0 @@ -import PropTypes from 'prop-types'; -import { - FormattedMessage, - useIntl, -} from 'react-intl'; - -import { - Button, - Col, - Modal, - Row, -} from '@folio/stripes/components'; -import stripesFinalForm from '@folio/stripes/final-form'; -import { ModalFooter } from '@folio/stripes-acq-components'; - -import { FieldClaimingDate } from '../../common/components'; - -const DelayClaimModal = ({ - onCancel, - handleSubmit, - open, -}) => { - const intl = useIntl(); - const modalLabel = intl.formatMessage({ id: 'ui-receiving.modal.delayClaim.heading' }); - - const start = ( - - ); - const end = ( - - ); - - const footer = ( - - ); - - return ( - -
- - - } /> - - -
-
- ); -}; - -DelayClaimModal.propTypes = { - handleSubmit: PropTypes.func.isRequired, - onCancel: PropTypes.func.isRequired, - open: PropTypes.bool, -}; - -export default stripesFinalForm({ - navigationCheck: true, - subscription: { values: true }, -})(DelayClaimModal); diff --git a/src/Piece/DelayClaimModal/DelayClaimModal.test.js b/src/Piece/DelayClaimModal/DelayClaimModal.test.js deleted file mode 100644 index f784d351..00000000 --- a/src/Piece/DelayClaimModal/DelayClaimModal.test.js +++ /dev/null @@ -1,73 +0,0 @@ -import moment from 'moment'; -import { MemoryRouter } from 'react-router-dom'; - -import user from '@folio/jest-config-stripes/testing-library/user-event'; -import { render, screen } from '@folio/jest-config-stripes/testing-library/react'; - -import DelayClaimModal from './DelayClaimModal'; - -const FORMAT = 'MM/DD/YYYY'; -const today = moment(); - -const defaultProps = { - onCancel: jest.fn(), - onSubmit: jest.fn(), - open: true, -}; - -const renderDelayClaimModal = (props = {}) => render( - , - { wrapper: MemoryRouter }, -); - -describe('DelayClaimModal', () => { - beforeEach(() => { - defaultProps.onCancel.mockClear(); - defaultProps.onSubmit.mockClear(); - }); - - it('should render delay claim modal', () => { - renderDelayClaimModal(); - - expect(screen.getByText('ui-receiving.modal.delayClaim.heading')).toBeInTheDocument(); - }); - - it('should validate "Delay to" field', async () => { - renderDelayClaimModal(); - - const saveBtn = screen.getByRole('button', { name: 'stripes-acq-components.FormFooter.save' }); - - await user.click(saveBtn); - expect(screen.getByText('stripes-acq-components.validation.required')).toBeInTheDocument(); - - await user.type(screen.getByPlaceholderText(FORMAT), today.format(FORMAT)); - await user.click(saveBtn); - expect(screen.getByText('ui-receiving.validation.dateAfter')).toBeInTheDocument(); - }); - - it('should submit valid form', async () => { - renderDelayClaimModal(); - - const date = today.add(3, 'days'); - - await user.type(screen.getByPlaceholderText(FORMAT), date.format(FORMAT)); - await user.click(screen.getByRole('button', { name: 'stripes-acq-components.FormFooter.save' })); - - expect(defaultProps.onSubmit).toHaveBeenCalledWith( - { claimingDate: date.format('YYYY-MM-DD') }, - expect.anything(), - expect.anything(), - ); - }); - - it('should call "onCancel" when the modal dismissed', async () => { - renderDelayClaimModal(); - - await user.click(screen.getByRole('button', { name: 'stripes-acq-components.FormFooter.cancel' })); - - expect(defaultProps.onCancel).toHaveBeenCalled(); - }); -}); diff --git a/src/Piece/DelayClaimModal/index.js b/src/Piece/DelayClaimModal/index.js deleted file mode 100644 index 974c50e9..00000000 --- a/src/Piece/DelayClaimModal/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as DelayClaimModal } from './DelayClaimModal'; diff --git a/src/Piece/PieceForm/PieceForm.js b/src/Piece/PieceForm/PieceForm.js index 6fd565ad..77ca5cf7 100644 --- a/src/Piece/PieceForm/PieceForm.js +++ b/src/Piece/PieceForm/PieceForm.js @@ -25,17 +25,16 @@ import { import { useStripes } from '@folio/stripes/core'; import { ViewMetaData } from '@folio/stripes/smart-components'; import { + DelayClaimsModal, DeleteHoldingsModal, + getClaimingIntervalFromDate, handleKeyCommand, PIECE_FORMAT, PIECE_STATUS, useModalToggle, } from '@folio/stripes-acq-components'; -import { - getClaimingIntervalFromDate, - setLocationValueFormMutator, -} from '../../common/utils'; +import { setLocationValueFormMutator } from '../../common/utils'; import { PIECE_ACTION_NAMES, PIECE_FORM_FIELD_NAMES, @@ -43,10 +42,8 @@ import { PIECE_MODAL_ACCORDION, PIECE_MODAL_ACCORDION_LABELS, } from '../constants'; -import { DelayClaimModal } from '../DelayClaimModal'; import { DeletePieceModal } from '../DeletePieceModal'; import { ReceivingStatusChangeLog } from '../ReceivingStatusChangeLog'; -import { SendClaimModal } from '../SendClaimModal'; import { ItemFields } from './ItemFields'; import { PieceFields } from './PieceFields'; import { PieceFormActionButtons } from './PieceFormActionButtons'; @@ -60,6 +57,7 @@ const PieceForm = ({ hasValidationErrors, initialValues, instanceId, + onClaimSend: onClaimSendProp, onClose, onDelete: onDeleteProp, onUnreceive: onUnreceiveProp, @@ -84,10 +82,8 @@ const PieceForm = ({ const { enumeration, - externalNote, format, id, - internalNote, itemId, bindItemId, isBound, @@ -110,7 +106,6 @@ const PieceForm = ({ const [isDeleteConfirmation, toggleDeleteConfirmation] = useModalToggle(); const [isDeleteHoldingsConfirmation, toggleDeleteHoldingsConfirmation] = useModalToggle(); const [isClaimDelayModalOpen, toggleClaimDelayModal] = useModalToggle(); - const [isClaimSendModalOpen, toggleClaimSendModal] = useModalToggle(); const { protectCreate, protectUpdate, protectDelete } = restrictionsByAcqUnit; const isEditMode = Boolean(id); @@ -124,6 +119,11 @@ const PieceForm = ({ ? PIECE_MODAL_ACCORDION.originalItemDetails : PIECE_MODAL_ACCORDION.itemDetails; + const onDeleteHoldingsModalCancel = useCallback(() => { + change(PIECE_FORM_SERVICE_FIELD_NAMES.postSubmitAction, null); + toggleDeleteHoldingsConfirmation(); + }, [change, toggleDeleteHoldingsConfirmation]); + const onChangeDisplayOnHolding = useCallback(({ target: { checked } }) => { change(PIECE_FORM_FIELD_NAMES.displayOnHolding, checked); @@ -191,13 +191,15 @@ const PieceForm = ({ onStatusChange(PIECE_STATUS.claimDelayed); }, [change, onStatusChange]); - const onClaimSend = useCallback(({ claimingDate, ...rest }) => { + const onClaimSend = useCallback(async () => { + const updatedFields = await onClaimSendProp(formValues); + batch(() => { - change(PIECE_FORM_FIELD_NAMES.claimingInterval, getClaimingIntervalFromDate(claimingDate)); - Object.entries(rest).forEach(([field, value]) => change(field, value)); + change(PIECE_FORM_SERVICE_FIELD_NAMES.postSubmitAction, PIECE_ACTION_NAMES.sendClaim); + Object.entries(updatedFields).forEach(([field, value]) => change(field, value)); }); - onStatusChange(PIECE_STATUS.claimSent); - }, [batch, change, onStatusChange]); + onSave(); + }, [batch, change, formValues, onClaimSendProp, onSave]); const onDelete = useCallback((options) => { return onDeleteProp({ id, enumeration }, options); @@ -232,7 +234,7 @@ const PieceForm = ({ isEditMode={isEditMode} onCreateAnotherPiece={onCreateAnotherPiece} onClaimDelay={toggleClaimDelayModal} - onClaimSend={toggleClaimSendModal} + onClaimSend={onClaimSend} onDelete={toggleDeleteConfirmation} onReceive={onQuickReceive} onUnreceivePiece={onUnreceive} @@ -364,25 +366,18 @@ const PieceForm = ({ { isDeleteHoldingsConfirmation && ( ) } - - - @@ -400,6 +395,7 @@ PieceForm.propTypes = { instanceId: PropTypes.string, locationIds: PropTypes.arrayOf(PropTypes.string).isRequired, locations: PropTypes.arrayOf(PropTypes.object), + onClaimSend: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, onDelete: PropTypes.func.isRequired, onUnreceive: PropTypes.func.isRequired, diff --git a/src/Piece/PieceForm/PieceFormActionButtons/constants.js b/src/Piece/PieceForm/PieceFormActionButtons/constants.js index d6e35765..8320d32c 100644 --- a/src/Piece/PieceForm/PieceFormActionButtons/constants.js +++ b/src/Piece/PieceForm/PieceFormActionButtons/constants.js @@ -5,15 +5,20 @@ import { Button, Icon, } from '@folio/stripes/components'; -import { PIECE_STATUS } from '@folio/stripes-acq-components'; +import { + DelayClaimActionMenuItem, + MarkUnreceivableActionMenuItem, + PIECE_STATUS, + SendClaimActionMenuItem, +} from '@folio/stripes-acq-components'; import { PIECE_ACTION_NAMES } from '../../constants'; export const EXPECTED_PIECES_ACTIONS = [ PIECE_ACTION_NAMES.saveAndCreate, PIECE_ACTION_NAMES.quickReceive, - PIECE_ACTION_NAMES.sendClaim, PIECE_ACTION_NAMES.markLate, + PIECE_ACTION_NAMES.sendClaim, PIECE_ACTION_NAMES.delayClaim, PIECE_ACTION_NAMES.unReceivable, PIECE_ACTION_NAMES.delete, @@ -49,19 +54,13 @@ export const PIECE_ACTIONS = ({ onUnreceivePiece, }) => ({ [PIECE_ACTION_NAMES.delayClaim]: ( - + /> ), [PIECE_ACTION_NAMES.delete]: isEditMode ? ( + /> ), [PIECE_ACTION_NAMES.unReceive]: ( + /> ), [PIECE_ACTION_NAMES.markLate]: ( - ); - const end = ( - - ); - - const footer = ( - - ); - - return ( - -
- - - } /> - - - - - - } - name="internalNote" - parse={identity} - /> - - - } - name="externalNote" - parse={identity} - /> - - -
-
- ); -}; - -SendClaimModal.propTypes = { - handleSubmit: PropTypes.func.isRequired, - onCancel: PropTypes.func.isRequired, - open: PropTypes.bool, -}; - -export default stripesFinalForm({ - navigationCheck: true, - subscription: { values: true }, -})(SendClaimModal); diff --git a/src/Piece/SendClaimModal/SendClaimModal.test.js b/src/Piece/SendClaimModal/SendClaimModal.test.js deleted file mode 100644 index 6d510184..00000000 --- a/src/Piece/SendClaimModal/SendClaimModal.test.js +++ /dev/null @@ -1,81 +0,0 @@ -import moment from 'moment'; -import { MemoryRouter } from 'react-router-dom'; - -import user from '@folio/jest-config-stripes/testing-library/user-event'; -import { render, screen } from '@folio/jest-config-stripes/testing-library/react'; - -import SendClaimModal from './SendClaimModal'; - -const FORMAT = 'MM/DD/YYYY'; -const today = moment(); - -const defaultProps = { - onCancel: jest.fn(), - onSubmit: jest.fn(), - open: true, -}; - -const renderSendClaimModal = (props = {}) => render( - , - { wrapper: MemoryRouter }, -); - -describe('SendClaimModal', () => { - beforeEach(() => { - defaultProps.onCancel.mockClear(); - defaultProps.onSubmit.mockClear(); - }); - - it('should render send claim modal', () => { - renderSendClaimModal(); - - expect(screen.getByText('ui-receiving.modal.sendClaim.heading')).toBeInTheDocument(); - }); - - it('should validate "Claim expiry date" field', async () => { - renderSendClaimModal(); - - const saveBtn = screen.getByRole('button', { name: 'stripes-acq-components.FormFooter.save' }); - - await user.click(saveBtn); - expect(screen.getByText('stripes-acq-components.validation.required')).toBeInTheDocument(); - - await user.type(screen.getByPlaceholderText(FORMAT), today.format(FORMAT)); - await user.click(saveBtn); - expect(screen.getByText('ui-receiving.validation.dateAfter')).toBeInTheDocument(); - }); - - it('should submit valid form', async () => { - renderSendClaimModal(); - - const date = today.add(5, 'days'); - const internalNote = 'Internal'; - const externalNote = 'External'; - - await user.type(screen.getByPlaceholderText(FORMAT), date.format(FORMAT)); - await user.type(screen.getByLabelText('ui-receiving.piece.internalNote'), internalNote); - await user.type(screen.getByLabelText('ui-receiving.piece.externalNote'), externalNote); - await user.click(screen.getByRole('button', { name: 'stripes-acq-components.FormFooter.save' })); - - expect(defaultProps.onSubmit).toHaveBeenCalledWith( - { - claimingDate: date.format('YYYY-MM-DD'), - internalNote, - externalNote, - }, - expect.anything(), - expect.anything(), - ); - }); - - it('should call "onCancel" when the modal dismissed', async () => { - renderSendClaimModal(); - - await user.click(screen.getByRole('button', { name: 'stripes-acq-components.FormFooter.cancel' })); - - expect(defaultProps.onCancel).toHaveBeenCalled(); - }); -}); diff --git a/src/Piece/SendClaimModal/index.js b/src/Piece/SendClaimModal/index.js deleted file mode 100644 index f44d666e..00000000 --- a/src/Piece/SendClaimModal/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as SendClaimModal } from './SendClaimModal'; diff --git a/src/Piece/hooks/index.js b/src/Piece/hooks/index.js index 3ddf9e26..a7813c05 100644 --- a/src/Piece/hooks/index.js +++ b/src/Piece/hooks/index.js @@ -1,3 +1,4 @@ +export { usePieceClaimSend } from './usePieceClaimSend'; export { usePieceHoldingAbandonmentCheck } from './usePieceHoldingAbandonmentCheck'; export { usePieceQuickReceiving } from './usePieceQuickReceiving'; export { usePieceStatusChangeLog } from './usePieceStatusChangeLog'; diff --git a/src/Piece/hooks/usePieceClaimSend/index.js b/src/Piece/hooks/usePieceClaimSend/index.js new file mode 100644 index 00000000..54926932 --- /dev/null +++ b/src/Piece/hooks/usePieceClaimSend/index.js @@ -0,0 +1 @@ +export { usePieceClaimSend } from './usePieceClaimSend'; diff --git a/src/Piece/hooks/usePieceClaimSend/usePieceClaimSend.js b/src/Piece/hooks/usePieceClaimSend/usePieceClaimSend.js new file mode 100644 index 00000000..8c7a4c85 --- /dev/null +++ b/src/Piece/hooks/usePieceClaimSend/usePieceClaimSend.js @@ -0,0 +1,128 @@ +import noop from 'lodash/noop'; +import { + useCallback, + useRef, + useState, +} from 'react'; +import { FormattedMessage } from 'react-intl'; + +import { + DATA_EXPORT_CONFIGS_API, + FOLIO_EXPORT_TYPE, + getClaimingIntervalFromDate, + PIECE_STATUS, + useClaimsSend, + useModalToggle, + usePiecesStatusBatchUpdate, +} from '@folio/stripes-acq-components'; +import { useOkapiKy } from '@folio/stripes/core'; + +export const usePieceClaimSend = (options = {}) => { + const { organizationId, tenantId } = options; + + const ky = useOkapiKy({ tenant: tenantId }); + const confirmClaimSendPromise = useRef(Promise); + const [isClaimSendModalOpen, toggleClaimSendModal] = useModalToggle(); + const [isIntegrationExists, setIntegrationExists] = useState(); + const [claimSendModalProps, setClaimSendModalProps] = useState({}); + + /* Callback to handle claim send after form submit */ + const [sendClaimsHandler, setSendClaimsHandler] = useState(() => noop); + + const { + isLoading: isClaimSendLoading, + sendClaims, + } = useClaimsSend({ tenantId }); + + const { + isLoading: isUpdatePiecesStatusLoading, + updatePiecesStatus, + } = usePiecesStatusBatchUpdate({ tenantId }); + + const isLoading = isClaimSendLoading || isUpdatePiecesStatusLoading; + + const checkIntegrationExistance = useCallback(async () => { + const searchParams = { + query: `configName==${FOLIO_EXPORT_TYPE.CLAIMS}_${organizationId}*`, + }; + + return ky.get(DATA_EXPORT_CONFIGS_API, { searchParams }) + .then(({ configs }) => configs.length > 0) + .catch(() => false); + }, [ky, organizationId]); + + /* Trigger "Send claims" modal rendering with dynamic props */ + const onClaimSend = useCallback(async ({ internalNote, externalNote }) => { + const isExists = await checkIntegrationExistance(); + const message = ( + + ); + + setIntegrationExists(isExists); + toggleClaimSendModal(); + + return new Promise((resolve, reject) => { + confirmClaimSendPromise.current = { resolve, reject }; + + setClaimSendModalProps({ + initialValues: { internalNote, externalNote }, + message, + }); + }); + }, [checkIntegrationExistance, toggleClaimSendModal]); + + /* Resolves the "Send claims" modal promise and set the "sendClaimsHandler" callback to handle after form submit */ + const onConfirmClaimSend = useCallback(({ + claimingDate, + externalNote, + internalNote, + }) => { + const claimingInterval = getClaimingIntervalFromDate(claimingDate); + + const callback = async ({ id }) => { + const result = isIntegrationExists + ? await sendClaims({ + data: { + claimingInterval, + claimingPieceIds: [id], + internalNote, + externalNote, + }, + }) + : await updatePiecesStatus({ + data: { + claimingInterval, + pieceIds: [id], + receivingStatus: PIECE_STATUS.claimSent, + }, + }); + + return result; + }; + + toggleClaimSendModal(); + setSendClaimsHandler(() => callback); + + confirmClaimSendPromise.current.resolve({ + claimingInterval, + externalNote, + internalNote, + }); + }, [isIntegrationExists, sendClaims, toggleClaimSendModal, updatePiecesStatus]); + + const onCancelClaimSend = useCallback(() => { + toggleClaimSendModal(); + }, [toggleClaimSendModal]); + + return { + claimSendModalProps, + isClaimSendModalOpen, + onCancelClaimSend, + onConfirmClaimSend, + onClaimSend, + sendClaimsHandler, + isLoading, + }; +}; diff --git a/src/common/constants/errorCodes.js b/src/common/constants/errorCodes.js index 66ee8022..ddfc8640 100644 --- a/src/common/constants/errorCodes.js +++ b/src/common/constants/errorCodes.js @@ -1,6 +1,8 @@ export const ERROR_CODES = { + cannotFindPiecesWithLatestStatusToProcess: 'cannotFindPiecesWithLatestStatusToProcess', createItemForPieceIsNotAllowedError: 'createItemForPieceIsNotAllowedError', createPiecePendingOrderError: 'createPiecePendingOrderError', + barcodeIsNotUnique: 'barcodeIsNotUnique', forbiddenDeleteSystemValues: 'forbiddenDeleteSystemValues', forbiddenDeleteUsedValue: 'forbiddenDeleteUsedValue', holdingsByIdNotFoundError: 'holdingsByIdNotFoundError', @@ -18,7 +20,7 @@ export const ERROR_CODES = { pieceUpdateFailed: 'pieceUpdateFailed', receivingProcessEncumbrancesError: 'receivingProcessEncumbrancesError', titleNotFound: 'titleNotFound', + unableToGenerateClaimsForOrgNoIntegrationDetails: 'unableToGenerateClaimsForOrgNoIntegrationDetails', userHasNoPermission: 'userHasNoPermission', userNotAMemberOfTheAcq: 'userNotAMemberOfTheAcq', - barcodeIsNotUnique: 'barcodeIsNotUnique', }; diff --git a/src/common/utils/getClaimingIntervalFromDate.js b/src/common/utils/getClaimingIntervalFromDate.js deleted file mode 100644 index 5eb9407c..00000000 --- a/src/common/utils/getClaimingIntervalFromDate.js +++ /dev/null @@ -1,7 +0,0 @@ -import moment from 'moment'; - -export const getClaimingIntervalFromDate = (date) => { - const currentDay = moment().startOf('day'); - - return moment(date).diff(currentDay, 'days'); -}; diff --git a/src/common/utils/getClaimingIntervalFromDate.test.js b/src/common/utils/getClaimingIntervalFromDate.test.js deleted file mode 100644 index 2abf7b24..00000000 --- a/src/common/utils/getClaimingIntervalFromDate.test.js +++ /dev/null @@ -1,12 +0,0 @@ -import moment from 'moment'; - -import { getClaimingIntervalFromDate } from './getClaimingIntervalFromDate'; - -describe('getClaimingIntervalFromDate', () => { - it('should return claiming interval calculated based on provided date', () => { - const today = moment().startOf('day'); - - expect(getClaimingIntervalFromDate(today)).toEqual(0); - expect(getClaimingIntervalFromDate(today.add(5, 'days'))).toEqual(5); - }); -}); diff --git a/src/common/utils/index.js b/src/common/utils/index.js index 037348f6..73a8ce71 100644 --- a/src/common/utils/index.js +++ b/src/common/utils/index.js @@ -1,5 +1,4 @@ export * from './buildPieceRequestsSearchParams'; -export * from './getClaimingIntervalFromDate'; export * from './getDehydratedPiece'; export * from './getHoldingsItemsAndPieces'; export * from './getItemById'; diff --git a/translations/ui-receiving/en.json b/translations/ui-receiving/en.json index 0ace1d2f..19053b06 100644 --- a/translations/ui-receiving/en.json +++ b/translations/ui-receiving/en.json @@ -29,6 +29,8 @@ "errors.userNotAMemberOfTheAcq": "Action is restricted by acquisition unit. User is not assigned to the specified acquisitions unit.", "errors.userHasNoPermission": "Action is restricted by acquisition unit. User is not assigned to the specified acquisitions unit.", "errors.barcodeIsNotUnique": "Barcode must be unique, piece and item data could not be updated.", + "errors.cannotFindPiecesWithLatestStatusToProcess": "The piece must have \"Late\" status to process.", + "errors.unableToGenerateClaimsForOrgNoIntegrationDetails": "Unable to generate claims because no claim integrations exist for the organization.", "exportSettings.error": "Failed to load data for export results (CSV)", "exportSettings.export": "Export", @@ -202,9 +204,6 @@ "piece.button.addPiece": "Add piece", "piece.action.button.saveAndCreateAnother": "Save and create another", "piece.action.button.quickReceive": "Quick receive", - "piece.action.button.sendClaim": "Send claim", - "piece.action.button.delayClaim": "Delay claim", - "piece.action.button.unReceivable": "Unreceivable", "piece.action.button.unReceive": "Unreceive", "piece.action.button.delete": "Delete", "piece.action.button.expect": "Expect", @@ -252,6 +251,8 @@ "piece.statusChangeLog.column.interval":"Interval", "piece.statusChangeLog.column.status":"Status change", "piece.statusChangeLog.column.updatedBy":"Updated by", + "piece.sendClaim.withIntegration.message":"This will generate a claim job which can be viewed in Export manager. Continue?", + "piece.sendClaim.withoutIntegration.message":"No claiming integration exists for this organization. Piece will be set to \"Claim sent\" status. Continue?", "piece.supplement": "Supplement", "piece.title": "Title", "piece.delete.confirm": "Confirm", @@ -269,11 +270,6 @@ "requests.message.itemId": "Item ({itemId})", "requests.footer.close": "Close", - "modal.delayClaim.heading": "Delay piece", - "modal.delayClaim.field.delayTo": "Delay to", - "modal.sendClaim.heading": "Send claim", - "modal.sendClaim.field.claimExpiryDate": "Claim expiry date", - "shortcut.receive": "Receive pieces/Quick receive", "shortcut.piece.saveAndCreateAnother": "Save a piece and create another", From d06d6cfa8e134cb9e3774d48185b6517688f9f87 Mon Sep 17 00:00:00 2001 From: Yury Saukou Date: Mon, 23 Dec 2024 19:36:31 +0400 Subject: [PATCH 2/3] update next release version in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12aa9c5e..aad606ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change history for ui-receiving -## 6.1.0 (IN PROGRESS) +## 7.0.0 (IN PROGRESS) * Add "Mark late" action to the piece form action menu. Refs UIREC-413. * Hide "Add piece" action when related order has "Pending" status and "Synchronized" workflow. Refs UIREC-362. From 7841abdeba32bbac1d73b90c9ca983c436f0dab5 Mon Sep 17 00:00:00 2001 From: Yury Saukou Date: Tue, 24 Dec 2024 10:06:18 +0400 Subject: [PATCH 3/3] update jest tests --- src/Piece/PieceForm/PieceForm.test.js | 26 +--- .../PieceFormActionButtons/constants.js | 1 + .../PieceFormActionButtons/utils.test.js | 8 +- .../PieceForm/PieceFormContainer.test.js | 131 ++++++++++++------ .../usePieceClaimSend/usePieceClaimSend.js | 5 +- 5 files changed, 99 insertions(+), 72 deletions(-) diff --git a/src/Piece/PieceForm/PieceForm.test.js b/src/Piece/PieceForm/PieceForm.test.js index f766231c..7485cd5a 100644 --- a/src/Piece/PieceForm/PieceForm.test.js +++ b/src/Piece/PieceForm/PieceForm.test.js @@ -1,11 +1,10 @@ -import moment from 'moment'; - import user from '@folio/jest-config-stripes/testing-library/user-event'; import { act, screen, waitFor, } from '@folio/jest-config-stripes/testing-library/react'; +import { dayjs } from '@folio/stripes/components'; import { FieldInventory, INVENTORY_RECORDS_TYPE, @@ -90,7 +89,7 @@ const logs = [ ]; const DATE_FORMAT = 'MM/DD/YYYY'; -const today = moment(); +const today = dayjs(); const renderPieceForm = (props = {}) => renderWithRouter( { await user.click(dropdownButton); - const unReceiveButton = screen.getByTestId('unReceivable-piece-button'); + const unreceivableButton = screen.getByTestId('unreceivable-button'); - await user.click(unReceiveButton); + await user.click(unreceivableButton); expect(defaultProps.onSubmit).toHaveBeenCalled(); }); @@ -392,7 +391,7 @@ describe('PieceForm', () => { it('should handle "Delay claim" action', async () => { await user.click(screen.getByTestId('delay-claim-button')); - await user.type(screen.getByRole('textbox', { name: 'ui-receiving.modal.delayClaim.field.delayTo' }), date.format(DATE_FORMAT)); + await user.type(screen.getByRole('textbox', { name: /field.delayTo/ }), date.format(DATE_FORMAT)); await user.click(await findButton('stripes-acq-components.FormFooter.save')); expect(defaultProps.onSubmit).toHaveBeenCalledWith( @@ -404,20 +403,5 @@ describe('PieceForm', () => { expect.anything(), ); }); - - it('should handle "Send claim" action', async () => { - await user.click(screen.getByTestId('send-claim-button')); - await user.type(screen.getByRole('textbox', { name: 'ui-receiving.modal.sendClaim.field.claimExpiryDate' }), date.format(DATE_FORMAT)); - await user.click(await findButton('stripes-acq-components.FormFooter.save')); - - expect(defaultProps.onSubmit).toHaveBeenCalledWith( - expect.objectContaining({ - claimingInterval: 3, - receivingStatus: PIECE_STATUS.claimSent, - }), - expect.anything(), - expect.anything(), - ); - }); }); }); diff --git a/src/Piece/PieceForm/PieceFormActionButtons/constants.js b/src/Piece/PieceForm/PieceFormActionButtons/constants.js index 8320d32c..24e56532 100644 --- a/src/Piece/PieceForm/PieceFormActionButtons/constants.js +++ b/src/Piece/PieceForm/PieceFormActionButtons/constants.js @@ -148,6 +148,7 @@ export const PIECE_ACTIONS = ({ ), [PIECE_ACTION_NAMES.unReceivable]: ( { onToggle(e); diff --git a/src/Piece/PieceForm/PieceFormActionButtons/utils.test.js b/src/Piece/PieceForm/PieceFormActionButtons/utils.test.js index ad6b780b..ba09d010 100644 --- a/src/Piece/PieceForm/PieceFormActionButtons/utils.test.js +++ b/src/Piece/PieceForm/PieceFormActionButtons/utils.test.js @@ -81,9 +81,9 @@ describe('getPieceActionMenus', () => { onToggle, onUnreceivePiece, }); - const receiveButton = result.find(i => i.props['data-testid'] === 'unReceive-piece-button'); + const unreceiveButton = result.find(i => i.props['data-testid'] === 'unReceive-piece-button'); - receiveButton.props.onClick(); + unreceiveButton.props.onClick(); expect(onUnreceivePiece).toHaveBeenCalledWith(); }); @@ -97,9 +97,9 @@ describe('getPieceActionMenus', () => { onStatusChange, onToggle, }); - const receiveButton = result.find(i => i.props['data-testid'] === 'unReceivable-piece-button'); + const unreceivableButton = result.find(i => i.props['data-testid'] === 'unreceivable-button'); - receiveButton.props.onClick(); + unreceivableButton.props.onClick(); expect(onStatusChange).toHaveBeenCalledWith(PIECE_STATUS.unreceivable); }); diff --git a/src/Piece/PieceForm/PieceFormContainer.test.js b/src/Piece/PieceForm/PieceFormContainer.test.js index 03ee1934..6ce05c68 100644 --- a/src/Piece/PieceForm/PieceFormContainer.test.js +++ b/src/Piece/PieceForm/PieceFormContainer.test.js @@ -1,14 +1,18 @@ import user from '@folio/jest-config-stripes/testing-library/user-event'; import { screen } from '@folio/jest-config-stripes/testing-library/react'; +import { dayjs } from '@folio/stripes/components'; import { ORDER_FORMATS, ORDER_STATUSES, PIECE_STATUS, useAcqRestrictions, + useClaimsSend, + useCurrentUserTenants, useLocationsQuery, useOrderLine, - useCurrentUserTenants, + usePiecesStatusBatchUpdate, } from '@folio/stripes-acq-components'; +import { useOkapiKy } from '@folio/stripes/core'; import { renderWithRouter } from '../../../test/jest/helpers'; import { @@ -26,11 +30,13 @@ import { PieceFormContainer } from './PieceFormContainer'; jest.mock('@folio/stripes-acq-components', () => ({ ...jest.requireActual('@folio/stripes-acq-components'), FieldInventory: jest.fn().mockReturnValue('FieldInventory'), - useCentralOrderingContext: jest.fn(), useAcqRestrictions: jest.fn(), + useCentralOrderingContext: jest.fn(), + useClaimsSend: jest.fn(), + useCurrentUserTenants: jest.fn(), useLocationsQuery: jest.fn(), useOrderLine: jest.fn(), - useCurrentUserTenants: jest.fn(), + usePiecesStatusBatchUpdate: jest.fn(), })); jest.mock('../../common/components/LineLocationsView/LineLocationsView', () => jest.fn().mockReturnValue('LineLocationsView')); jest.mock('../../common/hooks', () => ({ @@ -58,7 +64,10 @@ jest.mock('../hooks', () => ({ usePieceStatusChangeLog: jest.fn(() => ({ data: [] })), })); -const mutatePieceMock = jest.fn(() => Promise.resolve()); +const DATE_FORMAT = 'MM/DD/YYYY'; +const today = dayjs(); + +const mutatePieceMock = jest.fn(() => Promise.resolve({ id: 'piece-id' })); const unreceiveMock = jest.fn(() => Promise.resolve()); const onQuickReceiveMock = jest.fn(() => Promise.resolve()); const receiveMock = jest.fn(() => Promise.resolve()); @@ -117,48 +126,37 @@ const renderPieceFormContainer = (props = {}) => renderWithRouter( ); describe('PieceFormContainer', () => { + const sendClaims = jest.fn(() => Promise.resolve()); + const updatePiecesStatus = jest.fn(() => Promise.resolve()); + + const kyMock = { + get: jest.fn(() => ({ json: () => Promise.resolve({ configs: [] }) })), + }; + beforeEach(() => { - mutatePieceMock.mockClear(); - useAcqRestrictions - .mockClear() - .mockReturnValue({ restrictions }); - useHoldingItems - .mockClear() - .mockReturnValue({ itemsCount: 2 }); - useOrder - .mockClear() - .mockReturnValue({ order }); - useOrderLine - .mockClear() - .mockReturnValue({ orderLine }); - useLocationsQuery - .mockClear() - .mockReturnValue({ locations }); - usePieceMutator - .mockClear() - .mockReturnValue({ mutatePiece: mutatePieceMock }); - usePieces - .mockClear() - .mockReturnValue({ piecesCount: 2 }); - usePieceQuickReceiving - .mockClear() - .mockReturnValue({ - onCancelReceive: jest.fn(), - onConfirmReceive: jest.fn(), - onQuickReceive: onQuickReceiveMock, - }); - useTitle - .mockClear() - .mockReturnValue({ title }); - useReceive - .mockClear() - .mockReturnValue({ receive: receiveMock }); - useUnreceive - .mockClear() - .mockReturnValue({ unreceive: unreceiveMock }); - useCurrentUserTenants - .mockClear() - .mockReturnValue(tenants); + useAcqRestrictions.mockReturnValue({ restrictions }); + useClaimsSend.mockReturnValue({ sendClaims }); + useCurrentUserTenants.mockReturnValue(tenants); + useHoldingItems.mockReturnValue({ itemsCount: 2 }); + useOkapiKy.mockReturnValue(kyMock); + useOrder.mockReturnValue({ order }); + useOrderLine.mockReturnValue({ orderLine }); + useLocationsQuery.mockReturnValue({ locations }); + usePieceMutator.mockReturnValue({ mutatePiece: mutatePieceMock }); + usePieces.mockReturnValue({ piecesCount: 2 }); + usePiecesStatusBatchUpdate.mockReturnValue({ updatePiecesStatus }); + usePieceQuickReceiving.mockReturnValue({ + onCancelReceive: jest.fn(), + onConfirmReceive: jest.fn(), + onQuickReceive: onQuickReceiveMock, + }); + useTitle.mockReturnValue({ title }); + useReceive.mockReturnValue({ receive: receiveMock }); + useUnreceive.mockReturnValue({ unreceive: unreceiveMock }); + }); + + afterEach(() => { + jest.clearAllMocks(); }); it('should display the piece form', () => { @@ -235,4 +233,47 @@ describe('PieceFormContainer', () => { }), })); }); + + it('should handle "Send claim" action with claiming integration', async () => { + kyMock.get.mockReturnValueOnce({ json: () => Promise.resolve({ configs: [{ value: 'val' }] }) }); + + renderPieceFormContainer(); + + await user.click(await screen.findByTestId('dropdown-trigger-button')); + await user.click(screen.getByTestId('send-claim-button')); + + expect(screen.getByText('ui-receiving.piece.sendClaim.withIntegration.message')).toBeInTheDocument(); + + await user.type(screen.getByRole('textbox', { name: /sendClaim.field.claimExpiryDate/ }), today.add(3, 'days').format(DATE_FORMAT)); + await user.click(await screen.findByRole('button', { name: 'stripes-acq-components.FormFooter.save' })); + + expect(sendClaims).toHaveBeenCalledWith({ + data: { + claimingInterval: 3, + externalNote: undefined, + internalNote: undefined, + claimingPieceIds: ['piece-id'], + }, + }); + }); + + it('should handle "Send claim" action without claiming integration', async () => { + renderPieceFormContainer(); + + await user.click(await screen.findByTestId('dropdown-trigger-button')); + await user.click(screen.getByTestId('send-claim-button')); + + expect(screen.getByText('ui-receiving.piece.sendClaim.withoutIntegration.message')).toBeInTheDocument(); + + await user.type(screen.getByRole('textbox', { name: /sendClaim.field.claimExpiryDate/ }), today.add(3, 'days').format(DATE_FORMAT)); + await user.click(await screen.findByRole('button', { name: 'stripes-acq-components.FormFooter.save' })); + + expect(updatePiecesStatus).toHaveBeenCalledWith({ + data: { + claimingInterval: 3, + pieceIds: ['piece-id'], + receivingStatus: PIECE_STATUS.claimSent, + }, + }); + }); }); diff --git a/src/Piece/hooks/usePieceClaimSend/usePieceClaimSend.js b/src/Piece/hooks/usePieceClaimSend/usePieceClaimSend.js index 8c7a4c85..7b8e7aad 100644 --- a/src/Piece/hooks/usePieceClaimSend/usePieceClaimSend.js +++ b/src/Piece/hooks/usePieceClaimSend/usePieceClaimSend.js @@ -23,7 +23,7 @@ export const usePieceClaimSend = (options = {}) => { const ky = useOkapiKy({ tenant: tenantId }); const confirmClaimSendPromise = useRef(Promise); const [isClaimSendModalOpen, toggleClaimSendModal] = useModalToggle(); - const [isIntegrationExists, setIntegrationExists] = useState(); + const [isIntegrationExists, setIsIntegrationExists] = useState(); const [claimSendModalProps, setClaimSendModalProps] = useState({}); /* Callback to handle claim send after form submit */ @@ -47,6 +47,7 @@ export const usePieceClaimSend = (options = {}) => { }; return ky.get(DATA_EXPORT_CONFIGS_API, { searchParams }) + .json() .then(({ configs }) => configs.length > 0) .catch(() => false); }, [ky, organizationId]); @@ -60,7 +61,7 @@ export const usePieceClaimSend = (options = {}) => { /> ); - setIntegrationExists(isExists); + setIsIntegrationExists(isExists); toggleClaimSendModal(); return new Promise((resolve, reject) => {