2022-06-15 20:54:31 +00:00
|
|
|
import { SchedulingType } from "@prisma/client";
|
|
|
|
import { z } from "zod";
|
|
|
|
|
|
|
|
import type { CurrentSeats } from "@calcom/core/getUserAvailability";
|
|
|
|
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
2022-07-11 23:35:50 +00:00
|
|
|
import dayjs, { Dayjs } from "@calcom/dayjs";
|
2022-07-07 15:26:22 +00:00
|
|
|
import logger from "@calcom/lib/logger";
|
2022-06-15 20:54:31 +00:00
|
|
|
import { availabilityUserSelect } from "@calcom/prisma";
|
2022-06-27 21:01:46 +00:00
|
|
|
import { TimeRange } from "@calcom/types/schedule";
|
2022-06-15 20:54:31 +00:00
|
|
|
|
2022-06-19 15:02:00 +00:00
|
|
|
import isOutOfBounds from "@lib/isOutOfBounds";
|
2022-06-15 20:54:31 +00:00
|
|
|
import getSlots from "@lib/slots";
|
|
|
|
|
|
|
|
import { createRouter } from "@server/createRouter";
|
|
|
|
import { TRPCError } from "@trpc/server";
|
|
|
|
|
|
|
|
const getScheduleSchema = z
|
|
|
|
.object({
|
|
|
|
// startTime ISOString
|
2022-07-02 16:13:39 +00:00
|
|
|
startTime: z.string(),
|
2022-06-15 20:54:31 +00:00
|
|
|
// endTime ISOString
|
2022-07-02 16:13:39 +00:00
|
|
|
endTime: z.string(),
|
2022-06-15 20:54:31 +00:00
|
|
|
// Event type ID
|
|
|
|
eventTypeId: z.number().optional(),
|
2022-06-27 21:01:46 +00:00
|
|
|
// invitee timezone
|
|
|
|
timeZone: z.string().optional(),
|
2022-06-15 20:54:31 +00:00
|
|
|
// or list of users (for dynamic events)
|
|
|
|
usernameList: z.array(z.string()).optional(),
|
2022-07-07 15:26:22 +00:00
|
|
|
debug: z.boolean().optional(),
|
2022-06-15 20:54:31 +00:00
|
|
|
})
|
|
|
|
.refine(
|
|
|
|
(data) => !!data.eventTypeId || !!data.usernameList,
|
|
|
|
"Either usernameList or eventTypeId should be filled in."
|
|
|
|
);
|
|
|
|
|
|
|
|
export type Slot = {
|
|
|
|
time: string;
|
|
|
|
attendees?: number;
|
|
|
|
bookingUid?: string;
|
|
|
|
users?: string[];
|
|
|
|
};
|
|
|
|
|
|
|
|
const checkForAvailability = ({
|
|
|
|
time,
|
|
|
|
busy,
|
|
|
|
eventLength,
|
|
|
|
beforeBufferTime,
|
|
|
|
currentSeats,
|
|
|
|
}: {
|
|
|
|
time: Dayjs;
|
|
|
|
busy: (TimeRange | { start: string; end: string })[];
|
|
|
|
eventLength: number;
|
|
|
|
beforeBufferTime: number;
|
|
|
|
currentSeats?: CurrentSeats;
|
|
|
|
}) => {
|
|
|
|
if (currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString())) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-06-30 00:33:19 +00:00
|
|
|
const slotEndTime = time.add(eventLength, "minutes").utc();
|
2022-07-11 11:00:08 +00:00
|
|
|
const slotStartTime = time.subtract(beforeBufferTime, "minutes").utc();
|
2022-06-30 00:33:19 +00:00
|
|
|
|
|
|
|
return busy.every((busyTime) => {
|
|
|
|
const startTime = dayjs.utc(busyTime.start);
|
|
|
|
const endTime = dayjs.utc(busyTime.end);
|
2022-06-15 20:54:31 +00:00
|
|
|
|
2022-07-11 11:00:08 +00:00
|
|
|
if (endTime.isBefore(slotStartTime) || startTime.isAfter(slotEndTime)) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (slotStartTime.isBetween(startTime, endTime, null, "[)")) {
|
|
|
|
return false;
|
|
|
|
} else if (slotEndTime.isBetween(startTime, endTime, null, "(]")) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2022-06-15 20:54:31 +00:00
|
|
|
// Check if start times are the same
|
2022-06-30 00:33:19 +00:00
|
|
|
if (time.utc().isBetween(startTime, endTime, null, "[)")) {
|
2022-06-15 20:54:31 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
// Check if slot end time is between start and end time
|
|
|
|
else if (slotEndTime.isBetween(startTime, endTime)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
// Check if startTime is between slot
|
|
|
|
else if (startTime.isBetween(time, slotEndTime)) {
|
|
|
|
return false;
|
|
|
|
}
|
2022-06-27 21:01:46 +00:00
|
|
|
|
2022-06-15 20:54:31 +00:00
|
|
|
return true;
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
export const slotsRouter = createRouter().query("getSchedule", {
|
|
|
|
input: getScheduleSchema,
|
|
|
|
async resolve({ input, ctx }) {
|
2022-07-07 15:26:22 +00:00
|
|
|
if (input.debug === true) {
|
|
|
|
logger.setSettings({ minLevel: "debug" });
|
|
|
|
}
|
|
|
|
const startPrismaEventTypeGet = performance.now();
|
2022-06-15 20:54:31 +00:00
|
|
|
const eventType = await ctx.prisma.eventType.findUnique({
|
|
|
|
where: {
|
|
|
|
id: input.eventTypeId,
|
|
|
|
},
|
|
|
|
select: {
|
|
|
|
id: true,
|
|
|
|
minimumBookingNotice: true,
|
|
|
|
length: true,
|
|
|
|
seatsPerTimeSlot: true,
|
|
|
|
timeZone: true,
|
|
|
|
slotInterval: true,
|
|
|
|
beforeEventBuffer: true,
|
|
|
|
afterEventBuffer: true,
|
|
|
|
schedulingType: true,
|
2022-06-19 15:02:00 +00:00
|
|
|
periodType: true,
|
|
|
|
periodStartDate: true,
|
|
|
|
periodEndDate: true,
|
|
|
|
periodCountCalendarDays: true,
|
|
|
|
periodDays: true,
|
2022-06-15 20:54:31 +00:00
|
|
|
schedule: {
|
|
|
|
select: {
|
|
|
|
availability: true,
|
|
|
|
timeZone: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
availability: {
|
|
|
|
select: {
|
|
|
|
startTime: true,
|
|
|
|
endTime: true,
|
|
|
|
days: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
users: {
|
|
|
|
select: {
|
|
|
|
username: true,
|
|
|
|
...availabilityUserSelect,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
2022-07-07 15:26:22 +00:00
|
|
|
const endPrismaEventTypeGet = performance.now();
|
|
|
|
logger.debug(`Prisma eventType get took ${endPrismaEventTypeGet - startPrismaEventTypeGet}ms`);
|
2022-06-15 20:54:31 +00:00
|
|
|
if (!eventType) {
|
|
|
|
throw new TRPCError({ code: "NOT_FOUND" });
|
|
|
|
}
|
|
|
|
|
2022-07-02 16:13:39 +00:00
|
|
|
const startTime =
|
|
|
|
input.timeZone === "Etc/GMT"
|
|
|
|
? dayjs.utc(input.startTime)
|
2022-07-06 14:25:41 +00:00
|
|
|
: dayjs(input.startTime).utc().tz(input.timeZone);
|
2022-07-02 16:13:39 +00:00
|
|
|
const endTime =
|
2022-07-06 14:25:41 +00:00
|
|
|
input.timeZone === "Etc/GMT" ? dayjs.utc(input.endTime) : dayjs(input.endTime).utc().tz(input.timeZone);
|
2022-07-02 16:13:39 +00:00
|
|
|
|
2022-06-15 20:54:31 +00:00
|
|
|
if (!startTime.isValid() || !endTime.isValid()) {
|
|
|
|
throw new TRPCError({ message: "Invalid time range given.", code: "BAD_REQUEST" });
|
|
|
|
}
|
|
|
|
let currentSeats: CurrentSeats | undefined = undefined;
|
|
|
|
|
|
|
|
const userSchedules = await Promise.all(
|
|
|
|
eventType.users.map(async (currentUser) => {
|
|
|
|
const {
|
|
|
|
busy,
|
|
|
|
workingHours,
|
|
|
|
currentSeats: _currentSeats,
|
|
|
|
} = await getUserAvailability(
|
|
|
|
{
|
|
|
|
userId: currentUser.id,
|
|
|
|
dateFrom: startTime.format(),
|
|
|
|
dateTo: endTime.format(),
|
|
|
|
eventTypeId: input.eventTypeId,
|
2022-07-11 11:00:08 +00:00
|
|
|
afterEventBuffer: eventType.afterEventBuffer,
|
2022-06-15 20:54:31 +00:00
|
|
|
},
|
|
|
|
{ user: currentUser, eventType, currentSeats }
|
|
|
|
);
|
|
|
|
if (!currentSeats && _currentSeats) currentSeats = _currentSeats;
|
|
|
|
|
|
|
|
return {
|
|
|
|
workingHours,
|
|
|
|
busy,
|
|
|
|
};
|
|
|
|
})
|
|
|
|
);
|
2022-06-16 10:40:36 +00:00
|
|
|
|
2022-06-15 20:54:31 +00:00
|
|
|
const workingHours = userSchedules.flatMap((s) => s.workingHours);
|
2022-06-27 21:01:46 +00:00
|
|
|
|
2022-06-15 20:54:31 +00:00
|
|
|
const slots: Record<string, Slot[]> = {};
|
|
|
|
const availabilityCheckProps = {
|
|
|
|
eventLength: eventType.length,
|
|
|
|
beforeBufferTime: eventType.beforeEventBuffer,
|
|
|
|
currentSeats,
|
|
|
|
};
|
2022-06-19 15:02:00 +00:00
|
|
|
const isWithinBounds = (_time: Parameters<typeof isOutOfBounds>[0]) =>
|
|
|
|
!isOutOfBounds(_time, {
|
|
|
|
periodType: eventType.periodType,
|
|
|
|
periodStartDate: eventType.periodStartDate,
|
|
|
|
periodEndDate: eventType.periodEndDate,
|
|
|
|
periodCountCalendarDays: eventType.periodCountCalendarDays,
|
|
|
|
periodDays: eventType.periodDays,
|
|
|
|
});
|
2022-06-15 20:54:31 +00:00
|
|
|
|
2022-07-02 16:13:39 +00:00
|
|
|
let time = startTime;
|
2022-07-07 15:26:22 +00:00
|
|
|
let getSlotsTime = 0;
|
|
|
|
let checkForAvailabilityTime = 0;
|
|
|
|
let getSlotsCount = 0;
|
|
|
|
let checkForAvailabilityCount = 0;
|
2022-06-15 20:54:31 +00:00
|
|
|
do {
|
2022-07-07 15:26:22 +00:00
|
|
|
const startGetSlots = performance.now();
|
2022-06-15 20:54:31 +00:00
|
|
|
// get slots retrieves the available times for a given day
|
|
|
|
const times = getSlots({
|
|
|
|
inviteeDate: time,
|
|
|
|
eventLength: eventType.length,
|
|
|
|
workingHours,
|
|
|
|
minimumBookingNotice: eventType.minimumBookingNotice,
|
|
|
|
frequency: eventType.slotInterval || eventType.length,
|
|
|
|
});
|
2022-07-07 15:26:22 +00:00
|
|
|
const endGetSlots = performance.now();
|
|
|
|
getSlotsTime += endGetSlots - startGetSlots;
|
|
|
|
getSlotsCount++;
|
2022-06-15 20:54:31 +00:00
|
|
|
// if ROUND_ROBIN - slots stay available on some() - if normal / COLLECTIVE - slots only stay available on every()
|
2022-06-19 15:02:00 +00:00
|
|
|
const filterStrategy =
|
2022-06-15 20:54:31 +00:00
|
|
|
!eventType.schedulingType || eventType.schedulingType === SchedulingType.COLLECTIVE
|
2022-06-28 22:53:14 +00:00
|
|
|
? ("every" as const)
|
|
|
|
: ("some" as const);
|
2022-07-11 11:00:08 +00:00
|
|
|
|
2022-07-07 15:26:22 +00:00
|
|
|
const filteredTimes = times.filter(isWithinBounds).filter((time) =>
|
|
|
|
userSchedules[filterStrategy]((schedule) => {
|
|
|
|
const startCheckForAvailability = performance.now();
|
|
|
|
const result = checkForAvailability({ time, ...schedule, ...availabilityCheckProps });
|
|
|
|
const endCheckForAvailability = performance.now();
|
|
|
|
checkForAvailabilityCount++;
|
|
|
|
checkForAvailabilityTime += endCheckForAvailability - startCheckForAvailability;
|
|
|
|
return result;
|
|
|
|
})
|
|
|
|
);
|
2022-06-15 20:54:31 +00:00
|
|
|
|
2022-06-29 09:01:30 +00:00
|
|
|
slots[time.format("YYYY-MM-DD")] = filteredTimes.map((time) => ({
|
2022-06-15 20:54:31 +00:00
|
|
|
time: time.toISOString(),
|
|
|
|
users: eventType.users.map((user) => user.username || ""),
|
|
|
|
// Conditionally add the attendees and booking id to slots object if there is already a booking during that time
|
|
|
|
...(currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString()) && {
|
|
|
|
attendees:
|
|
|
|
currentSeats[
|
|
|
|
currentSeats.findIndex((booking) => booking.startTime.toISOString() === time.toISOString())
|
|
|
|
]._count.attendees,
|
|
|
|
bookingUid:
|
|
|
|
currentSeats[
|
|
|
|
currentSeats.findIndex((booking) => booking.startTime.toISOString() === time.toISOString())
|
|
|
|
].uid,
|
|
|
|
}),
|
|
|
|
}));
|
|
|
|
time = time.add(1, "day");
|
2022-06-28 22:53:14 +00:00
|
|
|
} while (time.isBefore(endTime));
|
2022-06-15 20:54:31 +00:00
|
|
|
|
2022-07-07 15:26:22 +00:00
|
|
|
logger.debug(`getSlots took ${getSlotsTime}ms and executed ${getSlotsCount} times`);
|
|
|
|
|
|
|
|
logger.debug(
|
|
|
|
`checkForAvailability took ${checkForAvailabilityTime}ms and executed ${checkForAvailabilityCount} times`
|
|
|
|
);
|
|
|
|
|
2022-06-15 20:54:31 +00:00
|
|
|
return {
|
|
|
|
slots,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
});
|