-
Notifications
You must be signed in to change notification settings - Fork 4
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) { | ||||||||||||||||||||||||||||||||||
| return true; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+139
to
+145
Contributor
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. Critical bug: Incorrect end time calculation in working hours check. The Fix the calculation: if (workingHour.days.includes(slotStartTime.day())) {
const start = slotStartTime.hour() * 60 + slotStartTime.minute();
- const end = slotStartTime.hour() * 60 + slotStartTime.minute();
+ const end = slotEndTime.hour() * 60 + slotEndTime.minute();
if (start < workingHour.startTime || end > workingHour.endTime) {
return true;
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||
| // 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, | ||||||||||||||||||||||||||||||||||
|
Contributor
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. 🛠️ Refactor suggestion Ensure consistent null-safety when accessing timeZone properties. The code inconsistently handles potentially undefined timeZone values:
Consider using consistent null-safety throughout: - organizerTimeZone: schedule.timeZone,
+ organizerTimeZone: schedule.timeZone || organizerTimeZone,- organizerTimeZone: userSchedule.timeZone,
+ organizerTimeZone: userSchedule.timeZone || organizerTimeZone,Also applies to: 517-517, 585-585 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| 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.
Add null check for optional timeZone property.
The code accesses
override.timeZonewithout checking if it exists, but theTimeRangetype definestimeZoneas optional. This could cause issues if the timezone is not provided.Consider adding a null check or default value:
const overrides = activeOverrides.flatMap((override) => { - const organizerUtcOffset = dayjs(override.start.toString()).tz(override.timeZone).utcOffset(); + const organizerTimeZone = override.timeZone || organizerTimeZone; // Use passed organizerTimeZone as fallback + const organizerUtcOffset = dayjs(override.start.toString()).tz(organizerTimeZone).utcOffset(); const inviteeUtcOffset = dayjs(override.start.toString()).tz(timeZone).utcOffset(); const offset = inviteeUtcOffset - organizerUtcOffset;🤖 Prompt for AI Agents