cal.pub0.org/apps/web/server/routers/viewer/slots.tsx

248 lines
7.4 KiB
TypeScript

import { SchedulingType } from "@prisma/client";
import { z } from "zod";
import type { CurrentSeats } from "@calcom/core/getUserAvailability";
import { getUserAvailability } from "@calcom/core/getUserAvailability";
import dayjs, { Dayjs } from "@calcom/dayjs";
import { availabilityUserSelect } from "@calcom/prisma";
import { stringToDayjs } from "@calcom/prisma/zod-utils";
import { TimeRange } from "@calcom/types/schedule";
import isOutOfBounds from "@lib/isOutOfBounds";
import getSlots from "@lib/slots";
import { createRouter } from "@server/createRouter";
import { TRPCError } from "@trpc/server";
const getScheduleSchema = z
.object({
// startTime ISOString
startTime: stringToDayjs,
// endTime ISOString
endTime: stringToDayjs,
// Event type ID
eventTypeId: z.number().optional(),
// invitee timezone
timeZone: z.string().optional(),
// or list of users (for dynamic events)
usernameList: z.array(z.string()).optional(),
})
.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,
afterBufferTime,
currentSeats,
}: {
time: Dayjs;
busy: (TimeRange | { start: string; end: string })[];
eventLength: number;
beforeBufferTime: number;
afterBufferTime: number;
currentSeats?: CurrentSeats;
}) => {
if (currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString())) {
return true;
}
const slotEndTime = time.add(eventLength, "minutes").utc();
const slotStartTimeWithBeforeBuffer = time.subtract(beforeBufferTime, "minutes").utc();
const slotEndTimeWithAfterBuffer = time.add(eventLength + afterBufferTime, "minutes").utc();
return busy.every((busyTime) => {
const startTime = dayjs.utc(busyTime.start);
const endTime = dayjs.utc(busyTime.end);
// Check if start times are the same
if (time.utc().isBetween(startTime, endTime, null, "[)")) {
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;
}
// Check if timeslot has before buffer time space free
else if (
slotStartTimeWithBeforeBuffer.isBetween(
startTime.subtract(beforeBufferTime, "minutes"),
endTime.add(afterBufferTime, "minutes")
)
) {
return false;
}
// Check if timeslot has after buffer time space free
else if (
slotEndTimeWithAfterBuffer.isBetween(
startTime.subtract(beforeBufferTime, "minutes"),
endTime.add(afterBufferTime, "minutes")
)
) {
return false;
}
return true;
});
};
export const slotsRouter = createRouter().query("getSchedule", {
input: getScheduleSchema,
async resolve({ input, ctx }) {
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,
periodType: true,
periodStartDate: true,
periodEndDate: true,
periodCountCalendarDays: true,
periodDays: true,
schedule: {
select: {
availability: true,
timeZone: true,
},
},
availability: {
select: {
startTime: true,
endTime: true,
days: true,
},
},
users: {
select: {
username: true,
...availabilityUserSelect,
},
},
},
});
if (!eventType) {
throw new TRPCError({ code: "NOT_FOUND" });
}
const { startTime, endTime } = input;
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,
},
{ user: currentUser, eventType, currentSeats }
);
if (!currentSeats && _currentSeats) currentSeats = _currentSeats;
return {
workingHours,
busy,
};
})
);
const workingHours = userSchedules.flatMap((s) => s.workingHours);
const slots: Record<string, Slot[]> = {};
const availabilityCheckProps = {
eventLength: eventType.length,
beforeBufferTime: eventType.beforeEventBuffer,
afterBufferTime: eventType.afterEventBuffer,
currentSeats,
};
const isWithinBounds = (_time: Parameters<typeof isOutOfBounds>[0]) =>
!isOutOfBounds(_time, {
periodType: eventType.periodType,
periodStartDate: eventType.periodStartDate,
periodEndDate: eventType.periodEndDate,
periodCountCalendarDays: eventType.periodCountCalendarDays,
periodDays: eventType.periodDays,
});
let time = input.timeZone === "Etc/GMT" ? startTime.utc() : startTime.tz(input.timeZone);
do {
// 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,
});
// if ROUND_ROBIN - slots stay available on some() - if normal / COLLECTIVE - slots only stay available on every()
const filterStrategy =
!eventType.schedulingType || eventType.schedulingType === SchedulingType.COLLECTIVE
? ("every" as const)
: ("some" as const);
const filteredTimes = times
.filter(isWithinBounds)
.filter((time) =>
userSchedules[filterStrategy]((schedule) =>
checkForAvailability({ time, ...schedule, ...availabilityCheckProps })
)
);
slots[time.format("YYYY-MM-DD")] = filteredTimes.map((time) => ({
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");
} while (time.isBefore(endTime));
return {
slots,
};
},
});