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:
co-authored by
keith@cal.com <keith@cal.com>
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
ab8556790e
commit
d770cfc65e
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user