diff --git a/lib/FindRecords/hooks/useRecordsSelect/useRecordsSelect.js b/lib/FindRecords/hooks/useRecordsSelect/useRecordsSelect.js index ba6b08e9..61389f57 100644 --- a/lib/FindRecords/hooks/useRecordsSelect/useRecordsSelect.js +++ b/lib/FindRecords/hooks/useRecordsSelect/useRecordsSelect.js @@ -95,12 +95,18 @@ export const useRecordsSelect = ({ records }) => { [selectedRecordsMap], ); + const resetSelectedRecords = useCallback(() => { + setSelectedRecordsMap({}); + emitChangeEvent({}); + }, [emitChangeEvent]); + return { allRecordsSelected, - selectedRecordsMap, + isRecordSelected, + resetSelectedRecords, selectedRecordsLength, - toggleSelectAll, + selectedRecordsMap, selectRecord, - isRecordSelected, + toggleSelectAll, }; }; diff --git a/lib/claiming/components/index.js b/lib/claiming/components/index.js index 7c64335c..23938172 100644 --- a/lib/claiming/components/index.js +++ b/lib/claiming/components/index.js @@ -1 +1,2 @@ export * from './menu-items'; +export * from './modals'; diff --git a/lib/claiming/components/modals/DelayClaimsModal/DelayClaimsModal.js b/lib/claiming/components/modals/DelayClaimsModal/DelayClaimsModal.js new file mode 100644 index 00000000..26e1536b --- /dev/null +++ b/lib/claiming/components/modals/DelayClaimsModal/DelayClaimsModal.js @@ -0,0 +1,85 @@ +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 '../../../../ModalFooter'; +import { FieldClaimingDate } from '../../FieldClaimingDate'; + +const DelayClaimsModal = ({ + claimsCount, + onCancel, + handleSubmit, + open, +}) => { + const intl = useIntl(); + const modalLabel = intl.formatMessage( + { id: 'stripes-acq-components.claiming.modal.delayClaim.heading' }, + { count: claimsCount }, + ); + + const start = ( + + ); + const end = ( + + ); + + const footer = ( + + ); + + return ( + +
+ + + } /> + + +
+
+ ); +}; + +DelayClaimsModal.propTypes = { + claimsCount: PropTypes.number, + handleSubmit: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + open: PropTypes.bool, +}; + +export default stripesFinalForm({ + navigationCheck: true, + subscription: { values: true }, +})(DelayClaimsModal); diff --git a/lib/claiming/components/modals/DelayClaimsModal/index.js b/lib/claiming/components/modals/DelayClaimsModal/index.js new file mode 100644 index 00000000..c2b2080c --- /dev/null +++ b/lib/claiming/components/modals/DelayClaimsModal/index.js @@ -0,0 +1 @@ +export { default as DelayClaimsModal } from './DelayClaimsModal'; diff --git a/lib/claiming/components/modals/SendClaimsModal/SendClaimsModal.js b/lib/claiming/components/modals/SendClaimsModal/SendClaimsModal.js new file mode 100644 index 00000000..956ce92e --- /dev/null +++ b/lib/claiming/components/modals/SendClaimsModal/SendClaimsModal.js @@ -0,0 +1,118 @@ +import identity from 'lodash/identity'; +import PropTypes from 'prop-types'; +import { Field } from 'react-final-form'; +import { + FormattedMessage, + useIntl, +} from 'react-intl'; + +import { + Button, + Col, + Modal, + Row, + TextArea, +} from '@folio/stripes/components'; +import stripesFinalForm from '@folio/stripes/final-form'; + +import { ModalFooter } from '../../../../ModalFooter'; +import { FieldClaimingDate } from '../../FieldClaimingDate'; + +const SendClaimsModal = ({ + claimsCount, + handleSubmit, + onCancel, + open, + message, +}) => { + const intl = useIntl(); + const modalLabel = intl.formatMessage( + { id: 'stripes-acq-components.claiming.modal.sendClaim.heading' }, + { count: claimsCount }, + ); + + const start = ( + + ); + const end = ( + + ); + + const footer = ( + + ); + + return ( + +
+ {message && ( + + + {message} + + + )} + + + + } /> + + + + + + } + name="internalNote" + parse={identity} + /> + + + } + name="externalNote" + parse={identity} + /> + + +
+
+ ); +}; + +SendClaimsModal.propTypes = { + claimsCount: PropTypes.number, + handleSubmit: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, + open: PropTypes.bool, + message: PropTypes.node, +}; + +export default stripesFinalForm({ + navigationCheck: true, + subscription: { values: true }, +})(SendClaimsModal); diff --git a/lib/claiming/components/modals/SendClaimsModal/SendClaimsModal.test.js b/lib/claiming/components/modals/SendClaimsModal/SendClaimsModal.test.js new file mode 100644 index 00000000..db75c474 --- /dev/null +++ b/lib/claiming/components/modals/SendClaimsModal/SendClaimsModal.test.js @@ -0,0 +1,81 @@ +import { render, screen } from '@testing-library/react'; +import user from '@testing-library/user-event'; +import { MemoryRouter } from 'react-router-dom'; + +import { dayjs } from '@folio/stripes/components'; + +import SendClaimsModal from './SendClaimsModal'; + +const FORMAT = 'MM/DD/YYYY'; +const today = dayjs(); + +const defaultProps = { + onCancel: jest.fn(), + onSubmit: jest.fn(), + open: true, +}; + +const renderSendClaimsModal = (props = {}) => render( + , + { wrapper: MemoryRouter }, +); + +describe('SendClaimsModal', () => { + beforeEach(() => { + defaultProps.onCancel.mockClear(); + defaultProps.onSubmit.mockClear(); + }); + + it('should render send claim modal', () => { + renderSendClaimsModal(); + + expect(screen.getByText('ui-receiving.modal.sendClaim.heading')).toBeInTheDocument(); + }); + + it('should validate "Claim expiry date" field', async () => { + renderSendClaimsModal(); + + 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 () => { + renderSendClaimsModal(); + + 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 () => { + renderSendClaimsModal(); + + await user.click(screen.getByRole('button', { name: 'stripes-acq-components.FormFooter.cancel' })); + + expect(defaultProps.onCancel).toHaveBeenCalled(); + }); +}); diff --git a/lib/claiming/components/modals/SendClaimsModal/index.js b/lib/claiming/components/modals/SendClaimsModal/index.js new file mode 100644 index 00000000..60f3fa57 --- /dev/null +++ b/lib/claiming/components/modals/SendClaimsModal/index.js @@ -0,0 +1 @@ +export { default as SendClaimsModal } from './SendClaimsModal'; diff --git a/lib/claiming/components/modals/index.js b/lib/claiming/components/modals/index.js new file mode 100644 index 00000000..d0304545 --- /dev/null +++ b/lib/claiming/components/modals/index.js @@ -0,0 +1,2 @@ +export { DelayClaimsModal } from './DelayClaimsModal'; +export { SendClaimsModal } from './SendClaimsModal'; diff --git a/lib/claiming/hooks/index.js b/lib/claiming/hooks/index.js index d9d4766b..43509495 100644 --- a/lib/claiming/hooks/index.js +++ b/lib/claiming/hooks/index.js @@ -1 +1,2 @@ +export { useClaimsDelay } from './useClaimsDelay'; export { useClaimsSend } from './useClaimsSend'; diff --git a/lib/claiming/hooks/useClaimsDelay/index.js b/lib/claiming/hooks/useClaimsDelay/index.js new file mode 100644 index 00000000..0be7a569 --- /dev/null +++ b/lib/claiming/hooks/useClaimsDelay/index.js @@ -0,0 +1 @@ +export { useClaimsDelay } from './useClaimsDelay'; diff --git a/lib/claiming/hooks/useClaimsDelay/useClaimsDelay.js b/lib/claiming/hooks/useClaimsDelay/useClaimsDelay.js new file mode 100644 index 00000000..864546c2 --- /dev/null +++ b/lib/claiming/hooks/useClaimsDelay/useClaimsDelay.js @@ -0,0 +1,26 @@ +import { useCallback } from 'react'; + +import { PIECE_STATUS } from '../../../constants'; +import { usePiecesStatusBatchUpdate } from '../../../hooks'; + +export const useClaimsDelay = () => { + const { + isLoading, + updatePiecesStatus, + } = usePiecesStatusBatchUpdate(); + + const delayClaims = useCallback(({ claimingInterval, pieceIds }) => { + return updatePiecesStatus({ + data: { + claimingInterval, + pieceIds, + receivingStatus: PIECE_STATUS.claimDelayed, + }, + }); + }, [updatePiecesStatus]); + + return { + isLoading, + delayClaims, + }; +}; diff --git a/lib/claiming/hooks/useClaimsSend/useClaimsSend.js b/lib/claiming/hooks/useClaimsSend/useClaimsSend.js index 9df355b4..6f70d0eb 100644 --- a/lib/claiming/hooks/useClaimsSend/useClaimsSend.js +++ b/lib/claiming/hooks/useClaimsSend/useClaimsSend.js @@ -9,7 +9,7 @@ export const useClaimsSend = () => { const ky = useOkapiKy(); const mutationFn = useCallback(({ data }) => { - return ky.post(SEND_CLAIMS_API, { json: data }); + return ky.post(SEND_CLAIMS_API, { json: data }).json(); }, [ky]); const { diff --git a/lib/claiming/index.js b/lib/claiming/index.js index f76fd6f1..c55977d1 100644 --- a/lib/claiming/index.js +++ b/lib/claiming/index.js @@ -1,2 +1,3 @@ export * from './components'; export * from './hooks'; +export * from './utils'; diff --git a/lib/claiming/utils/getClaimingIntervalFromDate.js b/lib/claiming/utils/getClaimingIntervalFromDate.js new file mode 100644 index 00000000..bb38aa7c --- /dev/null +++ b/lib/claiming/utils/getClaimingIntervalFromDate.js @@ -0,0 +1,7 @@ +import { dayjs } from '@folio/stripes/components'; + +export const getClaimingIntervalFromDate = (date) => { + const currentDay = dayjs().startOf('day'); + + return dayjs(date).diff(currentDay, 'days'); +}; diff --git a/lib/claiming/utils/getClaimingIntervalFromDate.test.js b/lib/claiming/utils/getClaimingIntervalFromDate.test.js new file mode 100644 index 00000000..3abc6b00 --- /dev/null +++ b/lib/claiming/utils/getClaimingIntervalFromDate.test.js @@ -0,0 +1,12 @@ +import { dayjs } from '@folio/stripes/components'; + +import { getClaimingIntervalFromDate } from './getClaimingIntervalFromDate'; + +describe('getClaimingIntervalFromDate', () => { + it('should return claiming interval calculated based on provided date', () => { + const today = dayjs().startOf('day'); + + expect(getClaimingIntervalFromDate(today)).toEqual(0); + expect(getClaimingIntervalFromDate(today.add(5, 'days'))).toEqual(5); + }); +}); diff --git a/lib/claiming/utils/index.js b/lib/claiming/utils/index.js new file mode 100644 index 00000000..6d40c1d4 --- /dev/null +++ b/lib/claiming/utils/index.js @@ -0,0 +1 @@ +export { getClaimingIntervalFromDate } from './getClaimingIntervalFromDate'; diff --git a/lib/hooks/usePiecesStatusBatchUpdate/usePiecesStatusBatchUpdate.js b/lib/hooks/usePiecesStatusBatchUpdate/usePiecesStatusBatchUpdate.js new file mode 100644 index 00000000..a322b1ff --- /dev/null +++ b/lib/hooks/usePiecesStatusBatchUpdate/usePiecesStatusBatchUpdate.js @@ -0,0 +1,24 @@ +import { useCallback } from 'react'; +import { useMutation } from 'react-query'; + +import { useOkapiKy } from '@folio/stripes/core'; + +import { PIECES_BATCH_STATUS_API } from '../../constants'; + +export const usePiecesStatusBatchUpdate = () => { + const ky = useOkapiKy(); + + const mutationFn = useCallback(({ data }) => { + return ky.put(PIECES_BATCH_STATUS_API, { json: data }).json(); + }, [ky]); + + const { + mutateAsync, + isLoading, + } = useMutation({ mutationFn }); + + return { + isLoading, + updatePiecesStatus: mutateAsync, + }; +}; diff --git a/translations/stripes-acq-components/en.json b/translations/stripes-acq-components/en.json index 4d68d25c..9255cab2 100644 --- a/translations/stripes-acq-components/en.json +++ b/translations/stripes-acq-components/en.json @@ -147,6 +147,12 @@ "claiming.action.sendClaim": "Send claim", "claiming.action.delayClaim": "Delay claim", "claiming.action.unreceivable": "Unreceivable", + "claiming.modal.delayClaim.heading": "{count, select, undefined {Delay claim} other {Delay {count, plural, one {claim} other {claims}}}}", + "claiming.modal.delayClaim.field.delayTo": "Delay to", + "claiming.modal.sendClaim.heading": "{count, select, undefined {Send claim} other {Send {count, plural, one {claim} other {claims}}}}", + "claiming.modal.sendClaim.field.claimExpiryDate": "Claim expiry date", + "claiming.modal.sendClaim.field.internalNote": "Internal note", + "claiming.modal.sendClaim.field.externalNote": "External note", "referenceNumbers.addReferenceNumbers": "Add vendor reference number", "referenceNumbers.refNumber": "Vendor reference number",