Fixing unconfirmed/recurring bookings (#4881)

This commit is contained in:
Leo Giovanetti
2022-10-06 13:23:22 -06:00
committed by GitHub
parent 0b29c5063a
commit 4e49d32c61
6 changed files with 85 additions and 88 deletions
+23 -30
View File
@@ -29,7 +29,7 @@ type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
type BookingItemProps = BookingItem & {
listingStatus: BookingListingStatus;
recurringBookings?: BookingItem[];
recurringBookings: inferQueryOutput<"viewer.bookings">["recurringInfo"];
};
function BookingListItem(booking: BookingItemProps) {
@@ -61,9 +61,9 @@ function BookingListItem(booking: BookingItemProps) {
};
/**
* Only pass down the recurring event id when we need to confirm the entire series, which happens in
* the "Recurring" tab, to support confirming discretionally in the "Recurring" tab.
* the "Recurring" tab and "Unconfirmed" tab, to support confirming discretionally in the "Recurring" tab.
*/
if (booking.listingStatus === "recurring" && booking.recurringEventId !== null) {
if ((isTabRecurring || isTabUnconfirmed) && isRecurring) {
body = Object.assign({}, body, { recurringEventId: booking.recurringEventId });
}
mutation.mutate(body);
@@ -75,14 +75,14 @@ function BookingListItem(booking: BookingItemProps) {
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
const isRejected = booking.status === BookingStatus.REJECTED;
const isPending = booking.status === BookingStatus.PENDING;
const isRecurring = booking.recurringEventId !== null;
const isTabRecurring = booking.listingStatus === "recurring";
const isTabUnconfirmed = booking.listingStatus === "unconfirmed";
const pendingActions: ActionType[] = [
{
id: "reject",
label:
booking.listingStatus === "recurring" && booking.recurringEventId !== null
? t("reject_all")
: t("reject"),
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("reject_all") : t("reject"),
onClick: () => {
setRejectionDialogIsOpen(true);
},
@@ -91,10 +91,7 @@ function BookingListItem(booking: BookingItemProps) {
},
{
id: "confirm",
label:
booking.listingStatus === "recurring" && booking.recurringEventId !== null
? t("confirm_all")
: t("confirm"),
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
onClick: () => {
bookingConfirm(true);
},
@@ -107,17 +104,10 @@ function BookingListItem(booking: BookingItemProps) {
let bookedActions: ActionType[] = [
{
id: "cancel",
label:
booking.listingStatus === "recurring" && booking.recurringEventId !== null
? t("cancel_all_remaining")
: t("cancel"),
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel"),
/* When cancelling we need to let the UI and the API know if the intention is to
cancel all remaining bookings or just that booking instance. */
href: `/cancel/${booking.uid}${
booking.listingStatus === "recurring" && booking.recurringEventId !== null
? "?allRemainingBookings=true"
: ""
}`,
href: `/cancel/${booking.uid}${isTabRecurring && isRecurring ? "?allRemainingBookings=true" : ""}`,
icon: Icon.FiX,
},
{
@@ -151,7 +141,7 @@ function BookingListItem(booking: BookingItemProps) {
},
];
if (booking.listingStatus === "recurring" && booking.recurringEventId !== null) {
if (isTabRecurring && isRecurring) {
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
}
@@ -188,12 +178,8 @@ function BookingListItem(booking: BookingItemProps) {
let recurringStrings: string[] = [];
let recurringDates: Date[] = [];
if (booking.recurringBookings && booking.eventType.recurringEvent?.freq !== undefined) {
[recurringStrings, recurringDates] = extractRecurringDates(
booking.recurringBookings,
user?.timeZone,
i18n
);
if (booking.recurringBookings !== undefined && booking.eventType.recurringEvent?.freq !== undefined) {
[recurringStrings, recurringDates] = extractRecurringDates(booking, user?.timeZone, i18n);
}
const location = booking.location || "";
@@ -403,16 +389,21 @@ const RecurringBookingsTooltip = ({
recurringDates,
}: RecurringBookingsTooltipProps) => {
const { t } = useLocale();
const now = new Date();
return (
(booking.recurringBookings &&
booking.eventType?.recurringEvent?.freq &&
(booking.listingStatus === "recurring" || booking.listingStatus === "cancelled") && (
(booking.listingStatus === "recurring" ||
booking.listingStatus === "unconfirmed" ||
booking.listingStatus === "cancelled") && (
<div className="underline decoration-gray-400 decoration-dashed underline-offset-2">
<div className="flex">
<Tooltip
content={recurringStrings.map((aDate, key) => (
<p key={key}>{aDate}</p>
<p key={key} className={classNames(recurringDates[key] < now && "line-through")}>
{aDate}
</p>
))}>
<div className="text-gray-600 dark:text-white">
<Icon.FiRefreshCcw
@@ -427,7 +418,9 @@ const RecurringBookingsTooltip = ({
: getEveryFreqFor({
t,
recurringEvent: booking.eventType.recurringEvent,
recurringCount: booking.recurringBookings.length,
recurringCount: recurringDates.filter((date) => {
return date >= now;
}).length,
})}
</p>
</div>
+18 -5
View File
@@ -47,14 +47,27 @@ export const parseRecurringDates = (
return [dateStrings, rule.all()];
};
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
export const extractRecurringDates = (
bookings: BookingItem[],
booking: inferQueryOutput<"viewer.bookings">["bookings"][number] & {
eventType: { recurringEvent: RecurringEvent | null };
recurringEventId: string | null;
recurringBookings: inferQueryOutput<"viewer.bookings">["recurringInfo"];
},
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());
const { count = 0, ...rest } =
booking.eventType.recurringEvent !== null ? booking.eventType.recurringEvent : {};
const recurringInfo = booking.recurringBookings.find(
(val) => val.recurringEventId === booking.recurringEventId
);
const allDates = new RRule({
...rest,
count: recurringInfo?._count.recurringEventId,
dtstart: recurringInfo?._min.startTime,
}).all();
const dateStrings = allDates.map((r) => {
return processDate(dayjs(r).tz(timeZone), i18n);
});
return [dateStrings, allDates];
};
+1 -12
View File
@@ -54,17 +54,6 @@ export default function Bookings() {
const isEmpty = !query.data?.pages[0]?.bookings.length;
// 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) {
recurringBookings = groupedBookings[booking.recurringEventId];
}
return { recurringBookings };
};
const shownBookings: Record<string, BookingOutput[]> = {};
const filterBookings = (booking: BookingOutput) => {
if (status === "recurring" || status === "cancelled") {
@@ -104,7 +93,7 @@ export default function Bookings() {
<BookingListItem
key={booking.id}
listingStatus={status}
{...defineRecurrentBookings(booking, shownBookings)}
recurringBookings={page.recurringInfo}
{...booking}
/>
))}
+2 -13
View File
@@ -55,20 +55,9 @@ export default function Bookings() {
const isEmpty = !query.data?.pages[0]?.bookings.length;
// 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) {
recurringBookings = groupedBookings[booking.recurringEventId];
}
return { recurringBookings };
};
const shownBookings: Record<string, BookingOutput[]> = {};
const filterBookings = (booking: BookingOutput) => {
if (status === "recurring" || status === "cancelled") {
if (status === "recurring" || status == "unconfirmed" || status === "cancelled") {
if (!booking.recurringEventId) {
return true;
}
@@ -111,7 +100,7 @@ export default function Bookings() {
<BookingListItem
key={booking.id}
listingStatus={status}
{...defineRecurrentBookings(booking, shownBookings)}
recurringBookings={page.recurringInfo}
{...booking}
/>
))}
+6 -6
View File
@@ -291,7 +291,7 @@ async function main() {
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").toDate(),
endTime: dayjs().add(1, "day").add(30, "minutes").toDate(),
status: BookingStatus.PENDING,
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
@@ -299,7 +299,7 @@ async function main() {
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(1, "week").toDate(),
endTime: dayjs().add(1, "day").add(1, "week").add(30, "minutes").toDate(),
status: BookingStatus.PENDING,
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
@@ -307,7 +307,7 @@ async function main() {
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(2, "week").toDate(),
endTime: dayjs().add(1, "day").add(2, "week").add(30, "minutes").toDate(),
status: BookingStatus.PENDING,
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
@@ -315,7 +315,7 @@ async function main() {
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(3, "week").toDate(),
endTime: dayjs().add(1, "day").add(3, "week").add(30, "minutes").toDate(),
status: BookingStatus.PENDING,
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
@@ -323,7 +323,7 @@ async function main() {
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(4, "week").toDate(),
endTime: dayjs().add(1, "day").add(4, "week").add(30, "minutes").toDate(),
status: BookingStatus.PENDING,
status: BookingStatus.ACCEPTED,
},
{
uid: uuid(),
@@ -331,7 +331,7 @@ async function main() {
recurringEventId: Buffer.from("yoga-class").toString("base64"),
startTime: dayjs().add(1, "day").add(5, "week").toDate(),
endTime: dayjs().add(1, "day").add(5, "week").add(30, "minutes").toDate(),
status: BookingStatus.PENDING,
status: BookingStatus.ACCEPTED,
},
],
},
+35 -22
View File
@@ -1,7 +1,7 @@
import { AppCategories, BookingStatus, IdentityProvider, MembershipRole, Prisma } from "@prisma/client";
import _ from "lodash";
import { authenticator } from "otplib";
import { z } from "zod";
import z from "zod";
import app_RoutingForms from "@calcom/app-store/ee/routing-forms/trpc-router";
import ethRouter from "@calcom/app-store/rainbow/trpc/router";
@@ -493,7 +493,19 @@ const loggedInViewerRouter = createProtectedRouter()
},
unconfirmed: {
endTime: { gte: new Date() },
status: { equals: BookingStatus.PENDING },
OR: [
{
AND: [
{ NOT: { recurringEventId: { equals: null } } },
{
status: { equals: BookingStatus.PENDING },
},
],
},
{
status: { equals: BookingStatus.PENDING },
},
],
},
};
const bookingListingOrderby: Record<
@@ -572,7 +584,25 @@ const loggedInViewerRouter = createProtectedRouter()
skip,
});
let bookings = bookingsQuery.map((booking) => {
const recurringInfo = await prisma.booking.groupBy({
by: ["recurringEventId"],
_min: {
startTime: true,
},
_count: {
recurringEventId: true,
},
where: {
recurringEventId: {
not: { equals: null },
},
status: {
notIn: [BookingStatus.CANCELLED],
},
},
});
const bookings = bookingsQuery.map((booking) => {
return {
...booking,
eventType: {
@@ -583,26 +613,8 @@ const loggedInViewerRouter = createProtectedRouter()
endTime: booking.endTime.toISOString(),
};
});
const bookingsFetched = bookings.length;
const seenBookings: Record<string, boolean> = {};
// Remove duplicate recurring bookings for upcoming status.
// Couldn't use distinct in query because the distinct column would be different for recurring and non recurring event.
// We might be actually sending less then the limit, due to this filter
// TODO: Figure out a way to fix it.
if (bookingListingByStatus === "upcoming") {
bookings = bookings.filter((booking) => {
if (!booking.recurringEventId) {
return true;
}
if (seenBookings[booking.recurringEventId]) {
return false;
}
seenBookings[booking.recurringEventId] = true;
return true;
});
}
let nextCursor: typeof skip | null = skip;
if (bookingsFetched > take) {
nextCursor += bookingsFetched;
@@ -612,6 +624,7 @@ const loggedInViewerRouter = createProtectedRouter()
return {
bookings,
recurringInfo,
nextCursor,
};
},