From 935d6c0027a957849d940cde0b5fa260de46e001 Mon Sep 17 00:00:00 2001 From: SanttuA Date: Tue, 20 Feb 2024 15:01:29 +0200 Subject: [PATCH] Added cooldown handling to my premises page (#305) When user does not have unit permissions i.e. when viewing external resources, reservation cooldowns are shown as unselectable slots in my premises page. --- .../AvailabilityTimelineContainer.js | 22 +- .../AvailabilityTimelineContainer.spec.js | 13 + .../TimelineGroups/TimelineGroup/utils.js | 44 ++- .../TimelineGroup/utils.spec.js | 250 +++++++++++++++++- 4 files changed, 319 insertions(+), 10 deletions(-) diff --git a/app/shared/availability-view/TimelineGroups/TimelineGroup/AvailabilityTimeline/AvailabilityTimelineContainer.js b/app/shared/availability-view/TimelineGroups/TimelineGroup/AvailabilityTimeline/AvailabilityTimelineContainer.js index d74166b72..53ec0a6a3 100644 --- a/app/shared/availability-view/TimelineGroups/TimelineGroup/AvailabilityTimeline/AvailabilityTimelineContainer.js +++ b/app/shared/availability-view/TimelineGroups/TimelineGroup/AvailabilityTimeline/AvailabilityTimelineContainer.js @@ -7,6 +7,7 @@ import { createSelector } from 'reselect'; import { showReservationInfoModal } from 'actions/uiActions'; import AvailabilityTimeline from './AvailabilityTimeline'; import utils from '../utils'; +import { isStaffForResource } from '../../../../../utils/resourceUtils'; export function selector() { function dateSelector(state, props) { return props.date; } @@ -22,6 +23,20 @@ export function selector() { resourceIdSelector, (resources, id) => resources[id] ); + + const hasStaffRightsSelector = createSelector( + resourceSelector, + resource => isStaffForResource(resource) + ); + + const timeRestrictionsSelector = createSelector( + resourceSelector, + resource => { + const { minPeriod, maxPeriod, cooldown } = resource; + return { minPeriod, maxPeriod, cooldown }; + } + ); + const reservationsSelector = createSelector( resourceSelector, dateSelector, @@ -37,8 +52,11 @@ export function selector() { reservationsSelector, dateSelector, resourceIdSelector, - (reservations, date, resourceId) => utils.getTimelineItems( - moment(date), reservations, resourceId + timeRestrictionsSelector, + hasStaffRightsSelector, + (reservations, date, resourceId, + timeRestrictions, hasStaffRights) => utils.getTimelineItems( + moment(date), reservations, resourceId, timeRestrictions, hasStaffRights ) ); diff --git a/app/shared/availability-view/TimelineGroups/TimelineGroup/AvailabilityTimeline/AvailabilityTimelineContainer.spec.js b/app/shared/availability-view/TimelineGroups/TimelineGroup/AvailabilityTimeline/AvailabilityTimelineContainer.spec.js index 135d3941c..ad6d0fa64 100644 --- a/app/shared/availability-view/TimelineGroups/TimelineGroup/AvailabilityTimeline/AvailabilityTimelineContainer.spec.js +++ b/app/shared/availability-view/TimelineGroups/TimelineGroup/AvailabilityTimeline/AvailabilityTimelineContainer.spec.js @@ -121,6 +121,19 @@ describe('shared/availability-view/AvailabilityTimelineContainer', () => { expect(actual[9]).toEqual({ key: '9', type: 'reservation', data: reservations[2] }); expect(actual[10].data.isSelectable).toBe(false); }); + + test('contains cooldown and staff rights info', () => { + const state = getState(); + const selection = { + begin: '2016-01-01T10:00:00', + end: '2016-01-01T10:30:00', + resourceId: 'resource-1', + }; + const props = { id: 'resource-1', date: '2016-01-01', selection }; + const actual = selector()(state, props).items; + expect(actual[0].data.hasStaffRights).toBeDefined(); + expect(actual[0].data.isWithinCooldown).toBeDefined(); + }); }); }); }); diff --git a/app/shared/availability-view/TimelineGroups/TimelineGroup/utils.js b/app/shared/availability-view/TimelineGroups/TimelineGroup/utils.js index f0581aaae..5a2a1c295 100644 --- a/app/shared/availability-view/TimelineGroups/TimelineGroup/utils.js +++ b/app/shared/availability-view/TimelineGroups/TimelineGroup/utils.js @@ -1,9 +1,12 @@ import some from 'lodash/some'; -import moment from 'moment'; +import Moment from 'moment'; +import { extendMoment } from 'moment-range'; import { slotSize, slotWidth, slotMargin } from 'constants/SlotConstants'; +const moment = extendMoment(Moment); + function getTimeSlotWidth({ startTime, endTime } = {}) { const diff = endTime ? endTime.diff(startTime, 'minutes') : slotSize; const slots = Math.floor(diff / slotSize); @@ -11,7 +14,10 @@ function getTimeSlotWidth({ startTime, endTime } = {}) { return (slotWidth * slots) - slotMargin; } -function getTimelineItems(date, reservations, resourceId) { +function getTimelineItems(date, reservations, resourceId, timeRestrictions, hasStaffRights) { + const { cooldown } = timeRestrictions; + // skip getting cooldowns if user has perms + const cooldownRanges = hasStaffRights ? [] : getCooldownRanges(reservations, cooldown); const items = []; let reservationPointer = 0; let timePointer = date.clone().startOf('day'); @@ -28,16 +34,24 @@ function getTimelineItems(date, reservations, resourceId) { timePointer = moment(reservation.end); reservationPointer += 1; } else { + const beginMoment = timePointer.format(); + const endMoment = timePointer.clone().add(slotSize, 'minutes').format(); + // skip cooldown check if user has perms + const isWithinCooldown = hasStaffRights ? false + : isSlotWithinCooldown(beginMoment, endMoment, cooldownRanges); + items.push({ key: String(items.length), type: 'reservation-slot', data: { - begin: timePointer.format(), - end: timePointer.clone().add(slotSize, 'minutes').format(), + begin: beginMoment, + end: endMoment, resourceId, // isSelectable: false by default to improve selector performance by allowing // addSelectionData to make some assumptions. isSelectable: false, + isWithinCooldown, + hasStaffRights }, }); timePointer.add(slotSize, 'minutes'); @@ -58,6 +72,7 @@ function markItemSelectable(item, isSelectable, openingHours, ext, after) { isSelectable && moment().isSameOrBefore(item.data.end) && (!openingHours || isInsideOpeningHours(item, openingHours)) + && !(item.data.isWithinCooldown && !item.data.hasStaffRights) ); const isExternalAndBeforeAfter = !ext && moment(item.data.begin).isSameOrBefore(after); if (isExternalAndBeforeAfter) { @@ -104,6 +119,27 @@ function addSelectionData(selection, resource, items) { }); } +function getCooldownRanges(reservations, cooldown) { + if (reservations && cooldown && cooldown !== '00:00:00') { + return reservations.map(reservation => moment.range( + moment(reservation.begin).subtract(moment.duration(cooldown)), + moment(reservation.end).add(moment.duration(cooldown)) + )); + } + return []; +} + +function isSlotWithinCooldown(begin, end, cooldownRanges) { + const slotRange = moment.range(begin, end); + for (let index = 0; index < cooldownRanges.length; index += 1) { + const cooldownRange = cooldownRanges[index]; + if (cooldownRange.overlaps(slotRange)) { + return true; + } + } + return false; +} + export default { addSelectionData, getTimelineItems, diff --git a/app/shared/availability-view/TimelineGroups/TimelineGroup/utils.spec.js b/app/shared/availability-view/TimelineGroups/TimelineGroup/utils.spec.js index cf6738f95..9c09e8e54 100644 --- a/app/shared/availability-view/TimelineGroups/TimelineGroup/utils.spec.js +++ b/app/shared/availability-view/TimelineGroups/TimelineGroup/utils.spec.js @@ -202,21 +202,23 @@ describe('shared/availability-view/utils', () => { }); describe('getTimelineItems', () => { + const timeRestrictions = { cooldown: '00:00:00', minPeriod: '00:30:00', maxPeriod: '01:00:00' }; + const hasStaffRights = true; test('returns reservation slots if reservations is undefined', () => { - const actual = utils.getTimelineItems(moment('2016-01-01T00:00:00Z'), undefined, '1'); + const actual = utils.getTimelineItems(moment('2016-01-01T00:00:00Z'), undefined, '1', timeRestrictions, hasStaffRights); expect(actual).toHaveLength(48); actual.forEach(item => expect(item.type).toBe('reservation-slot')); }); test('returns reservation slots if reservations is empty', () => { - const actual = utils.getTimelineItems(moment('2016-01-01T00:00:00Z'), [], '1'); + const actual = utils.getTimelineItems(moment('2016-01-01T00:00:00Z'), [], '1', timeRestrictions, hasStaffRights); expect(actual).toHaveLength(48); actual.forEach(item => expect(item.type).toBe('reservation-slot')); }); test('returns one reservation if entire day is a reservation', () => { const reservation = { id: 11, begin: '2016-01-01T00:00:00', end: '2016-01-02T00:00:00' }; - const actual = utils.getTimelineItems(moment('2016-01-01T00:00:00'), [reservation], '1'); + const actual = utils.getTimelineItems(moment('2016-01-01T00:00:00'), [reservation], '1', timeRestrictions, hasStaffRights); expect(actual).toHaveLength(1); expect(actual[0]).toEqual({ key: '0', @@ -231,7 +233,7 @@ describe('shared/availability-view/utils', () => { { id: 12, begin: '2016-01-01T12:30:00', end: '2016-01-01T20:00:00' }, { id: 13, begin: '2016-01-01T20:00:00', end: '2016-01-01T20:30:00' }, ]; - const actual = utils.getTimelineItems(moment('2016-01-01T00:00:00'), reservations, '1'); + const actual = utils.getTimelineItems(moment('2016-01-01T00:00:00'), reservations, '1', timeRestrictions, hasStaffRights); const expected = [ { key: '0', @@ -241,6 +243,8 @@ describe('shared/availability-view/utils', () => { end: moment('2016-01-01T00:30:00').format(), resourceId: '1', isSelectable: false, + hasStaffRights: true, + isWithinCooldown: false, }, }, { @@ -251,6 +255,8 @@ describe('shared/availability-view/utils', () => { end: moment('2016-01-01T01:00:00').format(), resourceId: '1', isSelectable: false, + hasStaffRights: true, + isWithinCooldown: false, }, }, { @@ -261,6 +267,8 @@ describe('shared/availability-view/utils', () => { end: moment('2016-01-01T01:30:00').format(), resourceId: '1', isSelectable: false, + hasStaffRights: true, + isWithinCooldown: false, }, }, { @@ -271,6 +279,8 @@ describe('shared/availability-view/utils', () => { end: moment('2016-01-01T02:00:00').format(), resourceId: '1', isSelectable: false, + hasStaffRights: true, + isWithinCooldown: false, }, }, { key: '4', type: 'reservation', data: reservations[0] }, @@ -282,6 +292,8 @@ describe('shared/availability-view/utils', () => { end: moment('2016-01-01T10:30:00').format(), resourceId: '1', isSelectable: false, + hasStaffRights: true, + isWithinCooldown: false, }, }, { @@ -292,6 +304,8 @@ describe('shared/availability-view/utils', () => { end: moment('2016-01-01T11:00:00').format(), resourceId: '1', isSelectable: false, + hasStaffRights: true, + isWithinCooldown: false, }, }, { @@ -302,6 +316,8 @@ describe('shared/availability-view/utils', () => { end: moment('2016-01-01T11:30:00').format(), resourceId: '1', isSelectable: false, + hasStaffRights: true, + isWithinCooldown: false, }, }, { @@ -312,6 +328,8 @@ describe('shared/availability-view/utils', () => { end: moment('2016-01-01T12:00:00').format(), resourceId: '1', isSelectable: false, + hasStaffRights: true, + isWithinCooldown: false, }, }, { @@ -322,6 +340,8 @@ describe('shared/availability-view/utils', () => { end: moment('2016-01-01T12:30:00').format(), resourceId: '1', isSelectable: false, + hasStaffRights: true, + isWithinCooldown: false, }, }, { key: '10', type: 'reservation', data: reservations[1] }, @@ -334,6 +354,8 @@ describe('shared/availability-view/utils', () => { end: moment('2016-01-01T21:00:00').format(), resourceId: '1', isSelectable: false, + hasStaffRights: true, + isWithinCooldown: false, }, }, { @@ -344,6 +366,8 @@ describe('shared/availability-view/utils', () => { end: moment('2016-01-01T21:30:00').format(), resourceId: '1', isSelectable: false, + hasStaffRights: true, + isWithinCooldown: false, }, }, { @@ -354,6 +378,8 @@ describe('shared/availability-view/utils', () => { end: moment('2016-01-01T22:00:00').format(), resourceId: '1', isSelectable: false, + hasStaffRights: true, + isWithinCooldown: false, }, }, { @@ -364,6 +390,8 @@ describe('shared/availability-view/utils', () => { end: moment('2016-01-01T22:30:00').format(), resourceId: '1', isSelectable: false, + hasStaffRights: true, + isWithinCooldown: false, }, }, { @@ -374,6 +402,8 @@ describe('shared/availability-view/utils', () => { end: moment('2016-01-01T23:00:00').format(), resourceId: '1', isSelectable: false, + hasStaffRights: true, + isWithinCooldown: false, }, }, { @@ -384,6 +414,8 @@ describe('shared/availability-view/utils', () => { end: moment('2016-01-01T23:30:00').format(), resourceId: '1', isSelectable: false, + hasStaffRights: true, + isWithinCooldown: false, }, }, { @@ -394,6 +426,216 @@ describe('shared/availability-view/utils', () => { end: moment('2016-01-02T00:00:00').format(), resourceId: '1', isSelectable: false, + hasStaffRights: true, + isWithinCooldown: false, + }, + }, + ]; + expect(actual).toEqual(expected); + }); + + test.each([true, false])('returns slots and reservations correctly when there is cooldown and hasStaffRights is %p', hasRights => { + const timeRestrictions2 = { cooldown: '01:00:00', minPeriod: '00:30:00', maxPeriod: '01:00:00' }; + const reservations = [ + { id: 11, begin: '2016-01-01T02:00:00', end: '2016-01-01T10:00:00' }, + { id: 12, begin: '2016-01-01T12:30:00', end: '2016-01-01T20:00:00' }, + { id: 13, begin: '2016-01-01T20:00:00', end: '2016-01-01T20:30:00' }, + ]; + const actual = utils.getTimelineItems(moment('2016-01-01T00:00:00'), reservations, '1', timeRestrictions2, hasRights); + const expected = [ + { + key: '0', + type: 'reservation-slot', + data: { + begin: moment('2016-01-01T00:00:00').format(), + end: moment('2016-01-01T00:30:00').format(), + resourceId: '1', + isSelectable: false, + hasStaffRights: hasRights, + isWithinCooldown: false, + }, + }, + { + key: '1', + type: 'reservation-slot', + data: { + begin: moment('2016-01-01T00:30:00').format(), + end: moment('2016-01-01T01:00:00').format(), + resourceId: '1', + isSelectable: false, + hasStaffRights: hasRights, + isWithinCooldown: false, + }, + }, + { + key: '2', + type: 'reservation-slot', + data: { + begin: moment('2016-01-01T01:00:00').format(), + end: moment('2016-01-01T01:30:00').format(), + resourceId: '1', + isSelectable: false, + hasStaffRights: hasRights, + isWithinCooldown: !hasRights, + }, + }, + { + key: '3', + type: 'reservation-slot', + data: { + begin: moment('2016-01-01T01:30:00').format(), + end: moment('2016-01-01T02:00:00').format(), + resourceId: '1', + isSelectable: false, + hasStaffRights: hasRights, + isWithinCooldown: !hasRights, + }, + }, + { key: '4', type: 'reservation', data: reservations[0] }, + { + key: '5', + type: 'reservation-slot', + data: { + begin: moment('2016-01-01T10:00:00').format(), + end: moment('2016-01-01T10:30:00').format(), + resourceId: '1', + isSelectable: false, + hasStaffRights: hasRights, + isWithinCooldown: !hasRights, + }, + }, + { + key: '6', + type: 'reservation-slot', + data: { + begin: moment('2016-01-01T10:30:00').format(), + end: moment('2016-01-01T11:00:00').format(), + resourceId: '1', + isSelectable: false, + hasStaffRights: hasRights, + isWithinCooldown: !hasRights, + }, + }, + { + key: '7', + type: 'reservation-slot', + data: { + begin: moment('2016-01-01T11:00:00').format(), + end: moment('2016-01-01T11:30:00').format(), + resourceId: '1', + isSelectable: false, + hasStaffRights: hasRights, + isWithinCooldown: false, + }, + }, + { + key: '8', + type: 'reservation-slot', + data: { + begin: moment('2016-01-01T11:30:00').format(), + end: moment('2016-01-01T12:00:00').format(), + resourceId: '1', + isSelectable: false, + hasStaffRights: hasRights, + isWithinCooldown: !hasRights, + }, + }, + { + key: '9', + type: 'reservation-slot', + data: { + begin: moment('2016-01-01T12:00:00').format(), + end: moment('2016-01-01T12:30:00').format(), + resourceId: '1', + isSelectable: false, + hasStaffRights: hasRights, + isWithinCooldown: !hasRights, + }, + }, + { key: '10', type: 'reservation', data: reservations[1] }, + { key: '11', type: 'reservation', data: reservations[2] }, + { + key: '12', + type: 'reservation-slot', + data: { + begin: moment('2016-01-01T20:30:00').format(), + end: moment('2016-01-01T21:00:00').format(), + resourceId: '1', + isSelectable: false, + hasStaffRights: hasRights, + isWithinCooldown: !hasRights, + }, + }, + { + key: '13', + type: 'reservation-slot', + data: { + begin: moment('2016-01-01T21:00:00').format(), + end: moment('2016-01-01T21:30:00').format(), + resourceId: '1', + isSelectable: false, + hasStaffRights: hasRights, + isWithinCooldown: !hasRights, + }, + }, + { + key: '14', + type: 'reservation-slot', + data: { + begin: moment('2016-01-01T21:30:00').format(), + end: moment('2016-01-01T22:00:00').format(), + resourceId: '1', + isSelectable: false, + hasStaffRights: hasRights, + isWithinCooldown: false, + }, + }, + { + key: '15', + type: 'reservation-slot', + data: { + begin: moment('2016-01-01T22:00:00').format(), + end: moment('2016-01-01T22:30:00').format(), + resourceId: '1', + isSelectable: false, + hasStaffRights: hasRights, + isWithinCooldown: false, + }, + }, + { + key: '16', + type: 'reservation-slot', + data: { + begin: moment('2016-01-01T22:30:00').format(), + end: moment('2016-01-01T23:00:00').format(), + resourceId: '1', + isSelectable: false, + hasStaffRights: hasRights, + isWithinCooldown: false, + }, + }, + { + key: '17', + type: 'reservation-slot', + data: { + begin: moment('2016-01-01T23:00:00').format(), + end: moment('2016-01-01T23:30:00').format(), + resourceId: '1', + isSelectable: false, + hasStaffRights: hasRights, + isWithinCooldown: false, + }, + }, + { + key: '18', + type: 'reservation-slot', + data: { + begin: moment('2016-01-01T23:30:00').format(), + end: moment('2016-01-02T00:00:00').format(), + resourceId: '1', + isSelectable: false, + hasStaffRights: hasRights, + isWithinCooldown: false, }, }, ];