Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b96460c
init: upcoming bookings atom
Ryukemeister Sep 23, 2025
873d79d
add comments
Ryukemeister Sep 23, 2025
b5da583
fix: better naming
Ryukemeister Sep 23, 2025
4d51bbb
Merge branch 'main' into rajiv/cal-5461-feature-upcoming-bookings-atom
Ryukemeister Sep 23, 2025
a386547
fixup
Ryukemeister Sep 24, 2025
21a94d1
Merge branch 'main' into rajiv/cal-5461-feature-upcoming-bookings-atom
Ryukemeister Sep 26, 2025
2ae295c
fix: use `usePrefetch` hook
Ryukemeister Sep 26, 2025
e53b366
add comment- for myself
Ryukemeister Sep 26, 2025
c718096
Merge branch 'main' into rajiv/cal-5461-feature-upcoming-bookings-atom
Ryukemeister Sep 29, 2025
fc70e03
Merge branch 'main' into rajiv/cal-5461-feature-upcoming-bookings-atom
Ryukemeister Sep 29, 2025
0c7e098
Merge branch 'main' into rajiv/cal-5461-feature-upcoming-bookings-atom
Ryukemeister Oct 1, 2025
f79d943
Merge branch 'main' into rajiv/cal-5461-feature-upcoming-bookings-atom
Ryukemeister Oct 1, 2025
87d0a53
Merge branch 'main' into rajiv/cal-5461-feature-upcoming-bookings-atom
Ryukemeister Oct 2, 2025
a07a0f9
chore: add changesets
Ryukemeister Oct 2, 2025
3eb7551
fix: ignore isTeamEvent prop if we have teamId
Ryukemeister Oct 2, 2025
1b65fa6
chore: update docs
Ryukemeister Oct 2, 2025
d5420e2
Merge branch 'main' into rajiv/cal-5461-feature-upcoming-bookings-atom
Ryukemeister Oct 13, 2025
c262d3c
chore: implement PR feedback
Ryukemeister Oct 13, 2025
bbef2a1
refactor: remove all unnecessary props
Ryukemeister Oct 13, 2025
3ff1eec
update fetch bookings count
Ryukemeister Oct 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eighty-walls-send.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@calcom/atoms": minor
---

This PR adds ability to display bookings of a user event for calendar view atom
9 changes: 4 additions & 5 deletions docs/platform/atoms/calendar-view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { CalendarView } from "@calcom/atoms";
export default function Booker( props : BookerProps ) {
return (
<div>
<CalendarView isTeamEvent={true} teamId={props.teamId} eventSlug={props.eventSlug} />
<CalendarView teamId={props.teamId} eventSlug={props.eventSlug} />
</div>
)
}
Expand All @@ -55,7 +55,6 @@ Below is a list of props that can be passed to the create event type atom

| Name | Required | Description |
|:----------------------------|:----------|:-------------------------------------------------------------------------------------------------------|
| username | Yes | Username of the person whose schedule is to be displayed |
| eventSlug | Yes | Unique slug created for a particular event | |
| isTeamEvent | No | Boolean indicating if it is a team event, to be passed only for team events |
| teamId | No | The id of the team for which the event is created, to be passed only for team events |
| eventSlug | Yes | Unique slug created for a particular event |
| username | Optional | Username of the person whose schedule is to be displayed, required only for individual events | | |
| teamId | Optional | Id of the team for which the event is created, required only for team events |
Comment on lines +58 to +60
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix table column delimiters.

The username row now has two extra | delimiters, which breaks the three-column layout when rendered. Please drop the trailing separators so the table stays well-formed.

-| username                   | Optional     | Username of the person whose schedule is to be displayed, required only for individual events                                          |                                             |                                        |
+| username                   | Optional     | Username of the person whose schedule is to be displayed, required only for individual events |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| eventSlug | Yes | Unique slug created for a particular event |
| username | Optional | Username of the person whose schedule is to be displayed, required only for individual events | | |
| teamId | Optional | Id of the team for which the event is created, required only for team events |
| eventSlug | Yes | Unique slug created for a particular event |
| username | Optional | Username of the person whose schedule is to be displayed, required only for individual events |
| teamId | Optional | Id of the team for which the event is created, required only for team events |
🤖 Prompt for AI Agents
In docs/platform/atoms/calendar-view.mdx around lines 58 to 60, the markdown
table row for "username" contains extra pipe characters that create a fourth
empty column and break the three-column layout; remove the two trailing '|'
delimiters from that row so it has exactly three pipe-separated columns (column
name, requirement, description) matching the other rows and keep the table
aligned.

42 changes: 27 additions & 15 deletions packages/features/calendar-view/LargeCalendar.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useMemo, useEffect } from "react";
import { useEffect, useMemo } from "react";

import dayjs from "@calcom/dayjs";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useAvailableTimeSlots } from "@calcom/features/bookings/Booker/components/hooks/useAvailableTimeSlots";
import type { BookerEvent } from "@calcom/features/bookings/types";
import { Calendar } from "@calcom/features/calendars/weeklyview";
import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events";
import { localStorage } from "@calcom/lib/webstorage";
import type { BookingStatus } from "@calcom/prisma/enums";

import { useOverlayCalendarStore } from "../bookings/Booker/components/OverlayCalendar/store";
import { useBookings } from "../../platform/atoms/hooks/bookings/useBookings";
import type { useScheduleForEventReturnType } from "../bookings/Booker/utils/event";
import { getQueryParam } from "../bookings/Booker/utils/query-param";

Expand All @@ -22,12 +22,11 @@ export const LargeCalendar = ({
schedule?: useScheduleForEventReturnType["data"];
isLoading: boolean;
event: {
data?: Pick<BookerEvent, "length"> | null;
data?: Pick<BookerEvent, "length" | "id"> | null;
};
}) => {
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
const selectedEventDuration = useBookerStoreContext((state) => state.selectedDuration);
const overlayEvents = useOverlayCalendarStore((state) => state.overlayBusyDates);
const displayOverlay =
getQueryParam("overlayCalendar") === "true" || localStorage?.getItem("overlayCalendarSwitchDefault");

Expand All @@ -40,25 +39,38 @@ export const LargeCalendar = ({
.add(extraDays - 1, "day")
.toDate();

const { data: upcomingBookings } = useBookings({
take: 150,
skip: 0,
status: ["upcoming", "past", "recurring"],
eventTypeId: event?.data?.id,
afterStart: startDate.toISOString(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question - the start and end dates here refer to 1 week or we would fetch for whole month?
Question 2- what if someone has more than 50 bookings?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the start and end dates here refer to 1 week or we would fetch for whole month

for 1 week because, that way we reduce the load to the server and only fetch data that's needed, which the user clicks the next week button that's when we fetch bookings for that particular the user is on

what if someone has more than 50 bookings?

hmm not sure here, should we increase the number of bookings then to 150? I think ideally its really hard to have 150 bookings for just for one week, what do you think about this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated bookings fetch count to 150

beforeEnd: endDate.toISOString(),
});

// HACK: force rerender when overlay events change
// Sine we dont use react router here we need to force rerender (ATOM SUPPORT)
// eslint-disable-next-line @typescript-eslint/no-empty-function

useEffect(() => {}, [displayOverlay]);

const overlayEventsForDate = useMemo(() => {
if (!overlayEvents || !displayOverlay) return [];
return overlayEvents.map((event, id) => {
if (!upcomingBookings) return [];

return upcomingBookings?.map((booking) => {
return {
id,
start: dayjs(event.start).toDate(),
end: dayjs(event.end).toDate(),
title: "Busy",
id: booking.id,
title: booking.title ?? `Busy`,
start: new Date(booking.start),
Comment on lines +62 to +63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Localize the “Busy” label.

Frontend strings should be wrapped with t() per our TSX guideline. Please move this fallback through the translation layer (e.g. t("busy")) to keep the atom localizable.

As per coding guidelines

end: new Date(booking.end),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we switching to new Date from dayjs(event.end).toDate() ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no real reason tbh, just for consistency with trouble shooter component here for consistency

options: {
status: "ACCEPTED",
borderColor: "black",
status: booking.status.toUpperCase() as BookingStatus,
"data-test-id": "troubleshooter-busy-event",
className: "border-[1.5px]",
},
} as CalendarEvent;
};
});
}, [overlayEvents, displayOverlay]);
}, [upcomingBookings]);

return (
<div className="h-full [--calendar-dates-sticky-offset:66px]">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AtomsWrapper } from "@/components/atoms-wrapper";
import { useMemo, useEffect, useState } from "react";
import { useMemo } from "react";
import { shallow } from "zustand/shallow";

import dayjs from "@calcom/dayjs";
Expand All @@ -16,50 +16,51 @@ import { useTimePreferences } from "@calcom/features/bookings/lib";
import { LargeCalendar } from "@calcom/features/calendar-view/LargeCalendar";
import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents";
import { useTimesForSchedule } from "@calcom/features/schedules/lib/use-schedule/useTimesForSchedule";
import { getRoutedTeamMemberIdsFromSearchParams } from "@calcom/lib/bookings/getRoutedTeamMemberIdsFromSearchParams";

import { formatUsername } from "../../booker/BookerPlatformWrapper";
import type {
BookerPlatformWrapperAtomPropsForIndividual,
BookerPlatformWrapperAtomPropsForTeam,
} from "../../booker/types";
import { useGetBookingForReschedule } from "../../hooks/bookings/useGetBookingForReschedule";
import { useAtomGetPublicEvent } from "../../hooks/event-types/public/useAtomGetPublicEvent";
import { useEventType } from "../../hooks/event-types/public/useEventType";
import { useTeamEventType } from "../../hooks/event-types/public/useTeamEventType";
import { useAvailableSlots } from "../../hooks/useAvailableSlots";

type CalendarViewPlatformWrapperAtomPropsForIndividual = {
username: string | string[];
eventSlug: string;
};

type CalendarViewPlatformWrapperAtomPropsForTeam = {
teamId: number;
eventSlug: string;
};

const CalendarViewPlatformWrapperComponent = (
props: BookerPlatformWrapperAtomPropsForIndividual | BookerPlatformWrapperAtomPropsForTeam
props:
| (CalendarViewPlatformWrapperAtomPropsForIndividual & { teamId?: number })
| (CalendarViewPlatformWrapperAtomPropsForTeam & { username?: string | string[] })
) => {
const {
eventSlug,
isTeamEvent,
hostsLimit,
allowUpdatingUrlParams = false,
teamMemberEmail,
crmAppSlug,
crmOwnerRecordType,
} = props;

const teamId: number | undefined = props.isTeamEvent ? props.teamId : undefined;
const isTeamEvent = !!props.teamId;
const teamId: number | undefined = props.teamId ? props.teamId : undefined;
const username = useMemo(() => {
if (props.username) {
return formatUsername(props.username);
}
return "";
}, [props.username]);

const { isPending } = useEventType(username, eventSlug, isTeamEvent);
const { isPending: isTeamPending } = useTeamEventType(teamId, eventSlug, isTeamEvent, hostsLimit);
const { isPending } = useEventType(username, props.eventSlug, isTeamEvent);

const { isPending: isTeamPending } = useTeamEventType(teamId, props.eventSlug, isTeamEvent);

const setSelectedDuration = useBookerStoreContext((state) => state.setSelectedDuration);
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);

const event = useAtomGetPublicEvent({
username,
eventSlug: props.eventSlug,
isTeamEvent: props.isTeamEvent,
isTeamEvent: isTeamEvent,
teamId,
selectedDuration,
});
Expand Down Expand Up @@ -90,67 +91,21 @@ const CalendarViewPlatformWrapperComponent = (
selectedDate,
});

useEffect(() => {
setSelectedDuration(props.duration ?? null);
}, [props.duration, setSelectedDuration]);

const { timezone } = useTimePreferences();
const isDynamic = useMemo(() => {
return getUsernameList(username ?? "").length > 1;
}, [username]);

const [routingParams, setRoutingParams] = useState<{
routedTeamMemberIds?: number[];
_shouldServeCache?: boolean;
skipContactOwner?: boolean;
isBookingDryRun?: boolean;
}>({});

useEffect(() => {
const searchParams = props.routingFormSearchParams
? new URLSearchParams(props.routingFormSearchParams)
: new URLSearchParams(window.location.search);

const routedTeamMemberIds = getRoutedTeamMemberIdsFromSearchParams(searchParams);
const skipContactOwner = searchParams.get("cal.skipContactOwner") === "true";

const _cacheParam = searchParams?.get("cal.cache");
const _shouldServeCache = _cacheParam ? _cacheParam === "true" : undefined;
const isBookingDryRun =
searchParams?.get("cal.isBookingDryRun")?.toLowerCase() === "true" ||
searchParams?.get("cal.sandbox")?.toLowerCase() === "true";
setRoutingParams({
...(skipContactOwner ? { skipContactOwner } : {}),
...(routedTeamMemberIds ? { routedTeamMemberIds } : {}),
...(_shouldServeCache ? { _shouldServeCache } : {}),
...(isBookingDryRun ? { isBookingDryRun } : {}),
});
}, [props.routingFormSearchParams]);
const bookingData = useBookerStoreContext((state) => state.bookingData);
const setBookingData = useBookerStoreContext((state) => state.setBookingData);

useGetBookingForReschedule({
uid: props.rescheduleUid ?? props.bookingUid ?? "",
onSuccess: (data) => {
setBookingData(data);
},
});

useInitializeBookerStoreContext({
...props,
teamMemberEmail,
crmAppSlug,
crmOwnerRecordType,
crmRecordId: props.crmRecordId,
eventId: event?.data?.id,
rescheduleUid: props.rescheduleUid ?? null,
bookingUid: props.bookingUid ?? null,
layout: "week_view",
org: props.entity?.orgSlug,
username,
bookingData,
isPlatform: true,
allowUpdatingUrlParams,
allowUpdatingUrlParams: false,
});

const schedule = useAvailableSlots({
Expand All @@ -160,23 +115,21 @@ const CalendarViewPlatformWrapperComponent = (
endTime,
timeZone: timezone,
duration: selectedDuration ?? undefined,
rescheduleUid: props.rescheduleUid,
teamMemberEmail: props.teamMemberEmail ?? undefined,
...(props.isTeamEvent
teamMemberEmail: undefined,
...(isTeamEvent
? {
isTeamEvent: props.isTeamEvent,
isTeamEvent: isTeamEvent,
teamId: teamId,
}
: {}),
enabled:
Boolean(teamId || username) &&
Boolean(month) &&
Boolean(timezone) &&
(props.isTeamEvent ? !isTeamPending : !isPending) &&
(isTeamEvent ? !isTeamPending : !isPending) &&
Boolean(event?.data?.id),
orgSlug: props.entity?.orgSlug ?? undefined,
eventTypeSlug: isDynamic ? "dynamic" : eventSlug || "",
...routingParams,
orgSlug: undefined,
eventTypeSlug: isDynamic ? "dynamic" : props.eventSlug || "",
});

return (
Expand All @@ -185,7 +138,7 @@ const CalendarViewPlatformWrapperComponent = (
<Header
isCalendarView={true}
isMyLink={true}
eventSlug={eventSlug}
eventSlug={props.eventSlug}
enabledLayouts={bookerLayout.bookerLayouts.enabledLayouts}
extraDays={7}
isMobile={false}
Expand All @@ -204,7 +157,9 @@ const CalendarViewPlatformWrapperComponent = (
};

export const CalendarViewPlatformWrapper = (
props: BookerPlatformWrapperAtomPropsForIndividual | BookerPlatformWrapperAtomPropsForTeam
props:
| (BookerPlatformWrapperAtomPropsForIndividual & { teamId?: number })
| Omit<BookerPlatformWrapperAtomPropsForTeam, "isTeamEvent">
) => {
return (
<BookerStoreProvider>
Expand Down
Loading