From b20106be3848e36c7d141eb11be3884a36f77e4b Mon Sep 17 00:00:00 2001 From: SanttuA Date: Thu, 19 Oct 2023 13:54:54 +0300 Subject: [PATCH] Added mass cancel reservations functionality (#283) Changes: - added a mass cancel form modal - added mass cancel modal to manage reservations page which is triggered by a button - changed manage reservations page select input to handle required status and error reporting --- app/i18n/messages/en.json | 15 ++ app/i18n/messages/fi.json | 15 ++ app/i18n/messages/sv.json | 15 ++ .../ManageReservationsPage.js | 32 +++- .../__tests__/ManageReservationsPage.spec.js | 38 ++++ .../manageReservationsPageSelector.spec.js | 4 + .../_manage-reservations-page.scss | 20 ++- .../manage-reservations/inputs/SelectField.js | 21 ++- .../inputs/__tests__/SelectField.spec.js | 27 ++- .../manageReservationsPageSelector.js | 2 + app/shared/modals/_modals.scss | 4 +- .../reservation-mass-cancel/MassCancelForm.js | 106 +++++++++++ .../MassCancelModal.js | 166 ++++++++++++++++++ .../reservation-mass-cancel/RequiredSpan.js | 7 + .../_reservation-mass-cancel-modal.scss | 9 + .../massCancelUtils.js | 87 +++++++++ .../tests/MassCancelForm.spec.js | 147 ++++++++++++++++ .../tests/MassCancelModal.spec.js | 101 +++++++++++ .../tests/RequiredSpan.spec.js | 17 ++ .../tests/massCancelUtils.spec.js | 102 +++++++++++ 20 files changed, 930 insertions(+), 5 deletions(-) create mode 100644 app/shared/modals/reservation-mass-cancel/MassCancelForm.js create mode 100644 app/shared/modals/reservation-mass-cancel/MassCancelModal.js create mode 100644 app/shared/modals/reservation-mass-cancel/RequiredSpan.js create mode 100644 app/shared/modals/reservation-mass-cancel/_reservation-mass-cancel-modal.scss create mode 100644 app/shared/modals/reservation-mass-cancel/massCancelUtils.js create mode 100644 app/shared/modals/reservation-mass-cancel/tests/MassCancelForm.spec.js create mode 100644 app/shared/modals/reservation-mass-cancel/tests/MassCancelModal.spec.js create mode 100644 app/shared/modals/reservation-mass-cancel/tests/RequiredSpan.spec.js create mode 100644 app/shared/modals/reservation-mass-cancel/tests/massCancelUtils.spec.js diff --git a/app/i18n/messages/en.json b/app/i18n/messages/en.json index 0c996a914..3c43c4081 100644 --- a/app/i18n/messages/en.json +++ b/app/i18n/messages/en.json @@ -52,6 +52,7 @@ "common.cancel": "Cancel", "common.cancelled": "Cancelled", "common.cancelling": "Cancelling...", + "common.cancelReservations": "Cancel reservations", "common.checkError": "Error: ", "common.customerGroup": "Customer Group", "common.comments": "Comments", @@ -176,6 +177,18 @@ "MapToggle.resultsText": "{count} {count, plural, one {premise} other {premises}} found", "MapToggle.showList": "List", "MapToggle.showMap": "Map", + "MassCancel.cancelReservations": "Cancel reservations", + "MassCancel.confirmCancel": "I am sure that I want to cancel all reservations for the period", + "MassCancel.datetime.endLabel": "Cancel reservations to", + "MassCancel.datetime.startLabel": "Cancel reservations from", + "MassCancel.error.confirmCancel": "Accept cancellations", + "MassCancel.error.endBeforeStart": "End time is before the start time", + "MassCancel.error.required.end": "End time is required", + "MassCancel.error.required.resource": "Resource is required", + "MassCancel.error.required.start": "Start time is required", + "MassCancel.error.startAfterEnd": "Start time is after the end time", + "MassCancel.instructions": "Cancel all reservations for the selected resource within the given time range. Customers will be notified of the cancellations as usual. Only unit admins can make cancellations through this method. Please note that once cancellations are made, they cannot be undone or reverted to the previous state.", + "MassCancel.resourceLabel": "Resource", "MiniModal.buttonText": "Done", "ModalHeader.closeButtonText": "Close", "Nav.FontSize.title": "Fontsize", @@ -222,6 +235,8 @@ "Notifications.loginMessage": "Sign in to continue.", "Notifications.loginToReserve": "Log in to reserve these premises.", "Notifications.loginToReserveStrongAuth": "Log in with Suomi.fi to reserve these premises.", + "Notifications.massCancelSuccessful": "Cancellations of the reservations were successful.", + "Notifications.noPermission": "You do not have permission to perform this action.", "Notifications.noRightToReserve": "You do not have the right to make this reservation.", "Notifications.reservationDeleteSuccessMessage": "Your reservation was successfully cancelled.", "Notifications.reservationUpdateSuccessMessage": "Your reservation has been updated.", diff --git a/app/i18n/messages/fi.json b/app/i18n/messages/fi.json index 0081c5347..62492131a 100644 --- a/app/i18n/messages/fi.json +++ b/app/i18n/messages/fi.json @@ -52,6 +52,7 @@ "common.cancel": "Peru", "common.cancelled": "Peruttu", "common.cancelling": "Perutaan...", + "common.cancelReservations": "Peruuta varauksia", "common.checkError": "Tarkista: ", "common.customerGroup": "Asiakasryhmä", "common.comments": "Kommentit", @@ -176,6 +177,18 @@ "MapToggle.resultsText": "Tiloja löytyi {count} {count, plural, one {kpl} other {kpl}}", "MapToggle.showList": "Lista", "MapToggle.showMap": "Kartta", + "MassCancel.cancelReservations": "Peruuta varaukset", + "MassCancel.confirmCancel": "Olen varma, että haluan peruuttaa kaikki varaukset aikaväliltä", + "MassCancel.datetime.endLabel": "Peruuta varaukset päättyen", + "MassCancel.datetime.startLabel": "Peruuta varaukset alkaen", + "MassCancel.error.confirmCancel": "Hyväksy peruutukset", + "MassCancel.error.endBeforeStart": "Loppuaika on ennen alkuaikaa", + "MassCancel.error.required.end": "Loppuaika on pakollinen tieto", + "MassCancel.error.required.resource": "Resurssi on pakollinen tieto", + "MassCancel.error.required.start": "Alkuaika on pakollinen tieto", + "MassCancel.error.startAfterEnd": "Alkuaika on loppuaikaa myöhemmin", + "MassCancel.instructions": "Peruuta kaikki valitun resurssin varaukset annetulta aikaväliltä. Tieto peruutuksista lähetetään asiakkaille normaaliin tapaan. Peruutuksia voi tehdä tätä kautta vain toimipisteen pääkäyttäjät. Tehtyjä peruutuksia ei voi jälkikäteen enää kumota eli palauttaa aikaisempaan tilaan.", + "MassCancel.resourceLabel": "Resurssi", "MiniModal.buttonText": "Valmis", "ModalHeader.closeButtonText": "Sulje", "Nav.FontSize.title": "Tekstikoko", @@ -222,6 +235,8 @@ "Notifications.loginMessage": "Kirjaudu sisään jatkaaksesi.", "Notifications.loginToReserve": "Kirjaudu sisään tehdäksesi varauksen tähän tilaan.", "Notifications.loginToReserveStrongAuth": "Kirjaudu sisään Suomi.fi:llä tehdäksesi varauksen tähän tilaan.", + "Notifications.massCancelSuccessful": "Varausten peruminen onnistui.", + "Notifications.noPermission": "Sinulla ei ole oikeutta suorittaa tätä toimintoa.", "Notifications.noRightToReserve": "Sinulla ei ole oikeutta tehdä tätä varausta.", "Notifications.reservationDeleteSuccessMessage": "Varauksen peruminen onnistui.", "Notifications.reservationUpdateSuccessMessage": "Varaus päivitetty.", diff --git a/app/i18n/messages/sv.json b/app/i18n/messages/sv.json index aa1d56269..257cde4be 100644 --- a/app/i18n/messages/sv.json +++ b/app/i18n/messages/sv.json @@ -52,6 +52,7 @@ "common.cancel": "Avbryt", "common.cancelled": "Avbokad", "common.cancelling": "Avbokas...", + "common.cancelReservations": "Avboka bokningar", "common.checkError": "Kontrollera: ", "common.customerGroup": "Kundgrupp", "common.comments": "Kommentarer", @@ -178,6 +179,18 @@ "MapToggle.resultsText": "{count} {count, plural, one {rum} other {rum}} hittades", "MapToggle.showList": "Resultat", "MapToggle.showMap": "Karta", + "MassCancel.cancelReservations": "Avbryt Reservationer", + "MassCancel.confirmCancel": "Jag är säker på att jag vill avboka alla reservationer för perioden", + "MassCancel.datetime.endLabel": "Avboka reservationer till", + "MassCancel.datetime.startLabel": "Avboka reservationer från", + "MassCancel.error.confirmCancel": "Godkänn avbokningar", + "MassCancel.error.endBeforeStart": "Sluttiden är före starttiden", + "MassCancel.error.required.end": "Sluttid krävs", + "MassCancel.error.required.resource": "Resurs krävs", + "MassCancel.error.required.start": "Starttid krävs", + "MassCancel.error.startAfterEnd": "Starttiden är senare än sluttiden", + "MassCancel.instructions": "Avboka alla reservationer för den valda resursen inom den angivna tidsperioden. Kunderna kommer att informeras om avbokningarna som vanligt. Endast enhetsadministratörer kan göra avbokningar via denna metod. Observera att när avbokningar har gjorts kan de inte ångras eller återställas till det tidigare tillståndet.", + "MassCancel.resourceLabel": "Resurs", "MiniModal.buttonText": "Klart", "ModalHeader.closeButtonText": "Stäng", "Nav.FontSize.title": "Textstorlek", @@ -224,6 +237,8 @@ "Notifications.loginMessage": "Logga in för att fortsätta.", "Notifications.loginToReserve": "Logga in för att boka detta utrymme.", "Notifications.loginToReserveStrongAuth": "Logga in via Suomi.fi för att boka detta utrymme.", + "Notifications.massCancelSuccessful": "Avbokningarna lyckades.", + "Notifications.noPermission": "Du har inte behörighet att utföra denna åtgärd.", "Notifications.noRightToReserve": "Du har inte rätt att göra denna bokning.", "Notifications.reservationDeleteSuccessMessage": "Bokningen avbokades.", "Notifications.reservationUpdateSuccessMessage": "Bokningen uppdaterades.", diff --git a/app/pages/manage-reservations/ManageReservationsPage.js b/app/pages/manage-reservations/ManageReservationsPage.js index d1f7b57f3..b6d88d6a1 100644 --- a/app/pages/manage-reservations/ManageReservationsPage.js +++ b/app/pages/manage-reservations/ManageReservationsPage.js @@ -5,6 +5,7 @@ import Loader from 'react-loader'; import Col from 'react-bootstrap/lib/Col'; import Grid from 'react-bootstrap/lib/Grid'; import Row from 'react-bootstrap/lib/Row'; +import Button from 'react-bootstrap/lib/Button'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { withRouter } from 'react-router'; @@ -37,6 +38,7 @@ import { getFilteredReservations } from './manageReservationsPageUtils'; 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'; class ManageReservationsPage extends React.Component { @@ -45,6 +47,7 @@ class ManageReservationsPage extends React.Component { this.state = { showOnlyFilters: [constants.RESERVATION_SHOWONLY_FILTERS.CAN_MODIFY], + showMassCancel: false, }; this.handleFetchReservations = this.handleFetchReservations.bind(this); @@ -54,6 +57,16 @@ class ManageReservationsPage extends React.Component { this.handleOpenInfoModal = this.handleOpenInfoModal.bind(this); this.handleEditClick = this.handleEditClick.bind(this); this.handleEditReservation = this.handleEditReservation.bind(this); + this.handleShowMassCancel = this.handleShowMassCancel.bind(this); + this.handleHideMassCancel = this.handleHideMassCancel.bind(this); + } + + handleShowMassCancel() { + this.setState({ showMassCancel: true }); + } + + handleHideMassCancel() { + this.setState({ showMassCancel: false }); } componentDidMount() { @@ -171,10 +184,12 @@ class ManageReservationsPage extends React.Component { locale, isFetchingReservations, isFetchingUnits, + fontSize, } = this.props; const { showOnlyFilters, + showMassCancel, } = this.state; const filters = getFiltersFromUrl(location, false); @@ -187,9 +202,18 @@ class ManageReservationsPage extends React.Component {
- +

{title}

+ + +
+
); } @@ -257,6 +286,7 @@ ManageReservationsPage.propTypes = { reservationsTotalCount: PropTypes.number.isRequired, isFetchingReservations: PropTypes.bool, isFetchingUnits: PropTypes.bool, + fontSize: PropTypes.string, }; export const UnwrappedManageReservationsPage = injectT(ManageReservationsPage); diff --git a/app/pages/manage-reservations/__tests__/ManageReservationsPage.spec.js b/app/pages/manage-reservations/__tests__/ManageReservationsPage.spec.js index 63171ecba..585141e1d 100644 --- a/app/pages/manage-reservations/__tests__/ManageReservationsPage.spec.js +++ b/app/pages/manage-reservations/__tests__/ManageReservationsPage.spec.js @@ -17,6 +17,7 @@ import PageWrapper from '../../PageWrapper'; import Reservation from 'utils/fixtures/Reservation'; import { getEditReservationUrl } from 'utils/reservationUtils'; import PageResultsText from '../PageResultsText'; +import MassCancelModal from '../../../shared/modals/reservation-mass-cancel/MassCancelModal'; describe('ManageReservationsFilters', () => { @@ -41,6 +42,7 @@ describe('ManageReservationsFilters', () => { reservationsTotalCount: 0, isFetchingReservations: false, isFetchingUnits: false, + fontSize: '__font-size-small', }; function getWrapper(props) { @@ -63,6 +65,16 @@ describe('ManageReservationsFilters', () => { expect(heading.text()).toBe('ManageReservationsPage.title'); }); + test('mass cancel button', () => { + const wrapper = getWrapper(); + const massCancelButton = wrapper.find('.app-ManageReservationsPage__filters').find('Button'); + expect(massCancelButton).toHaveLength(1); + expect(massCancelButton.prop('id')).toBe('cancel-reservations-btn'); + expect(massCancelButton.prop('onClick')).toBe(wrapper.instance().handleShowMassCancel); + expect(massCancelButton.prop('className')).toBe(defaultProps.fontSize); + expect(massCancelButton.children().at(0).text()).toBe('common.cancelReservations'); + }); + test('ManageReservationsFilters', () => { const wrapper = getWrapper(); const instance = wrapper.instance(); @@ -142,6 +154,16 @@ describe('ManageReservationsFilters', () => { const cancelModal = getWrapper().find(ReservationCancelModal); expect(cancelModal).toHaveLength(1); }); + + test('MassCancelModal', () => { + const wrapper = getWrapper(); + const instance = wrapper.instance(); + const massCancelModal = wrapper.find(MassCancelModal); + expect(massCancelModal).toHaveLength(1); + expect(massCancelModal.prop('onCancelSuccess')).toBe(instance.handleFetchReservations); + expect(massCancelModal.prop('onClose')).toBe(instance.handleHideMassCancel); + expect(massCancelModal.prop('show')).toBe(instance.state.showMassCancel); + }); }); describe('functions', () => { @@ -152,6 +174,22 @@ describe('ManageReservationsFilters', () => { }); }); + describe('handleShowMassCancel', () => { + const instance = getWrapper().instance(); + const spy = jest.spyOn(instance, 'setState'); + instance.handleShowMassCancel(); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy.mock.calls[0][0]).toStrictEqual({ showMassCancel: true }); + }); + + describe('handleHideMassCancel', () => { + const instance = getWrapper().instance(); + const spy = jest.spyOn(instance, 'setState'); + instance.handleHideMassCancel(); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy.mock.calls[0][0]).toStrictEqual({ showMassCancel: false }); + }); + describe('componentDidMount', () => { test('calls fetchUnits', () => { const instance = getWrapper().instance(); diff --git a/app/pages/manage-reservations/__tests__/manageReservationsPageSelector.spec.js b/app/pages/manage-reservations/__tests__/manageReservationsPageSelector.spec.js index 742e1ca1e..40ac45036 100644 --- a/app/pages/manage-reservations/__tests__/manageReservationsPageSelector.spec.js +++ b/app/pages/manage-reservations/__tests__/manageReservationsPageSelector.spec.js @@ -47,4 +47,8 @@ describe('/pages/favorites/favoritesPageSelector', () => { test('returns reservationsTotalCount', () => { expect(getSelector().reservationsTotalCount).toBeDefined(); }); + + test('returns fontSize', () => { + expect(getSelector().fontSize).toBeDefined(); + }); }); diff --git a/app/pages/manage-reservations/_manage-reservations-page.scss b/app/pages/manage-reservations/_manage-reservations-page.scss index 8052b46c8..b2bd74188 100644 --- a/app/pages/manage-reservations/_manage-reservations-page.scss +++ b/app/pages/manage-reservations/_manage-reservations-page.scss @@ -12,6 +12,24 @@ margin: 32px 0 16px; } + #cancel-btn-container { + text-align: right; + + @media(max-width:$screen-sm-min) { + text-align: initial; + } + + #cancel-reservations-btn { + margin-top: 32px; + + @media(max-width:$screen-sm-min) { + margin-top: 0px; + margin-bottom: 8px; + } + } + } + + &__filters { background-color: #dedfe1; padding: 0 0 40px; @@ -20,4 +38,4 @@ &__list { margin-top: 20px; } -} +} \ No newline at end of file diff --git a/app/pages/manage-reservations/inputs/SelectField.js b/app/pages/manage-reservations/inputs/SelectField.js index 4d12ecbab..dba5f205e 100644 --- a/app/pages/manage-reservations/inputs/SelectField.js +++ b/app/pages/manage-reservations/inputs/SelectField.js @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import ControlLabel from 'react-bootstrap/lib/ControlLabel'; import FormGroup from 'react-bootstrap/lib/FormGroup'; +import HelpBlock from 'react-bootstrap/lib/HelpBlock'; import Select from 'react-select'; import injectT from '../../../i18n/injectT'; @@ -29,11 +30,18 @@ function SelectField({ placeholder = t('common.select'), options, value, + isRequired, + error, }) { return (
- {label && {label}} + {label && ( + + {label} + {isRequired && *} + + )}