Fix for buffer not considering custom interval slots and event duration for slots when using custom intervals (#2079)

* modified buffer checks

* added custom interval consideration in getSlots fn

* further getslot call fixes

* added check for end of day availability slots

* removed debug remnants

* moved slot filtering into a function

* improved readability of code

* improved readability

* extracted getFilteredTimes outside useSlot

* added a buffer test

* added another buffer test

* edge case fix for eod availability and test fix

* removed unnecessary comments

* verbose comment

* fixed eod logic and updated expected test value

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Syed Ali Shahbaz 2022-03-12 12:22:27 +05:30 committed by GitHub
parent ada3317ba5
commit 788e2acaff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 152 additions and 51 deletions

View file

@ -139,6 +139,7 @@ function DatePicker({
frequency: eventLength, frequency: eventLength,
minimumBookingNotice, minimumBookingNotice,
workingHours, workingHours,
eventLength,
}).length }).length
); );
}; };

View file

@ -44,6 +44,7 @@ export default function TeamAvailabilityTimes(props: Props) {
inviteeDate: props.selectedDate, inviteeDate: props.selectedDate,
workingHours: data?.workingHours || [], workingHours: data?.workingHours || [],
minimumBookingNotice: 0, minimumBookingNotice: 0,
eventLength: props.frequency,
}) })
: []; : [];

View file

@ -34,6 +34,70 @@ type UseSlotsProps = {
afterBufferTime?: number; 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) => { export const useSlots = (props: UseSlotsProps) => {
const { const {
slotInterval, slotInterval,
@ -118,56 +182,19 @@ export const useSlots = (props: UseSlotsProps) => {
inviteeDate: date, inviteeDate: date,
workingHours: responseBody.workingHours, workingHours: responseBody.workingHours,
minimumBookingNotice, minimumBookingNotice,
eventLength,
}); });
// Check for conflicts const filterTimeProps = {
for (let i = times.length - 1; i >= 0; i -= 1) { times,
responseBody.busy.every((busyTime): boolean => { busy: responseBody.busy,
const startTime = dayjs(busyTime.start); eventLength,
const endTime = dayjs(busyTime.end); beforeBufferTime,
// Check if start times are the same afterBufferTime,
if (times[i].isBetween(startTime, endTime, null, "[)")) { };
times.splice(i, 1); const filteredTimes = getFilteredTimes(filterTimeProps);
}
// 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;
});
}
// temporary // temporary
const user = res.url.substring(res.url.lastIndexOf("/") + 1, res.url.indexOf("?")); const user = res.url.substring(res.url.lastIndexOf("/") + 1, res.url.indexOf("?"));
return times.map((time) => ({ return filteredTimes.map((time) => ({
time, time,
users: [user], users: [user],
})); }));

View file

@ -15,26 +15,34 @@ export type GetSlots = {
frequency: number; frequency: number;
workingHours: WorkingHours[]; workingHours: WorkingHours[];
minimumBookingNotice: number; minimumBookingNotice: number;
eventLength: number;
}; };
export type WorkingHoursTimeFrame = { startTime: number; endTime: number }; export type WorkingHoursTimeFrame = { startTime: number; endTime: number };
const splitAvailableTime = ( const splitAvailableTime = (
startTimeMinutes: number, startTimeMinutes: number,
endTimeMinutes: number, endTimeMinutes: number,
frequency: number frequency: number,
eventLength: number
): Array<WorkingHoursTimeFrame> => { ): Array<WorkingHoursTimeFrame> => {
let initialTime = startTimeMinutes; let initialTime = startTimeMinutes;
const finalizationTime = endTimeMinutes; const finalizationTime = endTimeMinutes;
const result = [] as Array<WorkingHoursTimeFrame>; const result = [] as Array<WorkingHoursTimeFrame>;
while (initialTime < finalizationTime) { while (initialTime < finalizationTime) {
const periodTime = initialTime + frequency; 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; initialTime += frequency;
} }
return result; return result;
}; };
const getSlots = ({ inviteeDate, frequency, minimumBookingNotice, workingHours }: GetSlots) => { const getSlots = ({ inviteeDate, frequency, minimumBookingNotice, workingHours, eventLength }: GetSlots) => {
// current date in invitee tz // current date in invitee tz
const startDate = dayjs().add(minimumBookingNotice, "minute"); const startDate = dayjs().add(minimumBookingNotice, "minute");
const startOfDay = dayjs.utc().startOf("day"); 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 // Here we split working hour in chunks for every frequency available that can fit in whole working hour
localWorkingHours.forEach((item, index) => { localWorkingHours.forEach((item, index) => {
slotsTimeFrameAvailable.push(...splitAvailableTime(item.startTime, item.endTime, frequency)); slotsTimeFrameAvailable.push(...splitAvailableTime(item.startTime, item.endTime, frequency, eventLength));
}); });
slotsTimeFrameAvailable.forEach((item) => { slotsTimeFrameAvailable.forEach((item) => {

View file

@ -5,6 +5,7 @@ import utc from "dayjs/plugin/utc";
import MockDate from "mockdate"; import MockDate from "mockdate";
import { MINUTES_DAY_END, MINUTES_DAY_START } from "@lib/availability"; import { MINUTES_DAY_END, MINUTES_DAY_START } from "@lib/availability";
import { getFilteredTimes } from "@lib/hooks/useSlots";
import getSlots from "@lib/slots"; import getSlots from "@lib/slots";
dayjs.extend(utc); dayjs.extend(utc);
@ -26,6 +27,7 @@ it("can fit 24 hourly slots for an empty day", async () => {
endTime: MINUTES_DAY_END, endTime: MINUTES_DAY_END,
}, },
], ],
eventLength: 60,
}) })
).toHaveLength(24); ).toHaveLength(24);
}); });
@ -45,6 +47,7 @@ it("only shows future booking slots on the same day", async () => {
endTime: MINUTES_DAY_END, endTime: MINUTES_DAY_END,
}, },
], ],
eventLength: 60,
}) })
).toHaveLength(12); ).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, endTime: MINUTES_DAY_END,
}, },
], ],
eventLength: 60,
}) })
).toHaveLength(0); ).toHaveLength(0);
}); });
@ -80,6 +84,7 @@ it("can cut off dates that due to invitee timezone differences fall on the previ
frequency: 60, frequency: 60,
minimumBookingNotice: 0, minimumBookingNotice: 0,
workingHours, workingHours,
eventLength: 60,
}) })
).toHaveLength(0); ).toHaveLength(0);
}); });
@ -98,6 +103,65 @@ it("adds minimum booking notice correctly", async () => {
endTime: MINUTES_DAY_END, endTime: MINUTES_DAY_END,
}, },
], ],
eventLength: 60,
}) })
).toHaveLength(11); ).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);
});