fix: OOO for team events (#14503)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
co-authored by
CarinaWolli
parent
c4b7062d65
commit
7b0321043d
@@ -8,7 +8,7 @@ import type { WorkingHours } from "@calcom/types/schedule";
|
||||
export const getAggregateWorkingHours = (
|
||||
usersWorkingHoursAndBusySlots: (Omit<
|
||||
Awaited<ReturnType<Awaited<typeof import("./getUserAvailability")>["getUserAvailability"]>>,
|
||||
"currentSeats" | "dateRanges"
|
||||
"currentSeats" | "dateRanges" | "oooExcludedDateRanges"
|
||||
> & { user?: { isFixed?: boolean } })[],
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
schedulingType: SchedulingType | null
|
||||
|
||||
@@ -5,18 +5,30 @@ import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { mergeOverlappingDateRanges } from "./date-range-utils/mergeOverlappingDateRanges";
|
||||
|
||||
export const getAggregatedAvailability = (
|
||||
userAvailability: { dateRanges: DateRange[]; user?: { isFixed?: boolean } }[],
|
||||
userAvailability: {
|
||||
dateRanges: DateRange[];
|
||||
oooExcludedDateRanges: DateRange[];
|
||||
user?: { isFixed?: boolean };
|
||||
}[],
|
||||
schedulingType: SchedulingType | null
|
||||
): DateRange[] => {
|
||||
const isTeamEvent =
|
||||
schedulingType === SchedulingType.COLLECTIVE ||
|
||||
schedulingType === SchedulingType.ROUND_ROBIN ||
|
||||
userAvailability.length > 1;
|
||||
const fixedHosts = userAvailability.filter(
|
||||
({ user }) => !schedulingType || schedulingType === SchedulingType.COLLECTIVE || user?.isFixed
|
||||
);
|
||||
|
||||
const dateRangesToIntersect = fixedHosts.map((s) => s.dateRanges);
|
||||
const dateRangesToIntersect = fixedHosts.map((s) =>
|
||||
!isTeamEvent ? s.dateRanges : s.oooExcludedDateRanges
|
||||
);
|
||||
|
||||
const unfixedHosts = userAvailability.filter(({ user }) => user?.isFixed !== true);
|
||||
if (unfixedHosts.length) {
|
||||
dateRangesToIntersect.push(unfixedHosts.flatMap((s) => s.dateRanges));
|
||||
dateRangesToIntersect.push(
|
||||
unfixedHosts.flatMap((s) => (!isTeamEvent ? s.dateRanges : s.oooExcludedDateRanges))
|
||||
);
|
||||
}
|
||||
|
||||
const availability = intersect(dateRangesToIntersect);
|
||||
|
||||
@@ -332,18 +332,6 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
||||
}
|
||||
}
|
||||
|
||||
const dateRanges = buildDateRanges({
|
||||
dateFrom,
|
||||
dateTo,
|
||||
availability,
|
||||
timeZone,
|
||||
});
|
||||
|
||||
const formattedBusyTimes = detailedBusyTimes.map((busy) => ({
|
||||
start: dayjs(busy.start),
|
||||
end: dayjs(busy.end),
|
||||
}));
|
||||
|
||||
const datesOutOfOffice = await getOutOfOfficeDays({
|
||||
userId: user.id,
|
||||
dateFrom,
|
||||
@@ -351,8 +339,23 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
||||
availability,
|
||||
});
|
||||
|
||||
const { dateRanges, oooExcludedDateRanges } = buildDateRanges({
|
||||
dateFrom,
|
||||
dateTo,
|
||||
availability,
|
||||
timeZone,
|
||||
outOfOffice: datesOutOfOffice,
|
||||
});
|
||||
|
||||
const formattedBusyTimes = detailedBusyTimes.map((busy) => ({
|
||||
start: dayjs(busy.start),
|
||||
end: dayjs(busy.end),
|
||||
}));
|
||||
|
||||
const dateRangesInWhichUserIsAvailable = subtract(dateRanges, formattedBusyTimes);
|
||||
|
||||
const dateRangesInWhichUserIsAvailableWithoutOOO = subtract(oooExcludedDateRanges, formattedBusyTimes);
|
||||
|
||||
log.debug(
|
||||
`getWorkingHours took ${endGetWorkingHours - startGetWorkingHours}ms for userId ${userId}`,
|
||||
JSON.stringify({
|
||||
@@ -368,6 +371,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
||||
busy: detailedBusyTimes,
|
||||
timeZone,
|
||||
dateRanges: dateRangesInWhichUserIsAvailable,
|
||||
oooExcludedDateRanges: dateRangesInWhichUserIsAvailableWithoutOOO,
|
||||
workingHours,
|
||||
dateOverrides,
|
||||
currentSeats,
|
||||
|
||||
@@ -357,7 +357,7 @@ export async function ensureAvailableUsers(
|
||||
}
|
||||
|
||||
for (const user of eventType.users) {
|
||||
const { dateRanges, busy: bufferedBusyTimes } = await getUserAvailability(
|
||||
const { oooExcludedDateRanges: dateRanges, busy: bufferedBusyTimes } = await getUserAvailability(
|
||||
{
|
||||
...input,
|
||||
userId: user.id,
|
||||
|
||||
@@ -251,7 +251,7 @@ describe("buildDateRanges", () => {
|
||||
|
||||
const timeZone = "America/New_York";
|
||||
|
||||
const results = buildDateRanges({ availability: items, timeZone, dateFrom, dateTo });
|
||||
const { dateRanges: results } = buildDateRanges({ availability: items, timeZone, dateFrom, dateTo });
|
||||
// [
|
||||
// { s: '2023-06-13T10:00:00-04:00', e: '2023-06-13T15:00:00-04:00' },
|
||||
// { s: '2023-06-14T08:00:00-04:00', e: '2023-06-14T17:00:00-04:00' }
|
||||
@@ -288,7 +288,7 @@ describe("buildDateRanges", () => {
|
||||
const dateFrom = dayjs.tz("2023-08-15", "Europe/Brussels").startOf("day");
|
||||
const dateTo = dayjs.tz("2023-08-15", "Europe/Brussels").endOf("day");
|
||||
|
||||
const result = buildDateRanges({ availability: item, timeZone, dateFrom, dateTo });
|
||||
const { dateRanges: result } = buildDateRanges({ availability: item, timeZone, dateFrom, dateTo });
|
||||
// this happened only on Europe/Brussels, Europe/Amsterdam was 2023-08-15T17:00:00-10:00 (as it should be)
|
||||
expect(result[0].end.format()).not.toBe("2023-08-14T17:00:00-10:00");
|
||||
});
|
||||
@@ -310,7 +310,7 @@ describe("buildDateRanges", () => {
|
||||
const dateFrom = dayjs("2023-06-13T00:00:00Z");
|
||||
const dateTo = dayjs("2023-06-15T00:00:00Z");
|
||||
|
||||
const results = buildDateRanges({ availability: items, timeZone, dateFrom, dateTo });
|
||||
const { dateRanges: results } = buildDateRanges({ availability: items, timeZone, dateFrom, dateTo });
|
||||
|
||||
expect(results[0]).toEqual({
|
||||
start: dayjs("2023-06-14T07:00:00Z").tz(timeZone),
|
||||
@@ -330,7 +330,7 @@ describe("buildDateRanges", () => {
|
||||
const dateFrom = dayjs("2023-06-13T10:00:00Z");
|
||||
const dateTo = dayjs("2023-06-13T10:30:00Z");
|
||||
|
||||
const results = buildDateRanges({ availability: items, timeZone, dateFrom, dateTo });
|
||||
const { dateRanges: results } = buildDateRanges({ availability: items, timeZone, dateFrom, dateTo });
|
||||
|
||||
expect(results[0]).toEqual({
|
||||
start: dayjs("2023-06-13T08:00:00Z").tz(timeZone),
|
||||
@@ -355,7 +355,7 @@ describe("buildDateRanges", () => {
|
||||
const dateFrom = dayjs("2023-06-13T00:00:00Z");
|
||||
const dateTo = dayjs("2023-06-15T00:00:00Z");
|
||||
|
||||
const results = buildDateRanges({ availability: items, timeZone, dateFrom, dateTo });
|
||||
const { dateRanges: results } = buildDateRanges({ availability: items, timeZone, dateFrom, dateTo });
|
||||
|
||||
expect(results[0]).toEqual({
|
||||
start: dayjs("2023-06-14T02:00:00Z").tz(timeZone),
|
||||
@@ -366,6 +366,54 @@ describe("buildDateRanges", () => {
|
||||
end: dayjs("2023-06-14T21:00:00Z").tz(timeZone),
|
||||
});
|
||||
});
|
||||
it("should handle OOO correctly", () => {
|
||||
const items = [
|
||||
{
|
||||
date: new Date(Date.UTC(2023, 5, 13)),
|
||||
startTime: new Date(Date.UTC(0, 0, 0, 10, 0)), // 10 AM
|
||||
endTime: new Date(Date.UTC(0, 0, 0, 15, 0)), // 3 PM
|
||||
},
|
||||
{
|
||||
days: [1, 2, 3, 4, 5],
|
||||
startTime: new Date(Date.UTC(2023, 5, 12, 8, 0)), // 8 AM
|
||||
endTime: new Date(Date.UTC(2023, 5, 12, 17, 0)), // 5 PM
|
||||
},
|
||||
];
|
||||
|
||||
const outOfOffice = {
|
||||
"2023-06-13": {
|
||||
fromUser: { id: 1, displayName: "Team Free Example" },
|
||||
},
|
||||
};
|
||||
|
||||
const dateFrom = dayjs("2023-06-13T00:00:00Z"); // 2023-06-12T20:00:00-04:00 (America/New_York)
|
||||
const dateTo = dayjs("2023-06-15T00:00:00Z");
|
||||
|
||||
const timeZone = "America/New_York";
|
||||
|
||||
const { dateRanges, oooExcludedDateRanges } = buildDateRanges({
|
||||
availability: items,
|
||||
timeZone,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
outOfOffice,
|
||||
});
|
||||
|
||||
expect(dateRanges[0]).toEqual({
|
||||
start: dayjs("2023-06-13T14:00:00Z").tz(timeZone),
|
||||
end: dayjs("2023-06-13T19:00:00Z").tz(timeZone),
|
||||
});
|
||||
|
||||
expect(dateRanges[1]).toEqual({
|
||||
start: dayjs("2023-06-14T12:00:00Z").tz(timeZone),
|
||||
end: dayjs("2023-06-14T21:00:00Z").tz(timeZone),
|
||||
});
|
||||
expect(oooExcludedDateRanges.length).toBe(1);
|
||||
expect(oooExcludedDateRanges[0]).toEqual({
|
||||
start: dayjs("2023-06-14T12:00:00Z").tz(timeZone),
|
||||
end: dayjs("2023-06-14T21:00:00Z").tz(timeZone),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("subtract", () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { IOutOfOfficeData } from "@calcom/core/getUserAvailability";
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import type { Availability } from "@calcom/prisma/client";
|
||||
@@ -103,17 +104,31 @@ export function processDateOverride({
|
||||
};
|
||||
}
|
||||
|
||||
function processOOO(outOfOffice: Dayjs, timeZone: string) {
|
||||
const utcOffset = outOfOffice.tz(timeZone).utcOffset();
|
||||
const utcDate = outOfOffice.subtract(utcOffset, "minute");
|
||||
|
||||
const OOOdate = utcDate.tz(timeZone);
|
||||
|
||||
return {
|
||||
start: OOOdate,
|
||||
end: OOOdate,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildDateRanges({
|
||||
availability,
|
||||
timeZone /* Organizer timeZone */,
|
||||
dateFrom /* Attendee dateFrom */,
|
||||
dateTo /* `` dateTo */,
|
||||
outOfOffice,
|
||||
}: {
|
||||
timeZone: string;
|
||||
availability: (DateOverride | WorkingHours)[];
|
||||
dateFrom: Dayjs;
|
||||
dateTo: Dayjs;
|
||||
}): DateRange[] {
|
||||
outOfOffice?: IOutOfOfficeData;
|
||||
}): { dateRanges: DateRange[]; oooExcludedDateRanges: DateRange[] } {
|
||||
const dateFromOrganizerTZ = dateFrom.tz(timeZone);
|
||||
const groupedWorkingHours = groupByDate(
|
||||
availability.reduce((processed: DateRange[], item) => {
|
||||
@@ -125,6 +140,11 @@ export function buildDateRanges({
|
||||
return processed;
|
||||
}, [])
|
||||
);
|
||||
const OOOdates = outOfOffice
|
||||
? Object.keys(outOfOffice).map((outOfOffice) => processOOO(dayjs(outOfOffice), timeZone))
|
||||
: [];
|
||||
|
||||
const groupedOOO = groupByDate(OOOdates);
|
||||
|
||||
const groupedDateOverrides = groupByDate(
|
||||
availability.reduce((processed: DateRange[], item) => {
|
||||
@@ -158,7 +178,16 @@ export function buildDateRanges({
|
||||
(ranges) => ranges.filter((range) => range.start.valueOf() !== range.end.valueOf())
|
||||
);
|
||||
|
||||
return dateRanges.flat();
|
||||
const oooExcludedDateRanges = Object.values({
|
||||
...groupedWorkingHours,
|
||||
...groupedDateOverrides,
|
||||
...groupedOOO,
|
||||
}).map(
|
||||
// remove 0-length overrides && OOO dates that were kept to cancel out working dates until now.
|
||||
(ranges) => ranges.filter((range) => range.start.valueOf() !== range.end.valueOf())
|
||||
);
|
||||
|
||||
return { dateRanges: dateRanges.flat(), oooExcludedDateRanges: oooExcludedDateRanges.flat() };
|
||||
}
|
||||
|
||||
export function groupByDate(ranges: DateRange[]): { [x: string]: DateRange[] } {
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ async function buildMember(member: Member, dateFrom: Dayjs, dateTo: Dayjs) {
|
||||
});
|
||||
const timeZone = schedule?.timeZone || member.user.timeZone;
|
||||
|
||||
const dateRanges = buildDateRanges({
|
||||
const { dateRanges } = buildDateRanges({
|
||||
dateFrom,
|
||||
dateTo,
|
||||
timeZone,
|
||||
|
||||
@@ -461,6 +461,7 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro
|
||||
const {
|
||||
busy,
|
||||
dateRanges,
|
||||
oooExcludedDateRanges,
|
||||
currentSeats: _currentSeats,
|
||||
timeZone,
|
||||
datesOutOfOffice,
|
||||
@@ -496,6 +497,7 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro
|
||||
return {
|
||||
timeZone,
|
||||
dateRanges,
|
||||
oooExcludedDateRanges,
|
||||
busy,
|
||||
user: currentUser,
|
||||
datesOutOfOffice,
|
||||
@@ -523,6 +525,11 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro
|
||||
const checkForAvailabilityCount = 0;
|
||||
const aggregatedAvailability = getAggregatedAvailability(allUsersAvailability, eventType.schedulingType);
|
||||
|
||||
const isTeamEvent =
|
||||
eventType.schedulingType === SchedulingType.COLLECTIVE ||
|
||||
eventType.schedulingType === SchedulingType.ROUND_ROBIN ||
|
||||
allUsersAvailability.length > 1;
|
||||
|
||||
const timeSlots = getSlots({
|
||||
inviteeDate: startTime,
|
||||
eventLength: input.duration || eventType.length,
|
||||
@@ -532,7 +539,7 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro
|
||||
frequency: eventType.slotInterval || input.duration || eventType.length,
|
||||
organizerTimeZone:
|
||||
eventType.timeZone || eventType?.schedule?.timeZone || allUsersAvailability?.[0]?.timeZone,
|
||||
datesOutOfOffice: allUsersAvailability[0]?.datesOutOfOffice,
|
||||
datesOutOfOffice: !isTeamEvent ? allUsersAvailability[0]?.datesOutOfOffice : undefined,
|
||||
});
|
||||
|
||||
let availableTimeSlots: typeof timeSlots = [];
|
||||
|
||||
Reference in New Issue
Block a user