Skip to content

Commit 3ef3284

Browse files
CarinaWolliCarinaWolli
andauthored
Date override fixes (#8330)
* fix date override for fixed round robin + time zone in date override * check if slot is within working hours of fixed hosts * add test for date override in different time zone * fix date overrides for not fixed hosts (round robin) * code clean up * fix added test * use the correct timezone of user for date overrides --------- Co-authored-by: CarinaWolli <[email protected]>
1 parent c4fe69d commit 3ef3284

File tree

4 files changed

+111
-10
lines changed

4 files changed

+111
-10
lines changed

apps/web/test/lib/getSchedule.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,24 @@ describe("getSchedule", () => {
784784
dateString: plus2DateString,
785785
}
786786
);
787+
788+
const scheduleForEventOnADayWithDateOverrideDifferentTimezone = await getSchedule(
789+
{
790+
eventTypeId: 1,
791+
eventTypeSlug: "",
792+
startTime: `${plus1DateString}T18:30:00.000Z`,
793+
endTime: `${plus2DateString}T18:29:59.999Z`,
794+
timeZone: Timezones["+6:00"],
795+
},
796+
ctx
797+
);
798+
// it should return the same as this is the utc time
799+
expect(scheduleForEventOnADayWithDateOverrideDifferentTimezone).toHaveTimeSlots(
800+
["08:30:00.000Z", "09:30:00.000Z", "10:30:00.000Z", "11:30:00.000Z"],
801+
{
802+
dateString: plus2DateString,
803+
}
804+
);
787805
});
788806

789807
test("that a user is considered busy when there's a booking they host", async () => {

packages/lib/slots.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -208,11 +208,22 @@ const getSlots = ({
208208
});
209209

210210
if (!!activeOverrides.length) {
211-
const overrides = activeOverrides.flatMap((override) => ({
212-
userIds: override.userId ? [override.userId] : [],
213-
startTime: override.start.getUTCHours() * 60 + override.start.getUTCMinutes(),
214-
endTime: override.end.getUTCHours() * 60 + override.end.getUTCMinutes(),
215-
}));
211+
const overrides = activeOverrides.flatMap((override) => {
212+
const organizerUtcOffset = dayjs(override.start.toString()).tz(override.timeZone).utcOffset();
213+
const inviteeUtcOffset = dayjs(override.start.toString()).tz(timeZone).utcOffset();
214+
const offset = inviteeUtcOffset - organizerUtcOffset;
215+
216+
return {
217+
userIds: override.userId ? [override.userId] : [],
218+
startTime:
219+
dayjs(override.start).utc().add(offset, "minute").hour() * 60 +
220+
dayjs(override.start).utc().add(offset, "minute").minute(),
221+
endTime:
222+
dayjs(override.end).utc().add(offset, "minute").hour() * 60 +
223+
dayjs(override.end).utc().add(offset, "minute").minute(),
224+
};
225+
});
226+
216227
// unset all working hours that relate to this user availability override
217228
overrides.forEach((override) => {
218229
let i = -1;

packages/trpc/server/routers/viewer/slots.ts

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type prisma from "@calcom/prisma";
1919
import { availabilityUserSelect } from "@calcom/prisma";
2020
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
2121
import type { EventBusyDate } from "@calcom/types/Calendar";
22+
import type { WorkingHours } from "@calcom/types/schedule";
2223

2324
import { TRPCError } from "@trpc/server";
2425

@@ -75,12 +76,21 @@ const checkIfIsAvailable = ({
7576
time,
7677
busy,
7778
eventLength,
79+
dateOverrides = [],
80+
workingHours = [],
7881
currentSeats,
82+
organizerTimeZone,
7983
}: {
8084
time: Dayjs;
8185
busy: EventBusyDate[];
8286
eventLength: number;
87+
dateOverrides?: {
88+
start: Date;
89+
end: Date;
90+
}[];
91+
workingHours?: WorkingHours[];
8392
currentSeats?: CurrentSeats;
93+
organizerTimeZone?: string;
8494
}): boolean => {
8595
if (currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString())) {
8696
return true;
@@ -89,6 +99,57 @@ const checkIfIsAvailable = ({
8999
const slotEndTime = time.add(eventLength, "minutes").utc();
90100
const slotStartTime = time.utc();
91101

102+
//check if date override for slot exists
103+
let dateOverrideExist = false;
104+
105+
if (
106+
dateOverrides.find((date) => {
107+
const utcOffset = organizerTimeZone ? dayjs.tz(date.start, organizerTimeZone).utcOffset() * -1 : 0;
108+
109+
if (
110+
dayjs(date.start).add(utcOffset, "minutes").format("YYYY MM DD") ===
111+
slotStartTime.format("YYYY MM DD")
112+
) {
113+
dateOverrideExist = true;
114+
if (dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes")) {
115+
return true;
116+
}
117+
if (
118+
slotEndTime.isBefore(dayjs(date.start).add(utcOffset, "minutes")) ||
119+
slotEndTime.isSame(dayjs(date.start).add(utcOffset, "minutes"))
120+
) {
121+
return true;
122+
}
123+
if (slotStartTime.isAfter(dayjs(date.end).add(utcOffset, "minutes"))) {
124+
return true;
125+
}
126+
}
127+
})
128+
) {
129+
// slot is not within the date override
130+
return false;
131+
}
132+
133+
if (dateOverrideExist) {
134+
return true;
135+
}
136+
137+
//if no date override for slot exists check if it is within normal work hours
138+
if (
139+
workingHours.find((workingHour) => {
140+
if (workingHour.days.includes(slotStartTime.day())) {
141+
const start = slotStartTime.hour() * 60 + slotStartTime.minute();
142+
const end = slotStartTime.hour() * 60 + slotStartTime.minute();
143+
if (start < workingHour.startTime || end > workingHour.endTime) {
144+
return true;
145+
}
146+
}
147+
})
148+
) {
149+
// slot is outside of working hours
150+
return false;
151+
}
152+
92153
return busy.every((busyTime) => {
93154
const startTime = dayjs.utc(busyTime.start).utc();
94155
const endTime = dayjs.utc(busyTime.end);
@@ -115,7 +176,6 @@ const checkIfIsAvailable = ({
115176
else if (startTime.isBetween(time, slotEndTime)) {
116177
return false;
117178
}
118-
119179
return true;
120180
});
121181
};
@@ -348,7 +408,11 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
348408
);
349409
// flattens availability of multiple users
350410
const dateOverrides = userAvailability.flatMap((availability) =>
351-
availability.dateOverrides.map((override) => ({ userId: availability.user.id, ...override }))
411+
availability.dateOverrides.map((override) => ({
412+
userId: availability.user.id,
413+
timeZone: availability.timeZone,
414+
...override,
415+
}))
352416
);
353417
const workingHours = getAggregateWorkingHours(userAvailability, eventType.schedulingType);
354418
const availabilityCheckProps = {
@@ -372,6 +436,9 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
372436

373437
const timeSlots: ReturnType<typeof getTimeSlots> = [];
374438

439+
const organizerTimeZone =
440+
eventType.timeZone || eventType?.schedule?.timeZone || userAvailability?.[0]?.timeZone;
441+
375442
for (
376443
let currentCheckedTime = startTime;
377444
currentCheckedTime.isBefore(endTime);
@@ -386,8 +453,7 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
386453
dateOverrides,
387454
minimumBookingNotice: eventType.minimumBookingNotice,
388455
frequency: eventType.slotInterval || input.duration || eventType.length,
389-
organizerTimeZone:
390-
eventType.timeZone || eventType?.schedule?.timeZone || userAvailability?.[0]?.timeZone,
456+
organizerTimeZone,
391457
})
392458
);
393459
}
@@ -423,13 +489,15 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
423489
time: slot.time,
424490
...schedule,
425491
...availabilityCheckProps,
492+
organizerTimeZone: schedule.timeZone,
426493
});
427494
const endCheckForAvailability = performance.now();
428495
checkForAvailabilityCount++;
429496
checkForAvailabilityTime += endCheckForAvailability - startCheckForAvailability;
430497
return isAvailable;
431498
});
432499
});
500+
433501
// what else are you going to call it?
434502
const looseHostAvailability = userAvailability.filter(({ user: { isFixed } }) => !isFixed);
435503
if (looseHostAvailability.length > 0) {
@@ -446,6 +514,7 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
446514
time: slot.time,
447515
...userSchedule,
448516
...availabilityCheckProps,
517+
organizerTimeZone: userSchedule.timeZone,
449518
});
450519
});
451520
return slot;
@@ -507,17 +576,19 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx:
507576
return false;
508577
}
509578

579+
const userSchedule = userAvailability.find(({ user: { id: userId } }) => userId === slotUserId);
580+
510581
return checkIfIsAvailable({
511582
time: slot.time,
512583
busy,
513584
...availabilityCheckProps,
585+
organizerTimeZone: userSchedule?.timeZone,
514586
});
515587
});
516588
return slot;
517589
})
518590
.filter((slot) => !!slot.userIds?.length);
519591
}
520-
521592
availableTimeSlots = availableTimeSlots.filter((slot) => isTimeWithinBounds(slot.time));
522593

523594
const computedAvailableSlots = availableTimeSlots.reduce(

packages/types/schedule.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export type TimeRange = {
22
userId?: number | null;
33
start: Date;
44
end: Date;
5+
timeZone?: string;
56
};
67

78
export type Schedule = TimeRange[][];

0 commit comments

Comments
 (0)