feat: Setting for rescheduling past bookings (#18358)

* feat: Setting for rescheduling past bookings

* fix stuff

* rename column

* add e2e test

* fix column name

* fix typo

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
Amit Sharma
2025-01-28 19:25:41 +00:00
committed by GitHub
co-authored by Anik Dhabal Babu Peer Richelsen CarinaWolli
parent 39891ee308
commit 438a73b0cd
17 changed files with 97 additions and 17 deletions
@@ -216,7 +216,7 @@ function BookingListItem(booking: BookingItemProps) {
];
const editBookingActions: ActionType[] = [
...(isBookingInPast
...(isBookingInPast && !booking.eventType.allowReschedulingPastBookings
? []
: [
{
+1
View File
@@ -37,6 +37,7 @@ export const getEventTypesFromDB = async (id: number) => {
price: true,
currency: true,
bookingFields: true,
allowReschedulingPastBookings: true,
disableGuests: true,
timeZone: true,
profile: {
@@ -57,6 +57,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
},
},
slug: true,
allowReschedulingPastBookings: true,
team: {
select: {
parentId: true,
@@ -131,7 +132,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
});
const isBookingInPast = booking.endTime && new Date(booking.endTime) < new Date();
if (isBookingInPast) {
if (isBookingInPast && !eventType.allowReschedulingPastBookings) {
const destinationUrlSearchParams = new URLSearchParams();
const responses = bookingSeat ? getSafe<string>(bookingSeat.data, ["responses"]) : booking.responses;
const name = getSafe<string>(responses, ["name"]);
@@ -730,22 +730,23 @@ export default function Success(props: PageProps) {
</span>
<>
{!props.recurringBookings && !isBookingInPast && (
<span className="text-default inline">
<span className="underline" data-testid="reschedule-link">
<Link
href={`/reschedule/${seatReferenceUid || bookingInfo?.uid}${
currentUserEmail
? `?rescheduledBy=${encodeURIComponent(currentUserEmail)}`
: ""
}`}
legacyBehavior>
{t("reschedule")}
</Link>
{!props.recurringBookings &&
(!isBookingInPast || eventType.allowReschedulingPastBookings) && (
<span className="text-default inline">
<span className="underline" data-testid="reschedule-link">
<Link
href={`/reschedule/${seatReferenceUid || bookingInfo?.uid}${
currentUserEmail
? `?rescheduledBy=${encodeURIComponent(currentUserEmail)}`
: ""
}`}
legacyBehavior>
{t("reschedule")}
</Link>
</span>
<span className="mx-2">{t("or_lowercase")}</span>
</span>
<span className="mx-2">{t("or_lowercase")}</span>
</span>
)}
)}
<button
data-testid="cancel"
+47
View File
@@ -50,6 +50,53 @@ test.describe("Reschedule Tests", async () => {
await booking.delete();
});
test("Should not show reschedule and request reschedule option if booking in past and disallowed", async ({
page,
users,
bookings,
}) => {
const user = await users.create();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const booking = await bookings.create(user.id, user.username, user.eventTypes[0].id!, {
status: BookingStatus.ACCEPTED,
startTime: dayjs().subtract(2, "day").toDate(),
endTime: dayjs().subtract(2, "day").add(30, "minutes").toDate(),
});
await prisma.eventType.update({
where: {
id: user.eventTypes[0].id,
},
data: {
allowReschedulingPastBookings: true,
},
});
await user.apiLogin();
await page.goto("/bookings/past");
await page.locator('[data-testid="edit_booking"]').nth(0).click();
await expect(page.locator('[data-testid="reschedule"]')).toBeVisible();
await expect(page.locator('[data-testid="reschedule_request"]')).toBeVisible();
await prisma.eventType.update({
where: {
id: user.eventTypes[0].id,
},
data: {
allowReschedulingPastBookings: false,
},
});
await page.reload();
await page.locator('[data-testid="edit_booking"]').nth(0).click();
await expect(page.locator('[data-testid="reschedule"]')).toBeHidden();
await expect(page.locator('[data-testid="reschedule_request"]')).toBeHidden();
});
test("Should display former time when rescheduling availability", async ({ page, users, bookings }) => {
const user = await users.create();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -2907,6 +2907,8 @@
"attribute_weight_enabled_description": "By enabling weights, it would be possible to assign higher priority to certain attributes per user. The higher the weight, the higher the priority.",
"routed": "Routed",
"reassigned": "Reassigned",
"allow_rescheduling_past_events": "Allow rescheduling past events",
"allow_rescheduling_past_events_description": "Enabling this option allows for past events to be rescheduled",
"rerouted": "Rerouted",
"salesforce_assigned": "Salesforce assignment",
"router_position": "Router Position",
@@ -476,6 +476,7 @@ export const EventAdvancedTab = ({
const eventTypeColorLocked = shouldLockDisableProps("eventTypeColor");
const lockTimeZoneToggleOnBookingPageLocked = shouldLockDisableProps("lockTimeZoneToggleOnBookingPage");
const multiplePrivateLinksLocked = shouldLockDisableProps("multiplePrivateLinks");
const reschedulingPastBookingsLocked = shouldLockDisableProps("allowReschedulingPastBookings");
const { isLocked, ...eventNameLocked } = shouldLockDisableProps("eventName");
if (isManagedEventType) {
@@ -893,6 +894,21 @@ export const EventAdvancedTab = ({
/>
)}
/>
<Controller
name="allowReschedulingPastBookings"
render={({ field: { value, onChange } }) => (
<SettingsToggle
labelClassName={classNames("text-sm")}
toggleSwitchAtTheEnd={true}
switchContainerClassName={classNames("border-subtle rounded-lg border py-6 px-4 sm:px-6")}
title={t("allow_rescheduling_past_events")}
{...reschedulingPastBookingsLocked}
description={t("allow_rescheduling_past_events_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
/>
)}
/>
<Controller
name="eventTypeColor"
render={() => (
+1
View File
@@ -88,6 +88,7 @@ const commons = {
seatsShowAttendees: null,
seatsShowAvailabilityCount: null,
onlyShowFirstAvailableSlot: false,
allowReschedulingPastBookings: false,
id: 0,
hideCalendarNotes: false,
hideCalendarEventDetails: false,
+1
View File
@@ -35,6 +35,7 @@ export const eventTypeSelect = Prisma.validator<Prisma.EventTypeSelect>()({
afterEventBuffer: true,
seatsPerTimeSlot: true,
onlyShowFirstAvailableSlot: true,
allowReschedulingPastBookings: true,
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
scheduleId: true,
@@ -501,6 +501,7 @@ export class EventTypeRepository {
onlyShowFirstAvailableSlot: true,
durationLimits: true,
assignAllTeamMembers: true,
allowReschedulingPastBookings: true,
assignRRMembersUsingSegment: true,
rrSegmentQueryValue: true,
isRRWeightsEnabled: true,
@@ -784,6 +785,7 @@ export class EventTypeRepository {
periodStartDate: true,
periodEndDate: true,
onlyShowFirstAvailableSlot: true,
allowReschedulingPastBookings: true,
periodCountCalendarDays: true,
rescheduleWithSameRoundRobinHost: true,
periodDays: true,
+1
View File
@@ -146,6 +146,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
rrSegmentQueryValue: null,
autoTranslateDescriptionEnabled: false,
useEventLevelSelectedCalendars: false,
allowReschedulingPastBookings: false,
...eventType,
};
};
@@ -90,6 +90,7 @@ export const useEventTypeForm = ({
requiresConfirmationForFreeEmail: eventType.requiresConfirmationForFreeEmail,
slotInterval: eventType.slotInterval,
minimumBookingNotice: eventType.minimumBookingNotice,
allowReschedulingPastBookings: eventType.allowReschedulingPastBookings,
metadata: eventType.metadata,
hosts: eventType.hosts.sort((a, b) => sortHosts(a, b, eventType.isRRWeightsEnabled)),
successRedirectUrl: eventType.successRedirectUrl || "",
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "EventType" ADD COLUMN "allowReschedulingPastBookings" BOOLEAN NOT NULL DEFAULT false;
+1
View File
@@ -166,6 +166,7 @@ model EventType {
fieldTranslations EventTypeTranslation[]
maxLeadThreshold Int?
selectedCalendars SelectedCalendar[]
allowReschedulingPastBookings Boolean @default(false)
/// @zod.custom(imports.eventTypeColor)
eventTypeColor Json?
+1
View File
@@ -748,6 +748,7 @@ export const allManagedEventTypeProps: { [k in keyof Omit<Prisma.EventTypeSelect
assignAllTeamMembers: true,
isRRWeightsEnabled: true,
eventTypeColor: true,
allowReschedulingPastBookings: true,
rescheduleWithSameRoundRobinHost: true,
maxLeadThreshold: true,
};
@@ -236,6 +236,7 @@ export async function getBookings({
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
eventTypeColor: true,
allowReschedulingPastBookings: true,
schedulingType: true,
length: true,
team: {
@@ -716,6 +716,7 @@ async function getExistingBookings(
seatsPerTimeSlot: true,
requiresConfirmationWillBlockSlot: true,
requiresConfirmation: true,
allowReschedulingPastBookings: true,
},
},
...(!!eventType?.seatsPerTimeSlot && {