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:
parent
ada3317ba5
commit
788e2acaff
5 changed files with 152 additions and 51 deletions
|
@ -139,6 +139,7 @@ function DatePicker({
|
||||||
frequency: eventLength,
|
frequency: eventLength,
|
||||||
minimumBookingNotice,
|
minimumBookingNotice,
|
||||||
workingHours,
|
workingHours,
|
||||||
|
eventLength,
|
||||||
}).length
|
}).length
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
|
|
@ -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],
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in a new issue