feat: upcoming bookings atom (#24010)

* init: upcoming bookings atom

* add comments

* fix: better naming

* fixup

* fix: use `usePrefetch` hook

* add comment- for myself

* chore: add changesets

* fix: ignore isTeamEvent prop if we have teamId

* chore: update docs

* chore: implement PR feedback

* refactor: remove all unnecessary props

* update fetch bookings count
This commit is contained in:
Rajiv Sahal
2025-10-14 12:37:37 +02:00
committed by GitHub
parent 243012e82f
commit 09ee39a3d8
4 changed files with 68 additions and 97 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@calcom/atoms": minor
---
This PR adds ability to display bookings of a user event for calendar view atom
+4 -5
View File
@@ -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>
)
}
@@ -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 |
@@ -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";
@@ -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");
@@ -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(),
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),
end: new Date(booking.end),
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]">
@@ -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";
@@ -16,33 +16,34 @@ 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";
const CalendarViewPlatformWrapperComponent = (
props: BookerPlatformWrapperAtomPropsForIndividual | BookerPlatformWrapperAtomPropsForTeam
) => {
const {
eventSlug,
isTeamEvent,
hostsLimit,
allowUpdatingUrlParams = false,
teamMemberEmail,
crmAppSlug,
crmOwnerRecordType,
} = props;
type CalendarViewPlatformWrapperAtomPropsForIndividual = {
username: string | string[];
eventSlug: string;
};
const teamId: number | undefined = props.isTeamEvent ? props.teamId : undefined;
type CalendarViewPlatformWrapperAtomPropsForTeam = {
teamId: number;
eventSlug: string;
};
const CalendarViewPlatformWrapperComponent = (
props:
| (CalendarViewPlatformWrapperAtomPropsForIndividual & { teamId?: number })
| (CalendarViewPlatformWrapperAtomPropsForTeam & { username?: string | string[] })
) => {
const isTeamEvent = !!props.teamId;
const teamId: number | undefined = props.teamId ? props.teamId : undefined;
const username = useMemo(() => {
if (props.username) {
return formatUsername(props.username);
@@ -50,16 +51,16 @@ const CalendarViewPlatformWrapperComponent = (
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,
});
@@ -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({
@@ -160,11 +115,10 @@ 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,
}
: {}),
@@ -172,11 +126,10 @@ const CalendarViewPlatformWrapperComponent = (
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 (
@@ -185,7 +138,7 @@ const CalendarViewPlatformWrapperComponent = (
<Header
isCalendarView={true}
isMyLink={true}
eventSlug={eventSlug}
eventSlug={props.eventSlug}
enabledLayouts={bookerLayout.bookerLayouts.enabledLayouts}
extraDays={7}
isMobile={false}
@@ -204,7 +157,9 @@ const CalendarViewPlatformWrapperComponent = (
};
export const CalendarViewPlatformWrapper = (
props: BookerPlatformWrapperAtomPropsForIndividual | BookerPlatformWrapperAtomPropsForTeam
props:
| (BookerPlatformWrapperAtomPropsForIndividual & { teamId?: number })
| Omit<BookerPlatformWrapperAtomPropsForTeam, "isTeamEvent">
) => {
return (
<BookerStoreProvider>