-
Notifications
You must be signed in to change notification settings - Fork 1
Advanced date override handling and timezone compatibility improvements #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: date-algorithm-base
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,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"; | ||
|
|
||
|
|
@@ -75,12 +76,21 @@ const checkIfIsAvailable = ({ | |
| time, | ||
| busy, | ||
| eventLength, | ||
| dateOverrides = [], | ||
| workingHours = [], | ||
| currentSeats, | ||
| organizerTimeZone, | ||
| }: { | ||
| time: Dayjs; | ||
| busy: EventBusyDate[]; | ||
| eventLength: number; | ||
| dateOverrides?: { | ||
| start: Date; | ||
| end: Date; | ||
| }[]; | ||
| workingHours?: WorkingHours[]; | ||
| currentSeats?: CurrentSeats; | ||
| organizerTimeZone?: string; | ||
| }): boolean => { | ||
| if (currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString())) { | ||
| return true; | ||
|
|
@@ -89,6 +99,57 @@ const checkIfIsAvailable = ({ | |
| const slotEndTime = time.add(eventLength, "minutes").utc(); | ||
| const slotStartTime = time.utc(); | ||
|
|
||
| //check if date override for slot exists | ||
| let dateOverrideExist = false; | ||
|
|
||
| if ( | ||
| dateOverrides.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") | ||
| ) { | ||
| dateOverrideExist = true; | ||
| 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; | ||
| } | ||
| } | ||
| }) | ||
| ) { | ||
| // slot is not within 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) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Working Hours Validation Uses Incorrect End TimeIn the Locations (1) |
||
| return true; | ||
| } | ||
| } | ||
| }) | ||
| ) { | ||
| // slot is outside of working hours | ||
| return false; | ||
| } | ||
|
|
||
| return busy.every((busyTime) => { | ||
| const startTime = dayjs.utc(busyTime.start).utc(); | ||
| const endTime = dayjs.utc(busyTime.end); | ||
|
|
@@ -115,7 +176,6 @@ const checkIfIsAvailable = ({ | |
| else if (startTime.isBetween(time, slotEndTime)) { | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| }); | ||
| }; | ||
|
|
@@ -348,7 +408,11 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, 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 = { | ||
|
|
@@ -372,6 +436,9 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx: | |
|
|
||
| const timeSlots: ReturnType<typeof getTimeSlots> = []; | ||
|
|
||
| const organizerTimeZone = | ||
| eventType.timeZone || eventType?.schedule?.timeZone || userAvailability?.[0]?.timeZone; | ||
|
|
||
| for ( | ||
| let currentCheckedTime = startTime; | ||
| currentCheckedTime.isBefore(endTime); | ||
|
|
@@ -386,8 +453,7 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx: | |
| dateOverrides, | ||
| minimumBookingNotice: eventType.minimumBookingNotice, | ||
| frequency: eventType.slotInterval || input.duration || eventType.length, | ||
| organizerTimeZone: | ||
| eventType.timeZone || eventType?.schedule?.timeZone || userAvailability?.[0]?.timeZone, | ||
| organizerTimeZone, | ||
| }) | ||
| ); | ||
| } | ||
|
|
@@ -423,13 +489,15 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx: | |
| time: slot.time, | ||
| ...schedule, | ||
| ...availabilityCheckProps, | ||
| organizerTimeZone: schedule.timeZone, | ||
| }); | ||
| const endCheckForAvailability = performance.now(); | ||
| checkForAvailabilityCount++; | ||
| checkForAvailabilityTime += endCheckForAvailability - startCheckForAvailability; | ||
| return isAvailable; | ||
| }); | ||
| }); | ||
|
|
||
| // what else are you going to call it? | ||
| const looseHostAvailability = userAvailability.filter(({ user: { isFixed } }) => !isFixed); | ||
| if (looseHostAvailability.length > 0) { | ||
|
|
@@ -446,6 +514,7 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx: | |
| time: slot.time, | ||
| ...userSchedule, | ||
| ...availabilityCheckProps, | ||
| organizerTimeZone: userSchedule.timeZone, | ||
| }); | ||
| }); | ||
| return slot; | ||
|
|
@@ -507,17 +576,19 @@ export async function getSchedule(input: z.infer<typeof getScheduleSchema>, ctx: | |
| return false; | ||
| } | ||
|
|
||
| const userSchedule = userAvailability.find(({ user: { id: userId } }) => userId === slotUserId); | ||
|
|
||
| return checkIfIsAvailable({ | ||
| time: slot.time, | ||
| busy, | ||
| ...availabilityCheckProps, | ||
| organizerTimeZone: userSchedule?.timeZone, | ||
| }); | ||
| }); | ||
| return slot; | ||
| }) | ||
| .filter((slot) => !!slot.userIds?.length); | ||
| } | ||
|
|
||
| availableTimeSlots = availableTimeSlots.filter((slot) => isTimeWithinBounds(slot.time)); | ||
|
|
||
| const computedAvailableSlots = availableTimeSlots.reduce( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: Dayjs Comparison Error
Strict equality (
===) is incorrectly used to compare Dayjs objects, which always returns false for different object instances. The.isSame()method should be used instead to properly compare the date/time values.Locations (1)
packages/trpc/server/routers/viewer/slots.ts#L113-L114