223 lines
7.9 KiB
TypeScript
223 lines
7.9 KiB
TypeScript
import dayjs, { Dayjs } from "@calcom/dayjs";
|
|
import { WorkingHours, TimeRange as DateOverride } from "@calcom/types/schedule";
|
|
|
|
import { getWorkingHours } from "./availability";
|
|
|
|
export type GetSlots = {
|
|
inviteeDate: Dayjs;
|
|
frequency: number;
|
|
workingHours: WorkingHours[];
|
|
dateOverrides?: DateOverride[];
|
|
minimumBookingNotice: number;
|
|
eventLength: number;
|
|
};
|
|
export type TimeFrame = { userIds?: number[]; startTime: number; endTime: number };
|
|
|
|
const minimumOfOne = (input: number) => (input < 1 ? 1 : input);
|
|
|
|
/**
|
|
* TODO: What does this function do?
|
|
* Why is it needed?
|
|
*/
|
|
const splitAvailableTime = (
|
|
startTimeMinutes: number,
|
|
endTimeMinutes: number,
|
|
frequency: number,
|
|
eventLength: number
|
|
): TimeFrame[] => {
|
|
let initialTime = startTimeMinutes;
|
|
const finalizationTime = endTimeMinutes;
|
|
const result = [] as TimeFrame[];
|
|
|
|
// Ensure that both the frequency and event length are at least 1 minute, if they
|
|
// would be zero, we would have an infinite loop in this while!
|
|
const frequencyMinimumOne = minimumOfOne(frequency);
|
|
const eventLengthMinimumOne = minimumOfOne(eventLength);
|
|
|
|
while (initialTime < finalizationTime) {
|
|
const periodTime = initialTime + frequencyMinimumOne;
|
|
const slotEndTime = initialTime + eventLengthMinimumOne;
|
|
/*
|
|
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 });
|
|
// Ensure that both the frequency and event length are at least 1 minute, if they
|
|
// would be zero, we would have an infinite loop in this while!
|
|
initialTime += frequencyMinimumOne;
|
|
}
|
|
return result;
|
|
};
|
|
|
|
function buildSlots({
|
|
startOfInviteeDay,
|
|
computedLocalAvailability,
|
|
frequency,
|
|
eventLength,
|
|
startDate,
|
|
}: {
|
|
computedLocalAvailability: TimeFrame[];
|
|
startOfInviteeDay: Dayjs;
|
|
startDate: Dayjs;
|
|
frequency: number;
|
|
eventLength: number;
|
|
}) {
|
|
const slotsTimeFrameAvailable: TimeFrame[] = [];
|
|
|
|
computedLocalAvailability.forEach((item) => {
|
|
const userSlotsTimeFrameAvailable = splitAvailableTime(
|
|
item.startTime,
|
|
item.endTime,
|
|
frequency,
|
|
eventLength
|
|
).map((slot) => ({ ...slot, userIds: item.userIds }));
|
|
|
|
slotsTimeFrameAvailable.push(...userSlotsTimeFrameAvailable);
|
|
});
|
|
|
|
const slots: { [x: string]: { time: Dayjs; userIds?: number[] } } = {};
|
|
slotsTimeFrameAvailable.forEach((item) => {
|
|
// XXX: Hack alert, as dayjs is supposedly not aware of timezone the current slot may have invalid UTC offset.
|
|
const timeZone =
|
|
(startOfInviteeDay as unknown as { $x: { $timezone: string } })["$x"]["$timezone"] || "UTC";
|
|
/*
|
|
* @calcom/web:dev: 2022-11-06T00:00:00-04:00
|
|
* @calcom/web:dev: 2022-11-06T01:00:00-04:00
|
|
* @calcom/web:dev: 2022-11-06T01:00:00-04:00 <-- note there is no offset change, but we did lose an hour.
|
|
* @calcom/web:dev: 2022-11-06T02:00:00-04:00
|
|
* @calcom/web:dev: 2022-11-06T03:00:00-04:00
|
|
* ...
|
|
*/
|
|
const slot = {
|
|
userIds: item.userIds,
|
|
time: dayjs.tz(startOfInviteeDay.add(item.startTime, "minute").format("YYYY-MM-DDTHH:mm:ss"), timeZone),
|
|
};
|
|
// If the startOfInviteeDay has a different UTC offset than the slot, a DST change has occurred.
|
|
// As the time has now fallen backwards, or forwards; this difference -
|
|
// needs to be manually added as this is not done for us. Usually 0.
|
|
slot.time = slot.time.add(startOfInviteeDay.utcOffset() - slot.time.utcOffset(), "minutes");
|
|
|
|
if (slots[slot.time.format()]) {
|
|
slots[slot.time.format()] = {
|
|
...slot,
|
|
userIds: [...(slots[slot.time.format()].userIds || []), ...(item.userIds || [])],
|
|
};
|
|
return;
|
|
}
|
|
// Validating slot its not on the past
|
|
if (slot.time.isBefore(startDate)) {
|
|
return;
|
|
}
|
|
slots[slot.time.format()] = slot;
|
|
});
|
|
|
|
return Object.values(slots);
|
|
}
|
|
|
|
const getSlots = ({
|
|
inviteeDate,
|
|
frequency,
|
|
minimumBookingNotice,
|
|
workingHours,
|
|
dateOverrides = [],
|
|
eventLength,
|
|
}: GetSlots) => {
|
|
// current date in invitee tz
|
|
const startDate = dayjs().add(minimumBookingNotice, "minute");
|
|
// This code is ran client side, startOf() does some conversions based on the
|
|
// local tz of the client. Sometimes this shifts the day incorrectly.
|
|
const startOfDayUTC = dayjs.utc().set("hour", 0).set("minute", 0).set("second", 0);
|
|
const startOfInviteeDay = inviteeDate.startOf("day");
|
|
// checks if the start date is in the past
|
|
|
|
/**
|
|
* TODO: change "day" for "hour" to stop displaying 1 day before today
|
|
* This is displaying a day as available as sometimes difference between two dates is < 24 hrs.
|
|
* But when doing timezones an available day for an owner can be 2 days available in other users tz.
|
|
*
|
|
* */
|
|
if (inviteeDate.isBefore(startDate, "day")) {
|
|
return [];
|
|
}
|
|
|
|
// Dayjs does not expose the timeZone value publicly through .get("timeZone")
|
|
// instead, we as devs are required to somewhat hack our way to get the ...
|
|
// tz value as string
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const timeZone: string = (inviteeDate as any)["$x"]["$timezone"];
|
|
|
|
const workingHoursUTC = workingHours.map((schedule) => ({
|
|
userId: schedule.userId,
|
|
days: schedule.days,
|
|
startTime: /* Why? */ startOfDayUTC.add(schedule.startTime, "minute"),
|
|
endTime: /* Why? */ startOfDayUTC.add(schedule.endTime, "minute"),
|
|
}));
|
|
|
|
const localWorkingHours = getWorkingHours(
|
|
{
|
|
// initialize current day with timeZone without conversion, just parse.
|
|
utcOffset: -dayjs.tz(dayjs(), timeZone).utcOffset(),
|
|
},
|
|
workingHoursUTC
|
|
).filter((hours) => hours.days.includes(inviteeDate.day()));
|
|
|
|
// Here we split working hour in chunks for every frequency available that can fit in whole working hours
|
|
const computedLocalAvailability: TimeFrame[] = [];
|
|
let tempComputeTimeFrame: TimeFrame | undefined;
|
|
const computeLength = localWorkingHours.length - 1;
|
|
const makeTimeFrame = (item: typeof localWorkingHours[0]): TimeFrame => ({
|
|
userIds: item.userId ? [item.userId] : [],
|
|
startTime: item.startTime,
|
|
endTime: item.endTime,
|
|
});
|
|
|
|
localWorkingHours.forEach((item, index) => {
|
|
if (!tempComputeTimeFrame) {
|
|
tempComputeTimeFrame = makeTimeFrame(item);
|
|
} else {
|
|
// please check the comment in splitAvailableTime func for the added 1 minute
|
|
if (tempComputeTimeFrame.endTime + 1 === item.startTime) {
|
|
// to deal with time that across the day, e.g. from 11:59 to to 12:01
|
|
tempComputeTimeFrame.endTime = item.endTime;
|
|
} else {
|
|
computedLocalAvailability.push(tempComputeTimeFrame);
|
|
tempComputeTimeFrame = makeTimeFrame(item);
|
|
}
|
|
}
|
|
if (index == computeLength) {
|
|
computedLocalAvailability.push(tempComputeTimeFrame);
|
|
}
|
|
});
|
|
// an override precedes all the local working hour availability logic.
|
|
const activeOverrides = dateOverrides.filter((override) => {
|
|
return dayjs.utc(override.start).isBetween(startOfInviteeDay, startOfInviteeDay.endOf("day"), null, "[)");
|
|
});
|
|
|
|
if (!!activeOverrides.length) {
|
|
const overrides = activeOverrides.flatMap((override) => ({
|
|
userIds: override.userId ? [override.userId] : [],
|
|
startTime: override.start.getUTCHours() * 60 + override.start.getUTCMinutes(),
|
|
endTime: override.end.getUTCHours() * 60 + override.end.getUTCMinutes(),
|
|
}));
|
|
overrides.forEach((override) => {
|
|
const index = computedLocalAvailability.findIndex(
|
|
(a) => !a.userIds?.length || (override.userIds[0] && a.userIds?.includes(override.userIds[0]))
|
|
);
|
|
if (index >= 0) {
|
|
computedLocalAvailability[index] = override;
|
|
}
|
|
});
|
|
}
|
|
|
|
return buildSlots({
|
|
computedLocalAvailability,
|
|
startOfInviteeDay,
|
|
startDate,
|
|
frequency,
|
|
eventLength,
|
|
});
|
|
};
|
|
|
|
export default getSlots;
|