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,
};
},