2021-06-29 01:45:58 +00:00
|
|
|
import dayjs, { Dayjs } from "dayjs";
|
2021-06-24 22:15:18 +00:00
|
|
|
import utc from "dayjs/plugin/utc";
|
2021-06-29 01:45:58 +00:00
|
|
|
import timezone from "dayjs/plugin/timezone";
|
2021-04-17 00:08:35 +00:00
|
|
|
dayjs.extend(utc);
|
|
|
|
dayjs.extend(timezone);
|
|
|
|
|
2021-06-30 01:35:08 +00:00
|
|
|
type WorkingHour = {
|
|
|
|
days: number[];
|
|
|
|
startTime: number;
|
|
|
|
endTime: number;
|
2021-04-17 00:08:35 +00:00
|
|
|
};
|
|
|
|
|
2021-06-29 22:35:13 +00:00
|
|
|
type GetSlots = {
|
2021-06-24 22:15:18 +00:00
|
|
|
inviteeDate: Dayjs;
|
|
|
|
frequency: number;
|
2021-06-30 01:35:08 +00:00
|
|
|
workingHours: WorkingHour[];
|
2021-06-24 22:15:18 +00:00
|
|
|
minimumBookingNotice?: number;
|
2021-06-29 22:35:13 +00:00
|
|
|
organizerTimeZone: string;
|
|
|
|
};
|
2021-04-17 00:08:35 +00:00
|
|
|
|
2021-06-29 22:35:13 +00:00
|
|
|
type Boundary = {
|
2021-06-24 22:15:18 +00:00
|
|
|
lowerBound: number;
|
|
|
|
upperBound: number;
|
2021-06-29 22:35:13 +00:00
|
|
|
};
|
2021-04-17 00:08:35 +00:00
|
|
|
|
2021-06-29 22:35:13 +00:00
|
|
|
const freqApply = (cb, value: number, frequency: number): number => cb(value / frequency) * frequency;
|
2021-04-17 00:08:35 +00:00
|
|
|
|
2021-06-24 22:15:18 +00:00
|
|
|
const intersectBoundary = (a: Boundary, b: Boundary) => {
|
2021-06-29 01:45:58 +00:00
|
|
|
if (a.upperBound < b.lowerBound || a.lowerBound > b.upperBound) {
|
2021-06-24 22:15:18 +00:00
|
|
|
return;
|
2021-04-17 00:08:35 +00:00
|
|
|
}
|
2021-06-24 22:15:18 +00:00
|
|
|
return {
|
|
|
|
lowerBound: Math.max(b.lowerBound, a.lowerBound),
|
2021-06-29 01:45:58 +00:00
|
|
|
upperBound: Math.min(b.upperBound, a.upperBound),
|
2021-06-24 22:15:18 +00:00
|
|
|
};
|
2021-06-29 01:45:58 +00:00
|
|
|
};
|
2021-04-17 00:08:35 +00:00
|
|
|
|
2021-06-24 22:15:18 +00:00
|
|
|
// say invitee is -60,1380, and boundary is -120,240 - the overlap is -60,240
|
2021-06-29 01:45:58 +00:00
|
|
|
const getOverlaps = (inviteeBoundary: Boundary, boundaries: Boundary[]) =>
|
|
|
|
boundaries.map((boundary) => intersectBoundary(inviteeBoundary, boundary)).filter(Boolean);
|
2021-04-17 00:08:35 +00:00
|
|
|
|
2021-06-29 01:45:58 +00:00
|
|
|
const organizerBoundaries = (
|
|
|
|
workingHours: [],
|
|
|
|
inviteeDate: Dayjs,
|
|
|
|
inviteeBounds: Boundary,
|
|
|
|
organizerTimeZone
|
|
|
|
): Boundary[] => {
|
2021-06-24 22:15:18 +00:00
|
|
|
const boundaries: Boundary[] = [];
|
|
|
|
|
2021-06-29 01:45:58 +00:00
|
|
|
const startDay: number = +inviteeDate
|
|
|
|
.utc()
|
|
|
|
.startOf("day")
|
|
|
|
.add(inviteeBounds.lowerBound, "minutes")
|
|
|
|
.format("d");
|
|
|
|
const endDay: number = +inviteeDate
|
|
|
|
.utc()
|
|
|
|
.startOf("day")
|
|
|
|
.add(inviteeBounds.upperBound, "minutes")
|
|
|
|
.format("d");
|
2021-06-24 22:15:18 +00:00
|
|
|
|
2021-06-29 01:45:58 +00:00
|
|
|
workingHours.forEach((item) => {
|
|
|
|
const lowerBound: number = item.startTime - dayjs().tz(organizerTimeZone).utcOffset();
|
|
|
|
const upperBound: number = item.endTime - dayjs().tz(organizerTimeZone).utcOffset();
|
2021-06-24 22:15:18 +00:00
|
|
|
if (startDay !== endDay) {
|
|
|
|
if (inviteeBounds.lowerBound < 0) {
|
|
|
|
// lowerBound edges into the previous day
|
|
|
|
if (item.days.includes(startDay)) {
|
|
|
|
boundaries.push({ lowerBound: lowerBound - 1440, upperBound: upperBound - 1440 });
|
|
|
|
}
|
|
|
|
if (item.days.includes(endDay)) {
|
|
|
|
boundaries.push({ lowerBound, upperBound });
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// upperBound edges into the next day
|
|
|
|
if (item.days.includes(endDay)) {
|
|
|
|
boundaries.push({ lowerBound: lowerBound + 1440, upperBound: upperBound + 1440 });
|
|
|
|
}
|
|
|
|
if (item.days.includes(startDay)) {
|
|
|
|
boundaries.push({ lowerBound, upperBound });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2021-07-08 21:14:29 +00:00
|
|
|
if (item.days.includes(startDay)) {
|
|
|
|
boundaries.push({ lowerBound, upperBound });
|
|
|
|
}
|
2021-04-17 00:08:35 +00:00
|
|
|
}
|
2021-06-24 22:15:18 +00:00
|
|
|
});
|
2021-07-08 21:14:29 +00:00
|
|
|
|
2021-06-24 22:15:18 +00:00
|
|
|
return boundaries;
|
|
|
|
};
|
2021-04-17 00:08:35 +00:00
|
|
|
|
2021-06-24 22:15:18 +00:00
|
|
|
const inviteeBoundary = (startTime: number, utcOffset: number, frequency: number): Boundary => {
|
|
|
|
const upperBound: number = freqApply(Math.floor, 1440 - utcOffset, frequency);
|
|
|
|
const lowerBound: number = freqApply(Math.ceil, startTime - utcOffset, frequency);
|
|
|
|
return {
|
2021-06-29 01:45:58 +00:00
|
|
|
lowerBound,
|
|
|
|
upperBound,
|
2021-06-24 22:15:18 +00:00
|
|
|
};
|
|
|
|
};
|
2021-04-17 00:08:35 +00:00
|
|
|
|
2021-06-29 01:45:58 +00:00
|
|
|
const getSlotsBetweenBoundary = (frequency: number, { lowerBound, upperBound }: Boundary) => {
|
2021-06-24 22:15:18 +00:00
|
|
|
const slots: Dayjs[] = [];
|
2021-06-29 01:45:58 +00:00
|
|
|
for (let minutes = 0; lowerBound + minutes <= upperBound - frequency; minutes += frequency) {
|
|
|
|
slots.push(
|
|
|
|
<Dayjs>dayjs
|
|
|
|
.utc()
|
|
|
|
.startOf("day")
|
|
|
|
.add(lowerBound + minutes, "minutes")
|
|
|
|
);
|
2021-04-17 00:08:35 +00:00
|
|
|
}
|
|
|
|
return slots;
|
|
|
|
};
|
|
|
|
|
2021-06-29 01:45:58 +00:00
|
|
|
const getSlots = ({
|
|
|
|
inviteeDate,
|
|
|
|
frequency,
|
|
|
|
minimumBookingNotice,
|
|
|
|
workingHours,
|
|
|
|
organizerTimeZone,
|
2021-06-29 22:35:13 +00:00
|
|
|
}: GetSlots): Dayjs[] => {
|
2021-07-22 22:52:27 +00:00
|
|
|
// current date in invitee tz
|
|
|
|
const currentDate = dayjs().utcOffset(inviteeDate.utcOffset());
|
|
|
|
const startDate = currentDate.add(minimumBookingNotice, "minutes"); // + minimum notice period
|
|
|
|
// when the invitee date is not the same as the current date, reset the date to the start of day
|
|
|
|
if (inviteeDate.date() !== currentDate.date()) {
|
|
|
|
inviteeDate = inviteeDate.startOf("day");
|
|
|
|
}
|
|
|
|
|
2021-07-23 20:19:23 +00:00
|
|
|
const startTime = startDate.isAfter(inviteeDate)
|
|
|
|
? // block out everything when inviteeDate is less than startDate
|
|
|
|
startDate.date() > inviteeDate.date()
|
|
|
|
? 1440
|
|
|
|
: startDate.hour() * 60 + startDate.minute()
|
|
|
|
: 0;
|
2021-06-24 22:15:18 +00:00
|
|
|
const inviteeBounds = inviteeBoundary(startTime, inviteeDate.utcOffset(), frequency);
|
2021-06-29 22:35:13 +00:00
|
|
|
|
2021-06-27 22:30:11 +00:00
|
|
|
return getOverlaps(
|
2021-06-29 01:45:58 +00:00
|
|
|
inviteeBounds,
|
|
|
|
organizerBoundaries(workingHours, inviteeDate, inviteeBounds, organizerTimeZone)
|
2021-06-27 22:30:11 +00:00
|
|
|
)
|
2021-06-29 01:45:58 +00:00
|
|
|
.reduce((slots, boundary: Boundary) => [...slots, ...getSlotsBetweenBoundary(frequency, boundary)], [])
|
2021-06-30 15:27:49 +00:00
|
|
|
.map((slot) =>
|
2021-07-11 16:05:49 +00:00
|
|
|
slot.utcOffset(inviteeDate.utcOffset()).month(inviteeDate.month()).date(inviteeDate.date())
|
2021-06-30 15:27:49 +00:00
|
|
|
);
|
2021-06-29 01:45:58 +00:00
|
|
|
};
|
2021-04-17 00:08:35 +00:00
|
|
|
|
2021-06-24 22:15:18 +00:00
|
|
|
export default getSlots;
|