Files
calendar/packages/lib/slots.ts
T
Keith WilliamsandGitHub 7180eb067a refactor: Move @calcom/core to @calcom/lib (#19655)
* refactor: Move @calcom/core to @calcom/lib

* Merging imports

* Clean up

* Ignoreing type error for now
2025-03-02 23:02:35 -03:00

172 lines
5.1 KiB
TypeScript

import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import type { IFromUser, IOutOfOfficeData, IToUser } from "@calcom/lib/getUserAvailability";
import { getTimeZone } from "./date-fns";
import type { DateRange } from "./date-ranges";
export type GetSlots = {
inviteeDate: Dayjs;
frequency: number;
dateRanges: DateRange[];
minimumBookingNotice: number;
eventLength: number;
offsetStart?: number;
datesOutOfOffice?: IOutOfOfficeData;
};
export type TimeFrame = { userIds?: number[]; startTime: number; endTime: number };
const minimumOfOne = (input: number) => (input < 1 ? 1 : input);
function buildSlotsWithDateRanges({
dateRanges,
frequency,
eventLength,
timeZone,
minimumBookingNotice,
offsetStart,
datesOutOfOffice,
}: {
dateRanges: DateRange[];
frequency: number;
eventLength: number;
timeZone: string;
minimumBookingNotice: number;
offsetStart?: number;
datesOutOfOffice?: IOutOfOfficeData;
}) {
// keep the old safeguards in; may be needed.
frequency = minimumOfOne(frequency);
eventLength = minimumOfOne(eventLength);
offsetStart = offsetStart ? minimumOfOne(offsetStart) : 0;
// there can only ever be one slot at a given start time, and based on duration also only a single length.
const slots = new Map<
string,
{
time: Dayjs;
userIds?: number[];
away?: boolean;
fromUser?: IFromUser;
toUser?: IToUser;
reason?: string;
emoji?: string;
}
>();
let interval = Number(process.env.NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL) || 1;
const intervalsWithDefinedStartTimes = [60, 30, 20, 15, 10, 5];
for (let i = 0; i < intervalsWithDefinedStartTimes.length; i++) {
if (frequency % intervalsWithDefinedStartTimes[i] === 0) {
interval = intervalsWithDefinedStartTimes[i];
break;
}
}
const startTimeWithMinNotice = dayjs.utc().add(minimumBookingNotice, "minute");
const orderedDateRanges = dateRanges.sort((a, b) => a.start.valueOf() - b.start.valueOf());
orderedDateRanges.forEach((range) => {
const dateYYYYMMDD = range.start.format("YYYY-MM-DD");
let slotStartTime = range.start.utc().isAfter(startTimeWithMinNotice)
? range.start
: startTimeWithMinNotice;
slotStartTime =
slotStartTime.minute() % interval !== 0
? slotStartTime.startOf("hour").add(Math.ceil(slotStartTime.minute() / interval) * interval, "minute")
: slotStartTime;
slotStartTime = slotStartTime.add(offsetStart ?? 0, "minutes").tz(timeZone);
// if the slotStartTime is between an existing slot, we need to adjust to the begin of the existing slot
// but that adjusted startTime must be legal.
const iterator = slots.keys();
let result = iterator.next();
while (!result.done) {
const utcResultValue = dayjs.utc(result.value);
// if the slotStartTime is between an existing slot, we need to adjust to the begin of the existing slot
if (
utcResultValue.isBefore(slotStartTime) &&
utcResultValue.add(frequency + (offsetStart ?? 0), "minutes").isAfter(slotStartTime)
) {
// however, the slot can now be before the start of this date range.
if (!utcResultValue.isBefore(range.start)) {
// it is between, if possible floor down to the start of the existing slot
slotStartTime = utcResultValue;
} else {
// if not possible to floor, we need to ceil up to the next slot.
slotStartTime = utcResultValue.add(frequency + (offsetStart ?? 0), "minutes");
}
// and then convert to the correct timezone - UTC mode is just for performance.
slotStartTime = slotStartTime.tz(timeZone);
}
result = iterator.next();
}
while (!slotStartTime.add(eventLength, "minutes").subtract(1, "second").utc().isAfter(range.end)) {
const dateOutOfOfficeExists = datesOutOfOffice?.[dateYYYYMMDD];
let slotData: {
time: Dayjs;
userIds?: number[];
away?: boolean;
fromUser?: IFromUser;
toUser?: IToUser;
reason?: string;
emoji?: string;
} = {
time: slotStartTime,
};
if (dateOutOfOfficeExists) {
const { toUser, fromUser, reason, emoji } = dateOutOfOfficeExists;
slotData = {
time: slotStartTime,
away: true,
...(fromUser && { fromUser }),
...(toUser && { toUser }),
...(reason && { reason }),
...(emoji && { emoji }),
};
}
slots.set(slotData.time.toISOString(), slotData);
slotStartTime = slotStartTime.add(frequency + (offsetStart ?? 0), "minutes");
}
});
return Array.from(slots.values());
}
const getSlots = ({
inviteeDate,
frequency,
minimumBookingNotice,
dateRanges,
eventLength,
offsetStart = 0,
datesOutOfOffice,
}: GetSlots): {
time: Dayjs;
userIds?: number[];
away?: boolean;
fromUser?: IFromUser;
toUser?: IToUser;
reason?: string;
emoji?: string;
}[] => {
return buildSlotsWithDateRanges({
dateRanges,
frequency,
eventLength,
timeZone: getTimeZone(inviteeDate),
minimumBookingNotice,
offsetStart,
datesOutOfOffice,
});
};
export default getSlots;