diff --git a/apps/web/components/booking/actions/bookingActions.ts b/apps/web/components/booking/actions/bookingActions.ts index bb7af61757..bd76e4c7e4 100644 --- a/apps/web/components/booking/actions/bookingActions.ts +++ b/apps/web/components/booking/actions/bookingActions.ts @@ -1,3 +1,4 @@ +import { isWithinMinimumRescheduleNotice } from "@calcom/features/bookings/lib/reschedule/isWithinMinimumRescheduleNotice"; import { BookingStatus, SchedulingType } from "@calcom/prisma/enums"; import type { ActionType } from "@calcom/ui/components/table"; @@ -122,8 +123,12 @@ export function getEditEventActions(context: BookingActionContext): ActionType[] ? `?seatReferenceUid=${seatReferenceUid}` : "" }`, - disabled: - (isBookingInPast && !booking.eventType.allowReschedulingPastBookings) || isDisabledRescheduling, + disabled: isActionDisabled("reschedule", { + ...context, + booking, + isBookingInPast, + isDisabledRescheduling, + }), }, { id: "reschedule_request", @@ -131,9 +136,12 @@ export function getEditEventActions(context: BookingActionContext): ActionType[] iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ", label: t("send_reschedule_request"), disabled: - (isBookingInPast && !booking.eventType.allowReschedulingPastBookings) || - isDisabledRescheduling || - booking.seatsReferences.length > 0, + isActionDisabled("reschedule_request", { + ...context, + booking, + isBookingInPast, + isDisabledRescheduling, + }) || booking.seatsReferences.length > 0, }, isBookingFromRoutingForm ? { @@ -229,12 +237,29 @@ export function shouldShowIndividualReportButton(context: BookingActionContext): } export function isActionDisabled(actionId: string, context: BookingActionContext): boolean { - const { booking, isBookingInPast, isDisabledRescheduling, isDisabledCancelling } = context; + const { booking, isBookingInPast, isDisabledRescheduling, isDisabledCancelling, isAttendee } = context; switch (actionId) { case "reschedule": case "reschedule_request": - return (isBookingInPast && !booking.eventType.allowReschedulingPastBookings) || isDisabledRescheduling; + // Only apply minimum reschedule notice restriction if user is NOT the organizer + // If user is an attendee (or not authenticated), apply the restriction + const isUserOrganizer = + !isAttendee && + booking.loggedInUser?.userId && + booking.user?.id && + booking.loggedInUser.userId === booking.user.id; + const isWithinMinimumNotice = + !isUserOrganizer && + isWithinMinimumRescheduleNotice( + new Date(booking.startTime), + booking.eventType.minimumRescheduleNotice ?? null + ); + return ( + (isBookingInPast && !booking.eventType.allowReschedulingPastBookings) || + isDisabledRescheduling || + isWithinMinimumNotice + ); case "cancel": return isDisabledCancelling || isBookingInPast; case "view_recordings": diff --git a/apps/web/lib/booking.ts b/apps/web/lib/booking.ts index d209dfafbb..87e96d0e56 100644 --- a/apps/web/lib/booking.ts +++ b/apps/web/lib/booking.ts @@ -44,6 +44,7 @@ export const getEventTypesFromDB = async (id: number) => { hideOrganizerEmail: true, disableCancelling: true, disableRescheduling: true, + minimumRescheduleNotice: true, disableGuests: true, timeZone: true, profile: { diff --git a/apps/web/lib/reschedule/[uid]/getServerSideProps.ts b/apps/web/lib/reschedule/[uid]/getServerSideProps.ts index 5c69b8a7c7..e3c009d649 100644 --- a/apps/web/lib/reschedule/[uid]/getServerSideProps.ts +++ b/apps/web/lib/reschedule/[uid]/getServerSideProps.ts @@ -47,6 +47,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }, select: { ...bookingMinimalSelect, + userId: true, responses: true, eventType: { select: { @@ -59,6 +60,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { allowReschedulingPastBookings: true, disableRescheduling: true, allowReschedulingCancelledBookings: true, + minimumRescheduleNotice: true, team: { select: { id: true, @@ -122,17 +124,21 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { booking: { uid, status: booking.status, + startTime: booking.startTime, endTime: booking.endTime, responses: booking.responses, + userId: booking.userId, eventType: { disableRescheduling: !!eventType?.disableRescheduling, allowReschedulingPastBookings: eventType.allowReschedulingPastBookings, allowBookingFromCancelledBookingReschedule: !!eventType.allowReschedulingCancelledBookings, + minimumRescheduleNotice: eventType.minimumRescheduleNotice, teamId: eventType.team?.id ?? null, }, }, eventUrl, forceRescheduleForCancelledBooking: allowRescheduleForCancelledBooking, + currentUserId: session?.user?.id ?? null, bookingSeat, }); diff --git a/apps/web/modules/bookings/views/bookings-single-view.tsx b/apps/web/modules/bookings/views/bookings-single-view.tsx index 04505d7469..4f45739c53 100644 --- a/apps/web/modules/bookings/views/bookings-single-view.tsx +++ b/apps/web/modules/bookings/views/bookings-single-view.tsx @@ -24,6 +24,7 @@ import { import { Price } from "@calcom/features/bookings/components/event-meta/Price"; import { getCalendarLinks, CalendarLinkType } from "@calcom/features/bookings/lib/getCalendarLinks"; import { RATING_OPTIONS, validateRating } from "@calcom/features/bookings/lib/rating"; +import { isWithinMinimumRescheduleNotice as isWithinMinimumRescheduleNoticeUtil } from "@calcom/features/bookings/lib/reschedule/isWithinMinimumRescheduleNotice"; import type { nameObjectSchema } from "@calcom/features/eventtypes/lib/eventNaming"; import { getEventName } from "@calcom/features/eventtypes/lib/eventNaming"; import { shouldShowFieldInCustomResponses } from "@calcom/lib/bookings/SystemField"; @@ -52,6 +53,7 @@ import { EmptyScreen } from "@calcom/ui/components/empty-screen"; import { EmailInput, TextArea } from "@calcom/ui/components/form"; import { Icon } from "@calcom/ui/components/icon"; import { showToast } from "@calcom/ui/components/toast"; +import { Tooltip } from "@calcom/ui/components/tooltip"; import { useCalcomTheme } from "@calcom/ui/styles"; import CancelBooking from "@calcom/web/components/booking/CancelBooking"; import EventReservationSchema from "@calcom/web/components/schemas/EventReservationSchema"; @@ -385,6 +387,15 @@ export default function Success(props: PageProps) { const canCancel = !eventType?.disableCancelling; const canReschedule = !eventType?.disableRescheduling; + // Check if reschedule should be disabled due to minimum reschedule notice + // Use server-side computed isHost prop instead of client-side computation + const isWithinMinimumRescheduleNotice = isHost + ? false // Organizers can always reschedule + : isWithinMinimumRescheduleNoticeUtil( + bookingInfo?.startTime ?? null, + eventType?.minimumRescheduleNotice ?? null + ); + const isRescheduleDisabled = !canReschedule || isWithinMinimumRescheduleNotice; const paymentStatusMessage = usePaymentStatus({ bookingStatus: bookingInfo.status, startTime: bookingInfo.startTime, @@ -489,7 +500,7 @@ export default function Success(props: PageProps) { {!isFeedbackMode && ( <>
+