diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx
index 225bd1612b..06ccdefc91 100644
--- a/apps/web/components/booking/BookingListItem.tsx
+++ b/apps/web/components/booking/BookingListItem.tsx
@@ -14,7 +14,6 @@ import { useState } from "react";
import { useMutation } from "react-query";
import dayjs from "@calcom/dayjs";
-import { parseRecurringEvent } from "@calcom/lib";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
@@ -28,7 +27,7 @@ import { HttpError } from "@lib/core/http/error";
import useMeQuery from "@lib/hooks/useMeQuery";
import { linkValueToString } from "@lib/linkValueToString";
import { LocationType } from "@lib/location";
-import { parseRecurringDates } from "@lib/parseDate";
+import { extractRecurringDates } from "@lib/parseDate";
import { inferQueryInput, inferQueryOutput, trpc } from "@lib/trpc";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
@@ -41,7 +40,7 @@ type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
type BookingItemProps = BookingItem & {
listingStatus: BookingListingStatus;
- recurringCount?: number;
+ recurringBookings?: BookingItem[];
};
function BookingListItem(booking: BookingItemProps) {
@@ -127,7 +126,11 @@ function BookingListItem(booking: BookingItemProps) {
booking.listingStatus === "recurring" && booking.recurringEventId !== null
? t("cancel_all_remaining")
: t("cancel"),
- href: `/cancel/${booking.uid}`,
+ href: `/cancel/${booking.uid}${
+ booking.listingStatus === "recurring" && booking.recurringEventId !== null
+ ? "?allRemainingBookings=true"
+ : ""
+ }`,
icon: XIcon,
},
{
@@ -202,14 +205,10 @@ function BookingListItem(booking: BookingItemProps) {
let recurringStrings: string[] = [];
let recurringDates: Date[] = [];
const today = new Date();
- if (booking.recurringCount && booking.eventType.recurringEvent?.freq !== undefined) {
- [recurringStrings, recurringDates] = parseRecurringDates(
- {
- startDate: booking.startTime,
- timeZone: user?.timeZone,
- recurringEvent: parseRecurringEvent(booking.eventType.recurringEvent),
- recurringCount: booking.recurringCount,
- },
+ if (booking.recurringBookings && booking.eventType.recurringEvent?.freq !== undefined) {
+ [recurringStrings, recurringDates] = extractRecurringDates(
+ booking.recurringBookings,
+ user?.timeZone,
i18n
);
if (booking.status === BookingStatus.PENDING) {
@@ -308,7 +307,7 @@ function BookingListItem(booking: BookingItemProps) {
{dayjs(booking.endTime).format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
- {booking.recurringCount &&
+ {booking.recurringBookings &&
booking.eventType?.recurringEvent?.freq &&
(booking.listingStatus === "recurring" || booking.listingStatus === "cancelled") && (
@@ -327,7 +326,7 @@ function BookingListItem(booking: BookingItemProps) {
: getEveryFreqFor({
t,
recurringEvent: booking.eventType.recurringEvent,
- recurringCount: booking.recurringCount,
+ recurringCount: booking.recurringBookings.length,
})}
diff --git a/apps/web/lib/parseDate.ts b/apps/web/lib/parseDate.ts
index 838e6f7957..1b07301e9c 100644
--- a/apps/web/lib/parseDate.ts
+++ b/apps/web/lib/parseDate.ts
@@ -5,6 +5,7 @@ import dayjs, { Dayjs } from "@calcom/dayjs";
import { RecurringEvent } from "@calcom/types/Calendar";
import { detectBrowserTimeFormat } from "@lib/timeFormat";
+import { inferQueryOutput } from "@lib/trpc";
import { parseZone } from "./parseZone";
@@ -46,3 +47,15 @@ export const parseRecurringDates = (
});
return [dateStrings, rule.all()];
};
+
+type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
+
+export const extractRecurringDates = (
+ bookings: BookingItem[],
+ timeZone: string | undefined,
+ i18n: I18n
+): [string[], Date[]] => {
+ const dateStrings = bookings.map((booking) => processDate(dayjs(booking.startTime).tz(timeZone), i18n));
+ const allDates = dateStrings.map((dateString) => dayjs(dateString).tz(timeZone).toDate());
+ return [dateStrings, allDates];
+};
diff --git a/apps/web/pages/api/cancel.ts b/apps/web/pages/api/cancel.ts
index 3884cd9632..79fdeefe9d 100644
--- a/apps/web/pages/api/cancel.ts
+++ b/apps/web/pages/api/cancel.ts
@@ -36,6 +36,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
const uid = asStringOrNull(req.body.uid) || "";
+ const allRemainingBookings = asStringOrNull(req.body.allRemainingBookings) || "";
const cancellationReason = asStringOrNull(req.body.reason) || "";
const session = await getSession({ req: req });
@@ -145,7 +146,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
},
attendees: attendeesList,
uid: bookingToDelete?.uid,
- recurringEvent: parseRecurringEvent(bookingToDelete.eventType?.recurringEvent),
+ /* Include recurringEvent information only when cancelling all bookings */
+ recurringEvent:
+ allRemainingBookings === "true"
+ ? parseRecurringEvent(bookingToDelete.eventType?.recurringEvent)
+ : undefined,
location: bookingToDelete?.location,
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
cancellationReason: cancellationReason,
@@ -168,12 +173,17 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// by cancelling first, and blocking whilst doing so; we can ensure a cancel
// action always succeeds even if subsequent integrations fail cancellation.
- if (bookingToDelete.eventType?.recurringEvent) {
+ if (bookingToDelete.eventType?.recurringEvent && allRemainingBookings === "true") {
const recurringEventId = bookingToDelete.recurringEventId;
const where = recurringEventId === null ? { uid } : { recurringEventId };
- // Proceed to mark as cancelled all recurring event instances
+ // Proceed to mark as cancelled all remaining recurring events instances (greater than or equal to right now)
await prisma.booking.updateMany({
- where,
+ where: {
+ ...where,
+ startTime: {
+ gte: new Date(),
+ },
+ },
data: {
status: BookingStatus.CANCELLED,
cancellationReason: cancellationReason,
diff --git a/apps/web/pages/bookings/[status].tsx b/apps/web/pages/bookings/[status].tsx
index d5205f2ba9..944fa9be14 100644
--- a/apps/web/pages/bookings/[status].tsx
+++ b/apps/web/pages/bookings/[status].tsx
@@ -18,7 +18,6 @@ import SkeletonLoader from "@components/booking/SkeletonLoader";
type BookingListingStatus = inferQueryInput<"viewer.bookings">["status"];
type BookingOutput = inferQueryOutput<"viewer.bookings">["bookings"][0];
-type BookingPage = inferQueryOutput<"viewer.bookings">;
export default function Bookings() {
const router = useRouter();
@@ -47,27 +46,31 @@ export default function Bookings() {
const isEmpty = !query.data?.pages[0]?.bookings.length;
- // Get the recurrentCount value from the grouped recurring bookings
- // created with the same recurringEventId
- const defineRecurrentCount = (booking: BookingOutput, page: BookingPage) => {
- let recurringCount = undefined;
+ // Get all recurring events of the series with the same recurringEventId
+ const defineRecurrentBookings = (
+ booking: BookingOutput,
+ groupedBookings: Record
+ ) => {
+ let recurringBookings = undefined;
if (booking.recurringEventId !== null) {
- recurringCount = page.groupedRecurringBookings.filter(
- (group) => group.recurringEventId === booking.recurringEventId
- )[0]._count; // If found, only one object exists, just assing the needed _count value
+ recurringBookings = groupedBookings[booking.recurringEventId];
}
- return { recurringCount };
+ return { recurringBookings };
};
- const shownBookings: Record = {};
+ const shownBookings: Record = {};
const filterBookings = (booking: BookingOutput) => {
if (status === "recurring" || status === "cancelled") {
if (!booking.recurringEventId) {
return true;
}
- if (shownBookings[booking.recurringEventId]) {
+ if (
+ shownBookings[booking.recurringEventId] !== undefined &&
+ shownBookings[booking.recurringEventId].length > 0
+ ) {
+ shownBookings[booking.recurringEventId].push(booking);
return false;
}
- shownBookings[booking.recurringEventId] = true;
+ shownBookings[booking.recurringEventId] = [booking];
}
return true;
};
@@ -89,11 +92,11 @@ export default function Bookings() {
{query.data.pages.map((page, index) => (
- {page.bookings.filter(filterBookings).map((booking) => (
+ {page.bookings.filter(filterBookings).map((booking: BookingOutput) => (
))}
diff --git a/apps/web/pages/cancel/[uid].tsx b/apps/web/pages/cancel/[uid].tsx
index 7bd4d30d86..d860367d30 100644
--- a/apps/web/pages/cancel/[uid].tsx
+++ b/apps/web/pages/cancel/[uid].tsx
@@ -13,7 +13,7 @@ import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { Button } from "@calcom/ui/Button";
import { TextField } from "@calcom/ui/form/fields";
-import { asStringOrUndefined } from "@lib/asStringOrNull";
+import { asStringOrNull, asStringOrUndefined } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { detectBrowserTimeFormat } from "@lib/timeFormat";
@@ -28,7 +28,7 @@ export default function Type(props: inferSSRProps) {
const { t } = useLocale();
// Get router variables
const router = useRouter();
- const { uid } = router.query;
+ const { uid, allRemainingBookings } = router.query;
const [loading, setLoading] = useState(false);
const [error, setError] = useState(props.booking ? null : t("booking_already_cancelled"));
const [cancellationReason, setCancellationReason] = useState("");
@@ -81,9 +81,13 @@ export default function Type(props: inferSSRProps) {
- {props.cancellationAllowed && !props.booking?.eventType.recurringEvent
- ? t("reschedule_instead")
- : t("event_is_in_the_past")}
+ {!props.booking?.eventType.recurringEvent
+ ? props.cancellationAllowed
+ ? t("reschedule_instead")
+ : t("event_is_in_the_past")
+ : allRemainingBookings === "true"
+ ? t("cancelling_all_recurring")
+ : t("cancelling_event_recurring")}
@@ -175,6 +179,7 @@ export default function Type(props: inferSSRProps) {
const payload = {
uid: uid,
reason: cancellationReason,
+ allRemainingBookings: !!props.recurringInstances,
};
telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
@@ -193,7 +198,7 @@ export default function Type(props: inferSSRProps) {
props.booking.title
}&eventPage=${props.profile.slug}&team=${
props.booking.eventType?.team ? 1 : 0
- }&recurring=${!!props.booking.eventType?.recurringEvent}`
+ }&recurring=${!!props.recurringInstances}`
);
} else {
setLoading(false);
@@ -224,6 +229,7 @@ export default function Type(props: inferSSRProps) {
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
const session = await getSession(context);
+ const allRemainingBookings = asStringOrNull(context.query.allRemainingBookings) || "";
const booking = await prisma.booking.findUnique({
where: {
uid: asStringOrUndefined(context.query.uid),
@@ -272,10 +278,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
});
let recurringInstances = null;
- if (booking.eventType?.recurringEvent) {
+ if (booking.eventType?.recurringEvent && allRemainingBookings === "true") {
recurringInstances = await prisma.booking.findMany({
where: {
recurringEventId: booking.recurringEventId,
+ NOT: [{ status: "CANCELLED" }, { status: "REJECTED" }],
},
select: {
startTime: true,
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 07dff34b51..990879319a 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -286,6 +286,8 @@
"cannot_cancel_booking": "You cannot cancel this booking",
"reschedule_instead": "Instead, you could also reschedule it.",
"event_is_in_the_past": "The event is in the past",
+ "cancelling_event_recurring": "The event is one instance of a recurring event.",
+ "cancelling_all_recurring": "These are all remaining instances in the recurring event.",
"error_with_status_code_occured": "An error with status code {{status}} occurred.",
"booking_already_cancelled": "This booking was already cancelled",
"go_back_home": "Go back home",
diff --git a/apps/web/server/routers/viewer.tsx b/apps/web/server/routers/viewer.tsx
index 4dde3ca090..60f9799e47 100644
--- a/apps/web/server/routers/viewer.tsx
+++ b/apps/web/server/routers/viewer.tsx
@@ -428,11 +428,6 @@ const loggedInViewerRouter = createProtectedRouter()
skip,
});
- const groupedRecurringBookings = await prisma.booking.groupBy({
- by: [Prisma.BookingScalarFieldEnum.recurringEventId],
- _count: true,
- });
-
let bookings = bookingsQuery.map((booking) => {
return {
...booking,
@@ -473,7 +468,6 @@ const loggedInViewerRouter = createProtectedRouter()
return {
bookings,
- groupedRecurringBookings,
nextCursor,
};
},