Skip to content

Commit 09ee39a

Browse files
authored
feat: upcoming bookings atom (calcom#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
1 parent 243012e commit 09ee39a

4 files changed

Lines changed: 67 additions & 96 deletions

File tree

.changeset/eighty-walls-send.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@calcom/atoms": minor
3+
---
4+
5+
This PR adds ability to display bookings of a user event for calendar view atom

docs/platform/atoms/calendar-view.mdx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { CalendarView } from "@calcom/atoms";
3030
export default function Booker( props : BookerProps ) {
3131
return (
3232
<div>
33-
<CalendarView isTeamEvent={true} teamId={props.teamId} eventSlug={props.eventSlug} />
33+
<CalendarView teamId={props.teamId} eventSlug={props.eventSlug} />
3434
</div>
3535
)
3636
}
@@ -55,7 +55,6 @@ Below is a list of props that can be passed to the create event type atom
5555

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

packages/features/calendar-view/LargeCalendar.tsx

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import { useMemo, useEffect } from "react";
1+
import { useEffect, useMemo } from "react";
22

33
import dayjs from "@calcom/dayjs";
44
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
55
import { useAvailableTimeSlots } from "@calcom/features/bookings/Booker/components/hooks/useAvailableTimeSlots";
66
import type { BookerEvent } from "@calcom/features/bookings/types";
77
import { Calendar } from "@calcom/features/calendars/weeklyview";
8-
import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events";
98
import { localStorage } from "@calcom/lib/webstorage";
9+
import type { BookingStatus } from "@calcom/prisma/enums";
1010

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

@@ -22,12 +22,11 @@ export const LargeCalendar = ({
2222
schedule?: useScheduleForEventReturnType["data"];
2323
isLoading: boolean;
2424
event: {
25-
data?: Pick<BookerEvent, "length"> | null;
25+
data?: Pick<BookerEvent, "length" | "id"> | null;
2626
};
2727
}) => {
2828
const selectedDate = useBookerStoreContext((state) => state.selectedDate);
2929
const selectedEventDuration = useBookerStoreContext((state) => state.selectedDuration);
30-
const overlayEvents = useOverlayCalendarStore((state) => state.overlayBusyDates);
3130
const displayOverlay =
3231
getQueryParam("overlayCalendar") === "true" || localStorage?.getItem("overlayCalendarSwitchDefault");
3332

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

42+
const { data: upcomingBookings } = useBookings({
43+
take: 150,
44+
skip: 0,
45+
status: ["upcoming", "past", "recurring"],
46+
eventTypeId: event?.data?.id,
47+
afterStart: startDate.toISOString(),
48+
beforeEnd: endDate.toISOString(),
49+
});
50+
4351
// HACK: force rerender when overlay events change
4452
// Sine we dont use react router here we need to force rerender (ATOM SUPPORT)
45-
// eslint-disable-next-line @typescript-eslint/no-empty-function
53+
4654
useEffect(() => {}, [displayOverlay]);
4755

4856
const overlayEventsForDate = useMemo(() => {
49-
if (!overlayEvents || !displayOverlay) return [];
50-
return overlayEvents.map((event, id) => {
57+
if (!upcomingBookings) return [];
58+
59+
return upcomingBookings?.map((booking) => {
5160
return {
52-
id,
53-
start: dayjs(event.start).toDate(),
54-
end: dayjs(event.end).toDate(),
55-
title: "Busy",
61+
id: booking.id,
62+
title: booking.title ?? `Busy`,
63+
start: new Date(booking.start),
64+
end: new Date(booking.end),
5665
options: {
57-
status: "ACCEPTED",
66+
borderColor: "black",
67+
status: booking.status.toUpperCase() as BookingStatus,
68+
"data-test-id": "troubleshooter-busy-event",
69+
className: "border-[1.5px]",
5870
},
59-
} as CalendarEvent;
71+
};
6072
});
61-
}, [overlayEvents, displayOverlay]);
73+
}, [upcomingBookings]);
6274

6375
return (
6476
<div className="h-full [--calendar-dates-sticky-offset:66px]">

packages/platform/atoms/calendar-view/wrappers/CalendarViewPlatformWrapper.tsx

Lines changed: 31 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AtomsWrapper } from "@/components/atoms-wrapper";
2-
import { useMemo, useEffect, useState } from "react";
2+
import { useMemo } from "react";
33
import { shallow } from "zustand/shallow";
44

55
import dayjs from "@calcom/dayjs";
@@ -16,50 +16,51 @@ import { useTimePreferences } from "@calcom/features/bookings/lib";
1616
import { LargeCalendar } from "@calcom/features/calendar-view/LargeCalendar";
1717
import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents";
1818
import { useTimesForSchedule } from "@calcom/features/schedules/lib/use-schedule/useTimesForSchedule";
19-
import { getRoutedTeamMemberIdsFromSearchParams } from "@calcom/lib/bookings/getRoutedTeamMemberIdsFromSearchParams";
2019

2120
import { formatUsername } from "../../booker/BookerPlatformWrapper";
2221
import type {
2322
BookerPlatformWrapperAtomPropsForIndividual,
2423
BookerPlatformWrapperAtomPropsForTeam,
2524
} from "../../booker/types";
26-
import { useGetBookingForReschedule } from "../../hooks/bookings/useGetBookingForReschedule";
2725
import { useAtomGetPublicEvent } from "../../hooks/event-types/public/useAtomGetPublicEvent";
2826
import { useEventType } from "../../hooks/event-types/public/useEventType";
2927
import { useTeamEventType } from "../../hooks/event-types/public/useTeamEventType";
3028
import { useAvailableSlots } from "../../hooks/useAvailableSlots";
3129

30+
type CalendarViewPlatformWrapperAtomPropsForIndividual = {
31+
username: string | string[];
32+
eventSlug: string;
33+
};
34+
35+
type CalendarViewPlatformWrapperAtomPropsForTeam = {
36+
teamId: number;
37+
eventSlug: string;
38+
};
39+
3240
const CalendarViewPlatformWrapperComponent = (
33-
props: BookerPlatformWrapperAtomPropsForIndividual | BookerPlatformWrapperAtomPropsForTeam
41+
props:
42+
| (CalendarViewPlatformWrapperAtomPropsForIndividual & { teamId?: number })
43+
| (CalendarViewPlatformWrapperAtomPropsForTeam & { username?: string | string[] })
3444
) => {
35-
const {
36-
eventSlug,
37-
isTeamEvent,
38-
hostsLimit,
39-
allowUpdatingUrlParams = false,
40-
teamMemberEmail,
41-
crmAppSlug,
42-
crmOwnerRecordType,
43-
} = props;
44-
45-
const teamId: number | undefined = props.isTeamEvent ? props.teamId : undefined;
45+
const isTeamEvent = !!props.teamId;
46+
const teamId: number | undefined = props.teamId ? props.teamId : undefined;
4647
const username = useMemo(() => {
4748
if (props.username) {
4849
return formatUsername(props.username);
4950
}
5051
return "";
5152
}, [props.username]);
5253

53-
const { isPending } = useEventType(username, eventSlug, isTeamEvent);
54-
const { isPending: isTeamPending } = useTeamEventType(teamId, eventSlug, isTeamEvent, hostsLimit);
54+
const { isPending } = useEventType(username, props.eventSlug, isTeamEvent);
55+
56+
const { isPending: isTeamPending } = useTeamEventType(teamId, props.eventSlug, isTeamEvent);
5557

56-
const setSelectedDuration = useBookerStoreContext((state) => state.setSelectedDuration);
5758
const selectedDuration = useBookerStoreContext((state) => state.selectedDuration);
5859

5960
const event = useAtomGetPublicEvent({
6061
username,
6162
eventSlug: props.eventSlug,
62-
isTeamEvent: props.isTeamEvent,
63+
isTeamEvent: isTeamEvent,
6364
teamId,
6465
selectedDuration,
6566
});
@@ -90,67 +91,21 @@ const CalendarViewPlatformWrapperComponent = (
9091
selectedDate,
9192
});
9293

93-
useEffect(() => {
94-
setSelectedDuration(props.duration ?? null);
95-
}, [props.duration, setSelectedDuration]);
96-
9794
const { timezone } = useTimePreferences();
9895
const isDynamic = useMemo(() => {
9996
return getUsernameList(username ?? "").length > 1;
10097
}, [username]);
10198

102-
const [routingParams, setRoutingParams] = useState<{
103-
routedTeamMemberIds?: number[];
104-
_shouldServeCache?: boolean;
105-
skipContactOwner?: boolean;
106-
isBookingDryRun?: boolean;
107-
}>({});
108-
109-
useEffect(() => {
110-
const searchParams = props.routingFormSearchParams
111-
? new URLSearchParams(props.routingFormSearchParams)
112-
: new URLSearchParams(window.location.search);
113-
114-
const routedTeamMemberIds = getRoutedTeamMemberIdsFromSearchParams(searchParams);
115-
const skipContactOwner = searchParams.get("cal.skipContactOwner") === "true";
116-
117-
const _cacheParam = searchParams?.get("cal.cache");
118-
const _shouldServeCache = _cacheParam ? _cacheParam === "true" : undefined;
119-
const isBookingDryRun =
120-
searchParams?.get("cal.isBookingDryRun")?.toLowerCase() === "true" ||
121-
searchParams?.get("cal.sandbox")?.toLowerCase() === "true";
122-
setRoutingParams({
123-
...(skipContactOwner ? { skipContactOwner } : {}),
124-
...(routedTeamMemberIds ? { routedTeamMemberIds } : {}),
125-
...(_shouldServeCache ? { _shouldServeCache } : {}),
126-
...(isBookingDryRun ? { isBookingDryRun } : {}),
127-
});
128-
}, [props.routingFormSearchParams]);
12999
const bookingData = useBookerStoreContext((state) => state.bookingData);
130-
const setBookingData = useBookerStoreContext((state) => state.setBookingData);
131-
132-
useGetBookingForReschedule({
133-
uid: props.rescheduleUid ?? props.bookingUid ?? "",
134-
onSuccess: (data) => {
135-
setBookingData(data);
136-
},
137-
});
138100

139101
useInitializeBookerStoreContext({
140102
...props,
141-
teamMemberEmail,
142-
crmAppSlug,
143-
crmOwnerRecordType,
144-
crmRecordId: props.crmRecordId,
145103
eventId: event?.data?.id,
146-
rescheduleUid: props.rescheduleUid ?? null,
147-
bookingUid: props.bookingUid ?? null,
148104
layout: "week_view",
149-
org: props.entity?.orgSlug,
150105
username,
151106
bookingData,
152107
isPlatform: true,
153-
allowUpdatingUrlParams,
108+
allowUpdatingUrlParams: false,
154109
});
155110

156111
const schedule = useAvailableSlots({
@@ -160,23 +115,21 @@ const CalendarViewPlatformWrapperComponent = (
160115
endTime,
161116
timeZone: timezone,
162117
duration: selectedDuration ?? undefined,
163-
rescheduleUid: props.rescheduleUid,
164-
teamMemberEmail: props.teamMemberEmail ?? undefined,
165-
...(props.isTeamEvent
118+
teamMemberEmail: undefined,
119+
...(isTeamEvent
166120
? {
167-
isTeamEvent: props.isTeamEvent,
121+
isTeamEvent: isTeamEvent,
168122
teamId: teamId,
169123
}
170124
: {}),
171125
enabled:
172126
Boolean(teamId || username) &&
173127
Boolean(month) &&
174128
Boolean(timezone) &&
175-
(props.isTeamEvent ? !isTeamPending : !isPending) &&
129+
(isTeamEvent ? !isTeamPending : !isPending) &&
176130
Boolean(event?.data?.id),
177-
orgSlug: props.entity?.orgSlug ?? undefined,
178-
eventTypeSlug: isDynamic ? "dynamic" : eventSlug || "",
179-
...routingParams,
131+
orgSlug: undefined,
132+
eventTypeSlug: isDynamic ? "dynamic" : props.eventSlug || "",
180133
});
181134

182135
return (
@@ -185,7 +138,7 @@ const CalendarViewPlatformWrapperComponent = (
185138
<Header
186139
isCalendarView={true}
187140
isMyLink={true}
188-
eventSlug={eventSlug}
141+
eventSlug={props.eventSlug}
189142
enabledLayouts={bookerLayout.bookerLayouts.enabledLayouts}
190143
extraDays={7}
191144
isMobile={false}
@@ -204,7 +157,9 @@ const CalendarViewPlatformWrapperComponent = (
204157
};
205158

206159
export const CalendarViewPlatformWrapper = (
207-
props: BookerPlatformWrapperAtomPropsForIndividual | BookerPlatformWrapperAtomPropsForTeam
160+
props:
161+
| (BookerPlatformWrapperAtomPropsForIndividual & { teamId?: number })
162+
| Omit<BookerPlatformWrapperAtomPropsForTeam, "isTeamEvent">
208163
) => {
209164
return (
210165
<BookerStoreProvider>

0 commit comments

Comments
 (0)