perf: Optimize team bookings query by fetching data for multiple users at once (#21137)

Co-authored-by: keith@cal.com <keith@cal.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot]
2025-05-06 11:47:06 -07:00
committed by GitHub
co-authored by keith@cal.com <keith@cal.com> Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent ab8556790e
commit d770cfc65e
4 changed files with 646 additions and 49 deletions
+56 -30
View File
@@ -182,6 +182,18 @@ export type GetUserAvailabilityInitialData = {
reason: Pick<OutOfOfficeReason, "id" | "emoji" | "reason"> | null;
})[];
busyTimesFromLimitsBookings: EventBusyDetails[];
busyTimesFromLimits?: Map<number, EventBusyDetails[]>;
eventTypeForLimits?: {
id: number;
bookingLimits?: unknown;
durationLimits?: unknown;
} | null;
teamBookingLimits?: Map<number, EventBusyDetails[]>;
teamForBookingLimits?: {
id: number;
bookingLimits?: unknown;
includeManagedEventsInLimits: boolean;
} | null;
};
export type GetAvailabilityUser = NonNullable<GetUserAvailabilityInitialData["user"]>;
@@ -352,40 +364,49 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
const bookingLimits = parseBookingLimit(eventType?.bookingLimits);
const durationLimits = parseDurationLimit(eventType?.durationLimits);
const busyTimesFromLimits =
eventType && (bookingLimits || durationLimits)
? await getBusyTimesFromLimits(
bookingLimits,
durationLimits,
dateFrom.tz(timeZone),
dateTo.tz(timeZone),
duration,
eventType,
initialData?.busyTimesFromLimitsBookings ?? [],
timeZone,
initialData?.rescheduleUid ?? undefined
)
: [];
let busyTimesFromLimits: EventBusyDetails[] = [];
if (initialData?.busyTimesFromLimits && initialData?.eventTypeForLimits) {
busyTimesFromLimits = initialData.busyTimesFromLimits.get(user.id) || [];
} else if (eventType && (bookingLimits || durationLimits)) {
// Fall back to individual query if not available in initialData
busyTimesFromLimits = await getBusyTimesFromLimits(
bookingLimits,
durationLimits,
dateFrom.tz(timeZone),
dateTo.tz(timeZone),
duration,
eventType,
initialData?.busyTimesFromLimitsBookings ?? [],
timeZone,
initialData?.rescheduleUid ?? undefined
);
}
const teamForBookingLimits =
initialData?.teamForBookingLimits ??
eventType?.team ??
(eventType?.parent?.team?.includeManagedEventsInLimits ? eventType?.parent?.team : null);
const teamBookingLimits = parseBookingLimit(teamForBookingLimits?.bookingLimits);
const busyTimesFromTeamLimits =
teamForBookingLimits && teamBookingLimits
? await getBusyTimesFromTeamLimits(
user,
teamBookingLimits,
dateFrom.tz(timeZone),
dateTo.tz(timeZone),
teamForBookingLimits.id,
teamForBookingLimits.includeManagedEventsInLimits,
timeZone,
initialData?.rescheduleUid ?? undefined
)
: [];
let busyTimesFromTeamLimits: EventBusyDetails[] = [];
if (initialData?.teamBookingLimits && teamForBookingLimits) {
busyTimesFromTeamLimits = initialData.teamBookingLimits.get(user.id) || [];
} else if (teamForBookingLimits && teamBookingLimits) {
// Fall back to individual query if not available in initialData
busyTimesFromTeamLimits = await getBusyTimesFromTeamLimits(
user,
teamBookingLimits,
dateFrom.tz(timeZone),
dateTo.tz(timeZone),
teamForBookingLimits.id,
teamForBookingLimits.includeManagedEventsInLimits,
timeZone,
initialData?.rescheduleUid ?? undefined
);
}
// TODO: only query what we need after applying limits (shrink date range)
const getBusyTimesStart = dateFrom.toISOString();
@@ -598,10 +619,15 @@ export const getPeriodStartDatesBetween = (
return monitorCallbackSync(_getPeriodStartDatesBetween, ...args);
};
const _getPeriodStartDatesBetween = (dateFrom: Dayjs, dateTo: Dayjs, period: IntervalLimitUnit) => {
const _getPeriodStartDatesBetween = (
dateFrom: Dayjs,
dateTo: Dayjs,
period: IntervalLimitUnit,
timeZone?: string
) => {
const dates = [];
let startDate = dayjs(dateFrom).startOf(period);
const endDate = dayjs(dateTo).endOf(period);
let startDate = timeZone ? dayjs(dateFrom).tz(timeZone).startOf(period) : dayjs(dateFrom).startOf(period);
const endDate = timeZone ? dayjs(dateTo).tz(timeZone).endOf(period) : dayjs(dateTo).endOf(period);
while (startDate.isBefore(endDate)) {
dates.push(startDate);
+17 -15
View File
@@ -14,31 +14,32 @@ export default class LimitManager {
/**
* Creates a busy map key
*/
private static createKey(start: Dayjs, unit: IntervalLimitUnit): BusyMapKey {
return `${unit}-${start.startOf(unit).toISOString()}`;
private static createKey(start: Dayjs, unit: IntervalLimitUnit, timeZone?: string): BusyMapKey {
const tzStart = timeZone ? start.tz(timeZone) : start;
return `${unit}-${tzStart.startOf(unit).toISOString()}`;
}
/**
* Checks if already marked busy by ancestors or siblings
*/
isAlreadyBusy(start: Dayjs, unit: IntervalLimitUnit) {
if (this.busyMap.has(LimitManager.createKey(start, "year"))) return true;
isAlreadyBusy(start: Dayjs, unit: IntervalLimitUnit, timeZone?: string) {
if (this.busyMap.has(LimitManager.createKey(start, "year", timeZone))) return true;
if (unit === "month" && this.busyMap.has(LimitManager.createKey(start, "month"))) {
if (unit === "month" && this.busyMap.has(LimitManager.createKey(start, "month", timeZone))) {
return true;
} else if (
unit === "week" &&
// weeks can be part of two months
((this.busyMap.has(LimitManager.createKey(start, "month")) &&
this.busyMap.has(LimitManager.createKey(start.endOf("week"), "month"))) ||
this.busyMap.has(LimitManager.createKey(start, "week")))
((this.busyMap.has(LimitManager.createKey(start, "month", timeZone)) &&
this.busyMap.has(LimitManager.createKey(start.endOf("week"), "month", timeZone))) ||
this.busyMap.has(LimitManager.createKey(start, "week", timeZone)))
) {
return true;
} else if (
unit === "day" &&
(this.busyMap.has(LimitManager.createKey(start, "month")) ||
this.busyMap.has(LimitManager.createKey(start, "week")) ||
this.busyMap.has(LimitManager.createKey(start, "day")))
(this.busyMap.has(LimitManager.createKey(start, "month", timeZone)) ||
this.busyMap.has(LimitManager.createKey(start, "week", timeZone)) ||
this.busyMap.has(LimitManager.createKey(start, "day", timeZone)))
) {
return true;
} else {
@@ -49,10 +50,11 @@ export default class LimitManager {
/**
* Adds a new busy time
*/
addBusyTime(start: Dayjs, unit: IntervalLimitUnit) {
this.busyMap.set(`${unit}-${start.toISOString()}`, {
start: start.toISOString(),
end: start.endOf(unit).toISOString(),
addBusyTime(start: Dayjs, unit: IntervalLimitUnit, timeZone?: string) {
const tzStart = timeZone ? start.tz(timeZone) : start;
this.busyMap.set(`${unit}-${tzStart.toISOString()}`, {
start: tzStart.toISOString(),
end: tzStart.endOf(unit).toISOString(),
});
}
+128
View File
@@ -17,6 +17,22 @@ type TeamBookingsParamsBase = {
shouldReturnCount?: boolean;
};
type TeamBookingsMultipleUsersParamsBase = {
users: { id: number; email: string }[];
teamId: number;
startDate: Date;
endDate: Date;
excludedUid?: string | null;
includeManagedEvents: boolean;
shouldReturnCount?: boolean;
};
type TeamBookingsMultipleUsersParamsWithCount = TeamBookingsMultipleUsersParamsBase & {
shouldReturnCount: true;
};
type TeamBookingsMultipleUsersParamsWithoutCount = TeamBookingsMultipleUsersParamsBase;
type TeamBookingsParamsWithCount = TeamBookingsParamsBase & {
shouldReturnCount: true;
};
@@ -666,4 +682,116 @@ export class BookingRepository {
},
});
}
static async getAllAcceptedTeamBookingsOfUsers(
params: TeamBookingsMultipleUsersParamsWithCount
): Promise<number>;
static async getAllAcceptedTeamBookingsOfUsers(
params: TeamBookingsMultipleUsersParamsWithoutCount
): Promise<Array<Booking>>;
static async getAllAcceptedTeamBookingsOfUsers(params: TeamBookingsMultipleUsersParamsBase) {
const { users, teamId, startDate, endDate, excludedUid, shouldReturnCount, includeManagedEvents } =
params;
const baseWhere: Prisma.BookingWhereInput = {
status: BookingStatus.ACCEPTED,
startTime: {
gte: startDate,
},
endTime: {
lte: endDate,
},
...(excludedUid && {
uid: {
not: excludedUid,
},
}),
};
const userIds = users.map((user) => user.id);
const userEmails = users.map((user) => user.email);
const whereCollectiveRoundRobinOwner: Prisma.BookingWhereInput = {
...baseWhere,
userId: {
in: userIds,
},
eventType: {
teamId,
},
};
const whereCollectiveRoundRobinBookingsAttendee: Prisma.BookingWhereInput = {
...baseWhere,
attendees: {
some: {
email: {
in: userEmails,
},
},
},
eventType: {
teamId,
},
};
const whereManagedBookings: Prisma.BookingWhereInput = {
...baseWhere,
userId: {
in: userIds,
},
eventType: {
parent: {
teamId,
},
},
};
if (shouldReturnCount) {
const collectiveRoundRobinBookingsOwner = await prisma.booking.count({
where: whereCollectiveRoundRobinOwner,
});
const collectiveRoundRobinBookingsAttendee = await prisma.booking.count({
where: whereCollectiveRoundRobinBookingsAttendee,
});
let managedBookings = 0;
if (includeManagedEvents) {
managedBookings = await prisma.booking.count({
where: whereManagedBookings,
});
}
const totalNrOfBooking =
collectiveRoundRobinBookingsOwner + collectiveRoundRobinBookingsAttendee + managedBookings;
return totalNrOfBooking;
}
const collectiveRoundRobinBookingsOwner = await prisma.booking.findMany({
where: whereCollectiveRoundRobinOwner,
});
const collectiveRoundRobinBookingsAttendee = await prisma.booking.findMany({
where: whereCollectiveRoundRobinBookingsAttendee,
});
let managedBookings: typeof collectiveRoundRobinBookingsAttendee = [];
if (includeManagedEvents) {
managedBookings = await prisma.booking.findMany({
where: whereManagedBookings,
});
}
return [
...collectiveRoundRobinBookingsOwner,
...collectiveRoundRobinBookingsAttendee,
...managedBookings,
];
}
}
@@ -15,11 +15,21 @@ import { RESERVED_SUBDOMAINS } from "@calcom/lib/constants";
import { getUTCOffsetByTimezone } from "@calcom/lib/dayjs";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { getAggregatedAvailability } from "@calcom/lib/getAggregatedAvailability";
import { getBusyTimesForLimitChecks } from "@calcom/lib/getBusyTimes";
import type { CurrentSeats, GetAvailabilityUser, IFromUser, IToUser } from "@calcom/lib/getUserAvailability";
import { getUsersAvailability } from "@calcom/lib/getUserAvailability";
import { getBusyTimesForLimitChecks, getStartEndDateforLimitCheck } from "@calcom/lib/getBusyTimes";
import type {
CurrentSeats,
GetAvailabilityUser,
IFromUser,
IToUser,
EventType,
} from "@calcom/lib/getUserAvailability";
import { getUsersAvailability, getPeriodStartDatesBetween } from "@calcom/lib/getUserAvailability";
import { descendingLimitKeys, intervalLimitKeyToUnit } from "@calcom/lib/intervalLimits/intervalLimit";
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
import { parseBookingLimit } from "@calcom/lib/intervalLimits/isBookingLimits";
import { parseDurationLimit } from "@calcom/lib/intervalLimits/isDurationLimits";
import LimitManager from "@calcom/lib/intervalLimits/limitManager";
import { checkBookingLimit } from "@calcom/lib/intervalLimits/server/checkBookingLimits";
import {
calculatePeriodLimits,
isTimeOutOfBounds,
@@ -28,6 +38,7 @@ import {
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import monitorCallbackAsync, { monitorCallbackSync } from "@calcom/lib/sentryWrapper";
import { getTotalBookingDuration } from "@calcom/lib/server/queries";
import { BookingRepository as BookingRepo } from "@calcom/lib/server/repository/booking";
import { EventTypeRepository } from "@calcom/lib/server/repository/eventType";
import { UserRepository, withSelectedCalendars } from "@calcom/lib/server/repository/user";
@@ -36,7 +47,7 @@ import prisma, { availabilityUserSelect } from "@calcom/prisma";
import { PeriodType, Prisma } from "@calcom/prisma/client";
import { SchedulingType } from "@calcom/prisma/enums";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import type { EventBusyDate } from "@calcom/types/Calendar";
import type { EventBusyDate, EventBusyDetails } from "@calcom/types/Calendar";
import type { CredentialPayload, CredentialForCalendarService } from "@calcom/types/Credential";
import { TRPCError } from "@trpc/server";
@@ -834,6 +845,395 @@ export function getAllDatesWithBookabilityStatus(availableDates: string[]) {
return allDates;
}
/**
* Gets busy times from limits for multiple users
*/
const getBusyTimesFromLimitsForUsers = async (
users: { id: number; email: string }[],
bookingLimits: IntervalLimit | null,
durationLimits: IntervalLimit | null,
dateFrom: Dayjs,
dateTo: Dayjs,
duration: number | undefined,
eventType: NonNullable<EventType>,
timeZone: string,
rescheduleUid?: string
) => {
const userBusyTimesMap = new Map<number, EventBusyDetails[]>();
if (!bookingLimits && !durationLimits) {
return userBusyTimesMap;
}
const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck(
dateFrom.toISOString(),
dateTo.toISOString(),
bookingLimits || durationLimits
);
const busyTimesFromLimitsBookings = await getBusyTimesForLimitChecks({
userIds: users.map((user) => user.id),
eventTypeId: eventType.id,
startDate: limitDateFrom.format(),
endDate: limitDateTo.format(),
rescheduleUid,
bookingLimits,
durationLimits,
});
const globalLimitManager = new LimitManager();
if (bookingLimits) {
for (const key of descendingLimitKeys) {
const limit = bookingLimits?.[key];
if (!limit) continue;
const unit = intervalLimitKeyToUnit(key);
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone);
for (const periodStart of periodStartDates) {
if (globalLimitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
const periodEnd = periodStart.endOf(unit);
let totalBookings = 0;
for (const booking of busyTimesFromLimitsBookings) {
const bookingStart = dayjs(booking.start).tz(timeZone);
const bookingDay = bookingStart.format("YYYY-MM-DD");
const periodStartDay = periodStart.format("YYYY-MM-DD");
const periodEndDay = periodEnd.format("YYYY-MM-DD");
if (bookingDay < periodStartDay || bookingDay > periodEndDay) {
continue;
}
totalBookings++;
if (totalBookings >= limit) {
globalLimitManager.addBusyTime(periodStart, unit, timeZone);
break;
}
}
}
}
}
for (const user of users) {
const userBookings = busyTimesFromLimitsBookings.filter((booking) => booking.userId === user.id);
const limitManager = new LimitManager();
for (const busyTime of globalLimitManager.getBusyTimes()) {
const start = dayjs(busyTime.start);
const end = dayjs(busyTime.end);
let unit: "year" | "month" | "week" | "day";
if (end.diff(start, "year") >= 1) {
unit = "year";
} else if (end.diff(start, "month") >= 1) {
unit = "month";
} else if (end.diff(start, "week") >= 1) {
unit = "week";
} else {
unit = "day";
}
limitManager.addBusyTime(start, unit, timeZone);
}
await monitorCallbackAsync(async () => {
if (bookingLimits) {
for (const key of descendingLimitKeys) {
const limit = bookingLimits?.[key];
if (!limit) continue;
const unit = intervalLimitKeyToUnit(key);
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone);
for (const periodStart of periodStartDates) {
if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
if (unit === "year") {
try {
await checkBookingLimit({
eventStartDate: periodStart.toDate(),
limitingNumber: limit,
eventId: eventType.id,
key,
user,
rescheduleUid,
timeZone,
});
} catch (_) {
limitManager.addBusyTime(periodStart, unit, timeZone);
if (
periodStartDates.every((start: Dayjs) => limitManager.isAlreadyBusy(start, unit, timeZone))
) {
break;
}
}
continue;
}
const periodEnd = periodStart.endOf(unit);
let totalBookings = 0;
for (const booking of userBookings) {
const bookingStart = dayjs(booking.start).tz(timeZone);
const bookingDay = bookingStart.format("YYYY-MM-DD");
const periodStartDay = periodStart.format("YYYY-MM-DD");
const periodEndDay = periodEnd.format("YYYY-MM-DD");
if (bookingDay < periodStartDay || bookingDay > periodEndDay) {
continue;
}
totalBookings++;
if (totalBookings >= limit) {
limitManager.addBusyTime(periodStart, unit, timeZone);
break;
}
}
}
}
}
if (durationLimits) {
for (const key of descendingLimitKeys) {
const limit = durationLimits?.[key];
if (!limit) continue;
const unit = intervalLimitKeyToUnit(key);
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone);
for (const periodStart of periodStartDates) {
if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
const selectedDuration = (duration || eventType.length) ?? 0;
if (selectedDuration > limit) {
limitManager.addBusyTime(periodStart, unit, timeZone);
continue;
}
if (unit === "year") {
const totalYearlyDuration = await getTotalBookingDuration({
eventId: eventType.id,
startDate: periodStart.toDate(),
endDate: periodStart.endOf(unit).toDate(),
rescheduleUid,
});
if (totalYearlyDuration + selectedDuration > limit) {
limitManager.addBusyTime(periodStart, unit, timeZone);
if (
periodStartDates.every((start: Dayjs) => limitManager.isAlreadyBusy(start, unit, timeZone))
) {
break;
}
}
continue;
}
const periodEnd = periodStart.endOf(unit);
let totalDuration = selectedDuration;
for (const booking of userBookings) {
const bookingStart = dayjs(booking.start).tz(timeZone);
const bookingDay = bookingStart.format("YYYY-MM-DD");
const periodStartDay = periodStart.format("YYYY-MM-DD");
const periodEndDay = periodEnd.format("YYYY-MM-DD");
if (bookingDay < periodStartDay || bookingDay > periodEndDay) {
continue;
}
totalDuration += dayjs(booking.end).diff(dayjs(booking.start), "minute");
if (totalDuration > limit) {
limitManager.addBusyTime(periodStart, unit, timeZone);
break;
}
}
}
}
}
});
userBusyTimesMap.set(user.id, limitManager.getBusyTimes());
}
return userBusyTimesMap;
};
/**
* Gets busy times from team booking limits for multiple users
*/
const getBusyTimesFromTeamLimitsForUsers = async (
users: { id: number; email: string }[],
bookingLimits: IntervalLimit,
dateFrom: Dayjs,
dateTo: Dayjs,
teamId: number,
includeManagedEvents: boolean,
timeZone: string,
rescheduleUid?: string
) => {
const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck(
dateFrom.toISOString(),
dateTo.toISOString(),
bookingLimits
);
const bookings = await BookingRepo.getAllAcceptedTeamBookingsOfUsers({
users,
teamId,
startDate: limitDateFrom.toDate(),
endDate: limitDateTo.toDate(),
excludedUid: rescheduleUid,
includeManagedEvents,
});
const busyTimes = bookings.map(({ id, startTime, endTime, eventTypeId, title, userId }) => ({
start: dayjs(startTime).toDate(),
end: dayjs(endTime).toDate(),
title,
source: `eventType-${eventTypeId}-booking-${id}`,
userId,
}));
const globalLimitManager = new LimitManager();
for (const key of descendingLimitKeys) {
const limit = bookingLimits?.[key];
if (!limit) continue;
const unit = intervalLimitKeyToUnit(key);
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone);
for (const periodStart of periodStartDates) {
if (globalLimitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
const periodEnd = periodStart.endOf(unit);
let totalBookings = 0;
for (const booking of busyTimes) {
const bookingStart = dayjs(booking.start).tz(timeZone);
const bookingDay = bookingStart.format("YYYY-MM-DD");
const periodStartDay = periodStart.format("YYYY-MM-DD");
const periodEndDay = periodEnd.format("YYYY-MM-DD");
if (bookingDay < periodStartDay || bookingDay > periodEndDay) {
continue;
}
totalBookings++;
if (totalBookings >= limit) {
globalLimitManager.addBusyTime(periodStart, unit, timeZone);
break;
}
}
}
}
const userBusyTimesMap = new Map();
for (const user of users) {
const userBusyTimes = busyTimes.filter((busyTime) => busyTime.userId === user.id);
const limitManager = new LimitManager();
for (const busyTime of globalLimitManager.getBusyTimes()) {
const start = dayjs(busyTime.start);
const end = dayjs(busyTime.end);
let unit: "year" | "month" | "week" | "day";
if (end.diff(start, "year") >= 1) {
unit = "year";
} else if (end.diff(start, "month") >= 1) {
unit = "month";
} else if (end.diff(start, "week") >= 1) {
unit = "week";
} else {
unit = "day";
}
limitManager.addBusyTime(start, unit, timeZone);
}
await monitorCallbackAsync(async () => {
const bookingLimitsParams = {
bookings: userBusyTimes,
bookingLimits,
dateFrom,
dateTo,
limitManager,
rescheduleUid,
teamId,
user,
includeManagedEvents,
timeZone,
};
for (const key of descendingLimitKeys) {
const limit = bookingLimits?.[key];
if (!limit) continue;
const unit = intervalLimitKeyToUnit(key);
const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone);
for (const periodStart of periodStartDates) {
if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue;
if (unit === "year") {
try {
await checkBookingLimit({
eventStartDate: periodStart.toDate(),
limitingNumber: limit,
key,
teamId,
user,
rescheduleUid,
includeManagedEvents,
timeZone,
});
} catch (_) {
limitManager.addBusyTime(periodStart, unit, timeZone);
if (
periodStartDates.every((start: Dayjs) => limitManager.isAlreadyBusy(start, unit, timeZone))
) {
return;
}
}
continue;
}
const periodEnd = periodStart.endOf(unit);
let totalBookings = 0;
for (const booking of userBusyTimes) {
const bookingStart = dayjs(booking.start).tz(timeZone);
const bookingDay = bookingStart.format("YYYY-MM-DD");
const periodStartDay = periodStart.format("YYYY-MM-DD");
const periodEndDay = periodEnd.format("YYYY-MM-DD");
if (bookingDay < periodStartDay || bookingDay > periodEndDay) {
continue;
}
totalBookings++;
if (totalBookings >= limit) {
limitManager.addBusyTime(periodStart, unit, timeZone);
break;
}
}
}
}
});
userBusyTimesMap.set(user.id, limitManager.getBusyTimes());
}
return userBusyTimesMap;
};
const calculateHostsAndAvailabilities = async ({
input,
eventType,
@@ -905,6 +1305,43 @@ const calculateHostsAndAvailabilities = async ({
});
}
let busyTimesFromLimitsMap: Map<number, EventBusyDetails[]> | undefined = undefined;
if (eventType && (bookingLimits || durationLimits)) {
const usersForLimits = usersWithCredentials.map((user) => ({ id: user.id, email: user.email }));
busyTimesFromLimitsMap = await getBusyTimesFromLimitsForUsers(
usersForLimits,
bookingLimits,
durationLimits,
startTime,
endTime,
typeof input.duration === "number" ? input.duration : undefined,
eventType,
usersWithCredentials[0]?.timeZone || "UTC",
input.rescheduleUid || undefined
);
}
const teamForBookingLimits =
eventType?.team ??
(eventType?.parent?.team?.includeManagedEventsInLimits ? eventType?.parent?.team : null);
const teamBookingLimits = parseBookingLimit(teamForBookingLimits?.bookingLimits);
let teamBookingLimitsMap: Map<number, EventBusyDetails[]> | undefined = undefined;
if (teamForBookingLimits && teamBookingLimits) {
const usersForTeamLimits = usersWithCredentials.map((user) => ({ id: user.id, email: user.email }));
teamBookingLimitsMap = await getBusyTimesFromTeamLimitsForUsers(
usersForTeamLimits,
teamBookingLimits,
startTime,
endTime,
teamForBookingLimits.id,
teamForBookingLimits.includeManagedEventsInLimits,
usersWithCredentials[0]?.timeZone || "UTC",
input.rescheduleUid || undefined
);
}
const users = monitorCallbackSync(function enrichUsersWithData() {
return usersWithCredentials.map((currentUser) => {
return {
@@ -940,6 +1377,10 @@ const calculateHostsAndAvailabilities = async ({
currentSeats,
rescheduleUid: input.rescheduleUid,
busyTimesFromLimitsBookings: busyTimesFromLimitsBookingsAllUsers,
busyTimesFromLimits: busyTimesFromLimitsMap,
eventTypeForLimits: eventType && (bookingLimits || durationLimits) ? eventType : null,
teamBookingLimits: teamBookingLimitsMap,
teamForBookingLimits: teamForBookingLimits,
},
});
/* We get all users working hours and busy slots */