From bd912689e51f686f2bbba514a61ff83189dd0654 Mon Sep 17 00:00:00 2001 From: Kartik Saini Date: Sat, 7 Dec 2024 15:10:14 +0530 Subject: [PATCH 1/4] feat: show ooo when not available --- packages/lib/slots.ts | 44 +++++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/packages/lib/slots.ts b/packages/lib/slots.ts index 645c71fa0abdb2..60dce2b50d345d 100644 --- a/packages/lib/slots.ts +++ b/packages/lib/slots.ts @@ -185,7 +185,6 @@ function buildSlotsWithDateRanges({ } dateRanges.forEach((range) => { - const dateYYYYMMDD = range.start.format("YYYY-MM-DD"); const startTimeWithMinNotice = dayjs.utc().add(minimumBookingNotice, "minute"); let slotStartTime = range.start.utc().isAfter(startTimeWithMinNotice) @@ -207,8 +206,7 @@ function buildSlotsWithDateRanges({ slotStartTime = slotStartTime.add(offsetStart ?? 0, "minutes").tz(timeZone); while (!slotStartTime.add(eventLength, "minutes").subtract(1, "second").utc().isAfter(rangeEnd)) { - const dateOutOfOfficeExists = datesOutOfOffice?.[dateYYYYMMDD]; - let slotData: { + const slotData: { time: Dayjs; userIds?: number[]; away?: boolean; @@ -220,24 +218,38 @@ function buildSlotsWithDateRanges({ time: slotStartTime, }; - if (dateOutOfOfficeExists) { - const { toUser, fromUser, reason, emoji } = dateOutOfOfficeExists; - - slotData = { - time: slotStartTime, - away: true, - ...(fromUser && { fromUser }), - ...(toUser && { toUser }), - ...(reason && { reason }), - ...(emoji && { emoji }), - }; - } - slots.push(slotData); slotStartTime = slotStartTime.add(frequency + (offsetStart ?? 0), "minutes"); } }); + // Show out of office dates even if not in dateRanges + if (datesOutOfOffice) { + Object.keys(datesOutOfOffice).forEach((date) => { + const dateOutOfOffice = datesOutOfOffice[date]; + const slotTime = dayjs(date).tz(timeZone).startOf("day"); + + const { toUser, fromUser, reason, emoji } = dateOutOfOffice; + + const oooSlot = { + time: slotTime, + away: true, + ...(fromUser && { fromUser }), + ...(toUser && { toUser }), + ...(reason && { reason }), + ...(emoji && { emoji }), + }; + + // Find the correct position to insert the out of office slot + const insertIndex = slots.findIndex((slot) => slot.time.isAfter(slotTime)); + if (insertIndex === -1) { + slots.push(oooSlot); + } else { + slots.splice(insertIndex, 0, oooSlot); + } + }); + } + return slots; } From ca2d3a9c897b2c56659fba930f043d57e08424d7 Mon Sep 17 00:00:00 2001 From: Kartik Saini Date: Sat, 7 Dec 2024 21:56:11 +0530 Subject: [PATCH 2/4] fix: send correct username for toUser if user belongs to org --- packages/lib/slots.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/lib/slots.ts b/packages/lib/slots.ts index 60dce2b50d345d..752c85d030e356 100644 --- a/packages/lib/slots.ts +++ b/packages/lib/slots.ts @@ -6,6 +6,7 @@ import type { WorkingHours, TimeRange as DateOverride } from "@calcom/types/sche import { getWorkingHours } from "./availability"; import { getTimeZone } from "./date-fns"; import type { DateRange } from "./date-ranges"; +import { UserRepository } from "./server/repository/user"; export type GetSlots = { inviteeDate: Dayjs; @@ -223,19 +224,26 @@ function buildSlotsWithDateRanges({ } }); - // Show out of office dates even if not in dateRanges if (datesOutOfOffice) { - Object.keys(datesOutOfOffice).forEach((date) => { + Object.keys(datesOutOfOffice).forEach(async (date) => { const dateOutOfOffice = datesOutOfOffice[date]; const slotTime = dayjs(date).tz(timeZone).startOf("day"); const { toUser, fromUser, reason, emoji } = dateOutOfOffice; + const enrichedToUser = toUser ? await UserRepository.enrichUserWithItsProfile({ user: toUser }) : null; + const oooSlot = { time: slotTime, away: true, ...(fromUser && { fromUser }), - ...(toUser && { toUser }), + ...(enrichedToUser && { + toUser: { + id: enrichedToUser.id, + username: enrichedToUser.username, + displayName: enrichedToUser.displayName, + }, + }), ...(reason && { reason }), ...(emoji && { emoji }), }; From 8cea19ff37adaa52a780de2d5603735c93d8cc48 Mon Sep 17 00:00:00 2001 From: Kartik Saini Date: Mon, 6 Jan 2025 14:39:45 +0530 Subject: [PATCH 3/4] enrich ooo user with profile in getUserAvailability --- packages/core/getUserAvailability.ts | 27 ++++++++++++++++++--------- packages/lib/slots.ts | 11 +---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/packages/core/getUserAvailability.ts b/packages/core/getUserAvailability.ts index 4baa35c80d964d..0d6640e9d22c10 100644 --- a/packages/core/getUserAvailability.ts +++ b/packages/core/getUserAvailability.ts @@ -22,8 +22,7 @@ import { safeStringify } from "@calcom/lib/safeStringify"; import { EventTypeRepository } from "@calcom/lib/server/repository/eventType"; import { UserRepository } from "@calcom/lib/server/repository/user"; import prisma from "@calcom/prisma"; -import { SchedulingType } from "@calcom/prisma/enums"; -import { BookingStatus } from "@calcom/prisma/enums"; +import { BookingStatus, SchedulingType } from "@calcom/prisma/enums"; import { EventTypeMetaDataSchema, stringToDayjsZod } from "@calcom/prisma/zod-utils"; import type { EventBusyDetails, IntervalLimitUnit } from "@calcom/types/Calendar"; import type { TimeRange } from "@calcom/types/schedule"; @@ -539,7 +538,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA }, })); - const datesOutOfOffice: IOutOfOfficeData = calculateOutOfOfficeRanges(outOfOfficeDays, availability); + const datesOutOfOffice: IOutOfOfficeData = await calculateOutOfOfficeRanges(outOfOfficeDays, availability); const { dateRanges, oooExcludedDateRanges } = buildDateRanges({ dateFrom, @@ -620,15 +619,17 @@ export interface IOutOfOfficeData { }; } -const calculateOutOfOfficeRanges = ( +const calculateOutOfOfficeRanges = async ( outOfOfficeDays: GetUserAvailabilityInitialData["outOfOfficeDays"], availability: GetUserAvailabilityParamsDTO["availability"] -): IOutOfOfficeData => { +): Promise => { if (!outOfOfficeDays || outOfOfficeDays.length === 0) { return {}; } - return outOfOfficeDays.reduce((acc: IOutOfOfficeData, { start, end, toUser, user, reason }) => { + const acc: IOutOfOfficeData = {}; + + for (const { start, end, toUser, user, reason } of outOfOfficeDays) { // here we should use startDate or today if start is before today // consider timezone in start and end date range const startDateRange = dayjs(start).utc().isBefore(dayjs().startOf("day").utc()) @@ -649,19 +650,27 @@ const calculateOutOfOfficeRanges = ( continue; // Skip to the next iteration if day not found in flattenDays } + const enrichedToUser = toUser ? await UserRepository.enrichUserWithItsProfile({ user: toUser }) : null; + acc[date.format("YYYY-MM-DD")] = { // @TODO: would be good having start and end availability time here, but for now should be good // you can obtain that from user availability defined outside of here fromUser: { id: user.id, displayName: user.name }, // optional chaining destructuring toUser - toUser: !!toUser ? { id: toUser.id, displayName: toUser.name, username: toUser.username } : null, + ...(!!enrichedToUser && { + toUser: { + id: enrichedToUser.id, + username: enrichedToUser.username, + displayName: enrichedToUser.name, + }, + }), reason: !!reason ? reason.reason : null, emoji: !!reason ? reason.emoji : null, }; } + } - return acc; - }, {}); + return acc; }; type GetUsersAvailabilityProps = { diff --git a/packages/lib/slots.ts b/packages/lib/slots.ts index 752c85d030e356..f270268445fc71 100644 --- a/packages/lib/slots.ts +++ b/packages/lib/slots.ts @@ -6,7 +6,6 @@ import type { WorkingHours, TimeRange as DateOverride } from "@calcom/types/sche import { getWorkingHours } from "./availability"; import { getTimeZone } from "./date-fns"; import type { DateRange } from "./date-ranges"; -import { UserRepository } from "./server/repository/user"; export type GetSlots = { inviteeDate: Dayjs; @@ -231,19 +230,11 @@ function buildSlotsWithDateRanges({ const { toUser, fromUser, reason, emoji } = dateOutOfOffice; - const enrichedToUser = toUser ? await UserRepository.enrichUserWithItsProfile({ user: toUser }) : null; - const oooSlot = { time: slotTime, away: true, ...(fromUser && { fromUser }), - ...(enrichedToUser && { - toUser: { - id: enrichedToUser.id, - username: enrichedToUser.username, - displayName: enrichedToUser.displayName, - }, - }), + ...(toUser && { toUser }), ...(reason && { reason }), ...(emoji && { emoji }), }; From 8e8fb31b7303143f60b51cf4c69ca5648ab9cea8 Mon Sep 17 00:00:00 2001 From: Kartik Saini Date: Wed, 15 Jan 2025 11:22:53 +0530 Subject: [PATCH 4/4] fix: set correct ooo slots --- packages/lib/slots.ts | 63 ++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/packages/lib/slots.ts b/packages/lib/slots.ts index f270268445fc71..94a087c9278de1 100644 --- a/packages/lib/slots.ts +++ b/packages/lib/slots.ts @@ -184,7 +184,9 @@ function buildSlotsWithDateRanges({ } } + const processedOOODates = new Set(); dateRanges.forEach((range) => { + const dateYYYYMMDD = range.start.format("YYYY-MM-DD"); const startTimeWithMinNotice = dayjs.utc().add(minimumBookingNotice, "minute"); let slotStartTime = range.start.utc().isAfter(startTimeWithMinNotice) @@ -205,8 +207,28 @@ function buildSlotsWithDateRanges({ slotStartTime = slotStartTime.add(offsetStart ?? 0, "minutes").tz(timeZone); + // Add OOO slot if exists and is before current range + if (datesOutOfOffice) { + Object.entries(datesOutOfOffice).forEach(([dateStr, oooData]) => { + const oooDate = dayjs(dateStr); + if (oooDate.isBefore(dateYYYYMMDD, "day") && !processedOOODates.has(dateStr)) { + slots.push({ + time: oooDate.startOf("day"), + away: true, + ...(oooData.fromUser && { fromUser: oooData.fromUser }), + ...(oooData.toUser && { toUser: oooData.toUser }), + ...(oooData.reason && { reason: oooData.reason }), + ...(oooData.emoji && { emoji: oooData.emoji }), + }); + // when the date range increases in the next iteration, we don't want to process this date again + processedOOODates.add(dateStr); + } + }); + } + while (!slotStartTime.add(eventLength, "minutes").subtract(1, "second").utc().isAfter(rangeEnd)) { - const slotData: { + const dateOutOfOfficeExists = datesOutOfOffice?.[dateYYYYMMDD]; + let slotData: { time: Dayjs; userIds?: number[]; away?: boolean; @@ -218,37 +240,24 @@ function buildSlotsWithDateRanges({ time: slotStartTime, }; + if (dateOutOfOfficeExists) { + const { toUser, fromUser, reason, emoji } = dateOutOfOfficeExists; + + slotData = { + time: slotStartTime, + away: true, + ...(fromUser && { fromUser }), + ...(toUser && { toUser }), + ...(reason && { reason }), + ...(emoji && { emoji }), + }; + processedOOODates.add(dateYYYYMMDD); + } slots.push(slotData); slotStartTime = slotStartTime.add(frequency + (offsetStart ?? 0), "minutes"); } }); - if (datesOutOfOffice) { - Object.keys(datesOutOfOffice).forEach(async (date) => { - const dateOutOfOffice = datesOutOfOffice[date]; - const slotTime = dayjs(date).tz(timeZone).startOf("day"); - - const { toUser, fromUser, reason, emoji } = dateOutOfOffice; - - const oooSlot = { - time: slotTime, - away: true, - ...(fromUser && { fromUser }), - ...(toUser && { toUser }), - ...(reason && { reason }), - ...(emoji && { emoji }), - }; - - // Find the correct position to insert the out of office slot - const insertIndex = slots.findIndex((slot) => slot.time.isAfter(slotTime)); - if (insertIndex === -1) { - slots.push(oooSlot); - } else { - slots.splice(insertIndex, 0, oooSlot); - } - }); - } - return slots; }