Recurring event improvements (#2750)
* Loading state and rejected bookings gone * Listing fixes * Tweaking upcoming vs recurring bookings tabs * Tweaking new emails to avoid recurringEvent param * Tweaks to support recurring events in common flows * Missed one renderEmail * Removing uneeded references * Reverting manual plural fixes * Refactoring recurring event strings * Correcting merge issues * Relying on newly introduced obj * Fixing mobile * Final tweaks * Latest fixes Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Bailey Pumfleet <pumfleet@hey.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Peer Richelsen
Bailey Pumfleet
kodiakhq[bot]
parent
67a7c578b1
commit
514fca7a53
@@ -18,7 +18,7 @@ import { parseRecurringEvent } from "@calcom/lib";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import { Frequency } from "@calcom/prisma/zod-utils";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import Button from "@calcom/ui/Button";
|
||||
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||
@@ -63,9 +63,9 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
};
|
||||
/**
|
||||
* Only pass down the recurring event id when we need to confirm the entire series, which happens in
|
||||
* the "Upcoming" tab, to support confirming discretionally in the "Recurring" tab.
|
||||
* the "Recurring" tab, to support confirming discretionally in the "Recurring" tab.
|
||||
*/
|
||||
if (booking.listingStatus === "upcoming" && booking.recurringEventId !== null) {
|
||||
if (booking.listingStatus === "recurring" && booking.recurringEventId !== null) {
|
||||
body = Object.assign({}, body, { recurringEventId: booking.recurringEventId });
|
||||
}
|
||||
const res = await fetch("/api/book/confirm", {
|
||||
@@ -96,7 +96,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
{
|
||||
id: "reject",
|
||||
label:
|
||||
booking.listingStatus === "upcoming" && booking.recurringEventId !== null
|
||||
booking.listingStatus === "recurring" && booking.recurringEventId !== null
|
||||
? t("reject_all")
|
||||
: t("reject"),
|
||||
onClick: () => {
|
||||
@@ -108,7 +108,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
{
|
||||
id: "confirm",
|
||||
label:
|
||||
booking.listingStatus === "upcoming" && booking.recurringEventId !== null
|
||||
booking.listingStatus === "recurring" && booking.recurringEventId !== null
|
||||
? t("confirm_all")
|
||||
: t("confirm"),
|
||||
onClick: () => {
|
||||
@@ -120,10 +120,13 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
},
|
||||
];
|
||||
|
||||
const bookedActions: ActionType[] = [
|
||||
let bookedActions: ActionType[] = [
|
||||
{
|
||||
id: "cancel",
|
||||
label: t("cancel"),
|
||||
label:
|
||||
booking.listingStatus === "recurring" && booking.recurringEventId !== null
|
||||
? t("cancel_all_remaining")
|
||||
: t("cancel"),
|
||||
href: `/cancel/${booking.uid}`,
|
||||
icon: XIcon,
|
||||
},
|
||||
@@ -159,6 +162,10 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
},
|
||||
];
|
||||
|
||||
if (booking.listingStatus === "recurring" && booking.recurringEventId !== null) {
|
||||
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
||||
}
|
||||
|
||||
const RequestSentMessage = () => {
|
||||
return (
|
||||
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
|
||||
@@ -191,10 +198,12 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
setLocationMutation.mutate({ bookingId: booking.id, newLocation });
|
||||
};
|
||||
|
||||
// Calculate the booking date(s)
|
||||
// Calculate the booking date(s) and setup recurring event data to show
|
||||
let recurringStrings: string[] = [];
|
||||
if (booking.recurringCount && booking.eventType.recurringEvent?.freq !== null) {
|
||||
[recurringStrings] = parseRecurringDates(
|
||||
let recurringDates: Date[] = [];
|
||||
const today = new Date();
|
||||
if (booking.recurringCount && booking.eventType.recurringEvent?.freq !== undefined) {
|
||||
[recurringStrings, recurringDates] = parseRecurringDates(
|
||||
{
|
||||
startDate: booking.startTime,
|
||||
recurringEvent: parseRecurringEvent(booking.eventType.recurringEvent),
|
||||
@@ -202,6 +211,11 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
},
|
||||
i18n
|
||||
);
|
||||
if (booking.status === BookingStatus.PENDING) {
|
||||
// Only take into consideration next up instances if booking is confirmed
|
||||
recurringDates = recurringDates.filter((aDate) => aDate >= today);
|
||||
recurringStrings = recurringDates.map((_, key) => recurringStrings[key]);
|
||||
}
|
||||
}
|
||||
|
||||
let location = booking.location || "";
|
||||
@@ -285,9 +299,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
</Dialog>
|
||||
|
||||
<tr className="flex hover:bg-neutral-50">
|
||||
<td
|
||||
className="hidden whitespace-nowrap align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:w-56"
|
||||
onClick={onClick}>
|
||||
<td className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:w-64" onClick={onClick}>
|
||||
<div className="cursor-pointer py-4">
|
||||
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
@@ -297,24 +309,27 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
<div className="text-sm text-gray-400">
|
||||
{booking.recurringCount &&
|
||||
booking.eventType?.recurringEvent?.freq &&
|
||||
booking.listingStatus === "upcoming" && (
|
||||
(booking.listingStatus === "recurring" || 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 className="text-gray-600 dark:text-white">
|
||||
<RefreshIcon className="mr-1 -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
{`${t("every_for_freq", {
|
||||
freq: t(
|
||||
`${Frequency[booking.eventType.recurringEvent.freq].toString().toLowerCase()}`
|
||||
),
|
||||
})} ${booking.recurringCount} ${t(
|
||||
`${Frequency[booking.eventType.recurringEvent.freq].toString().toLowerCase()}`,
|
||||
{ count: booking.recurringCount }
|
||||
)}`}
|
||||
</p>
|
||||
<div className="text-gray-600 dark:text-white">
|
||||
<RefreshIcon className="float-left mr-1 mt-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
<p className="pl-[21px]">
|
||||
{booking.status === BookingStatus.ACCEPTED
|
||||
? `${t("event_remaining", {
|
||||
count: recurringDates.length,
|
||||
})}`
|
||||
: getEveryFreqFor({
|
||||
t,
|
||||
recurringEvent: booking.eventType.recurringEvent,
|
||||
recurringCount: booking.recurringCount,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Button } from "@calcom/ui/Button";
|
||||
|
||||
import useTheme from "@lib/hooks/useTheme";
|
||||
@@ -17,6 +18,7 @@ type Props = {
|
||||
name: string | null;
|
||||
slug: string | null;
|
||||
};
|
||||
recurringEvent: RecurringEvent | null;
|
||||
team?: string | null;
|
||||
setIsCancellationMode: (value: boolean) => void;
|
||||
theme: string | null;
|
||||
@@ -59,11 +61,13 @@ export default function CancelBooking(props: Props) {
|
||||
rows={3}
|
||||
/>
|
||||
<div className="flex rtl:space-x-reverse">
|
||||
<div className="w-full">
|
||||
<Button color="secondary" onClick={() => router.push("/reschedule/" + booking?.uid)}>
|
||||
{t("reschedule_this")}
|
||||
</Button>
|
||||
</div>
|
||||
{!props.recurringEvent && (
|
||||
<div className="w-full">
|
||||
<Button color="secondary" onClick={() => router.push("/reschedule/" + booking?.uid)}>
|
||||
{t("reschedule_this")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full space-x-2 text-right">
|
||||
<Button color="secondary" onClick={() => props.setIsCancellationMode(false)}>
|
||||
{t("nevermind")}
|
||||
@@ -92,7 +96,7 @@ export default function CancelBooking(props: Props) {
|
||||
await router.push(
|
||||
`/cancel/success?name=${props.profile.name}&title=${booking?.title}&eventPage=${
|
||||
profile.slug
|
||||
}&team=${team ? 1 : 0}`
|
||||
}&team=${team ? 1 : 0}&recurring=${!!props.recurringEvent}`
|
||||
);
|
||||
} else {
|
||||
setLoading(false);
|
||||
|
||||
@@ -33,8 +33,8 @@ import {
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
import { Frequency } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { timeZone } from "@lib/clock";
|
||||
@@ -296,33 +296,6 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
|
||||
<ClockIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
{eventType.length} {t("minutes")}
|
||||
</p>
|
||||
{!rescheduleUid && eventType.recurringEvent?.count && eventType.recurringEvent?.freq && (
|
||||
<div className="text-gray-600 dark:text-white">
|
||||
<RefreshIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||
{t("every_for_freq", {
|
||||
freq: t(
|
||||
`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max={eventType.recurringEvent.count}
|
||||
className="w-15 h-7 rounded-sm border-gray-300 bg-white text-gray-600 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500 dark:bg-gray-600 dark:text-white sm:text-sm"
|
||||
defaultValue={eventType.recurringEvent.count}
|
||||
onChange={(event) => {
|
||||
setRecurringEventCount(parseInt(event?.target.value));
|
||||
}}
|
||||
/>
|
||||
<p className="inline text-gray-600 dark:text-white">
|
||||
{t(`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`, {
|
||||
count: recurringEventCount,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{eventType.price > 0 && (
|
||||
<div className="text-gray-600 dark:text-white">
|
||||
<CreditCardIcon className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4 dark:text-gray-400" />
|
||||
@@ -335,31 +308,29 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
|
||||
</IntlProvider>
|
||||
</div>
|
||||
)}
|
||||
{!rescheduleUid && eventType.recurringEvent?.count && eventType.recurringEvent?.freq && (
|
||||
{!rescheduleUid && eventType.recurringEvent && (
|
||||
<div className="text-gray-600 dark:text-white">
|
||||
<RefreshIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||
{t("every_for_freq", {
|
||||
freq: t(
|
||||
`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max={eventType.recurringEvent.count}
|
||||
className="w-15 h-7 rounded-sm border-gray-300 bg-white text-gray-600 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500 dark:bg-gray-600 dark:text-white sm:text-sm"
|
||||
defaultValue={eventType.recurringEvent.count}
|
||||
onChange={(event) => {
|
||||
setRecurringEventCount(parseInt(event?.target.value));
|
||||
}}
|
||||
/>
|
||||
<p className="inline text-gray-600 dark:text-white">
|
||||
{t(`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`, {
|
||||
count: recurringEventCount,
|
||||
})}
|
||||
</p>
|
||||
<RefreshIcon className="float-left mr-[10px] mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
<div className="ml-[27px]">
|
||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max={eventType.recurringEvent.count}
|
||||
className="w-15 h-7 rounded-sm border-gray-300 bg-white text-gray-600 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500 dark:bg-gray-600 dark:text-white sm:text-sm"
|
||||
defaultValue={eventType.recurringEvent.count}
|
||||
onChange={(event) => {
|
||||
setRecurringEventCount(parseInt(event?.target.value));
|
||||
}}
|
||||
/>
|
||||
<p className="inline text-gray-600 dark:text-white">
|
||||
{t("occurrence", {
|
||||
count: recurringEventCount,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<TimezoneDropdown />
|
||||
@@ -460,29 +431,29 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
|
||||
<ClockIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
{eventType.length} {t("minutes")}
|
||||
</p>
|
||||
{!rescheduleUid && eventType.recurringEvent?.count && eventType.recurringEvent?.freq && (
|
||||
{!rescheduleUid && eventType.recurringEvent && (
|
||||
<div className="text-gray-600 dark:text-white">
|
||||
<RefreshIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||
{t("every_for_freq", {
|
||||
freq: t(`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`),
|
||||
})}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max={eventType.recurringEvent.count}
|
||||
className="w-15 h-7 rounded-sm border-gray-300 bg-white text-gray-600 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500 dark:bg-gray-600 dark:text-white sm:text-sm"
|
||||
defaultValue={eventType.recurringEvent.count}
|
||||
onChange={(event) => {
|
||||
setRecurringEventCount(parseInt(event?.target.value));
|
||||
}}
|
||||
/>
|
||||
<p className="inline text-gray-600 dark:text-white">
|
||||
{t(`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`, {
|
||||
count: recurringEventCount,
|
||||
})}
|
||||
</p>
|
||||
<RefreshIcon className="float-left mr-[10px] mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
<div className="ml-[27px]">
|
||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max={eventType.recurringEvent.count}
|
||||
className="w-15 h-7 rounded-sm border-gray-300 bg-white text-gray-600 shadow-sm [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500 dark:bg-gray-600 dark:text-white sm:text-sm"
|
||||
defaultValue={eventType.recurringEvent.count}
|
||||
onChange={(event) => {
|
||||
setRecurringEventCount(parseInt(event?.target.value));
|
||||
}}
|
||||
/>
|
||||
<p className="inline text-gray-600 dark:text-white">
|
||||
{t("occurrence", {
|
||||
count: recurringEventCount,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{eventType.price > 0 && (
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { Frequency } from "@calcom/prisma/zod-utils";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { createPaymentLink } from "@calcom/stripe/client";
|
||||
import { Button } from "@calcom/ui/Button";
|
||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||
@@ -517,12 +517,11 @@ const BookingPage = ({
|
||||
<div className="mb-3 text-gray-600 dark:text-white">
|
||||
<RefreshIcon className="mr-[10px] -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||
{`${t("every_for_freq", {
|
||||
freq: t(`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`),
|
||||
})} ${recurringEventCount} ${t(
|
||||
`${Frequency[eventType.recurringEvent.freq].toString().toLowerCase()}`,
|
||||
{ count: parseInt(recurringEventCount.toString()) }
|
||||
)}`}
|
||||
{getEveryFreqFor({
|
||||
t,
|
||||
recurringEvent: eventType.recurringEvent,
|
||||
recurringCount: recurringEventCount,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -64,7 +64,9 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
|
||||
{recurringEvent?.count && recurringEvent.count > 0 && (
|
||||
<li className="mr-4 flex items-center whitespace-nowrap">
|
||||
<RefreshIcon className="mr-1.5 inline h-4 w-4 text-neutral-400" aria-hidden="true" />
|
||||
{t("repeats_up_to", { count: recurringEvent.count })}
|
||||
{t("repeats_up_to", {
|
||||
count: recurringEvent.count,
|
||||
})}
|
||||
</li>
|
||||
)}
|
||||
{eventType.price > 0 && (
|
||||
|
||||
@@ -126,7 +126,7 @@ export default function RecurringEventController({
|
||||
}}
|
||||
/>
|
||||
<p className="mr-2 text-neutral-900">
|
||||
{t(`${Frequency[recurringEventState.freq].toString().toLowerCase()}`, {
|
||||
{t("occurrence", {
|
||||
count: recurringEventState.count,
|
||||
})}
|
||||
</p>
|
||||
|
||||
@@ -221,7 +221,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
(eventType.recurringEvent?.count &&
|
||||
recurringEventCountQuery &&
|
||||
(parseInt(recurringEventCountQuery) <= eventType.recurringEvent.count
|
||||
? recurringEventCountQuery
|
||||
? parseInt(recurringEventCountQuery)
|
||||
: eventType.recurringEvent.count)) ||
|
||||
null;
|
||||
|
||||
|
||||
@@ -224,7 +224,7 @@ async function patchHandler(req: NextApiRequest) {
|
||||
}
|
||||
|
||||
if (recurringEventId) {
|
||||
// The booking to confirm is a recurring event and comes from /booking/upcoming, proceeding to mark all related
|
||||
// The booking to confirm is a recurring event and comes from /booking/recurring, proceeding to mark all related
|
||||
// bookings as confirmed. Prisma updateMany does not support relations, so doing this in two steps for now.
|
||||
const unconfirmedRecurringBookings = await prisma.booking.findMany({
|
||||
where: {
|
||||
|
||||
@@ -35,6 +35,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
select: {
|
||||
...bookingMinimalSelect,
|
||||
recurringEventId: true,
|
||||
userId: true,
|
||||
user: {
|
||||
select: {
|
||||
@@ -146,15 +147,28 @@ 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.
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
uid,
|
||||
},
|
||||
data: {
|
||||
status: BookingStatus.CANCELLED,
|
||||
cancellationReason: cancellationReason,
|
||||
},
|
||||
});
|
||||
if (bookingToDelete.eventType?.recurringEvent) {
|
||||
// Proceed to mark as cancelled all recurring event instances
|
||||
await prisma.booking.updateMany({
|
||||
where: {
|
||||
recurringEventId: bookingToDelete.recurringEventId,
|
||||
},
|
||||
data: {
|
||||
status: BookingStatus.CANCELLED,
|
||||
cancellationReason: cancellationReason,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
uid,
|
||||
},
|
||||
data: {
|
||||
status: BookingStatus.CANCELLED,
|
||||
cancellationReason: cancellationReason,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** TODO: Remove this without breaking functionality */
|
||||
if (bookingToDelete.location === "integrations:daily") {
|
||||
|
||||
@@ -58,7 +58,19 @@ export default function Bookings() {
|
||||
}
|
||||
return { recurringCount };
|
||||
};
|
||||
|
||||
const shownBookings: Record<string, boolean> = {};
|
||||
const filterBookings = (booking: BookingOutput) => {
|
||||
if (status === "recurring" || status === "cancelled") {
|
||||
if (!booking.recurringEventId) {
|
||||
return true;
|
||||
}
|
||||
if (shownBookings[booking.recurringEventId]) {
|
||||
return false;
|
||||
}
|
||||
shownBookings[booking.recurringEventId] = true;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return (
|
||||
<Shell heading={t("bookings")} subtitle={t("bookings_description")} customLoader={<SkeletonLoader />}>
|
||||
<WipeMyCalActionButton trpc={trpc} bookingStatus={status} bookingsEmpty={isEmpty} />
|
||||
@@ -77,7 +89,7 @@ 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.map((booking) => (
|
||||
{page.bookings.filter(filterBookings).map((booking) => (
|
||||
<BookingListItem
|
||||
key={booking.id}
|
||||
listingStatus={status}
|
||||
|
||||
+106
-17
@@ -1,16 +1,20 @@
|
||||
import { CalendarIcon, XIcon } from "@heroicons/react/solid";
|
||||
import { CalendarIcon, XIcon, RefreshIcon } from "@heroicons/react/solid";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||
import dayjs from "dayjs";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
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 { getSession } from "@lib/auth";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
|
||||
import { detectBrowserTimeFormat } from "@lib/timeFormat";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
@@ -28,8 +32,8 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(props.booking ? null : t("booking_already_cancelled"));
|
||||
const [cancellationReason, setCancellationReason] = useState<string>("");
|
||||
const [moreEventsVisible, setMoreEventsVisible] = useState(false);
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HeadSeo
|
||||
@@ -67,27 +71,84 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
||||
<XIcon className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-headline">
|
||||
<div className="mt-3 sm:mt-5">
|
||||
<h3
|
||||
className="text-center text-lg font-medium leading-6 text-gray-900"
|
||||
id="modal-headline">
|
||||
{props.cancellationAllowed
|
||||
? t("really_cancel_booking")
|
||||
: t("cannot_cancel_booking")}
|
||||
</h3>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-gray-500">
|
||||
{props.cancellationAllowed ? t("reschedule_instead") : t("event_is_in_the_past")}
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
{props.cancellationAllowed && !props.booking?.eventType.recurringEvent
|
||||
? t("reschedule_instead")
|
||||
: t("event_is_in_the_past")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 border-t border-b py-4">
|
||||
<h2 className="font-cal mb-2 text-lg font-medium text-gray-600">
|
||||
<h2 className="font-cal mb-2 text-center text-lg font-medium text-gray-600">
|
||||
{props.booking?.title}
|
||||
</h2>
|
||||
<p className="text-gray-500">
|
||||
<CalendarIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||
{dayjs(props.booking?.startTime).format(
|
||||
detectBrowserTimeFormat + ", dddd DD MMMM YYYY"
|
||||
{props.booking?.eventType.recurringEvent &&
|
||||
props.booking?.eventType.recurringEvent.freq &&
|
||||
props.recurringInstances && (
|
||||
<div className="text-center text-gray-500">
|
||||
<RefreshIcon className="mr-3 -mt-1 ml-[2px] inline-block h-4 w-4 text-gray-400" />
|
||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||
{getEveryFreqFor({
|
||||
t,
|
||||
recurringEvent: props.booking.eventType.recurringEvent,
|
||||
recurringCount: props.recurringInstances.length,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</p>
|
||||
<div className="text-gray-500">
|
||||
<div className="flex flex-row items-start justify-center space-x-3">
|
||||
{props.booking?.eventType.recurringEvent && props.recurringInstances ? (
|
||||
<>
|
||||
<CalendarIcon className="mt-2 ml-1 h-4 w-4" />
|
||||
<div className="mb-1 inline py-1 text-left">
|
||||
<div className="">
|
||||
{dayjs(props.recurringInstances[0].startTime).format(
|
||||
detectBrowserTimeFormat + ", dddd DD MMMM YYYY"
|
||||
)}
|
||||
<Collapsible
|
||||
open={moreEventsVisible}
|
||||
onOpenChange={() => setMoreEventsVisible(!moreEventsVisible)}>
|
||||
<CollapsibleTrigger
|
||||
type="button"
|
||||
className={classNames(
|
||||
"-ml-4 block w-full text-center",
|
||||
moreEventsVisible ? "hidden" : ""
|
||||
)}>
|
||||
{t("plus_more", { count: props.recurringInstances.length - 1 })}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
{props.booking?.eventType.recurringEvent?.count &&
|
||||
props.recurringInstances.slice(1).map((dateObj, idx) => (
|
||||
<div key={idx} className="">
|
||||
{dayjs(dateObj.startTime).format(
|
||||
detectBrowserTimeFormat + ", dddd DD MMMM YYYY"
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CalendarIcon className="mt-1 mr-1 h-4 w-4" />
|
||||
{dayjs(props.booking?.startTime).format(
|
||||
detectBrowserTimeFormat + ", dddd DD MMMM YYYY"
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,9 +162,11 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
className="mb-5 sm:mb-6"
|
||||
/>
|
||||
<div className="space-x-2 text-center rtl:space-x-reverse">
|
||||
<Button color="secondary" onClick={() => router.push("/reschedule/" + uid)}>
|
||||
{t("reschedule_this")}
|
||||
</Button>
|
||||
{!props.booking.eventType?.recurringEvent && (
|
||||
<Button color="secondary" onClick={() => router.push("/reschedule/" + uid)}>
|
||||
{t("reschedule_this")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
data-testid="cancel"
|
||||
onClick={async () => {
|
||||
@@ -130,7 +193,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}`
|
||||
);
|
||||
} else {
|
||||
setLoading(false);
|
||||
@@ -167,6 +230,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
},
|
||||
select: {
|
||||
...bookingMinimalSelect,
|
||||
recurringEventId: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
@@ -178,6 +242,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
},
|
||||
eventType: {
|
||||
select: {
|
||||
length: true,
|
||||
recurringEvent: true,
|
||||
team: {
|
||||
select: {
|
||||
slug: true,
|
||||
@@ -199,8 +265,30 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
const bookingObj = Object.assign({}, booking, {
|
||||
startTime: booking.startTime.toString(),
|
||||
endTime: booking.endTime.toString(),
|
||||
eventType: {
|
||||
...booking.eventType,
|
||||
recurringEvent: parseRecurringEvent(booking.eventType?.recurringEvent),
|
||||
},
|
||||
});
|
||||
|
||||
let recurringInstances = null;
|
||||
if (booking.eventType?.recurringEvent) {
|
||||
recurringInstances = await prisma.booking.findMany({
|
||||
where: {
|
||||
recurringEventId: booking.recurringEventId,
|
||||
},
|
||||
select: {
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
},
|
||||
});
|
||||
recurringInstances = recurringInstances.map((recurr) => ({
|
||||
...recurr,
|
||||
startTime: recurr.startTime.toString(),
|
||||
endTime: recurr.endTime.toString(),
|
||||
}));
|
||||
}
|
||||
|
||||
const profile = {
|
||||
name: booking.eventType?.team?.name || booking.user?.name || null,
|
||||
slug: booking.eventType?.team?.slug || booking.user?.username || null,
|
||||
@@ -212,6 +300,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
props: {
|
||||
profile,
|
||||
booking: bookingObj,
|
||||
recurringInstances,
|
||||
cancellationAllowed:
|
||||
(!!session?.user && session.user?.id === booking.user?.id) || booking.startTime >= new Date(),
|
||||
trpcState: ssr.dehydrate(),
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function CancelSuccess() {
|
||||
const { t } = useLocale();
|
||||
// Get router variables
|
||||
const router = useRouter();
|
||||
const { title, name, eventPage } = router.query;
|
||||
const { title, name, eventPage, recurring } = router.query;
|
||||
const { data: session, status } = useSession();
|
||||
const loading = status === "loading";
|
||||
return (
|
||||
@@ -52,7 +52,10 @@ export default function CancelSuccess() {
|
||||
<div className="mt-5">
|
||||
{!loading && !session?.user && <Button href={eventPage as string}>Pick another</Button>}
|
||||
{!loading && session?.user && (
|
||||
<Button data-testid="back-to-bookings" href="/bookings" StartIcon={ArrowLeftIcon}>
|
||||
<Button
|
||||
data-testid="back-to-bookings"
|
||||
href={recurring !== "" ? "/bookings/recurring" : "/bookings"}
|
||||
StartIcon={ArrowLeftIcon}>
|
||||
{t("back_to_bookings")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -158,7 +158,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
(eventTypeObject?.recurringEvent?.count &&
|
||||
recurringEventCountQuery &&
|
||||
(parseInt(recurringEventCountQuery) <= eventTypeObject.recurringEvent.count
|
||||
? recurringEventCountQuery
|
||||
? parseInt(recurringEventCountQuery)
|
||||
: eventType.recurringEvent?.count)) ||
|
||||
null;
|
||||
|
||||
|
||||
+19
-16
@@ -26,6 +26,7 @@ import {
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
import Button from "@calcom/ui/Button";
|
||||
@@ -270,7 +271,7 @@ export default function Success(props: SuccessProps) {
|
||||
data-testid="success-page">
|
||||
{userIsOwner && !isEmbed && (
|
||||
<div className="-mb-7 ml-9 mt-7">
|
||||
<Link href="/bookings">
|
||||
<Link href={eventType.recurringEvent?.count ? "/bookings/recurring" : "/bookings"}>
|
||||
<a className="flex items-center text-black dark:text-white">
|
||||
<ArrowLeftIcon className="mr-1 h-4 w-4" /> {t("back_to_bookings")}
|
||||
</a>
|
||||
@@ -344,10 +345,9 @@ export default function Success(props: SuccessProps) {
|
||||
<div className="font-medium">{t("when")}</div>
|
||||
<div className="col-span-2 mb-6">
|
||||
<RecurringBookings
|
||||
isReschedule={reschedule === "true"}
|
||||
eventType={props.eventType}
|
||||
recurringBookings={props.recurringBookings}
|
||||
listingStatus={(listingStatus as string) || "upcoming"}
|
||||
listingStatus={(listingStatus as string) || "recurring"}
|
||||
date={date}
|
||||
is24h={is24h}
|
||||
/>
|
||||
@@ -429,16 +429,21 @@ export default function Success(props: SuccessProps) {
|
||||
<button className="underline" onClick={() => setIsCancellationMode(true)}>
|
||||
{t("cancel")}
|
||||
</button>
|
||||
<div className="mx-2">{t("or_lowercase")}</div>
|
||||
<div className="underline">
|
||||
<Link href={"/reschedule/" + bookingInfo?.uid}>{t("Reschedule")}</Link>
|
||||
</div>
|
||||
{!props.recurringBookings && (
|
||||
<>
|
||||
<div className="mx-2">{t("or_lowercase")}</div>
|
||||
<div className="underline">
|
||||
<Link href={"/reschedule/" + bookingInfo?.uid}>{t("Reschedule")}</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<CancelBooking
|
||||
booking={{ uid: bookingInfo?.uid, title: bookingInfo?.title }}
|
||||
profile={{ name: props.profile.name, slug: props.profile.slug }}
|
||||
recurringEvent={eventType.recurringEvent}
|
||||
team={eventType?.team?.name}
|
||||
setIsCancellationMode={setIsCancellationMode}
|
||||
theme={isSuccessBookingPage ? props.profile.theme : "light"}
|
||||
@@ -605,7 +610,6 @@ export default function Success(props: SuccessProps) {
|
||||
}
|
||||
|
||||
type RecurringBookingsProps = {
|
||||
isReschedule: boolean;
|
||||
eventType: SuccessProps["eventType"];
|
||||
recurringBookings: SuccessProps["recurringBookings"];
|
||||
date: dayjs.Dayjs;
|
||||
@@ -613,13 +617,7 @@ type RecurringBookingsProps = {
|
||||
listingStatus: string;
|
||||
};
|
||||
|
||||
function RecurringBookings({
|
||||
isReschedule = false,
|
||||
eventType,
|
||||
recurringBookings,
|
||||
date,
|
||||
listingStatus,
|
||||
}: RecurringBookingsProps) {
|
||||
function RecurringBookings({ eventType, recurringBookings, date, listingStatus }: RecurringBookingsProps) {
|
||||
const [moreEventsVisible, setMoreEventsVisible] = useState(false);
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -627,8 +625,13 @@ function RecurringBookings({
|
||||
? recurringBookings.sort((a, b) => (dayjs(a).isAfter(dayjs(b)) ? 1 : -1))
|
||||
: null;
|
||||
|
||||
return !isReschedule && recurringBookingsSorted && listingStatus === "upcoming" ? (
|
||||
return recurringBookingsSorted && listingStatus === "recurring" ? (
|
||||
<>
|
||||
{eventType.recurringEvent?.count && (
|
||||
<span className="font-medium">
|
||||
{getEveryFreqFor({ t, recurringEvent: eventType.recurringEvent })}
|
||||
</span>
|
||||
)}
|
||||
{eventType.recurringEvent?.count &&
|
||||
recurringBookingsSorted.slice(0, 4).map((dateStr, idx) => (
|
||||
<div key={idx} className="mb-2">
|
||||
|
||||
@@ -97,7 +97,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
(eventType.recurringEvent?.count &&
|
||||
recurringEventCountQuery &&
|
||||
(parseInt(recurringEventCountQuery) <= eventType.recurringEvent.count
|
||||
? recurringEventCountQuery
|
||||
? parseInt(recurringEventCountQuery)
|
||||
: eventType.recurringEvent.count)) ||
|
||||
null;
|
||||
|
||||
|
||||
@@ -478,6 +478,7 @@
|
||||
"danger_zone": "Danger Zone",
|
||||
"back": "Back",
|
||||
"cancel": "Cancel",
|
||||
"cancel_all_remaining": "Cancel all remaining",
|
||||
"apply": "Apply",
|
||||
"cancel_event": "Cancel event",
|
||||
"continue": "Continue",
|
||||
@@ -553,7 +554,11 @@
|
||||
"repeats_up_to_one": "Repeats up to {{count}} time",
|
||||
"repeats_up_to_other": "Repeats up to {{count}} times",
|
||||
"every_for_freq": "Every {{freq}} for",
|
||||
"event_remaining_one": "{{count}} event remaining",
|
||||
"event_remaining_other": "{{count}} events remaining",
|
||||
"repeats_every": "Repeats every",
|
||||
"occurrence_one": "occurrence",
|
||||
"occurrence_other": "occurrences",
|
||||
"weekly_one": "week",
|
||||
"weekly_other": "weeks",
|
||||
"monthly_one": "month",
|
||||
|
||||
@@ -319,7 +319,12 @@ const loggedInViewerRouter = createProtectedRouter()
|
||||
// handled separately for each occurrence
|
||||
OR: [
|
||||
{
|
||||
AND: [{ NOT: { recurringEventId: { equals: null } } }, { status: BookingStatus.PENDING }],
|
||||
AND: [
|
||||
{ NOT: { recurringEventId: { equals: null } } },
|
||||
{ NOT: { status: { equals: BookingStatus.PENDING } } },
|
||||
{ NOT: { status: { equals: BookingStatus.CANCELLED } } },
|
||||
{ NOT: { status: { equals: BookingStatus.REJECTED } } },
|
||||
],
|
||||
},
|
||||
{
|
||||
AND: [
|
||||
|
||||
@@ -3,22 +3,26 @@ import timezone from "dayjs/plugin/timezone";
|
||||
import { TFunction } from "next-i18next";
|
||||
import rrule from "rrule";
|
||||
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
import { RecurringEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { Info } from "./Info";
|
||||
|
||||
dayjs.extend(timezone);
|
||||
|
||||
function getRecurringWhen(props: { calEvent: CalendarEvent }) {
|
||||
const t = props.calEvent.attendees[0].language.translate;
|
||||
const { calEvent: { recurringEvent } = {} } = props;
|
||||
return recurringEvent?.count && recurringEvent?.freq
|
||||
? ` - ${t("every_for_freq", {
|
||||
freq: t(`${rrule.FREQUENCIES[recurringEvent.freq].toString().toLowerCase()}`),
|
||||
})} ${recurringEvent.count} ${t(`${rrule.FREQUENCIES[recurringEvent.freq].toString().toLowerCase()}`, {
|
||||
count: recurringEvent.count,
|
||||
})}`
|
||||
: "";
|
||||
function getRecurringWhen({ calEvent }: { calEvent: CalendarEvent }) {
|
||||
if (calEvent.recurringEvent) {
|
||||
const t = calEvent.attendees[0].language.translate;
|
||||
const rruleOptions = new rrule(calEvent.recurringEvent).options;
|
||||
const recurringEvent: RecurringEvent = {
|
||||
freq: rruleOptions.freq,
|
||||
count: rruleOptions.count || 1,
|
||||
interval: rruleOptions.interval,
|
||||
};
|
||||
return ` - ${getEveryFreqFor({ t, recurringEvent })}`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function WhenInfo(props: { calEvent: CalendarEvent; timeZone: string; t: TFunction }) {
|
||||
|
||||
@@ -15,7 +15,11 @@ export const OrganizerRequestEmail = (props: React.ComponentProps<typeof Organiz
|
||||
<CallToActionTable>
|
||||
<CallToAction
|
||||
label={props.calEvent.organizer.language.translate("confirm_or_reject_request")}
|
||||
href={process.env.NEXT_PUBLIC_WEBAPP_URL + "/bookings/upcoming"}
|
||||
href={
|
||||
process.env.NEXT_PUBLIC_WEBAPP_URL + props.calEvent.recurringEvent?.count
|
||||
? "/bookings/recurring"
|
||||
: "/bookings/upcoming"
|
||||
}
|
||||
/>
|
||||
</CallToActionTable>
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export default class AttendeeScheduledEmail extends BaseEmail {
|
||||
}
|
||||
|
||||
protected getiCalEventAsString(): string | undefined {
|
||||
// Taking care of recurrence rule beforehand
|
||||
// Taking care of recurrence rule
|
||||
let recurrenceRule: string | undefined = undefined;
|
||||
if (this.calEvent.recurringEvent?.count) {
|
||||
// ics appends "RRULE:" already, so removing it from RRule generated string
|
||||
|
||||
@@ -35,7 +35,9 @@ export default class OrganizerRequestEmail extends OrganizerScheduledEmail {
|
||||
"someone_requested_an_event",
|
||||
"",
|
||||
`${this.calEvent.organizer.language.translate("confirm_or_reject_request")}
|
||||
${process.env.NEXT_PUBLIC_WEBAPP_URL} + "/bookings/upcoming"`
|
||||
${process.env.NEXT_PUBLIC_WEBAPP_URL} + ${
|
||||
this.calEvent.recurringEvent?.count ? "/bookings/recurring" : "/bookings/upcoming"
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ dayjs.extend(toArray);
|
||||
|
||||
export default class OrganizerScheduledEmail extends BaseEmail {
|
||||
calEvent: CalendarEvent;
|
||||
|
||||
t: TFunction;
|
||||
|
||||
constructor(calEvent: CalendarEvent) {
|
||||
@@ -31,7 +30,7 @@ export default class OrganizerScheduledEmail extends BaseEmail {
|
||||
}
|
||||
|
||||
protected getiCalEventAsString(): string | undefined {
|
||||
// Taking care of recurrence rule beforehand
|
||||
// Taking care of recurrence rule
|
||||
let recurrenceRule: string | undefined = undefined;
|
||||
if (this.calEvent.recurringEvent?.count) {
|
||||
// ics appends "RRULE:" already, so removing it from RRule generated string
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"ics": "^2.31.0",
|
||||
"next-i18next": "^11.0.0",
|
||||
"react-hot-toast": "^2.1.0",
|
||||
"rrule": "^2.6.9",
|
||||
"tsdav": "2.0.2",
|
||||
"tslog": "^3.2.1",
|
||||
"uuid": "^8.3.2"
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { TFunction } from "next-i18next";
|
||||
|
||||
import { Frequency } from "@calcom/prisma/zod-utils";
|
||||
import { RecurringEvent } from "@calcom/types/Calendar";
|
||||
|
||||
export const getRecurringFreq = ({
|
||||
t,
|
||||
recurringEvent,
|
||||
}: {
|
||||
t: TFunction;
|
||||
recurringEvent: RecurringEvent;
|
||||
}): string => {
|
||||
if (recurringEvent.interval && recurringEvent.freq) {
|
||||
return t("every_for_freq", {
|
||||
freq: `${recurringEvent.interval > 1 ? recurringEvent.interval : ""} ${t(
|
||||
Frequency[recurringEvent.freq].toString().toLowerCase(),
|
||||
{
|
||||
count: recurringEvent.interval,
|
||||
}
|
||||
)}`,
|
||||
});
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
export const getEveryFreqFor = ({
|
||||
t,
|
||||
recurringEvent,
|
||||
recurringCount,
|
||||
recurringFreq,
|
||||
}: {
|
||||
t: TFunction;
|
||||
recurringEvent: RecurringEvent;
|
||||
recurringCount?: number;
|
||||
recurringFreq?: string;
|
||||
}): string => {
|
||||
if (recurringEvent.freq) {
|
||||
return `${recurringFreq || getRecurringFreq({ t, recurringEvent })} ${
|
||||
recurringCount || recurringEvent.count
|
||||
} ${t("occurrences", {
|
||||
count: recurringCount || recurringEvent.count,
|
||||
})}`;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
Reference in New Issue
Block a user