Cancelling recurring vs single event (#3448)

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Leo Giovanettiandkodiakhq[bot] authored and GitHub committed 2022-07-19 19:06:23 +00:00
1 parent 805419d58b
commit 63660a37f9
7 files changed
+73 -45

No files matched your search

+13 -14
View File
@@ -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>
+13
View File
@@ -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];
};
+14 -4
View File
@@ -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,
+17 -14
View File
@@ -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}
/>
))}
+14 -7
View File
@@ -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",
-6
View File
@@ -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,
};
},