Fixing unconfirmed/recurring bookings (#4881)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user