Fix/Round Robin (#6121)
* Identify owner for a timeslot and check busyness against his schedule only * Fix TS errors * Fix flatmap * Fix xisting unit tests * Prevent duplicate slots by merging, userId -> userIds (multi) * Small fix to potential undefined * Moved duplicate prevention to buildSlots function * Apply date override on a per user basis * Prevent -1 being added to computedLocalAvailability * Removed console.log * Apply override properly on COLLECTIVE * Default timeZone to UTC when undefined * isSame doesn't work when the day shifts, isBetween instead * Iterate over all slot.userIds schedules to identify the first user schedule that matches * add round-robin test Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
co-authored by
Alex van Andel
Peer Richelsen
parent
ed02c10f54
commit
0e0a78c47e
@@ -101,7 +101,8 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={dayjs(slot.time).format()}>
|
||||
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
|
||||
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */}
|
||||
{/* Current there is no way to disable Next.js Links */}
|
||||
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
||||
<div
|
||||
|
||||
@@ -8,17 +8,17 @@ MockDate.set("2021-06-20T11:59:59Z");
|
||||
const HAWAII_AND_NEWYORK_TEAM = [
|
||||
{
|
||||
timeZone: "America/Detroit", // GMT -4 per 22th of Aug, 2022
|
||||
workingHours: [{ days: [1, 2, 3, 4, 5], startTime: 780, endTime: 1260 }],
|
||||
workingHours: [{ userId: 1, days: [1, 2, 3, 4, 5], startTime: 780, endTime: 1260 }],
|
||||
busy: [],
|
||||
dateOverrides: [],
|
||||
},
|
||||
{
|
||||
timeZone: "Pacific/Honolulu", // GMT -10 per 22th of Aug, 2022
|
||||
workingHours: [
|
||||
{ days: [3, 4, 5], startTime: 0, endTime: 360 },
|
||||
{ days: [6], startTime: 0, endTime: 180 },
|
||||
{ days: [2, 3, 4], startTime: 780, endTime: 1439 },
|
||||
{ days: [5], startTime: 780, endTime: 1439 },
|
||||
{ userId: 1, days: [3, 4, 5], startTime: 0, endTime: 360 },
|
||||
{ userId: 2, days: [6], startTime: 0, endTime: 180 },
|
||||
{ userId: 3, days: [2, 3, 4], startTime: 780, endTime: 1439 },
|
||||
{ userId: 4, days: [5], startTime: 780, endTime: 1439 },
|
||||
],
|
||||
busy: [],
|
||||
dateOverrides: [],
|
||||
@@ -44,6 +44,7 @@ it("Sydney and Shiraz can live in harmony 🙏", async () => {
|
||||
],
|
||||
"endTime": 180,
|
||||
"startTime": 0,
|
||||
"userId": 2,
|
||||
},
|
||||
Object {
|
||||
"days": Array [
|
||||
@@ -63,4 +64,57 @@ it("Sydney and Shiraz can live in harmony 🙏", async () => {
|
||||
},
|
||||
]
|
||||
`);
|
||||
|
||||
expect(getAggregateWorkingHours(HAWAII_AND_NEWYORK_TEAM, "ROUND_ROBIN")).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"days": Array [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
],
|
||||
"endTime": 1260,
|
||||
"startTime": 780,
|
||||
"userId": 1,
|
||||
},
|
||||
Object {
|
||||
"days": Array [
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
],
|
||||
"endTime": 360,
|
||||
"startTime": 0,
|
||||
"userId": 1,
|
||||
},
|
||||
Object {
|
||||
"days": Array [
|
||||
6,
|
||||
],
|
||||
"endTime": 180,
|
||||
"startTime": 0,
|
||||
"userId": 2,
|
||||
},
|
||||
Object {
|
||||
"days": Array [
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
],
|
||||
"endTime": 1439,
|
||||
"startTime": 780,
|
||||
"userId": 3,
|
||||
},
|
||||
Object {
|
||||
"days": Array [
|
||||
5,
|
||||
],
|
||||
"endTime": 1439,
|
||||
"startTime": 780,
|
||||
"userId": 4,
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ describe("Tests the slot logic", () => {
|
||||
minimumBookingNotice: 0,
|
||||
workingHours: [
|
||||
{
|
||||
userId: 1,
|
||||
days: Array.from(Array(7).keys()),
|
||||
startTime: MINUTES_DAY_START,
|
||||
endTime: MINUTES_DAY_END,
|
||||
@@ -38,6 +39,7 @@ describe("Tests the slot logic", () => {
|
||||
minimumBookingNotice: 0,
|
||||
workingHours: [
|
||||
{
|
||||
userId: 1,
|
||||
days: Array.from(Array(7).keys()),
|
||||
startTime: MINUTES_DAY_START,
|
||||
endTime: MINUTES_DAY_END,
|
||||
@@ -56,6 +58,7 @@ describe("Tests the slot logic", () => {
|
||||
minimumBookingNotice: 0,
|
||||
workingHours: [
|
||||
{
|
||||
userId: 1,
|
||||
days: [0],
|
||||
startTime: 23 * 60, // 23h
|
||||
endTime: MINUTES_DAY_END,
|
||||
@@ -69,6 +72,7 @@ describe("Tests the slot logic", () => {
|
||||
it("can cut off dates that due to invitee timezone differences fall on the previous day", async () => {
|
||||
const workingHours = [
|
||||
{
|
||||
userId: 1,
|
||||
days: [0],
|
||||
startTime: MINUTES_DAY_START,
|
||||
endTime: 1 * 60, // 1h
|
||||
@@ -94,6 +98,7 @@ describe("Tests the slot logic", () => {
|
||||
minimumBookingNotice: 1500,
|
||||
workingHours: [
|
||||
{
|
||||
userId: 1,
|
||||
days: Array.from(Array(7).keys()),
|
||||
startTime: MINUTES_DAY_START,
|
||||
endTime: MINUTES_DAY_END,
|
||||
|
||||
@@ -225,13 +225,19 @@ export async function getUserAvailability(
|
||||
}
|
||||
}
|
||||
|
||||
const userSchedule = currentUser.schedules.filter(
|
||||
(schedule) => !currentUser.defaultScheduleId || schedule.id === currentUser.defaultScheduleId
|
||||
)[0];
|
||||
|
||||
const schedule =
|
||||
!eventType?.metadata?.config?.useHostSchedulesForTeamEvent && eventType?.schedule
|
||||
? { ...eventType?.schedule }
|
||||
: {
|
||||
...currentUser.schedules.filter(
|
||||
(schedule) => !currentUser.defaultScheduleId || schedule.id === currentUser.defaultScheduleId
|
||||
)[0],
|
||||
...userSchedule,
|
||||
availability: userSchedule.availability.map((a) => ({
|
||||
...a,
|
||||
userId: currentUser.id,
|
||||
})),
|
||||
};
|
||||
|
||||
const startGetWorkingHours = performance.now();
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function TeamAvailabilityTimes(props: Props) {
|
||||
}
|
||||
);
|
||||
|
||||
const times = !isLoading
|
||||
const slots = !isLoading
|
||||
? getSlots({
|
||||
frequency: props.frequency,
|
||||
inviteeDate: props.selectedDate,
|
||||
@@ -44,18 +44,18 @@ export default function TeamAvailabilityTimes(props: Props) {
|
||||
return (
|
||||
<div className={classNames("min-w-60 flex-grow p-5 pl-0", props.className)}>
|
||||
{props.HeaderComponent}
|
||||
{isLoading && times.length === 0 && <Loader />}
|
||||
{!isLoading && times.length === 0 && (
|
||||
{isLoading && slots.length === 0 && <Loader />}
|
||||
{!isLoading && slots.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center pt-4">
|
||||
<span className="text-sm text-gray-500">No Available Slots</span>
|
||||
</div>
|
||||
)}
|
||||
{times.map((time) => (
|
||||
<div key={time.format()} className="flex flex-row items-center">
|
||||
{slots.map((slot) => (
|
||||
<div key={slot.time.format()} className="flex flex-row items-center">
|
||||
<a
|
||||
className="min-w-48 border-brand text-bookingdarker hover:bg-brand hover:text-brandcontrast dark:hover:bg-darkmodebrand dark:hover:text-darkmodebrandcontrast mb-2 mr-3 block flex-grow rounded-sm border bg-white py-2 text-center font-medium dark:border-transparent dark:bg-gray-600 dark:text-neutral-200 dark:hover:border-black dark:hover:bg-black dark:hover:text-white"
|
||||
data-testid="time">
|
||||
{time.format("HH:mm")}
|
||||
{slot.time.format("HH:mm")}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function TeamAvailabilityTimes(props: Props) {
|
||||
}
|
||||
);
|
||||
|
||||
const times = !isLoading
|
||||
const slots = !isLoading
|
||||
? getSlots({
|
||||
frequency: props.frequency,
|
||||
inviteeDate: props.selectedDate,
|
||||
@@ -48,8 +48,8 @@ export default function TeamAvailabilityTimes(props: Props) {
|
||||
return (
|
||||
<div className={classNames("min-w-60 flex-grow pl-0", props.className)}>
|
||||
{props.HeaderComponent}
|
||||
{isLoading && times.length === 0 && <SkeletonLoader />}
|
||||
{!isLoading && times.length === 0 ? (
|
||||
{isLoading && slots.length === 0 && <SkeletonLoader />}
|
||||
{!isLoading && slots.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center pt-4">
|
||||
<span className="text-sm text-gray-500">{t("no_available_slots")}</span>
|
||||
</div>
|
||||
@@ -57,12 +57,12 @@ export default function TeamAvailabilityTimes(props: Props) {
|
||||
<>{!isLoading && <p className="mb-3 text-sm text-gray-600">{t("time_available")}</p>}</>
|
||||
)}
|
||||
<div className="max-h-[390px] overflow-scroll">
|
||||
{times.map((time) => (
|
||||
<div key={time.format()} className="flex flex-row items-center ">
|
||||
{slots.map((slot) => (
|
||||
<div key={slot.time.format()} className="flex flex-row items-center ">
|
||||
<a
|
||||
className="min-w-48 border-brand text-bookingdarker mb-2 mr-3 block flex-grow rounded-md border bg-white py-2 text-center font-medium dark:border-transparent dark:bg-gray-600 dark:text-neutral-200 "
|
||||
data-testid="time">
|
||||
{time.tz(props.selectedTimeZone.toString()).format("HH:mm")}
|
||||
{slot.time.tz(props.selectedTimeZone.toString()).format("HH:mm")}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -63,7 +63,7 @@ export function getWorkingHours(
|
||||
timeZone?: string;
|
||||
utcOffset?: number;
|
||||
},
|
||||
availability: { days: number[]; startTime: ConfigType; endTime: ConfigType }[]
|
||||
availability: { userId?: number | null; days: number[]; startTime: ConfigType; endTime: ConfigType }[]
|
||||
) {
|
||||
if (!availability.length) {
|
||||
return [];
|
||||
@@ -90,28 +90,34 @@ export function getWorkingHours(
|
||||
return currentWorkingHours;
|
||||
}
|
||||
if (sameDayStartTime !== sameDayEndTime) {
|
||||
currentWorkingHours.push({
|
||||
const newWorkingHours: WorkingHours = {
|
||||
days: schedule.days,
|
||||
startTime: sameDayStartTime,
|
||||
endTime: sameDayEndTime,
|
||||
});
|
||||
};
|
||||
if (schedule.userId) newWorkingHours.userId = schedule.userId;
|
||||
currentWorkingHours.push(newWorkingHours);
|
||||
}
|
||||
// check for overflow to the previous day
|
||||
// overflowing days constraint to 0-6 day range (Sunday-Saturday)
|
||||
if (startTime < MINUTES_DAY_START || endTime < MINUTES_DAY_START) {
|
||||
currentWorkingHours.push({
|
||||
const newWorkingHours: WorkingHours = {
|
||||
days: schedule.days.map((day) => (day - 1 >= 0 ? day - 1 : 6)),
|
||||
startTime: startTime + MINUTES_IN_DAY,
|
||||
endTime: Math.min(endTime + MINUTES_IN_DAY, MINUTES_DAY_END),
|
||||
});
|
||||
};
|
||||
if (schedule.userId) newWorkingHours.userId = schedule.userId;
|
||||
currentWorkingHours.push(newWorkingHours);
|
||||
}
|
||||
// else, check for overflow in the next day
|
||||
else if (startTime > MINUTES_DAY_END || endTime > MINUTES_IN_DAY) {
|
||||
currentWorkingHours.push({
|
||||
const newWorkingHours: WorkingHours = {
|
||||
days: schedule.days.map((day) => (day + 1) % 7),
|
||||
startTime: Math.max(startTime - MINUTES_IN_DAY, MINUTES_DAY_START),
|
||||
endTime: endTime - MINUTES_IN_DAY,
|
||||
});
|
||||
};
|
||||
if (schedule.userId) newWorkingHours.userId = schedule.userId;
|
||||
currentWorkingHours.push(newWorkingHours);
|
||||
}
|
||||
|
||||
return currentWorkingHours;
|
||||
|
||||
+59
-27
@@ -11,7 +11,7 @@ export type GetSlots = {
|
||||
minimumBookingNotice: number;
|
||||
eventLength: number;
|
||||
};
|
||||
export type TimeFrame = { startTime: number; endTime: number };
|
||||
export type TimeFrame = { userIds?: number[]; startTime: number; endTime: number };
|
||||
|
||||
const minimumOfOne = (input: number) => (input < 1 ? 1 : input);
|
||||
|
||||
@@ -66,14 +66,21 @@ function buildSlots({
|
||||
const slotsTimeFrameAvailable: TimeFrame[] = [];
|
||||
|
||||
computedLocalAvailability.forEach((item) => {
|
||||
slotsTimeFrameAvailable.push(...splitAvailableTime(item.startTime, item.endTime, frequency, eventLength));
|
||||
const userSlotsTimeFrameAvailable = splitAvailableTime(
|
||||
item.startTime,
|
||||
item.endTime,
|
||||
frequency,
|
||||
eventLength
|
||||
).map((slot) => ({ ...slot, userIds: item.userIds }));
|
||||
|
||||
slotsTimeFrameAvailable.push(...userSlotsTimeFrameAvailable);
|
||||
});
|
||||
|
||||
const slots: Dayjs[] = [];
|
||||
|
||||
const slots: { [x: string]: { time: Dayjs; userIds?: number[] } } = {};
|
||||
slotsTimeFrameAvailable.forEach((item) => {
|
||||
// XXX: Hack alert, as dayjs is supposedly not aware of timezone the current slot may have invalid UTC offset.
|
||||
const timeZone = (startOfInviteeDay as unknown as { $x: { $timezone: string } })["$x"]["$timezone"];
|
||||
const timeZone =
|
||||
(startOfInviteeDay as unknown as { $x: { $timezone: string } })["$x"]["$timezone"] || "UTC";
|
||||
/*
|
||||
* @calcom/web:dev: 2022-11-06T00:00:00-04:00
|
||||
* @calcom/web:dev: 2022-11-06T01:00:00-04:00
|
||||
@@ -82,21 +89,30 @@ function buildSlots({
|
||||
* @calcom/web:dev: 2022-11-06T03:00:00-04:00
|
||||
* ...
|
||||
*/
|
||||
let slot = dayjs.tz(
|
||||
startOfInviteeDay.add(item.startTime, "minute").format("YYYY-MM-DDTHH:mm:ss"),
|
||||
timeZone
|
||||
);
|
||||
const slot = {
|
||||
userIds: item.userIds,
|
||||
time: dayjs.tz(startOfInviteeDay.add(item.startTime, "minute").format("YYYY-MM-DDTHH:mm:ss"), timeZone),
|
||||
};
|
||||
// If the startOfInviteeDay has a different UTC offset than the slot, a DST change has occurred.
|
||||
// As the time has now fallen backwards, or forwards; this difference -
|
||||
// needs to be manually added as this is not done for us. Usually 0.
|
||||
slot = slot.add(startOfInviteeDay.utcOffset() - slot.utcOffset(), "minutes");
|
||||
// Validating slot its not on the past
|
||||
if (!slot.isBefore(startDate)) {
|
||||
slots.push(slot);
|
||||
slot.time = slot.time.add(startOfInviteeDay.utcOffset() - slot.time.utcOffset(), "minutes");
|
||||
|
||||
if (slots[slot.time.format()]) {
|
||||
slots[slot.time.format()] = {
|
||||
...slot,
|
||||
userIds: [...(slots[slot.time.format()].userIds || []), ...(item.userIds || [])],
|
||||
};
|
||||
return;
|
||||
}
|
||||
// Validating slot its not on the past
|
||||
if (slot.time.isBefore(startDate)) {
|
||||
return;
|
||||
}
|
||||
slots[slot.time.format()] = slot;
|
||||
});
|
||||
|
||||
return slots;
|
||||
return Object.values(slots);
|
||||
}
|
||||
|
||||
const getSlots = ({
|
||||
@@ -131,19 +147,8 @@ const getSlots = ({
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const timeZone: string = (inviteeDate as any)["$x"]["$timezone"];
|
||||
|
||||
// an override precedes all the local working hour availability logic.
|
||||
const activeOverrides = dateOverrides.filter((override) =>
|
||||
dayjs.utc(override.start).tz(timeZone).isSame(startOfInviteeDay, "day")
|
||||
);
|
||||
if (!!activeOverrides.length) {
|
||||
const computedLocalAvailability = activeOverrides.flatMap((override) => ({
|
||||
startTime: override.start.getUTCHours() * 60 + override.start.getUTCMinutes(),
|
||||
endTime: override.end.getUTCHours() * 60 + override.end.getUTCMinutes(),
|
||||
}));
|
||||
return buildSlots({ computedLocalAvailability, startDate, startOfInviteeDay, eventLength, frequency });
|
||||
}
|
||||
|
||||
const workingHoursUTC = workingHours.map((schedule) => ({
|
||||
userId: schedule.userId,
|
||||
days: schedule.days,
|
||||
startTime: /* Why? */ startOfDayUTC.add(schedule.startTime, "minute"),
|
||||
endTime: /* Why? */ startOfDayUTC.add(schedule.endTime, "minute"),
|
||||
@@ -162,6 +167,7 @@ const getSlots = ({
|
||||
let tempComputeTimeFrame: TimeFrame | undefined;
|
||||
const computeLength = localWorkingHours.length - 1;
|
||||
const makeTimeFrame = (item: typeof localWorkingHours[0]): TimeFrame => ({
|
||||
userIds: item.userId ? [item.userId] : [],
|
||||
startTime: item.startTime,
|
||||
endTime: item.endTime,
|
||||
});
|
||||
@@ -183,8 +189,34 @@ const getSlots = ({
|
||||
computedLocalAvailability.push(tempComputeTimeFrame);
|
||||
}
|
||||
});
|
||||
// an override precedes all the local working hour availability logic.
|
||||
const activeOverrides = dateOverrides.filter((override) => {
|
||||
return dayjs.utc(override.start).isBetween(startOfInviteeDay, startOfInviteeDay.endOf("day"), null, "[)");
|
||||
});
|
||||
|
||||
return buildSlots({ computedLocalAvailability, startOfInviteeDay, startDate, frequency, eventLength });
|
||||
if (!!activeOverrides.length) {
|
||||
const overrides = activeOverrides.flatMap((override) => ({
|
||||
userIds: override.userId ? [override.userId] : [],
|
||||
startTime: override.start.getUTCHours() * 60 + override.start.getUTCMinutes(),
|
||||
endTime: override.end.getUTCHours() * 60 + override.end.getUTCMinutes(),
|
||||
}));
|
||||
overrides.forEach((override) => {
|
||||
const index = computedLocalAvailability.findIndex(
|
||||
(a) => !a.userIds?.length || (override.userIds[0] && a.userIds?.includes(override.userIds[0]))
|
||||
);
|
||||
if (index >= 0) {
|
||||
computedLocalAvailability[index] = override;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return buildSlots({
|
||||
computedLocalAvailability,
|
||||
startOfInviteeDay,
|
||||
startDate,
|
||||
frequency,
|
||||
eventLength,
|
||||
});
|
||||
};
|
||||
|
||||
export default getSlots;
|
||||
|
||||
@@ -13,7 +13,6 @@ import getTimeSlots from "@calcom/lib/slots";
|
||||
import prisma, { availabilityUserSelect } from "@calcom/prisma";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { EventBusyDate } from "@calcom/types/Calendar";
|
||||
import { TimeRange } from "@calcom/types/schedule";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
@@ -47,6 +46,7 @@ const getScheduleSchema = z
|
||||
|
||||
export type Slot = {
|
||||
time: string;
|
||||
userIds?: number[];
|
||||
attendees?: number;
|
||||
bookingUid?: string;
|
||||
users?: string[];
|
||||
@@ -255,12 +255,16 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
|
||||
workingHours,
|
||||
dateOverrides,
|
||||
busy,
|
||||
userId: currentUser.id,
|
||||
};
|
||||
})
|
||||
);
|
||||
// flattens availability of multiple users
|
||||
const dateOverrides = userAvailability.flatMap((availability) => availability.dateOverrides);
|
||||
const dateOverrides = userAvailability.flatMap((availability) =>
|
||||
availability.dateOverrides.map((override) => ({ userId: availability.userId, ...override }))
|
||||
);
|
||||
const workingHours = getAggregateWorkingHours(userAvailability, eventType.schedulingType);
|
||||
|
||||
const computedAvailableSlots: Record<string, Slot[]> = {};
|
||||
const availabilityCheckProps = {
|
||||
eventLength: eventType.length,
|
||||
@@ -281,7 +285,6 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
|
||||
let checkForAvailabilityTime = 0;
|
||||
let getSlotsCount = 0;
|
||||
let checkForAvailabilityCount = 0;
|
||||
|
||||
do {
|
||||
const startGetSlots = performance.now();
|
||||
// get slots retrieves the available times for a given day
|
||||
@@ -297,38 +300,63 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
|
||||
const endGetSlots = performance.now();
|
||||
getSlotsTime += endGetSlots - startGetSlots;
|
||||
getSlotsCount++;
|
||||
// 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 availableTimeSlots = timeSlots.filter(isTimeWithinBounds).filter((time) =>
|
||||
userAvailability[filterStrategy]((schedule) => {
|
||||
const startCheckForAvailability = performance.now();
|
||||
const isAvailable = checkIfIsAvailable({ time, ...schedule, ...availabilityCheckProps });
|
||||
const endCheckForAvailability = performance.now();
|
||||
checkForAvailabilityCount++;
|
||||
checkForAvailabilityTime += endCheckForAvailability - startCheckForAvailability;
|
||||
return isAvailable;
|
||||
const isCollective = !eventType.schedulingType || eventType.schedulingType === SchedulingType.COLLECTIVE;
|
||||
|
||||
const availableTimeSlots = timeSlots
|
||||
.filter((slot) => isTimeWithinBounds(slot.time))
|
||||
.filter((slot) =>
|
||||
isCollective
|
||||
? // The slot should be available for every user
|
||||
userAvailability.every((schedule) => {
|
||||
const startCheckForAvailability = performance.now();
|
||||
const isAvailable = checkIfIsAvailable({
|
||||
time: slot.time,
|
||||
...schedule,
|
||||
...availabilityCheckProps,
|
||||
});
|
||||
const endCheckForAvailability = performance.now();
|
||||
checkForAvailabilityCount++;
|
||||
checkForAvailabilityTime += endCheckForAvailability - startCheckForAvailability;
|
||||
return isAvailable;
|
||||
})
|
||||
: (() => {
|
||||
// The slot should be available for the atleast one of the slot owners.
|
||||
return slot.userIds?.some((slotUserId) => {
|
||||
const userSchedule = userAvailability.find(({ userId }) => userId === slotUserId);
|
||||
if (!userSchedule) {
|
||||
throw new TRPCError({
|
||||
message: "Shouldn't happen that we don't have a matching user schedule here",
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
});
|
||||
}
|
||||
return checkIfIsAvailable({
|
||||
time: slot.time,
|
||||
...userSchedule,
|
||||
...availabilityCheckProps,
|
||||
});
|
||||
});
|
||||
})()
|
||||
);
|
||||
|
||||
computedAvailableSlots[currentCheckedTime.format("YYYY-MM-DD")] = availableTimeSlots.map(
|
||||
({ time: time, ...passThroughProps }) => ({
|
||||
...passThroughProps,
|
||||
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,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
computedAvailableSlots[currentCheckedTime.format("YYYY-MM-DD")] = availableTimeSlots.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,
|
||||
}),
|
||||
}));
|
||||
currentCheckedTime = currentCheckedTime.add(1, "day");
|
||||
} while (currentCheckedTime.isBefore(endTime));
|
||||
|
||||
|
||||
Vendored
+2
@@ -1,4 +1,5 @@
|
||||
export type TimeRange = {
|
||||
userId?: number | null;
|
||||
start: Date;
|
||||
end: Date;
|
||||
};
|
||||
@@ -15,4 +16,5 @@ export type WorkingHours = {
|
||||
days: number[];
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
userId?: number | null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user