Cancelling recurring vs single event (#3448)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
805419d58b
commit
63660a37f9
7 files changed
+73
-45
No files matched your search
@@ -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")}
|
||||
</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{booking.recurringCount &&
|
||||
{booking.recurringBookings &&
|
||||
booking.eventType?.recurringEvent?.freq &&
|
||||
(booking.listingStatus === "recurring" || booking.listingStatus === "cancelled") && (
|
||||
<div className="underline decoration-gray-400 decoration-dashed underline-offset-2">
|
||||
@@ -327,7 +326,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
: getEveryFreqFor({
|
||||
t,
|
||||
recurringEvent: booking.eventType.recurringEvent,
|
||||
recurringCount: booking.recurringCount,
|
||||
recurringCount: booking.recurringBookings.length,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, BookingOutput[]>
|
||||
) => {
|
||||
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<string, boolean> = {};
|
||||
const shownBookings: Record<string, BookingOutput[]> = {};
|
||||
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() {
|
||||
<tbody className="divide-y divide-gray-200 bg-white" data-testid="bookings">
|
||||
{query.data.pages.map((page, index) => (
|
||||
<Fragment key={index}>
|
||||
{page.bookings.filter(filterBookings).map((booking) => (
|
||||
{page.bookings.filter(filterBookings).map((booking: BookingOutput) => (
|
||||
<BookingListItem
|
||||
key={booking.id}
|
||||
listingStatus={status}
|
||||
{...defineRecurrentCount(booking, page)}
|
||||
{...defineRecurrentBookings(booking, shownBookings)}
|
||||
{...booking}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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<typeof getServerSideProps>) {
|
||||
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<string | null>(props.booking ? null : t("booking_already_cancelled"));
|
||||
const [cancellationReason, setCancellationReason] = useState<string>("");
|
||||
@@ -81,9 +81,13 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
{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")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 border-t border-b py-4">
|
||||
@@ -175,6 +179,7 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
const payload = {
|
||||
uid: uid,
|
||||
reason: cancellationReason,
|
||||
allRemainingBookings: !!props.recurringInstances,
|
||||
};
|
||||
|
||||
telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
|
||||
@@ -193,7 +198,7 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
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<typeof getServerSideProps>) {
|
||||
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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user