Skip to content

Commit

Permalink
Added mass cancel reservations functionality
Browse files Browse the repository at this point in the history
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
  • Loading branch information
SanttuA committed Oct 17, 2023
1 parent 95cb931 commit 17f29a0
Show file tree
Hide file tree
Showing 20 changed files with 930 additions and 5 deletions.
15 changes: 15 additions & 0 deletions app/i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.",
Expand Down
15 changes: 15 additions & 0 deletions app/i18n/messages/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.",
Expand Down
15 changes: 15 additions & 0 deletions app/i18n/messages/sv.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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.",
Expand Down
32 changes: 31 additions & 1 deletion app/pages/manage-reservations/ManageReservationsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand All @@ -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() {
Expand Down Expand Up @@ -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);
Expand All @@ -187,9 +202,18 @@ class ManageReservationsPage extends React.Component {
<div className="app-ManageReservationsPage__filters">
<Grid>
<Row>
<Col sm={12}>
<Col sm={9}>
<h1>{title}</h1>
</Col>
<Col id="cancel-btn-container" sm={3}>
<Button
className={fontSize}
id="cancel-reservations-btn"
onClick={this.handleShowMassCancel}
>
{t('common.cancelReservations')}
</Button>
</Col>
</Row>
</Grid>
<ManageReservationsFilters
Expand Down Expand Up @@ -241,6 +265,11 @@ class ManageReservationsPage extends React.Component {
</div>
<ReservationInfoModal />
<ReservationCancelModal />
<MassCancelModal
onCancelSuccess={this.handleFetchReservations}
onClose={this.handleHideMassCancel}
show={showMassCancel}
/>
</div>
);
}
Expand All @@ -257,6 +286,7 @@ ManageReservationsPage.propTypes = {
reservationsTotalCount: PropTypes.number.isRequired,
isFetchingReservations: PropTypes.bool,
isFetchingUnits: PropTypes.bool,
fontSize: PropTypes.string,
};

export const UnwrappedManageReservationsPage = injectT(ManageReservationsPage);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -41,6 +42,7 @@ describe('ManageReservationsFilters', () => {
reservationsTotalCount: 0,
isFetchingReservations: false,
isFetchingUnits: false,
fontSize: '__font-size-small',
};

function getWrapper(props) {
Expand All @@ -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();
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,8 @@ describe('/pages/favorites/favoritesPageSelector', () => {
test('returns reservationsTotalCount', () => {
expect(getSelector().reservationsTotalCount).toBeDefined();
});

test('returns fontSize', () => {
expect(getSelector().fontSize).toBeDefined();
});
});
20 changes: 19 additions & 1 deletion app/pages/manage-reservations/_manage-reservations-page.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,4 +38,4 @@
&__list {
margin-top: 20px;
}
}
}
21 changes: 20 additions & 1 deletion app/pages/manage-reservations/inputs/SelectField.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -29,11 +30,18 @@ function SelectField({
placeholder = t('common.select'),
options,
value,
isRequired,
error,
}) {
return (
<div className="app-SelectField">
<FormGroup controlId={id}>
{label && <ControlLabel>{label}</ControlLabel>}
{label && (
<ControlLabel>
{label}
{isRequired && <span aria-hidden>*</span>}
</ControlLabel>
)}
<Select
className="app-Select"
classNamePrefix="app-Select"
Expand All @@ -54,6 +62,15 @@ function SelectField({
placeholder={placeholder}
value={getOption(value, options)}
/>
{error && (
<HelpBlock
className="has-error"
id={`${id}-error`}
role="alert"
>
{t(error)}
</HelpBlock>
)}
</FormGroup>
</div>
);
Expand All @@ -74,6 +91,8 @@ SelectField.propTypes = {
PropTypes.array,
PropTypes.number,
]),
isRequired: PropTypes.bool,
error: PropTypes.string,
};

export default injectT(SelectField);
Loading

0 comments on commit 17f29a0

Please sign in to comment.