{day === null ? (
@@ -194,10 +209,7 @@ const Days = ({
onClick={() => {
props.onChange(day);
}}
- disabled={
- (includedDates && !includedDates.includes(yyyymmdd(day))) ||
- excludedDates.includes(yyyymmdd(day))
- }
+ disabled={disabled}
active={isActive(day)}
/>
)}
@@ -293,4 +305,41 @@ const DatePicker = ({
);
};
+/**
+ * Takes care of selecting a valid date in the month if the selected date is not available in the month
+ */
+const useHandleInitialDateSelection = ({
+ daysToRenderForTheMonth,
+ selected,
+ onChange,
+}: {
+ daysToRenderForTheMonth: { day: Dayjs | null; disabled: boolean }[];
+ selected: Dayjs | Dayjs[] | null | undefined;
+ onChange: (date: Dayjs | null) => void;
+}) => {
+ // Let's not do something for now in case of multiple selected dates as behaviour is unclear and it's not needed at the moment
+ if (selected instanceof Array) {
+ return;
+ }
+ const firstAvailableDateOfTheMonth = daysToRenderForTheMonth.find((day) => !day.disabled)?.day;
+
+ const isSelectedDateAvailable = selected
+ ? daysToRenderForTheMonth.some(({ day, disabled }) => {
+ if (day && yyyymmdd(day) === yyyymmdd(selected) && !disabled) return true;
+ })
+ : false;
+
+ if (firstAvailableDateOfTheMonth) {
+ // If selected date not available in the month, select the first available date of the month
+ if (!isSelectedDateAvailable) {
+ onChange(firstAvailableDateOfTheMonth);
+ }
+ } else {
+ // No date is available and if we were asked to select something inform that it couldn't be selected. This would actually help in not showing the timeslots section(with No Time Available) when no date in the month is available
+ if (selected) {
+ onChange(null);
+ }
+ }
+};
+
export default DatePicker;
diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts
index f6ac593d94..b63b1ca47c 100644
--- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts
+++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts
@@ -1,10 +1,16 @@
/* Schedule any workflow reminder that falls within 72 hours for email */
+import type { Prisma } from "@prisma/client";
import client from "@sendgrid/client";
import sgMail from "@sendgrid/mail";
+import { createEvent } from "ics";
+import type { DateArray } from "ics";
import type { NextApiRequest, NextApiResponse } from "next";
+import { RRule } from "rrule";
+import { v4 as uuidv4 } from "uuid";
import dayjs from "@calcom/dayjs";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
+import { parseRecurringEvent } from "@calcom/lib";
import { defaultHandler } from "@calcom/lib/server";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
import prisma from "@calcom/prisma";
@@ -20,6 +26,65 @@ const senderEmail = process.env.SENDGRID_EMAIL as string;
sgMail.setApiKey(sendgridAPIKey);
+type Booking = Prisma.BookingGetPayload<{
+ include: {
+ eventType: true;
+ user: true;
+ attendees: true;
+ };
+}>;
+
+function getiCalEventAsString(booking: Booking) {
+ let recurrenceRule: string | undefined = undefined;
+ const recurringEvent = parseRecurringEvent(booking.eventType?.recurringEvent);
+ if (recurringEvent?.count) {
+ recurrenceRule = new RRule(recurringEvent).toString().replace("RRULE:", "");
+ }
+
+ const uid = uuidv4();
+
+ const icsEvent = createEvent({
+ uid,
+ startInputType: "utc",
+ start: dayjs(booking.startTime.toISOString() || "")
+ .utc()
+ .toArray()
+ .slice(0, 6)
+ .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
+ duration: {
+ minutes: dayjs(booking.endTime.toISOString() || "").diff(
+ dayjs(booking.startTime.toISOString() || ""),
+ "minute"
+ ),
+ },
+ title: booking.eventType?.title || "",
+ description: booking.description || "",
+ location: booking.location || "",
+ organizer: {
+ email: booking.user?.email || "",
+ name: booking.user?.name || "",
+ },
+ attendees: [
+ {
+ name: booking.attendees[0].name,
+ email: booking.attendees[0].email,
+ partstat: "ACCEPTED",
+ role: "REQ-PARTICIPANT",
+ rsvp: true,
+ },
+ ],
+ method: "REQUEST",
+ ...{ recurrenceRule },
+ status: "CONFIRMED",
+ });
+
+ if (icsEvent.error) {
+ throw icsEvent.error;
+ }
+
+ return icsEvent.value;
+}
+
async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.headers.authorization || req.query.apiKey;
if (process.env.CRON_API_KEY !== apiKey) {
@@ -258,6 +323,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
enable: sandboxMode,
},
},
+ attachments: reminder.workflowStep.includeCalendarEvent
+ ? [
+ {
+ content: Buffer.from(getiCalEventAsString(reminder.booking) || "").toString("base64"),
+ filename: "event.ics",
+ type: "text/calendar; method=REQUEST",
+ disposition: "attachment",
+ contentId: uuidv4(),
+ },
+ ]
+ : undefined,
});
}
diff --git a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx
index 4760d25175..a5950cc49c 100644
--- a/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx
+++ b/packages/features/ee/workflows/components/WorkflowDetailsPage.tsx
@@ -113,6 +113,7 @@ export default function WorkflowDetailsPage(props: Props) {
sender: isSMSAction(action) ? sender || SENDER_ID : SENDER_ID,
senderName: !isSMSAction(action) ? senderName || SENDER_NAME : SENDER_NAME,
numberVerificationPending: false,
+ includeCalendarEvent: false,
};
steps?.push(step);
form.setValue("steps", steps);
diff --git a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx
index 228c890c9e..65f9e14ac5 100644
--- a/packages/features/ee/workflows/components/WorkflowStepContainer.tsx
+++ b/packages/features/ee/workflows/components/WorkflowStepContainer.tsx
@@ -861,6 +861,29 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
{form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody?.message || ""}
)}
+ {isEmailSubjectNeeded && (
+
+ (
+
+ form.setValue(
+ `steps.${step.stepNumber - 1}.includeCalendarEvent`,
+ e.target.checked
+ )
+ }
+ />
+ )}
+ />
+
+ )}
{!props.readOnly && (