From 4e49d32c611c5ea11a7a46c4ec1e9ac854ffb59f Mon Sep 17 00:00:00 2001 From: Leo Giovanetti Date: Thu, 6 Oct 2022 16:23:22 -0300 Subject: [PATCH] Fixing unconfirmed/recurring bookings (#4881) --- .../components/booking/BookingListItem.tsx | 53 ++++++++--------- apps/web/lib/parseDate.ts | 23 ++++++-- apps/web/pages/bookings/[status].tsx | 13 +---- apps/web/pages/v2/bookings/[status].tsx | 15 +---- packages/prisma/seed.ts | 12 ++-- packages/trpc/server/routers/viewer.tsx | 57 ++++++++++++------- 6 files changed, 85 insertions(+), 88 deletions(-) diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 0680e8c1f2..56ca8bf9fb 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -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") && (
( -

{aDate}

+

+ {aDate} +

))}>
{ + return date >= now; + }).length, })}

diff --git a/apps/web/lib/parseDate.ts b/apps/web/lib/parseDate.ts index 5aaaef5bad..2f8d50a224 100644 --- a/apps/web/lib/parseDate.ts +++ b/apps/web/lib/parseDate.ts @@ -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]; }; diff --git a/apps/web/pages/bookings/[status].tsx b/apps/web/pages/bookings/[status].tsx index 85c0905cb0..23d7fb99de 100644 --- a/apps/web/pages/bookings/[status].tsx +++ b/apps/web/pages/bookings/[status].tsx @@ -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 - ) => { - let recurringBookings = undefined; - if (booking.recurringEventId !== null) { - recurringBookings = groupedBookings[booking.recurringEventId]; - } - return { recurringBookings }; - }; const shownBookings: Record = {}; const filterBookings = (booking: BookingOutput) => { if (status === "recurring" || status === "cancelled") { @@ -104,7 +93,7 @@ export default function Bookings() { ))} diff --git a/apps/web/pages/v2/bookings/[status].tsx b/apps/web/pages/v2/bookings/[status].tsx index bc259d080c..7479891a08 100644 --- a/apps/web/pages/v2/bookings/[status].tsx +++ b/apps/web/pages/v2/bookings/[status].tsx @@ -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 - ) => { - let recurringBookings = undefined; - if (booking.recurringEventId !== null) { - recurringBookings = groupedBookings[booking.recurringEventId]; - } - return { recurringBookings }; - }; const shownBookings: Record = {}; 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() { ))} diff --git a/packages/prisma/seed.ts b/packages/prisma/seed.ts index 42679c77a6..4e0609eac4 100644 --- a/packages/prisma/seed.ts +++ b/packages/prisma/seed.ts @@ -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, }, ], }, diff --git a/packages/trpc/server/routers/viewer.tsx b/packages/trpc/server/routers/viewer.tsx index 2d1abea80a..2eaf991fa7 100644 --- a/packages/trpc/server/routers/viewer.tsx +++ b/packages/trpc/server/routers/viewer.tsx @@ -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 = {}; - - // 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, }; },