Skip to content

Commit 4c73695

Browse files
alishaz-polymathhackice20ThyMinimalDevdevin-ai-integration[bot]
authored
fix: refresh slots on timezone change for booker timezone restrictions (calcom#27491)
* fix: refresh slots on timezone change for booker timezone restrictions * refactor: use useMemo for timezone change detection * revert: remove unnecessary formatting changes * feat: add timezone refresh for platform components Add timezone change detection and slot refresh to BookerPlatformWrapper and EventTypeCalendarViewComponent to handle restriction schedules with useBookerTimezone enabled. * refactor: extract timezone slot refresh logic into reusable hook * Update packages/platform/atoms/calendar-view/EventTypeCalendarViewComponent.tsx Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> * Update packages/platform/atoms/calendar-view/EventTypeCalendarViewComponent.tsx Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> * fix * fix: prevent unnecessary getSchedule calls when useBookerTimezone is disabled * fix: add timezone fields to BookerEvent type * fix: add missing properties to BookerEvent and BookerEventProfile types * trying to fix type errors * Add restrictionScheduleId and useBookerTimezone fields * fix: correct import path for useBookerTime hook Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * fix: explicitly include restrictionScheduleId and useBookerTimezone in getPublicEvent return Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * chore: trigger fresh CI build Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * fix: cast event.data to BookerEvent for timezone fields access Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * refactor: address review feedback for timezone slot refresh Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * refactor: add explicit return type to event handler to ensure type propagation Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> * refactor: extract useStableTimezone hook and remove dead timezone detection code Co-Authored-By: ali@cal.com <alishahbaz7@gmail.com> --------- Co-authored-by: hackice20 <yashkam431@gmail.com> Co-authored-by: Yash <116657771+hackice20@users.noreply.github.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent ab4eff1 commit 4c73695

9 files changed

Lines changed: 140 additions & 22 deletions

File tree

apps/web/modules/bookings/components/BookerWebWrapper.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { useInitializeBookerStore } from "@calcom/features/bookings/Booker/store
1919
import { useEvent, useScheduleForEvent } from "@calcom/web/modules/schedules/hooks/useEvent";
2020
import { useBrandColors } from "@calcom/features/bookings/Booker/utils/use-brand-colors";
2121
import type { getPublicEvent } from "@calcom/features/eventtypes/lib/getPublicEvent";
22-
import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR, WEBAPP_URL } from "@calcom/lib/constants";
22+
import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR, WEBAPP_URL } from "@calcom/lib/constants";
2323
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
2424
import { localStorage } from "@calcom/lib/webstorage";
2525
import { usePathname, useRouter, useSearchParams } from "next/navigation";
@@ -42,11 +42,11 @@ const BookerWebWrapperComponent = (props: BookerWebWrapperAtomProps): JSX.Elemen
4242
});
4343
const event = props.eventData
4444
? {
45-
data: props.eventData,
46-
isSuccess: true,
47-
isError: false,
48-
isPending: false,
49-
}
45+
data: props.eventData,
46+
isSuccess: true,
47+
isError: false,
48+
isPending: false,
49+
}
5050
: clientFetchedEvent;
5151

5252
const bookerLayout = useBookerLayout(event.data?.profile?.bookerLayouts);
@@ -149,6 +149,9 @@ const BookerWebWrapperComponent = (props: BookerWebWrapperAtomProps): JSX.Elemen
149149
useApiV2: props.useApiV2,
150150
bookerLayout,
151151
...(props.entity.orgSlug ? { orgSlug: props.entity.orgSlug } : {}),
152+
restrictionSchedule: event.data?.restrictionScheduleId
153+
? { id: event.data.restrictionScheduleId, useBookerTimezone: event.data.useBookerTimezone }
154+
: undefined,
152155
});
153156
const bookings = useBookings({
154157
event,

apps/web/modules/schedules/hooks/useEvent.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
66
import { trpc } from "@calcom/trpc/react";
77

88
import { useBookerTime } from "@calcom/features/bookings/Booker/hooks/useBookerTime";
9+
import { useStableTimezone } from "@calcom/features/bookings/Booker/hooks/useStableTimezone";
910

1011
export type useEventReturnType = ReturnType<typeof useEvent>;
1112
export type useScheduleForEventReturnType = ReturnType<typeof useScheduleForEvent>;
@@ -71,6 +72,7 @@ export const useScheduleForEvent = ({
7172
isTeamEvent,
7273
useApiV2 = true,
7374
bookerLayout,
75+
restrictionSchedule,
7476
}: {
7577
username?: string | null;
7678
eventSlug?: string | null;
@@ -92,21 +94,24 @@ export const useScheduleForEvent = ({
9294
extraDays: number;
9395
columnViewExtraDays: { current: number };
9496
};
97+
restrictionSchedule?: { id: number | null; useBookerTimezone: boolean };
9598
}) => {
96-
const { timezone } = useBookerTime();
99+
const { timezone: rawTimezone } = useBookerTime();
97100
const [usernameFromStore, eventSlugFromStore, monthFromStore, durationFromStore] = useBookerStoreContext(
98101
(state) => [state.username, state.eventSlug, state.month, state.selectedDuration],
99102
shallow
100103
);
101104

105+
const effectiveTimezone = useStableTimezone(rawTimezone, restrictionSchedule);
106+
102107
const searchParams = useCompatSearchParams();
103108
const rescheduleUid = searchParams?.get("rescheduleUid");
104109

105110
const schedule = useSchedule({
106111
username: usernameFromStore ?? username,
107112
eventSlug: eventSlugFromStore ?? eventSlug,
108113
eventId,
109-
timezone,
114+
timezone: effectiveTimezone,
110115
selectedDate,
111116
dayCount,
112117
rescheduleUid,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { renderHook } from "@testing-library/react";
5+
import { describe, it, expect } from "vitest";
6+
7+
import { useStableTimezone } from "./useStableTimezone";
8+
9+
describe("useStableTimezone", () => {
10+
it("returns the raw timezone when no restriction schedule is provided", () => {
11+
const { result } = renderHook(() => useStableTimezone("America/New_York"));
12+
expect(result.current).toBe("America/New_York");
13+
});
14+
15+
it("returns the raw timezone when restriction schedule is undefined", () => {
16+
const { result } = renderHook(() => useStableTimezone("America/New_York", undefined));
17+
expect(result.current).toBe("America/New_York");
18+
});
19+
20+
it("returns the raw timezone when restriction schedule id is null", () => {
21+
const { result } = renderHook(() =>
22+
useStableTimezone("America/New_York", { id: null, useBookerTimezone: false })
23+
);
24+
expect(result.current).toBe("America/New_York");
25+
});
26+
27+
it("returns the raw timezone when restriction schedule id is 0", () => {
28+
const { result } = renderHook(() =>
29+
useStableTimezone("America/New_York", { id: 0, useBookerTimezone: false })
30+
);
31+
expect(result.current).toBe("America/New_York");
32+
});
33+
34+
it("returns the raw timezone when useBookerTimezone is true (timezone should follow the booker)", () => {
35+
const { result } = renderHook(() =>
36+
useStableTimezone("America/New_York", { id: 1, useBookerTimezone: true })
37+
);
38+
expect(result.current).toBe("America/New_York");
39+
});
40+
41+
it("pins to initial timezone when restriction schedule exists and useBookerTimezone is false", () => {
42+
let timezone = "America/New_York";
43+
const { result, rerender } = renderHook(() =>
44+
useStableTimezone(timezone, { id: 1, useBookerTimezone: false })
45+
);
46+
expect(result.current).toBe("America/New_York");
47+
48+
timezone = "Europe/London";
49+
rerender();
50+
expect(result.current).toBe("America/New_York");
51+
});
52+
53+
it("follows timezone changes when useBookerTimezone is true", () => {
54+
let timezone = "America/New_York";
55+
const { result, rerender } = renderHook(() =>
56+
useStableTimezone(timezone, { id: 1, useBookerTimezone: true })
57+
);
58+
expect(result.current).toBe("America/New_York");
59+
60+
timezone = "Europe/London";
61+
rerender();
62+
expect(result.current).toBe("Europe/London");
63+
});
64+
65+
it("follows timezone changes when there is no restriction schedule", () => {
66+
let timezone = "America/New_York";
67+
const { result, rerender } = renderHook(() => useStableTimezone(timezone));
68+
expect(result.current).toBe("America/New_York");
69+
70+
timezone = "Europe/London";
71+
rerender();
72+
expect(result.current).toBe("Europe/London");
73+
});
74+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { useRef } from "react";
2+
3+
export function useStableTimezone(
4+
timezone: string,
5+
restrictionSchedule?: { id: number | null; useBookerTimezone: boolean }
6+
): string {
7+
const initialRef = useRef(timezone);
8+
const shouldPin =
9+
restrictionSchedule?.id != null &&
10+
restrictionSchedule.id > 0 &&
11+
restrictionSchedule.useBookerTimezone === false;
12+
return shouldPin ? initialRef.current : timezone;
13+
}

packages/features/bookings/types.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { ErrorOption, FieldPath } from "react-hook-form";
2-
31
import type { RegularBookingCreateResult } from "@calcom/features/bookings/lib/dto/types";
42
import type { Slots } from "@calcom/features/calendars/lib/types";
53
import type { PublicEventType } from "@calcom/features/eventtypes/lib/getPublicEvent";
@@ -26,7 +24,10 @@ type BookerEventUser = Pick<
2624
bookerUrl: string;
2725
};
2826

29-
type BookerEventProfile = Pick<PublicEvent["profile"], "name" | "image" | "bookerLayouts">;
27+
type BookerEventProfile = Pick<
28+
PublicEvent["profile"],
29+
"name" | "image" | "bookerLayouts" | "brandColor" | "darkBrandColor" | "theme" | "weekStart" | "username"
30+
>;
3031

3132
// Re-export Slots from the server-safe location
3233
export type { Slots };
@@ -40,10 +41,10 @@ export type BookerEvent = Pick<
4041
| "recurringEvent"
4142
| "entity"
4243
| "locations"
43-
| "enablePerHostLocations"
4444
| "metadata"
4545
| "isDynamic"
4646
| "requiresConfirmation"
47+
| "requiresBookerEmailVerification"
4748
| "price"
4849
| "currency"
4950
| "lockTimeZoneToggleOnBookingPage"
@@ -66,13 +67,14 @@ export type BookerEvent = Pick<
6667
| "interfaceLanguage"
6768
| "team"
6869
| "owner"
70+
| "restrictionScheduleId"
71+
| "useBookerTimezone"
6972
> & {
7073
subsetOfUsers: BookerEventUser[];
7174
showInstantEventConnectNowModal: boolean;
75+
enablePerHostLocations?: boolean;
7276
} & { profile: BookerEventProfile };
7377

74-
export type ValidationErrors<T extends object> = { key: FieldPath<T>; error: ErrorOption }[];
75-
7678
export type EventPrice = { currency: string; price: number; displayAlternateSymbol?: boolean };
7779

7880
export enum EventDetailBlocks {

packages/features/eventtypes/lib/getPublicEvent.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ export const getPublicEventSelect = (fetchAllUsers: boolean) => {
172172
hidden: true,
173173
assignAllTeamMembers: true,
174174
rescheduleWithSameRoundRobinHost: true,
175+
restrictionScheduleId: true,
176+
useBookerTimezone: true,
175177
parent: {
176178
select: {
177179
team: {
@@ -341,6 +343,8 @@ export const getPublicEvent = async (
341343
return {
342344
...defaultEvent,
343345
bookingFields: getBookingFieldsWithSystemFields({ ...defaultEvent, disableBookingTitle }),
346+
restrictionScheduleId: null,
347+
useBookerTimezone: false,
344348
// Clears meta data since we don't want to send this in the public api.
345349
subsetOfUsers: users.map((user) => ({
346350
...user,
@@ -592,6 +596,8 @@ export const getPublicEvent = async (
592596
disableRescheduling: event.disableRescheduling,
593597
allowReschedulingCancelledBookings: event.allowReschedulingCancelledBookings,
594598
interfaceLanguage: event.interfaceLanguage,
599+
restrictionScheduleId: event.restrictionScheduleId,
600+
useBookerTimezone: event.useBookerTimezone,
595601
};
596602
};
597603

packages/platform/atoms/booker/BookerPlatformWrapper.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import {
77
useBookerStoreContext,
88
useInitializeBookerStoreContext,
99
} from "@calcom/features/bookings/Booker/BookerStoreProvider";
10-
import { useBookerLayout } from "@calcom/features/bookings/Booker/hooks/useBookerLayout";
11-
import { useBookingForm } from "@calcom/features/bookings/Booker/hooks/useBookingForm";
12-
import { useLocalSet } from "@calcom/features/bookings/Booker/hooks/useLocalSet";
10+
import { useBookerLayout } from "@calcom/features/bookings/Booker/components/hooks/useBookerLayout";
11+
import { useBookingForm } from "@calcom/features/bookings/Booker/components/hooks/useBookingForm";
12+
import { useLocalSet } from "@calcom/features/bookings/Booker/components/hooks/useLocalSet";
13+
import { useStableTimezone } from "@calcom/features/bookings/Booker/hooks/useStableTimezone";
1314
import { useInitializeBookerStore } from "@calcom/features/bookings/Booker/store";
1415
import { useTimePreferences } from "@calcom/features/bookings/lib";
1516
import type { ConnectedDestinationCalendars } from "@calcom/features/calendars/lib/getConnectedDestinationCalendars";
@@ -243,7 +244,13 @@ const BookerPlatformWrapperComponent = (
243244
return restFormValues;
244245
}, [restFormValues]);
245246

246-
const { timezone } = useTimePreferences();
247+
const { timezone: rawTimezone } = useTimePreferences();
248+
const timezone = useStableTimezone(
249+
rawTimezone,
250+
event?.data?.restrictionScheduleId != null
251+
? { id: event.data.restrictionScheduleId, useBookerTimezone: event.data.useBookerTimezone }
252+
: undefined
253+
);
247254

248255
const [calculatedStartTime, calculatedEndTime] = useTimesForSchedule({
249256
month,
@@ -315,7 +322,7 @@ const BookerPlatformWrapperComponent = (
315322
}
316323
}, [schedule.data, schedule.isPending, schedule.error, onTimeslotsLoaded]);
317324

318-
const bookerForm = useBookingForm({
325+
const bookerForm= useBookingForm({
319326
event: event?.data,
320327
sessionEmail:
321328
session?.data?.email && clientId

packages/platform/atoms/calendar-view/EventTypeCalendarViewComponent.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Header } from "@calcom/features/bookings/components/Header";
99
import { BookerSection } from "@calcom/features/bookings/components/Section";
1010
import { useAvailableTimeSlots } from "@calcom/features/bookings/Booker/hooks/useAvailableTimeSlots";
1111
import { useBookerLayout } from "@calcom/features/bookings/Booker/hooks/useBookerLayout";
12+
import { useStableTimezone } from "@calcom/features/bookings/Booker/hooks/useStableTimezone";
1213
import { useTimePreferences } from "@calcom/features/bookings/lib";
1314
import { LargeCalendar } from "./components/LargeCalendar";
1415
import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents";
@@ -68,7 +69,13 @@ export const EventTypeCalendarViewComponent = (
6869
bookerLayout,
6970
});
7071

71-
const { timezone } = useTimePreferences();
72+
const { timezone: rawTimezone } = useTimePreferences();
73+
const timezone = useStableTimezone(
74+
rawTimezone,
75+
event?.data?.restrictionScheduleId != null
76+
? { id: event.data.restrictionScheduleId, useBookerTimezone: event.data.useBookerTimezone }
77+
: undefined
78+
);
7279
const isDynamic = useMemo(() => {
7380
return getUsernameList(username ?? "").length > 1;
7481
}, [username]);
@@ -112,7 +119,7 @@ export const EventTypeCalendarViewComponent = (
112119
const selectedEventDuration = useBookerStoreContext((state) => state.selectedDuration);
113120
const eventDuration = selectedEventDuration || event?.data?.length || 30;
114121

115-
const availableTimeSlots = useAvailableTimeSlots({ schedule: schedule.data, eventDuration });
122+
const availableTimeSlots= useAvailableTimeSlots({ schedule: schedule.data, eventDuration });
116123

117124
return (
118125
<AtomsWrapper>

packages/trpc/server/routers/publicViewer/event.handler.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { PublicEventType } from "@calcom/features/eventtypes/lib/getPublicEvent";
12
import { EventRepository } from "@calcom/features/eventtypes/repositories/EventRepository";
23

34
import type { TEventInputSchema } from "./event.schema";
@@ -7,7 +8,7 @@ interface EventHandlerOptions {
78
userId?: number;
89
}
910

10-
export const eventHandler = async ({ input, userId }: EventHandlerOptions) => {
11+
export const eventHandler = async ({ input, userId }: EventHandlerOptions): Promise<PublicEventType> => {
1112
return await EventRepository.getPublicEvent(input, userId);
1213
};
1314

0 commit comments

Comments
 (0)