2023-07-05 16:47:41 +00:00
|
|
|
import type { Dayjs } from "@calcom/dayjs";
|
|
|
|
import dayjs from "@calcom/dayjs";
|
|
|
|
import type { Availability } from "@calcom/prisma/client";
|
|
|
|
|
|
|
|
export type DateRange = {
|
|
|
|
start: Dayjs;
|
|
|
|
end: Dayjs;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type DateOverride = Pick<Availability, "date" | "startTime" | "endTime">;
|
|
|
|
export type WorkingHours = Pick<Availability, "days" | "startTime" | "endTime">;
|
|
|
|
|
|
|
|
export function processWorkingHours({
|
|
|
|
item,
|
|
|
|
timeZone,
|
|
|
|
dateFrom,
|
|
|
|
dateTo,
|
|
|
|
}: {
|
|
|
|
item: WorkingHours;
|
|
|
|
timeZone: string;
|
|
|
|
dateFrom: Dayjs;
|
|
|
|
dateTo: Dayjs;
|
|
|
|
}) {
|
|
|
|
const results = [];
|
|
|
|
for (let date = dateFrom.tz(timeZone).startOf("day"); dateTo.isAfter(date); date = date.add(1, "day")) {
|
2023-10-11 18:30:35 +00:00
|
|
|
const fromOffset = dateFrom.tz(timeZone).utcOffset();
|
|
|
|
const offset = date.tz(timeZone).utcOffset();
|
2023-08-09 20:31:48 +00:00
|
|
|
|
2023-10-11 18:30:35 +00:00
|
|
|
// it always has to be start of the day (midnight) even when DST changes
|
|
|
|
const dateInTz = date.add(fromOffset - offset, "minutes").tz(timeZone);
|
2023-08-09 20:31:48 +00:00
|
|
|
if (!item.days.includes(dateInTz.day())) {
|
2023-07-05 16:47:41 +00:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2023-10-11 18:30:35 +00:00
|
|
|
let start = dateInTz
|
|
|
|
.add(item.startTime.getUTCHours(), "hours")
|
|
|
|
.add(item.startTime.getUTCMinutes(), "minutes");
|
2023-08-09 20:31:48 +00:00
|
|
|
|
2023-10-11 18:30:35 +00:00
|
|
|
let end = dateInTz.add(item.endTime.getUTCHours(), "hours").add(item.endTime.getUTCMinutes(), "minutes");
|
2023-08-09 20:31:48 +00:00
|
|
|
|
|
|
|
const offsetBeginningOfDay = dayjs(start.format("YYYY-MM-DD hh:mm")).tz(timeZone).utcOffset();
|
|
|
|
const offsetDiff = start.utcOffset() - offsetBeginningOfDay; // there will be 60 min offset on the day day of DST change
|
|
|
|
|
|
|
|
start = start.add(offsetDiff, "minute");
|
|
|
|
end = end.add(offsetDiff, "minute");
|
2023-07-05 16:47:41 +00:00
|
|
|
|
|
|
|
const startResult = dayjs.max(start, dateFrom.tz(timeZone));
|
|
|
|
const endResult = dayjs.min(end, dateTo.tz(timeZone));
|
|
|
|
|
|
|
|
if (startResult.isAfter(endResult)) {
|
|
|
|
// if an event ends before start, it's not a result.
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
results.push({
|
|
|
|
start: startResult,
|
|
|
|
end: endResult,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function processDateOverride({ item, timeZone }: { item: DateOverride; timeZone: string }) {
|
2023-08-14 08:38:08 +00:00
|
|
|
const startDate = dayjs
|
|
|
|
.utc(item.date)
|
|
|
|
.startOf("day")
|
|
|
|
.add(item.startTime.getUTCHours(), "hours")
|
|
|
|
.add(item.startTime.getUTCMinutes(), "minutes")
|
|
|
|
.second(0)
|
|
|
|
.tz(timeZone, true);
|
|
|
|
const endDate = dayjs
|
|
|
|
.utc(item.date)
|
|
|
|
.startOf("day")
|
|
|
|
.add(item.endTime.getUTCHours(), "hours")
|
|
|
|
.add(item.endTime.getUTCMinutes(), "minutes")
|
|
|
|
.second(0)
|
|
|
|
.tz(timeZone, true);
|
2023-07-05 16:47:41 +00:00
|
|
|
return {
|
2023-08-14 08:38:08 +00:00
|
|
|
start: startDate,
|
|
|
|
end: endDate,
|
2023-07-05 16:47:41 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function buildDateRanges({
|
|
|
|
availability,
|
|
|
|
timeZone /* Organizer timeZone */,
|
|
|
|
dateFrom /* Attendee dateFrom */,
|
|
|
|
dateTo /* `` dateTo */,
|
|
|
|
}: {
|
|
|
|
timeZone: string;
|
|
|
|
availability: (DateOverride | WorkingHours)[];
|
|
|
|
dateFrom: Dayjs;
|
|
|
|
dateTo: Dayjs;
|
|
|
|
}): DateRange[] {
|
|
|
|
const groupedWorkingHours = groupByDate(
|
|
|
|
availability.reduce((processed: DateRange[], item) => {
|
|
|
|
if ("days" in item) {
|
|
|
|
processed = processed.concat(processWorkingHours({ item, timeZone, dateFrom, dateTo }));
|
|
|
|
}
|
|
|
|
return processed;
|
|
|
|
}, [])
|
|
|
|
);
|
|
|
|
const groupedDateOverrides = groupByDate(
|
|
|
|
availability.reduce((processed: DateRange[], item) => {
|
|
|
|
if ("date" in item && !!item.date) {
|
|
|
|
processed.push(processDateOverride({ item, timeZone }));
|
|
|
|
}
|
|
|
|
return processed;
|
|
|
|
}, [])
|
|
|
|
);
|
|
|
|
|
|
|
|
const dateRanges = Object.values({
|
|
|
|
...groupedWorkingHours,
|
|
|
|
...groupedDateOverrides,
|
|
|
|
}).map(
|
|
|
|
// remove 0-length overrides that were kept to cancel out working dates until now.
|
2023-08-08 02:03:01 +00:00
|
|
|
(ranges) => ranges.filter((range) => range.start.valueOf() !== range.end.valueOf())
|
2023-07-05 16:47:41 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
return dateRanges.flat();
|
|
|
|
}
|
|
|
|
|
|
|
|
export function groupByDate(ranges: DateRange[]): { [x: string]: DateRange[] } {
|
|
|
|
const results = ranges.reduce(
|
|
|
|
(
|
|
|
|
previousValue: {
|
|
|
|
[date: string]: DateRange[];
|
|
|
|
},
|
|
|
|
currentValue
|
|
|
|
) => {
|
2023-07-19 18:44:41 +00:00
|
|
|
const dateString = dayjs(currentValue.start).format("YYYY-MM-DD");
|
2023-07-05 16:47:41 +00:00
|
|
|
|
|
|
|
previousValue[dateString] =
|
|
|
|
typeof previousValue[dateString] === "undefined"
|
|
|
|
? [currentValue]
|
|
|
|
: [...previousValue[dateString], currentValue];
|
|
|
|
return previousValue;
|
|
|
|
},
|
|
|
|
{}
|
|
|
|
);
|
|
|
|
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function intersect(ranges: DateRange[][]): DateRange[] {
|
|
|
|
if (!ranges.length) return [];
|
|
|
|
// Get the ranges of the first user
|
|
|
|
let commonAvailability = ranges[0];
|
|
|
|
|
|
|
|
// For each of the remaining users, find the intersection of their ranges with the current common availability
|
|
|
|
for (let i = 1; i < ranges.length; i++) {
|
|
|
|
const userRanges = ranges[i];
|
|
|
|
|
|
|
|
const intersectedRanges: {
|
|
|
|
start: Dayjs;
|
|
|
|
end: Dayjs;
|
|
|
|
}[] = [];
|
|
|
|
|
|
|
|
commonAvailability.forEach((commonRange) => {
|
|
|
|
userRanges.forEach((userRange) => {
|
|
|
|
const intersection = getIntersection(commonRange, userRange);
|
|
|
|
if (intersection !== null) {
|
|
|
|
// If the current common range intersects with the user range, add the intersected time range to the new array
|
|
|
|
intersectedRanges.push(intersection);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
commonAvailability = intersectedRanges;
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the common availability is empty, there is no time when all users are available
|
|
|
|
if (commonAvailability.length === 0) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
return commonAvailability;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getIntersection(range1: DateRange, range2: DateRange) {
|
2023-08-02 17:03:04 +00:00
|
|
|
const start = range1.start.utc().isAfter(range2.start) ? range1.start : range2.start;
|
|
|
|
const end = range1.end.utc().isBefore(range2.end) ? range1.end : range2.end;
|
|
|
|
if (start.utc().isBefore(end)) {
|
2023-07-05 16:47:41 +00:00
|
|
|
return { start, end };
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-07-11 15:11:08 +00:00
|
|
|
export function subtract(
|
|
|
|
sourceRanges: (DateRange & { [x: string]: unknown })[],
|
|
|
|
excludedRanges: DateRange[]
|
|
|
|
) {
|
2023-07-05 16:47:41 +00:00
|
|
|
const result: DateRange[] = [];
|
|
|
|
|
2023-07-11 15:11:08 +00:00
|
|
|
for (const { start: sourceStart, end: sourceEnd, ...passThrough } of sourceRanges) {
|
2023-07-05 16:47:41 +00:00
|
|
|
let currentStart = sourceStart;
|
|
|
|
|
|
|
|
const overlappingRanges = excludedRanges.filter(
|
|
|
|
({ start, end }) => start.isBefore(sourceEnd) && end.isAfter(sourceStart)
|
|
|
|
);
|
|
|
|
|
|
|
|
overlappingRanges.sort((a, b) => (a.start.isAfter(b.start) ? 1 : -1));
|
|
|
|
|
|
|
|
for (const { start: excludedStart, end: excludedEnd } of overlappingRanges) {
|
|
|
|
if (excludedStart.isAfter(currentStart)) {
|
|
|
|
result.push({ start: currentStart, end: excludedStart });
|
|
|
|
}
|
|
|
|
currentStart = excludedEnd.isAfter(currentStart) ? excludedEnd : currentStart;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (sourceEnd.isAfter(currentStart)) {
|
2023-07-11 15:11:08 +00:00
|
|
|
result.push({ start: currentStart, end: sourceEnd, ...passThrough });
|
2023-07-05 16:47:41 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|