diff --git a/apps/web/components/booking/DatePicker.tsx b/apps/web/components/booking/DatePicker.tsx index bc0ba2c6..da5ed3b1 100644 --- a/apps/web/components/booking/DatePicker.tsx +++ b/apps/web/components/booking/DatePicker.tsx @@ -139,6 +139,7 @@ function DatePicker({ frequency: eventLength, minimumBookingNotice, workingHours, + eventLength, }).length ); }; diff --git a/apps/web/ee/components/team/availability/TeamAvailabilityTimes.tsx b/apps/web/ee/components/team/availability/TeamAvailabilityTimes.tsx index 5cd6be67..a0425d90 100644 --- a/apps/web/ee/components/team/availability/TeamAvailabilityTimes.tsx +++ b/apps/web/ee/components/team/availability/TeamAvailabilityTimes.tsx @@ -44,6 +44,7 @@ export default function TeamAvailabilityTimes(props: Props) { inviteeDate: props.selectedDate, workingHours: data?.workingHours || [], minimumBookingNotice: 0, + eventLength: props.frequency, }) : []; diff --git a/apps/web/lib/hooks/useSlots.ts b/apps/web/lib/hooks/useSlots.ts index 8845cf2a..ce400e42 100644 --- a/apps/web/lib/hooks/useSlots.ts +++ b/apps/web/lib/hooks/useSlots.ts @@ -34,6 +34,70 @@ type UseSlotsProps = { afterBufferTime?: number; }; +type getFilteredTimesProps = { + times: dayjs.Dayjs[]; + busy: TimeRange[]; + eventLength: number; + beforeBufferTime: number; + afterBufferTime: number; +}; + +export const getFilteredTimes = (props: getFilteredTimesProps) => { + const { times, busy, eventLength, beforeBufferTime, afterBufferTime } = props; + const finalizationTime = times[times.length - 1].add(eventLength, "minutes"); + // Check for conflicts + for (let i = times.length - 1; i >= 0; i -= 1) { + // const totalSlotLength = eventLength + beforeBufferTime + afterBufferTime; + // Check if the slot surpasses the user's availability end time + const slotEndTimeWithAfterBuffer = times[i].add(eventLength + afterBufferTime, "minutes"); + if (slotEndTimeWithAfterBuffer.isAfter(finalizationTime, "minute")) { + times.splice(i, 1); + } else { + const slotStartTime = times[i]; + const slotEndTime = times[i].add(eventLength, "minutes"); + const slotStartTimeWithBeforeBuffer = times[i].subtract(beforeBufferTime, "minutes"); + busy.every((busyTime): boolean => { + const startTime = dayjs(busyTime.start); + const endTime = dayjs(busyTime.end); + // Check if start times are the same + if (slotStartTime.isBetween(startTime, endTime, null, "[)")) { + times.splice(i, 1); + } + // Check if slot end time is between start and end time + else if (slotEndTime.isBetween(startTime, endTime)) { + times.splice(i, 1); + } + // Check if startTime is between slot + else if (startTime.isBetween(slotStartTime, slotEndTime)) { + times.splice(i, 1); + } + // Check if timeslot has before buffer time space free + else if ( + slotStartTimeWithBeforeBuffer.isBetween( + startTime.subtract(beforeBufferTime, "minutes"), + endTime.add(afterBufferTime, "minutes") + ) + ) { + times.splice(i, 1); + } + // Check if timeslot has after buffer time space free + else if ( + slotEndTimeWithAfterBuffer.isBetween( + startTime.subtract(beforeBufferTime, "minutes"), + endTime.add(afterBufferTime, "minutes") + ) + ) { + times.splice(i, 1); + } else { + return true; + } + return false; + }); + } + } + return times; +}; + export const useSlots = (props: UseSlotsProps) => { const { slotInterval, @@ -118,56 +182,19 @@ export const useSlots = (props: UseSlotsProps) => { inviteeDate: date, workingHours: responseBody.workingHours, minimumBookingNotice, + eventLength, }); - // Check for conflicts - for (let i = times.length - 1; i >= 0; i -= 1) { - responseBody.busy.every((busyTime): boolean => { - const startTime = dayjs(busyTime.start); - const endTime = dayjs(busyTime.end); - // Check if start times are the same - if (times[i].isBetween(startTime, endTime, null, "[)")) { - times.splice(i, 1); - } - // Check if slot end time is between start and end time - else if (times[i].add(eventLength, "minutes").isBetween(startTime, endTime)) { - times.splice(i, 1); - } - // Check if startTime is between slot - else if (startTime.isBetween(times[i], times[i].add(eventLength, "minutes"))) { - times.splice(i, 1); - } - // Check if time is between afterBufferTime and beforeBufferTime - else if ( - times[i].isBetween( - startTime.subtract(beforeBufferTime, "minutes"), - endTime.add(afterBufferTime, "minutes") - ) - ) { - times.splice(i, 1); - } - // considering preceding event's after buffer time - else if ( - i > 0 && - times[i - 1] - .add(eventLength + afterBufferTime, "minutes") - .isBetween( - startTime.subtract(beforeBufferTime, "minutes"), - endTime.add(afterBufferTime, "minutes"), - null, - "[)" - ) - ) { - times.splice(i, 1); - } else { - return true; - } - return false; - }); - } - + const filterTimeProps = { + times, + busy: responseBody.busy, + eventLength, + beforeBufferTime, + afterBufferTime, + }; + const filteredTimes = getFilteredTimes(filterTimeProps); // temporary const user = res.url.substring(res.url.lastIndexOf("/") + 1, res.url.indexOf("?")); - return times.map((time) => ({ + return filteredTimes.map((time) => ({ time, users: [user], })); diff --git a/apps/web/lib/slots.ts b/apps/web/lib/slots.ts index 8f2c132f..2ed82d76 100644 --- a/apps/web/lib/slots.ts +++ b/apps/web/lib/slots.ts @@ -15,26 +15,34 @@ export type GetSlots = { frequency: number; workingHours: WorkingHours[]; minimumBookingNotice: number; + eventLength: number; }; export type WorkingHoursTimeFrame = { startTime: number; endTime: number }; const splitAvailableTime = ( startTimeMinutes: number, endTimeMinutes: number, - frequency: number + frequency: number, + eventLength: number ): Array => { let initialTime = startTimeMinutes; const finalizationTime = endTimeMinutes; const result = [] as Array; while (initialTime < finalizationTime) { const periodTime = initialTime + frequency; - result.push({ startTime: initialTime, endTime: periodTime }); + const slotEndTime = initialTime + eventLength; + /* + check if the slot end time surpasses availability end time of the user + 1 minute is added to round up the hour mark so that end of the slot is considered in the check instead of x9 + eg: if finalization time is 11:59, slotEndTime is 12:00, we ideally want the slot to be available + */ + if (slotEndTime <= finalizationTime + 1) result.push({ startTime: initialTime, endTime: periodTime }); initialTime += frequency; } return result; }; -const getSlots = ({ inviteeDate, frequency, minimumBookingNotice, workingHours }: GetSlots) => { +const getSlots = ({ inviteeDate, frequency, minimumBookingNotice, workingHours, eventLength }: GetSlots) => { // current date in invitee tz const startDate = dayjs().add(minimumBookingNotice, "minute"); const startOfDay = dayjs.utc().startOf("day"); @@ -59,7 +67,7 @@ const getSlots = ({ inviteeDate, frequency, minimumBookingNotice, workingHours } // Here we split working hour in chunks for every frequency available that can fit in whole working hour localWorkingHours.forEach((item, index) => { - slotsTimeFrameAvailable.push(...splitAvailableTime(item.startTime, item.endTime, frequency)); + slotsTimeFrameAvailable.push(...splitAvailableTime(item.startTime, item.endTime, frequency, eventLength)); }); slotsTimeFrameAvailable.forEach((item) => { diff --git a/apps/web/test/lib/slots.test.ts b/apps/web/test/lib/slots.test.ts index 4377f95d..b0a34720 100644 --- a/apps/web/test/lib/slots.test.ts +++ b/apps/web/test/lib/slots.test.ts @@ -5,6 +5,7 @@ import utc from "dayjs/plugin/utc"; import MockDate from "mockdate"; import { MINUTES_DAY_END, MINUTES_DAY_START } from "@lib/availability"; +import { getFilteredTimes } from "@lib/hooks/useSlots"; import getSlots from "@lib/slots"; dayjs.extend(utc); @@ -26,6 +27,7 @@ it("can fit 24 hourly slots for an empty day", async () => { endTime: MINUTES_DAY_END, }, ], + eventLength: 60, }) ).toHaveLength(24); }); @@ -45,6 +47,7 @@ it("only shows future booking slots on the same day", async () => { endTime: MINUTES_DAY_END, }, ], + eventLength: 60, }) ).toHaveLength(12); }); @@ -62,6 +65,7 @@ it("can cut off dates that due to invitee timezone differences fall on the next endTime: MINUTES_DAY_END, }, ], + eventLength: 60, }) ).toHaveLength(0); }); @@ -80,6 +84,7 @@ it("can cut off dates that due to invitee timezone differences fall on the previ frequency: 60, minimumBookingNotice: 0, workingHours, + eventLength: 60, }) ).toHaveLength(0); }); @@ -98,6 +103,65 @@ it("adds minimum booking notice correctly", async () => { endTime: MINUTES_DAY_END, }, ], + eventLength: 60, }) ).toHaveLength(11); }); + +it("adds buffer time", async () => { + expect( + getFilteredTimes({ + times: getSlots({ + inviteeDate: dayjs.utc().add(1, "day"), + frequency: 60, + minimumBookingNotice: 0, + workingHours: [ + { + days: Array.from(Array(7).keys()), + startTime: MINUTES_DAY_START, + endTime: MINUTES_DAY_END, + }, + ], + eventLength: 60, + }), + busy: [ + { + start: dayjs.utc("2021-06-21 12:50:00", "YYYY-MM-DD HH:mm:ss").toDate(), + end: dayjs.utc("2021-06-21 13:50:00", "YYYY-MM-DD HH:mm:ss").toDate(), + }, + ], + eventLength: 60, + beforeBufferTime: 15, + afterBufferTime: 15, + }) + ).toHaveLength(20); +}); + +it("adds buffer time with custom slot interval", async () => { + expect( + getFilteredTimes({ + times: getSlots({ + inviteeDate: dayjs.utc().add(1, "day"), + frequency: 5, + minimumBookingNotice: 0, + workingHours: [ + { + days: Array.from(Array(7).keys()), + startTime: MINUTES_DAY_START, + endTime: MINUTES_DAY_END, + }, + ], + eventLength: 60, + }), + busy: [ + { + start: dayjs.utc("2021-06-21 12:50:00", "YYYY-MM-DD HH:mm:ss").toDate(), + end: dayjs.utc("2021-06-21 13:50:00", "YYYY-MM-DD HH:mm:ss").toDate(), + }, + ], + eventLength: 60, + beforeBufferTime: 15, + afterBufferTime: 15, + }) + ).toHaveLength(239); +});