Skip to content

Commit

Permalink
feat: Setting for rescheduling past bookings (calcom#18358)
Browse files Browse the repository at this point in the history
* feat: Setting for rescheduling past bookings

* fix stuff

* rename column

* add e2e test

* fix column name

* fix typo

---------

Co-authored-by: Anik Dhabal Babu <[email protected]>
Co-authored-by: Peer Richelsen <[email protected]>
Co-authored-by: CarinaWolli <[email protected]>
  • Loading branch information
4 people authored Jan 28, 2025
1 parent 39891ee commit 438a73b
Show file tree
Hide file tree
Showing 17 changed files with 97 additions and 17 deletions.
2 changes: 1 addition & 1 deletion apps/web/components/booking/BookingListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ function BookingListItem(booking: BookingItemProps) {
];

const editBookingActions: ActionType[] = [
...(isBookingInPast
...(isBookingInPast && !booking.eventType.allowReschedulingPastBookings
? []
: [
{
Expand Down
1 change: 1 addition & 0 deletions apps/web/lib/booking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const getEventTypesFromDB = async (id: number) => {
price: true,
currency: true,
bookingFields: true,
allowReschedulingPastBookings: true,
disableGuests: true,
timeZone: true,
profile: {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/lib/reschedule/[uid]/getServerSideProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
},
},
slug: true,
allowReschedulingPastBookings: true,
team: {
select: {
parentId: true,
Expand Down Expand Up @@ -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"]);
Expand Down
31 changes: 16 additions & 15 deletions apps/web/modules/bookings/views/bookings-single-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
47 changes: 47 additions & 0 deletions apps/web/playwright/reschedule.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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={() => (
Expand Down
1 change: 1 addition & 0 deletions packages/lib/defaultEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ const commons = {
seatsShowAttendees: null,
seatsShowAvailabilityCount: null,
onlyShowFirstAvailableSlot: false,
allowReschedulingPastBookings: false,
id: 0,
hideCalendarNotes: false,
hideCalendarEventDetails: false,
Expand Down
1 change: 1 addition & 0 deletions packages/lib/server/eventTypeSelect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions packages/lib/server/repository/eventType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ export class EventTypeRepository {
onlyShowFirstAvailableSlot: true,
durationLimits: true,
assignAllTeamMembers: true,
allowReschedulingPastBookings: true,
assignRRMembersUsingSegment: true,
rrSegmentQueryValue: true,
isRRWeightsEnabled: true,
Expand Down Expand Up @@ -784,6 +785,7 @@ export class EventTypeRepository {
periodStartDate: true,
periodEndDate: true,
onlyShowFirstAvailableSlot: true,
allowReschedulingPastBookings: true,
periodCountCalendarDays: true,
rescheduleWithSameRoundRobinHost: true,
periodDays: true,
Expand Down
1 change: 1 addition & 0 deletions packages/lib/test/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
rrSegmentQueryValue: null,
autoTranslateDescriptionEnabled: false,
useEventLevelSelectedCalendars: false,
allowReschedulingPastBookings: false,
...eventType,
};
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 || "",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "EventType" ADD COLUMN "allowReschedulingPastBookings" BOOLEAN NOT NULL DEFAULT false;
1 change: 1 addition & 0 deletions packages/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ model EventType {
fieldTranslations EventTypeTranslation[]
maxLeadThreshold Int?
selectedCalendars SelectedCalendar[]
allowReschedulingPastBookings Boolean @default(false)
/// @zod.custom(imports.eventTypeColor)
eventTypeColor Json?
Expand Down
1 change: 1 addition & 0 deletions packages/prisma/zod-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ export async function getBookings({
seatsShowAttendees: true,
seatsShowAvailabilityCount: true,
eventTypeColor: true,
allowReschedulingPastBookings: true,
schedulingType: true,
length: true,
team: {
Expand Down
1 change: 1 addition & 0 deletions packages/trpc/server/routers/viewer/slots/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,7 @@ async function getExistingBookings(
seatsPerTimeSlot: true,
requiresConfirmationWillBlockSlot: true,
requiresConfirmation: true,
allowReschedulingPastBookings: true,
},
},
...(!!eventType?.seatsPerTimeSlot && {
Expand Down

0 comments on commit 438a73b

Please sign in to comment.