From 96dac9daeaaa4dce46eb63ea8825cfbf80e7dca6 Mon Sep 17 00:00:00 2001 From: SanttuA Date: Thu, 18 Jan 2024 09:28:58 +0200 Subject: [PATCH] Added confirm cash modal into manage reservations page When staff wants to confirm reservation cash payment via dropdown quick action in manage reservations page, they get an extra confirm step asking do they really want to confirm the payment --- app/i18n/messages/en.json | 2 + app/i18n/messages/fi.json | 2 + app/i18n/messages/sv.json | 2 + .../ManageReservationsPage.js | 31 +++++++ .../__tests__/ManageReservationsPage.spec.js | 66 ++++++++++++++ .../ManageReservationsDropdown.js | 3 +- app/shared/modals/_modals.scss | 1 + .../ConfirmCashModal.js | 64 ++++++++++++++ .../ConfirmCashModal.spec.js | 87 +++++++++++++++++++ .../_reservation-confirm-cash-modal.scss | 4 + 10 files changed, 261 insertions(+), 1 deletion(-) create mode 100644 app/shared/modals/reservation-confirm-cash/ConfirmCashModal.js create mode 100644 app/shared/modals/reservation-confirm-cash/ConfirmCashModal.spec.js create mode 100644 app/shared/modals/reservation-confirm-cash/_reservation-confirm-cash-modal.scss diff --git a/app/i18n/messages/en.json b/app/i18n/messages/en.json index d4e44d5b9..284d30a29 100644 --- a/app/i18n/messages/en.json +++ b/app/i18n/messages/en.json @@ -119,6 +119,8 @@ "common.yes": "Yes", "SelectControl.noOptions": "No options", "SelectControl.clearLabel": "Clear selection", + "ConfirmCashPaymentModal.title": "Cash payment approving", + "ConfirmCashPaymentModal.body": "Are you sure you want to approve the cash payment?", "ConfirmReservationModal.beforeText": "Original reservation time:", "ConfirmReservationModal.editTitle": "Changing reservation", "ConfirmReservationModal.formInfo": "Please fill in the following data for your preliminary reservation. The fields marked with an asterisk (*) are mandatory.", diff --git a/app/i18n/messages/fi.json b/app/i18n/messages/fi.json index d51c52084..5e134fc75 100644 --- a/app/i18n/messages/fi.json +++ b/app/i18n/messages/fi.json @@ -119,6 +119,8 @@ "common.yes": "Kyllä", "SelectControl.noOptions": "Ei valintoja", "SelectControl.clearLabel": "Poista valinta", + "ConfirmCashPaymentModal.title": "Käteismaksun hyväksyminen", + "ConfirmCashPaymentModal.body": "Haluatko varmasti hyväksyä käteismaksun?", "ConfirmReservationModal.beforeText": "Alkuperäinen varausaika:", "ConfirmReservationModal.editTitle": "Varauksen muuttaminen", "ConfirmReservationModal.formInfo": "Täytä vielä seuraavat tiedot alustavaa varausta varten. Tähdellä (*) merkityt tiedot ovat pakollisia.", diff --git a/app/i18n/messages/sv.json b/app/i18n/messages/sv.json index 7c3cc8712..201c54a1e 100644 --- a/app/i18n/messages/sv.json +++ b/app/i18n/messages/sv.json @@ -119,6 +119,8 @@ "common.yes": "Ja", "SelectControl.noOptions": "Inga val", "SelectControl.clearLabel": "Ta bort val", + "ConfirmCashPaymentModal.title": "Godkännande av kontantbetalning", + "ConfirmCashPaymentModal.body": "Är du säker på att du vill godkänna kontantbetalning?", "ConfirmReservationModal.beforeText": "Ursprunglig bokningstid", "ConfirmReservationModal.editTitle": "Ändra bokningen", "ConfirmReservationModal.formInfo": "Fyll ännu i följande uppgifter för den preliminära bokningen. Uppgifterna markerade med en asterisk (*) är obligatoriska.", diff --git a/app/pages/manage-reservations/ManageReservationsPage.js b/app/pages/manage-reservations/ManageReservationsPage.js index b6d88d6a1..875ba45da 100644 --- a/app/pages/manage-reservations/ManageReservationsPage.js +++ b/app/pages/manage-reservations/ManageReservationsPage.js @@ -39,6 +39,7 @@ import ReservationInfoModal from 'shared/modals/reservation-info'; import ReservationCancelModal from 'shared/modals/reservation-cancel'; import PageResultsText from './PageResultsText'; import MassCancelModal from '../../shared/modals/reservation-mass-cancel/MassCancelModal'; +import ConfirmCashModal from '../../shared/modals/reservation-confirm-cash/ConfirmCashModal'; class ManageReservationsPage extends React.Component { @@ -48,6 +49,8 @@ class ManageReservationsPage extends React.Component { this.state = { showOnlyFilters: [constants.RESERVATION_SHOWONLY_FILTERS.CAN_MODIFY], showMassCancel: false, + showConfirmCash: false, + selectedReservation: null, }; this.handleFetchReservations = this.handleFetchReservations.bind(this); @@ -59,6 +62,9 @@ class ManageReservationsPage extends React.Component { this.handleEditReservation = this.handleEditReservation.bind(this); this.handleShowMassCancel = this.handleShowMassCancel.bind(this); this.handleHideMassCancel = this.handleHideMassCancel.bind(this); + this.handleShowConfirmCash = this.handleShowConfirmCash.bind(this); + this.handleHideConfirmCash = this.handleHideConfirmCash.bind(this); + this.handleConfirmCash = this.handleConfirmCash.bind(this); } handleShowMassCancel() { @@ -69,6 +75,14 @@ class ManageReservationsPage extends React.Component { this.setState({ showMassCancel: false }); } + handleShowConfirmCash(reservation) { + this.setState({ showConfirmCash: true, selectedReservation: reservation }); + } + + handleHideConfirmCash() { + this.setState({ showConfirmCash: false, selectedReservation: null }); + } + componentDidMount() { this.props.actions.fetchUnits(); this.handleFetchReservations(); @@ -165,6 +179,9 @@ class ManageReservationsPage extends React.Component { case constants.RESERVATION_STATE.CONFIRMED: actions.confirmPreliminaryReservation(reservation); break; + case constants.RESERVATION_STATE.WAITING_FOR_CASH_PAYMENT: + this.handleShowConfirmCash(reservation); + break; case constants.RESERVATION_STATE.DENIED: actions.denyPreliminaryReservation(reservation); break; @@ -173,6 +190,14 @@ class ManageReservationsPage extends React.Component { } } + // handles confirming cash payments via cash confirm modal onSubmit + handleConfirmCash() { + const { actions } = this.props; + const { selectedReservation } = this.state; + actions.confirmPreliminaryReservation(selectedReservation); + this.handleHideConfirmCash(); + } + render() { const { t, @@ -190,6 +215,7 @@ class ManageReservationsPage extends React.Component { const { showOnlyFilters, showMassCancel, + showConfirmCash, } = this.state; const filters = getFiltersFromUrl(location, false); @@ -270,6 +296,11 @@ class ManageReservationsPage extends React.Component { onClose={this.handleHideMassCancel} show={showMassCancel} /> + ); } diff --git a/app/pages/manage-reservations/__tests__/ManageReservationsPage.spec.js b/app/pages/manage-reservations/__tests__/ManageReservationsPage.spec.js index 585141e1d..85dd0dce5 100644 --- a/app/pages/manage-reservations/__tests__/ManageReservationsPage.spec.js +++ b/app/pages/manage-reservations/__tests__/ManageReservationsPage.spec.js @@ -18,6 +18,7 @@ import Reservation from 'utils/fixtures/Reservation'; import { getEditReservationUrl } from 'utils/reservationUtils'; import PageResultsText from '../PageResultsText'; import MassCancelModal from '../../../shared/modals/reservation-mass-cancel/MassCancelModal'; +import ConfirmCashModal from '../../../shared/modals/reservation-confirm-cash/ConfirmCashModal'; describe('ManageReservationsFilters', () => { @@ -164,6 +165,16 @@ describe('ManageReservationsFilters', () => { expect(massCancelModal.prop('onClose')).toBe(instance.handleHideMassCancel); expect(massCancelModal.prop('show')).toBe(instance.state.showMassCancel); }); + + test('ConfirmCashModal', () => { + const wrapper = getWrapper(); + const instance = wrapper.instance(); + const confirmCashModal = wrapper.find(ConfirmCashModal); + expect(confirmCashModal).toHaveLength(1); + expect(confirmCashModal.prop('onSubmit')).toBe(instance.handleConfirmCash); + expect(confirmCashModal.prop('onClose')).toBe(instance.handleHideConfirmCash); + expect(confirmCashModal.prop('show')).toBe(instance.state.showConfirmCash); + }); }); describe('functions', () => { @@ -190,6 +201,31 @@ describe('ManageReservationsFilters', () => { expect(spy.mock.calls[0][0]).toStrictEqual({ showMassCancel: false }); }); + describe('handleShowConfirmCash', () => { + test('calls setState with correct params', () => { + const instance = getWrapper().instance(); + const spy = jest.spyOn(instance, 'setState'); + const reservation = { id: '123' }; + instance.handleShowConfirmCash(reservation); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy.mock.calls[0][0]).toStrictEqual( + { showConfirmCash: true, selectedReservation: reservation } + ); + }); + }); + + describe('handleHideConfirmCash', () => { + test('calls setState with correct params', () => { + const instance = getWrapper().instance(); + const spy = jest.spyOn(instance, 'setState'); + instance.handleHideConfirmCash(); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy.mock.calls[0][0]).toStrictEqual( + { showConfirmCash: false, selectedReservation: null } + ); + }); + }); + describe('componentDidMount', () => { test('calls fetchUnits', () => { const instance = getWrapper().instance(); @@ -361,6 +397,15 @@ describe('ManageReservationsFilters', () => { .toBe(reservation); }); + test('calls correct function when status is waiting for cash', () => { + const instance = getWrapper().instance(); + const spy = jest.spyOn(instance, 'handleShowConfirmCash'); + const status = constants.RESERVATION_STATE.WAITING_FOR_CASH_PAYMENT; + instance.handleEditReservation(reservation, status); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(reservation); + }); + test('calls correct function when status is denied', () => { const instance = getWrapper().instance(); const status = constants.RESERVATION_STATE.DENIED; @@ -380,5 +425,26 @@ describe('ManageReservationsFilters', () => { expect(defaultProps.actions.denyPreliminaryReservation.mock.calls.length).toBe(0); }); }); + + describe('handleConfirmCash', () => { + test('calls confirmPreliminaryReservation with correct params', () => { + const reservation = Reservation.build(); + const instance = getWrapper().instance(); + instance.setState({ selectedReservation: reservation }); + instance.handleConfirmCash(); + expect(defaultProps.actions.confirmPreliminaryReservation.mock.calls.length).toBe(1); + expect(defaultProps.actions.confirmPreliminaryReservation.mock.calls[0][0]) + .toBe(reservation); + }); + + test('sets correct state values', () => { + const reservation = Reservation.build(); + const instance = getWrapper().instance(); + instance.setState({ selectedReservation: reservation, showConfirmCash: true }); + instance.handleConfirmCash(); + expect(instance.state.showConfirmCash).toBe(false); + expect(instance.state.selectedReservation).toBe(null); + }); + }); }); }); diff --git a/app/pages/manage-reservations/dropdown-action/ManageReservationsDropdown.js b/app/pages/manage-reservations/dropdown-action/ManageReservationsDropdown.js index 87c0b5385..3f3230ff3 100644 --- a/app/pages/manage-reservations/dropdown-action/ManageReservationsDropdown.js +++ b/app/pages/manage-reservations/dropdown-action/ManageReservationsDropdown.js @@ -46,7 +46,8 @@ function ManageReservationsDropdown({ )} {userCanModify && isWaitingForCashPayment && ( onEditReservation(reservation, reservationStates.CONFIRMED)} + onClick={ + () => onEditReservation(reservation, reservationStates.WAITING_FOR_CASH_PAYMENT)} > {t('common.confirmCashPayment')} diff --git a/app/shared/modals/_modals.scss b/app/shared/modals/_modals.scss index 60d01669e..b3bc095cd 100644 --- a/app/shared/modals/_modals.scss +++ b/app/shared/modals/_modals.scss @@ -4,6 +4,7 @@ @import './reservation-success/reservation-success-modal'; @import './reservation-payment/reservation-payment-modal'; @import './reservation-mass-cancel/reservation-mass-cancel-modal'; +@import './reservation-confirm-cash/reservation-confirm-cash-modal'; .modal-body { overflow-y: scroll; diff --git a/app/shared/modals/reservation-confirm-cash/ConfirmCashModal.js b/app/shared/modals/reservation-confirm-cash/ConfirmCashModal.js new file mode 100644 index 000000000..600209352 --- /dev/null +++ b/app/shared/modals/reservation-confirm-cash/ConfirmCashModal.js @@ -0,0 +1,64 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Modal from 'react-bootstrap/lib/Modal'; +import Button from 'react-bootstrap/lib/Button'; +import { connect } from 'react-redux'; + +import injectT from '../../../i18n/injectT'; +import { currentLanguageSelector } from '../../../state/selectors/translationSelectors'; +import { fontSizeSelector } from '../../../state/selectors/accessibilitySelectors'; + +function ConfirmCashModal({ + show, onClose, t, onSubmit, fontSize +}) { + return ( + onClose()} + show={show} + > + + {t('ConfirmCashPaymentModal.title')} + + + {t('ConfirmCashPaymentModal.body')} + + + + + + + ); +} + +ConfirmCashModal.propTypes = { + onClose: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + show: PropTypes.bool.isRequired, + t: PropTypes.func.isRequired, + fontSize: PropTypes.string.isRequired, +}; + +function mapStateToProps(state) { + return { + currentLanguage: currentLanguageSelector(state), + fontSize: fontSizeSelector(state), + }; +} + +const UnconnectedConfirmCashModal = injectT(ConfirmCashModal); +export { UnconnectedConfirmCashModal }; +export default connect(mapStateToProps, null)(injectT(ConfirmCashModal)); diff --git a/app/shared/modals/reservation-confirm-cash/ConfirmCashModal.spec.js b/app/shared/modals/reservation-confirm-cash/ConfirmCashModal.spec.js new file mode 100644 index 000000000..473e3c4aa --- /dev/null +++ b/app/shared/modals/reservation-confirm-cash/ConfirmCashModal.spec.js @@ -0,0 +1,87 @@ +import React from 'react'; +import Button from 'react-bootstrap/lib/Button'; +import Modal from 'react-bootstrap/lib/Modal'; + +import { shallowWithIntl } from 'utils/testUtils'; +import { + UnconnectedConfirmCashModal as ConfirmCashModal, +} from './ConfirmCashModal'; + +describe('shared/modals/reservation-confirm-cash/ConfirmCashModal', () => { + const defaultProps = { + onClose: () => {}, + onSubmit: () => {}, + fontSize: 'test-large', + show: true, + }; + + function getWrapper(extraProps = {}) { + return shallowWithIntl(); + } + + describe('render', () => { + test('renders a Modal component', () => { + const modalComponent = getWrapper().find(Modal); + expect(modalComponent.length).toBe(1); + expect(modalComponent.prop('animation')).toBe(false); + expect(modalComponent.prop('className')).toBe(defaultProps.fontSize); + expect(modalComponent.prop('onHide')).toBeDefined(); + expect(modalComponent.prop('show')).toBe(defaultProps.show); + }); + + describe('Modal header', () => { + function getModalHeaderWrapper(props) { + return getWrapper(props).find(Modal.Header); + } + + test('is rendered', () => { + expect(getModalHeaderWrapper()).toHaveLength(1); + }); + + test('contains a close button', () => { + expect(getModalHeaderWrapper().props().closeButton).toBe(true); + expect(getModalHeaderWrapper().props().closeLabel).toBe('ModalHeader.closeButtonText'); + }); + + test('title is correct', () => { + const modalTitle = getModalHeaderWrapper().find(Modal.Title); + expect(modalTitle.length).toBe(1); + expect(modalTitle.prop('children')).toBe('ConfirmCashPaymentModal.title'); + }); + }); + + describe('Modal body', () => { + function getModalBodyWrapper(props) { + return getWrapper(props).find(Modal.Body); + } + + test('is rendered', () => { + const body = getModalBodyWrapper(); + expect(body).toHaveLength(1); + expect(body.prop('children')).toBe('ConfirmCashPaymentModal.body'); + }); + }); + + describe('Modal footer', () => { + test('is rendered', () => { + const footer = getWrapper().find(Modal.Footer); + expect(footer).toHaveLength(1); + }); + + test('buttons are rendered', () => { + const footer = getWrapper().find(Modal.Footer); + const buttons = footer.find(Button); + expect(buttons).toHaveLength(2); + expect(buttons.at(0).prop('bsStyle')).toBe('primary'); + expect(buttons.at(0).prop('className')).toBe(defaultProps.fontSize); + expect(buttons.at(0).prop('onClick')).toBeDefined(); + expect(buttons.at(0).prop('children')).toBe('common.back'); + + expect(buttons.at(1).prop('bsStyle')).toBe('success'); + expect(buttons.at(1).prop('className')).toBe(defaultProps.fontSize); + expect(buttons.at(1).prop('onClick')).toBeDefined(); + expect(buttons.at(1).prop('children')).toBe('common.confirmCashPayment'); + }); + }); + }); +}); diff --git a/app/shared/modals/reservation-confirm-cash/_reservation-confirm-cash-modal.scss b/app/shared/modals/reservation-confirm-cash/_reservation-confirm-cash-modal.scss new file mode 100644 index 000000000..f0219102f --- /dev/null +++ b/app/shared/modals/reservation-confirm-cash/_reservation-confirm-cash-modal.scss @@ -0,0 +1,4 @@ +.btn-success { + color: #008540; + border-color: #008540; +} \ No newline at end of file