From 754e63e27d002fcd6c27c5043ab2cb95108e1a05 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Thu, 13 Apr 2023 15:05:50 +0200 Subject: [PATCH 1/7] fix date override for fixed round robin + time zone in date override --- packages/lib/slots.ts | 21 ++++++-- packages/trpc/server/routers/viewer/slots.tsx | 52 ++++++++++++++++++- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/packages/lib/slots.ts b/packages/lib/slots.ts index 4d65d228af7d32..41c8603e1f0091 100644 --- a/packages/lib/slots.ts +++ b/packages/lib/slots.ts @@ -208,11 +208,22 @@ const getSlots = ({ }); 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(), - })); + const overrides = activeOverrides.flatMap((override) => { + const organizerUtcOffset = dayjs(override.start.toString()).tz(organizerTimeZone).utcOffset(); + const inviteeUtcOffset = dayjs(override.start.toString()).tz(timeZone).utcOffset(); + const offset = inviteeUtcOffset - organizerUtcOffset; + + return { + userIds: override.userId ? [override.userId] : [], + startTime: + dayjs(override.start).utc().add(offset, "minute").hour() * 60 + + dayjs(override.start).utc().add(offset, "minute").minute(), + endTime: + dayjs(override.end).utc().add(offset, "minute").hour() * 60 + + dayjs(override.end).utc().add(offset, "minute").minute(), + }; + }); + // unset all working hours that relate to this user availability override overrides.forEach((override) => { let i = -1; diff --git a/packages/trpc/server/routers/viewer/slots.tsx b/packages/trpc/server/routers/viewer/slots.tsx index 9cd3a19fa844e6..2f6bb9452eabd4 100644 --- a/packages/trpc/server/routers/viewer/slots.tsx +++ b/packages/trpc/server/routers/viewer/slots.tsx @@ -58,12 +58,21 @@ const checkIfIsAvailable = ({ time, busy, eventLength, + dateOverrides, currentSeats, + isFixedHost, + organizerTimeZone, }: { time: Dayjs; busy: EventBusyDate[]; eventLength: number; + dateOverrides: { + start: Date; + end: Date; + }[]; currentSeats?: CurrentSeats; + isFixedHost?: boolean; + organizerTimeZone?: string; }): boolean => { if (currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString())) { return true; @@ -72,6 +81,41 @@ const checkIfIsAvailable = ({ const slotEndTime = time.add(eventLength, "minutes").utc(); const slotStartTime = time.utc(); + let fixedDateOverrides: { + start: Date; + end: Date; + }[] = []; + + if (isFixedHost) { + fixedDateOverrides = dateOverrides; + } + if ( + fixedDateOverrides.find((date) => { + const utcOffset = organizerTimeZone ? dayjs.tz(date.start, organizerTimeZone).utcOffset() * -1 : 0; + + if ( + dayjs(date.start).add(utcOffset, "minutes").format("YYYY MM DD") === + slotStartTime.format("YYYY MM DD") + ) { + if (dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes")) { + return true; + } + if ( + slotEndTime.isBefore(dayjs(date.start).add(utcOffset, "minutes")) || + slotEndTime.isSame(dayjs(date.start).add(utcOffset, "minutes")) + ) { + return true; + } + if (slotStartTime.isAfter(dayjs(date.end).add(utcOffset, "minutes"))) { + return true; + } + } + }) + ) { + // not available: slot is not withing the date override + return false; + } + return busy.every((busyTime) => { const startTime = dayjs.utc(busyTime.start).utc(); const endTime = dayjs.utc(busyTime.end); @@ -305,6 +349,9 @@ export async function getSchedule(input: z.infer, ctx: const timeSlots: ReturnType = []; + const organizerTimeZone = + eventType.timeZone || eventType?.schedule?.timeZone || userAvailability?.[0]?.timeZone; + for ( let currentCheckedTime = startTime; currentCheckedTime.isBefore(endTime); @@ -319,8 +366,7 @@ export async function getSchedule(input: z.infer, ctx: dateOverrides, minimumBookingNotice: eventType.minimumBookingNotice, frequency: eventType.slotInterval || input.duration || eventType.length, - organizerTimeZone: - eventType.timeZone || eventType?.schedule?.timeZone || userAvailability?.[0]?.timeZone, + organizerTimeZone: organizerTimeZone, }) ); } @@ -334,6 +380,8 @@ export async function getSchedule(input: z.infer, ctx: time: slot.time, ...schedule, ...availabilityCheckProps, + isFixedHost: true, + organizerTimeZone: organizerTimeZone, }); const endCheckForAvailability = performance.now(); checkForAvailabilityCount++; From ae94d69b65c4999a376a1f89b64427bd92094a2e Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Thu, 13 Apr 2023 17:22:41 +0200 Subject: [PATCH 2/7] check if slot is within working hours of fixed hosts --- packages/trpc/server/routers/viewer/slots.tsx | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/trpc/server/routers/viewer/slots.tsx b/packages/trpc/server/routers/viewer/slots.tsx index 2f6bb9452eabd4..12378a75511bed 100644 --- a/packages/trpc/server/routers/viewer/slots.tsx +++ b/packages/trpc/server/routers/viewer/slots.tsx @@ -15,6 +15,7 @@ import type prisma from "@calcom/prisma"; import { availabilityUserSelect } from "@calcom/prisma"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { EventBusyDate } from "@calcom/types/Calendar"; +import type { WorkingHours } from "@calcom/types/schedule"; import { TRPCError } from "@trpc/server"; @@ -59,6 +60,7 @@ const checkIfIsAvailable = ({ busy, eventLength, dateOverrides, + workingHours, currentSeats, isFixedHost, organizerTimeZone, @@ -70,6 +72,7 @@ const checkIfIsAvailable = ({ start: Date; end: Date; }[]; + workingHours: WorkingHours[]; currentSeats?: CurrentSeats; isFixedHost?: boolean; organizerTimeZone?: string; @@ -85,10 +88,13 @@ const checkIfIsAvailable = ({ start: Date; end: Date; }[] = []; - if (isFixedHost) { fixedDateOverrides = dateOverrides; } + + //check if date override for slot exsists + let dateOverrideExist = false; + if ( fixedDateOverrides.find((date) => { const utcOffset = organizerTimeZone ? dayjs.tz(date.start, organizerTimeZone).utcOffset() * -1 : 0; @@ -97,6 +103,7 @@ const checkIfIsAvailable = ({ dayjs(date.start).add(utcOffset, "minutes").format("YYYY MM DD") === slotStartTime.format("YYYY MM DD") ) { + dateOverrideExist = true; if (dayjs(date.start).add(utcOffset, "minutes") === dayjs(date.end).add(utcOffset, "minutes")) { return true; } @@ -112,7 +119,27 @@ const checkIfIsAvailable = ({ } }) ) { - // not available: slot is not withing the date override + // slot is not withing the date override + return false; + } + + if (dateOverrideExist) { + return true; + } + + //if no date override for slot exists check if it is within normal work hours + if ( + workingHours.find((workingHour) => { + if (workingHour.days.includes(slotStartTime.day())) { + const start = slotStartTime.hour() * 60 + slotStartTime.minute(); + const end = slotStartTime.hour() * 60 + slotStartTime.minute(); + if (start < workingHour.startTime || end > workingHour.endTime) { + return true; + } + } + }) + ) { + // slot is outside of working hours return false; } @@ -142,7 +169,6 @@ const checkIfIsAvailable = ({ else if (startTime.isBetween(time, slotEndTime)) { return false; } - return true; }); }; @@ -372,6 +398,7 @@ export async function getSchedule(input: z.infer, ctx: } let availableTimeSlots: typeof timeSlots = []; + availableTimeSlots = timeSlots.filter((slot) => { const fixedHosts = userAvailability.filter((availability) => availability.user.isFixed); return fixedHosts.every((schedule) => { @@ -389,6 +416,7 @@ export async function getSchedule(input: z.infer, ctx: return isAvailable; }); }); + // what else are you going to call it? const looseHostAvailability = userAvailability.filter(({ user: { isFixed } }) => !isFixed); if (looseHostAvailability.length > 0) { From dbf4bfaa890226f51a1813a18dbefd5d13ceae19 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Fri, 14 Apr 2023 13:21:53 +0200 Subject: [PATCH 3/7] add test for date override in different time zone --- apps/web/test/lib/getSchedule.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/apps/web/test/lib/getSchedule.test.ts b/apps/web/test/lib/getSchedule.test.ts index ec19757ca29c6e..ca38e93b84af31 100644 --- a/apps/web/test/lib/getSchedule.test.ts +++ b/apps/web/test/lib/getSchedule.test.ts @@ -784,6 +784,24 @@ describe("getSchedule", () => { dateString: plus2DateString, } ); + + const scheduleForEventOnADayWithDateOverrideDifferentTimezone = await getSchedule( + { + eventTypeId: 1, + eventTypeSlug: "", + startTime: `${plus1DateString}T18:30:00.000Z`, + endTime: `${plus2DateString}T18:29:59.999Z`, + timeZone: Timezones["+6:00"], + }, + ctx + ); + // it should return the same as this is the utc time + expect(scheduleForEventOnADayWithDateOverride).toHaveTimeSlots( + ["08:30:00.000Z", "09:30:00.000Z", "10:30:00.000Z", "11:30:00.000Z"], + { + dateString: plus2DateString, + } + ); }); test("that a user is considered busy when there's a booking they host", async () => { From 40ae4e5b19159896e5a0a9cdef7976b1a8256d61 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Mon, 17 Apr 2023 13:49:36 +0200 Subject: [PATCH 4/7] fix date overrides for not fixed hosts (round robin) --- packages/trpc/server/routers/viewer/slots.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/trpc/server/routers/viewer/slots.ts b/packages/trpc/server/routers/viewer/slots.ts index cadad0b481ac87..e5b1d400876a39 100644 --- a/packages/trpc/server/routers/viewer/slots.ts +++ b/packages/trpc/server/routers/viewer/slots.ts @@ -79,7 +79,6 @@ const checkIfIsAvailable = ({ dateOverrides, workingHours, currentSeats, - isFixedHost, organizerTimeZone, }: { time: Dayjs; @@ -91,7 +90,6 @@ const checkIfIsAvailable = ({ }[]; workingHours: WorkingHours[]; currentSeats?: CurrentSeats; - isFixedHost?: boolean; organizerTimeZone?: string; }): boolean => { if (currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString())) { @@ -101,19 +99,16 @@ const checkIfIsAvailable = ({ const slotEndTime = time.add(eventLength, "minutes").utc(); const slotStartTime = time.utc(); - let fixedDateOverrides: { + const fixedDateOverrides: { start: Date; end: Date; }[] = []; - if (isFixedHost) { - fixedDateOverrides = dateOverrides; - } - //check if date override for slot exsists + //check if date override for slot exists let dateOverrideExist = false; if ( - fixedDateOverrides.find((date) => { + dateOverrides.find((date) => { const utcOffset = organizerTimeZone ? dayjs.tz(date.start, organizerTimeZone).utcOffset() * -1 : 0; if ( @@ -136,7 +131,7 @@ const checkIfIsAvailable = ({ } }) ) { - // slot is not withing the date override + // slot is not within the date override return false; } @@ -495,7 +490,6 @@ export async function getSchedule(input: z.infer, ctx: time: slot.time, ...schedule, ...availabilityCheckProps, - isFixedHost: true, organizerTimeZone: organizerTimeZone, }); const endCheckForAvailability = performance.now(); @@ -521,6 +515,7 @@ export async function getSchedule(input: z.infer, ctx: time: slot.time, ...userSchedule, ...availabilityCheckProps, + organizerTimeZone: organizerTimeZone, }); }); return slot; @@ -581,18 +576,19 @@ export async function getSchedule(input: z.infer, ctx: if (!busy?.length && eventType.seatsPerTimeSlot === null) { return false; } - return checkIfIsAvailable({ time: slot.time, busy, + dateOverrides: [], //todo do we need the date overrides here? + workingHours: [], //todo do we need the date overrides here? ...availabilityCheckProps, + organizerTimeZone: organizerTimeZone, }); }); return slot; }) .filter((slot) => !!slot.userIds?.length); } - availableTimeSlots = availableTimeSlots.filter((slot) => isTimeWithinBounds(slot.time)); const computedAvailableSlots = availableTimeSlots.reduce( From 31402030a714cb4531cae96f58b68086b298e318 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Mon, 17 Apr 2023 13:57:35 +0200 Subject: [PATCH 5/7] code clean up --- packages/trpc/server/routers/viewer/slots.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/trpc/server/routers/viewer/slots.ts b/packages/trpc/server/routers/viewer/slots.ts index e5b1d400876a39..08775331ac2cc0 100644 --- a/packages/trpc/server/routers/viewer/slots.ts +++ b/packages/trpc/server/routers/viewer/slots.ts @@ -76,19 +76,19 @@ const checkIfIsAvailable = ({ time, busy, eventLength, - dateOverrides, - workingHours, + dateOverrides = [], + workingHours = [], currentSeats, organizerTimeZone, }: { time: Dayjs; busy: EventBusyDate[]; eventLength: number; - dateOverrides: { + dateOverrides?: { start: Date; end: Date; }[]; - workingHours: WorkingHours[]; + workingHours?: WorkingHours[]; currentSeats?: CurrentSeats; organizerTimeZone?: string; }): boolean => { @@ -579,8 +579,6 @@ export async function getSchedule(input: z.infer, ctx: return checkIfIsAvailable({ time: slot.time, busy, - dateOverrides: [], //todo do we need the date overrides here? - workingHours: [], //todo do we need the date overrides here? ...availabilityCheckProps, organizerTimeZone: organizerTimeZone, }); From d5d798bfc3a2e844971ffb295593e5ce5ce38e97 Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Mon, 17 Apr 2023 14:07:14 +0200 Subject: [PATCH 6/7] fix added test --- apps/web/test/lib/getSchedule.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/test/lib/getSchedule.test.ts b/apps/web/test/lib/getSchedule.test.ts index ca38e93b84af31..fb1475a5fc5987 100644 --- a/apps/web/test/lib/getSchedule.test.ts +++ b/apps/web/test/lib/getSchedule.test.ts @@ -796,7 +796,7 @@ describe("getSchedule", () => { ctx ); // it should return the same as this is the utc time - expect(scheduleForEventOnADayWithDateOverride).toHaveTimeSlots( + expect(scheduleForEventOnADayWithDateOverrideDifferentTimezone).toHaveTimeSlots( ["08:30:00.000Z", "09:30:00.000Z", "10:30:00.000Z", "11:30:00.000Z"], { dateString: plus2DateString, From ee38fd295fd294b9fc787eba482bde24bbfea69b Mon Sep 17 00:00:00 2001 From: CarinaWolli Date: Tue, 18 Apr 2023 16:46:40 +0200 Subject: [PATCH 7/7] use the correct timezone of user for date overrides --- packages/lib/slots.ts | 2 +- packages/trpc/server/routers/viewer/slots.ts | 22 +++++++++++--------- packages/types/schedule.d.ts | 1 + 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/lib/slots.ts b/packages/lib/slots.ts index 41c8603e1f0091..9c688e94ad8315 100644 --- a/packages/lib/slots.ts +++ b/packages/lib/slots.ts @@ -209,7 +209,7 @@ const getSlots = ({ if (!!activeOverrides.length) { const overrides = activeOverrides.flatMap((override) => { - const organizerUtcOffset = dayjs(override.start.toString()).tz(organizerTimeZone).utcOffset(); + const organizerUtcOffset = dayjs(override.start.toString()).tz(override.timeZone).utcOffset(); const inviteeUtcOffset = dayjs(override.start.toString()).tz(timeZone).utcOffset(); const offset = inviteeUtcOffset - organizerUtcOffset; diff --git a/packages/trpc/server/routers/viewer/slots.ts b/packages/trpc/server/routers/viewer/slots.ts index 08775331ac2cc0..3a8d4a69fb1e88 100644 --- a/packages/trpc/server/routers/viewer/slots.ts +++ b/packages/trpc/server/routers/viewer/slots.ts @@ -99,11 +99,6 @@ const checkIfIsAvailable = ({ const slotEndTime = time.add(eventLength, "minutes").utc(); const slotStartTime = time.utc(); - const fixedDateOverrides: { - start: Date; - end: Date; - }[] = []; - //check if date override for slot exists let dateOverrideExist = false; @@ -413,7 +408,11 @@ export async function getSchedule(input: z.infer, ctx: ); // flattens availability of multiple users const dateOverrides = userAvailability.flatMap((availability) => - availability.dateOverrides.map((override) => ({ userId: availability.user.id, ...override })) + availability.dateOverrides.map((override) => ({ + userId: availability.user.id, + timeZone: availability.timeZone, + ...override, + })) ); const workingHours = getAggregateWorkingHours(userAvailability, eventType.schedulingType); const availabilityCheckProps = { @@ -454,7 +453,7 @@ export async function getSchedule(input: z.infer, ctx: dateOverrides, minimumBookingNotice: eventType.minimumBookingNotice, frequency: eventType.slotInterval || input.duration || eventType.length, - organizerTimeZone: organizerTimeZone, + organizerTimeZone, }) ); } @@ -490,7 +489,7 @@ export async function getSchedule(input: z.infer, ctx: time: slot.time, ...schedule, ...availabilityCheckProps, - organizerTimeZone: organizerTimeZone, + organizerTimeZone: schedule.timeZone, }); const endCheckForAvailability = performance.now(); checkForAvailabilityCount++; @@ -515,7 +514,7 @@ export async function getSchedule(input: z.infer, ctx: time: slot.time, ...userSchedule, ...availabilityCheckProps, - organizerTimeZone: organizerTimeZone, + organizerTimeZone: userSchedule.timeZone, }); }); return slot; @@ -576,11 +575,14 @@ export async function getSchedule(input: z.infer, ctx: if (!busy?.length && eventType.seatsPerTimeSlot === null) { return false; } + + const userSchedule = userAvailability.find(({ user: { id: userId } }) => userId === slotUserId); + return checkIfIsAvailable({ time: slot.time, busy, ...availabilityCheckProps, - organizerTimeZone: organizerTimeZone, + organizerTimeZone: userSchedule?.timeZone, }); }); return slot; diff --git a/packages/types/schedule.d.ts b/packages/types/schedule.d.ts index 7b36261da4a521..7e390124a10124 100644 --- a/packages/types/schedule.d.ts +++ b/packages/types/schedule.d.ts @@ -2,6 +2,7 @@ export type TimeRange = { userId?: number | null; start: Date; end: Date; + timeZone?: string; }; export type Schedule = TimeRange[][];