diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 409978789b..db97c62f23 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -173,7 +173,7 @@ function BookingListItem(booking: BookingItem) { { id: "edit", label: t("edit_booking"), - href: "", + href: `/reschedule/${booking.uid}`, }, { id: "reschedule_request", diff --git a/apps/web/components/booking/pages/AvailabilityPage.tsx b/apps/web/components/booking/pages/AvailabilityPage.tsx index b927eb3cc4..0b09922d65 100644 --- a/apps/web/components/booking/pages/AvailabilityPage.tsx +++ b/apps/web/components/booking/pages/AvailabilityPage.tsx @@ -6,6 +6,7 @@ import { ClockIcon, CreditCardIcon, GlobeIcon, + InformationCircleIcon, } from "@heroicons/react/solid"; import * as Collapsible from "@radix-ui/react-collapsible"; import { useContracts } from "contexts/contractsContext"; @@ -16,11 +17,12 @@ import { useRouter } from "next/router"; import { useEffect, useMemo, useState } from "react"; import { FormattedNumber, IntlProvider } from "react-intl"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; + import { asStringOrNull } from "@lib/asStringOrNull"; import { timeZone } from "@lib/clock"; import { BASE_URL } from "@lib/config/constants"; import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally"; -import { useLocale } from "@lib/hooks/useLocale"; import useTheme from "@lib/hooks/useTheme"; import { isBrandingHidden } from "@lib/isBrandingHidden"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; @@ -42,7 +44,7 @@ dayjs.extend(customParseFormat); type Props = AvailabilityTeamPageProps | AvailabilityPageProps; -const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage }: Props) => { +const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage, booking }: Props) => { const router = useRouter(); const { rescheduleUid } = router.query; const { isReady, Theme } = useTheme(profile.theme); @@ -155,9 +157,13 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage truncateAfter={5} />
-

{profile.name}

-
+

{profile.name}

+
{eventType.title} +

+ + {eventType.description} +

{eventType.length} {t("minutes")} @@ -177,7 +183,6 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
-

{eventType.description}

@@ -204,16 +209,20 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage truncateAfter={3} />

{profile.name}

-

+

{eventType.title}

-

- +

+ + {eventType.description} +

+

+ {eventType.length} {t("minutes")}

{eventType.price > 0 && ( -

- +

+ - -

{eventType.description}

{previousPage === `${BASE_URL}/${profile.slug}` && (
- - + + {timeZone()} {isTimeOptionsOpen ? ( diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index 08557ace73..88aa6fb9ff 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -1,7 +1,14 @@ -import { CalendarIcon, ClockIcon, CreditCardIcon, ExclamationIcon } from "@heroicons/react/solid"; +import { + CalendarIcon, + ClockIcon, + CreditCardIcon, + ExclamationIcon, + InformationCircleIcon, +} from "@heroicons/react/solid"; import { EventTypeCustomInputType } from "@prisma/client"; +import classNames from "classnames"; import { useContracts } from "contexts/contractsContext"; -import dayjs from "dayjs"; +import dayjs, { Dayjs } from "dayjs"; import { useSession } from "next-auth/react"; import dynamic from "next/dynamic"; import Head from "next/head"; @@ -12,6 +19,7 @@ import { FormattedNumber, IntlProvider } from "react-intl"; import { ReactMultiEmail } from "react-multi-email"; import { useMutation } from "react-query"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; import { HttpError } from "@calcom/lib/http-error"; import { createPaymentLink } from "@calcom/stripe/client"; import { Button } from "@calcom/ui/Button"; @@ -20,7 +28,6 @@ import { EmailInput, Form } from "@calcom/ui/form/fields"; import { asStringOrNull } from "@lib/asStringOrNull"; import { timeZone } from "@lib/clock"; import { ensureArray } from "@lib/ensureArray"; -import { useLocale } from "@lib/hooks/useLocale"; import useTheme from "@lib/hooks/useTheme"; import { LocationType } from "@lib/location"; import createBooking from "@lib/mutations/bookings/create-booking"; @@ -56,6 +63,7 @@ type BookingFormValues = { }; const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPageProps) => { + console.log({ booking }); const { t, i18n } = useLocale(); const router = useRouter(); const { contracts } = useContracts(); @@ -161,6 +169,7 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag name: primaryAttendee.name || "", email: primaryAttendee.email || "", guests: booking.attendees.slice(1).map((attendee) => attendee.email), + notes: booking.description || "", }; }; @@ -199,7 +208,7 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag } }; - const parseDate = (date: string | null) => { + const parseDate = (date: string | null | Dayjs) => { if (!date) return "No date"; const parsedZone = parseZone(date); if (!parsedZone?.isValid()) return "Invalid date"; @@ -255,6 +264,13 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag })), }); }; + const userOwnerIds = eventType.users.map((user) => user.id); + const isUserOwnerRescheduling = !!( + session?.user.id && + rescheduleUid && + userOwnerIds.indexOf(session?.user.id) > -1 + ); + const disableInput = isUserOwnerRescheduling; return (
@@ -296,13 +312,17 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag

{eventType.title}

+

+ + {eventType.description} +

- + {eventType.length} {t("minutes")}

{eventType.price > 0 && (

- + )}

- + {parseDate(date)}

{eventTypeDetail.isWeb3Active && eventType.metadata.smartContractAddress && ( @@ -321,7 +341,16 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag {t("requires_ownership_of_a_token") + " " + eventType.metadata.smartContractAddress}

)} -

{eventType.description}

+ {booking?.startTime && rescheduleUid && ( +
+ {/* Add translation */} +

Former time

+

+ + {typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime))} +

+
+ )}
@@ -336,8 +365,12 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag name="name" id="name" required - className="focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm" + className={classNames( + "focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm", + disableInput ? "bg-gray-200 dark:text-gray-500" : "" + )} placeholder={t("example_name")} + disabled={disableInput} />
@@ -351,9 +384,13 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag
@@ -370,6 +407,7 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag {...bookingForm.register("locationType", { required: true })} value={location.type} defaultChecked={selectedLocation === location.type} + disabled={disableInput} /> {locationLabels[location.type]} @@ -392,6 +430,7 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag placeholder={t("enter_phone_number")} id="phone" required + disabled={disableInput} /> @@ -414,8 +453,12 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag })} id={"custom_" + input.id} rows={3} - className="focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm" + className={classNames( + "focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm", + disableInput ? "bg-gray-200 dark:text-gray-500" : "" + )} placeholder={input.placeholder} + disabled={disableInput} /> )} {input.type === EventTypeCustomInputType.TEXT && ( @@ -427,6 +470,7 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag id={"custom_" + input.id} className="focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm" placeholder={input.placeholder} + disabled={isUserOwnerRescheduling} /> )} {input.type === EventTypeCustomInputType.NUMBER && ( @@ -518,8 +562,12 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag {...bookingForm.register("notes")} id="notes" rows={3} - className="focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm" + className={classNames( + "focus:border-brand block w-full rounded-sm border-gray-300 shadow-sm focus:ring-black dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 sm:text-sm", + disableInput ? "bg-gray-200 dark:text-gray-500" : "" + )} placeholder={t("share_additional_notes")} + disabled={disableInput} />
diff --git a/apps/web/components/ui/TableActions.tsx b/apps/web/components/ui/TableActions.tsx index 74307a30cf..193d169fb5 100644 --- a/apps/web/components/ui/TableActions.tsx +++ b/apps/web/components/ui/TableActions.tsx @@ -12,7 +12,9 @@ export type ActionType = { label: string; disabled?: boolean; color?: "primary" | "secondary"; -} & ({ href?: never; onClick: () => any } | { href?: string; onClick?: never }) & { actions?: ActionType[] }; +} & ({ href?: never; onClick: () => any } | { href?: string; onClick?: never }) & { + actions?: ActionType[]; + }; interface Props { actions: ActionType[]; diff --git a/apps/web/components/ui/form/PhoneInput.tsx b/apps/web/components/ui/form/PhoneInput.tsx index 2689273e84..5455c21bae 100644 --- a/apps/web/components/ui/form/PhoneInput.tsx +++ b/apps/web/components/ui/form/PhoneInput.tsx @@ -20,7 +20,8 @@ function PhoneInput({ control, name, ...rest }: PhoneInputProps ); diff --git a/apps/web/lib/emails/email-manager.ts b/apps/web/lib/emails/email-manager.ts index 32d51a73f7..ae681f70cd 100644 --- a/apps/web/lib/emails/email-manager.ts +++ b/apps/web/lib/emails/email-manager.ts @@ -4,6 +4,7 @@ import AttendeeAwaitingPaymentEmail from "@lib/emails/templates/attendee-awaitin import AttendeeCancelledEmail from "@lib/emails/templates/attendee-cancelled-email"; import AttendeeDeclinedEmail from "@lib/emails/templates/attendee-declined-email"; import AttendeeRequestEmail from "@lib/emails/templates/attendee-request-email"; +import AttendeeRequestRescheduledEmail from "@lib/emails/templates/attendee-request-reschedule-email"; import AttendeeRescheduledEmail from "@lib/emails/templates/attendee-rescheduled-email"; import AttendeeScheduledEmail from "@lib/emails/templates/attendee-scheduled-email"; import ForgotPasswordEmail, { PasswordReset } from "@lib/emails/templates/forgot-password-email"; @@ -11,12 +12,11 @@ import OrganizerCancelledEmail from "@lib/emails/templates/organizer-cancelled-e import OrganizerPaymentRefundFailedEmail from "@lib/emails/templates/organizer-payment-refund-failed-email"; import OrganizerRequestEmail from "@lib/emails/templates/organizer-request-email"; import OrganizerRequestReminderEmail from "@lib/emails/templates/organizer-request-reminder-email"; +import OrganizerRequestRescheduleEmail from "@lib/emails/templates/organizer-request-reschedule-email"; import OrganizerRescheduledEmail from "@lib/emails/templates/organizer-rescheduled-email"; import OrganizerScheduledEmail from "@lib/emails/templates/organizer-scheduled-email"; import TeamInviteEmail, { TeamInvite } from "@lib/emails/templates/team-invite-email"; -import OrganizerRequestRescheduledEmail from "./templates/organizer-request-reschedule-email"; - export const sendScheduledEmails = async (calEvent: CalendarEvent) => { const emailsToSend: Promise[] = []; @@ -211,41 +211,33 @@ export const sendTeamInviteEmail = async (teamInviteEvent: TeamInvite) => { }); }; -export const sendRequestRescheduleEmail = async (calEvent: CalendarEvent) => { - await new Promise((resolve, reject) => { - try { - const rescheduleEmail = new OrganizerRequestRescheduledEmail(calEvent); - resolve(rescheduleEmail.sendEmail()); - } catch (e) { - reject(console.error("RescheduleEmail.sendEmail failed", e)); - } - }); - +export const sendRequestRescheduleEmail = async ( + calEvent: CalendarEvent, + metadata: { rescheduleLink: string } +) => { const emailsToSend: Promise[] = []; - // emailsToSend.push( - // ...calEvent.attendees.map((attendee) => { - // return new Promise((resolve, reject) => { - // try { - // const requestRescheduleEmail = new AttendeeRequesRescheduledEmail(calEvent, attendee); - // resolve(requestRescheduleEmail.sendEmail()); - // } catch (e) { - // reject(console.error("AttendeeRequestRescheduledEmail.sendEmail failed", e)); - // } - // }); - // }) - // ); + emailsToSend.push( + new Promise((resolve, reject) => { + try { + const requestRescheduleEmail = new AttendeeRequestRescheduledEmail(calEvent, metadata); + resolve(requestRescheduleEmail.sendEmail()); + } catch (e) { + reject(console.error("AttendeeRequestRescheduledEmail.sendEmail failed", e)); + } + }) + ); - // emailsToSend.push( - // new Promise((resolve, reject) => { - // try { - // const requestRescheduleEmail = new OrganizerRequestRescheduledEmail(calEvent); - // resolve(requestRescheduleEmail.sendEmail()); - // } catch (e) { - // reject(console.error("OrganizerRequestRescheduledEmail.sendEmail failed", e)); - // } - // }) - // ); + emailsToSend.push( + new Promise((resolve, reject) => { + try { + const requestRescheduleEmail = new OrganizerRequestRescheduleEmail(calEvent, metadata); + resolve(requestRescheduleEmail.sendEmail()); + } catch (e) { + reject(console.error("OrganizerRequestRescheduledEmail.sendEmail failed", e)); + } + }) + ); await Promise.all(emailsToSend); }; diff --git a/apps/web/lib/emails/templates/attendee-request-reschedule-email.ts b/apps/web/lib/emails/templates/attendee-request-reschedule-email.ts new file mode 100644 index 0000000000..ec3b522e52 --- /dev/null +++ b/apps/web/lib/emails/templates/attendee-request-reschedule-email.ts @@ -0,0 +1,219 @@ +import dayjs from "dayjs"; +import localizedFormat from "dayjs/plugin/localizedFormat"; +import timezone from "dayjs/plugin/timezone"; +import toArray from "dayjs/plugin/toArray"; +import utc from "dayjs/plugin/utc"; +import { createEvent, DateArray, Person } from "ics"; + +import { getCancelLink } from "@calcom/lib/CalEventParser"; +import { Attendee } from "@calcom/prisma/client"; +import { CalendarEvent } from "@calcom/types/Calendar"; + +import { + emailHead, + emailSchedulingBodyHeader, + emailBodyLogo, + emailScheduledBodyHeaderContent, + emailSchedulingBodyDivider, +} from "./common"; +import OrganizerScheduledEmail from "./organizer-scheduled-email"; + +dayjs.extend(utc); +dayjs.extend(timezone); +dayjs.extend(localizedFormat); +dayjs.extend(toArray); + +export default class AttendeeRequestRescheduledEmail extends OrganizerScheduledEmail { + private metadata: { rescheduleLink: string }; + constructor(calEvent: CalendarEvent, metadata: { rescheduleLink: string }) { + super(calEvent); + this.metadata = metadata; + } + protected getNodeMailerPayload(): Record { + const toAddresses = [this.calEvent.attendees[0].email]; + + return { + icalEvent: { + filename: "event.ics", + content: this.getiCalEventAsString(), + }, + from: `Cal.com <${this.getMailerOptions().from}>`, + to: toAddresses.join(","), + subject: `${this.calEvent.organizer.language.translate("rescheduled_event_type_subject", { + eventType: this.calEvent.type, + name: this.calEvent.attendees[0].name, + date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( + "h:mma" + )}, ${this.calEvent.organizer.language.translate( + this.getOrganizerStart().format("dddd").toLowerCase() + )}, ${this.calEvent.organizer.language.translate( + this.getOrganizerStart().format("MMMM").toLowerCase() + )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, + })}`, + html: this.getHtmlBody(), + text: this.getTextBody(), + }; + } + + // @OVERRIDE + protected getiCalEventAsString(): string | undefined { + console.log("overriding"); + const icsEvent = createEvent({ + start: dayjs(this.calEvent.startTime) + .utc() + .toArray() + .slice(0, 6) + .map((v, i) => (i === 1 ? v + 1 : v)) as DateArray, + startInputType: "utc", + productId: "calendso/ics", + title: this.calEvent.organizer.language.translate("ics_event_title", { + eventType: this.calEvent.type, + name: this.calEvent.attendees[0].name, + }), + description: this.getTextBody(), + duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") }, + organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email }, + attendees: this.calEvent.attendees.map((attendee: Person) => ({ + name: attendee.name, + email: attendee.email, + })), + status: "CANCELLED", + method: "CANCEL", + }); + if (icsEvent.error) { + throw icsEvent.error; + } + return icsEvent.value; + } + // @OVERRIDE + protected getWhen(): string { + return ` +

+
+

${this.calEvent.organizer.language.translate("when")}

+

+ ${this.calEvent.organizer.language.translate( + this.getOrganizerStart().format("dddd").toLowerCase() + )}, ${this.calEvent.organizer.language.translate( + this.getOrganizerStart().format("MMMM").toLowerCase() + )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format( + "YYYY" + )} | ${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( + "h:mma" + )} (${this.getTimezone()}) +

+
`; + } + + protected getTextBody(): string { + return ` +${this.calEvent.organizer.language.translate("request_reschedule_title_attendee")} +${this.calEvent.organizer.language.translate("request_reschedule_subtitle", { + organizer: this.calEvent.organizer.name, +})}, +${this.getWhat()} +${this.getWhen()} +${this.getLocation()} +${this.getAdditionalNotes()} +${this.calEvent.organizer.language.translate("need_to_reschedule_or_cancel")} +${getCancelLink(this.calEvent)} +`.replace(/(<([^>]+)>)/gi, ""); + } + + protected getHtmlBody(): string { + const headerContent = this.calEvent.organizer.language.translate("rescheduled_event_type_subject", { + eventType: this.calEvent.type, + name: this.calEvent.attendees[0].name, + date: `${this.getOrganizerStart().format("h:mma")} - ${this.getOrganizerEnd().format( + "h:mma" + )}, ${this.calEvent.organizer.language.translate( + this.getOrganizerStart().format("dddd").toLowerCase() + )}, ${this.calEvent.organizer.language.translate( + this.getOrganizerStart().format("MMMM").toLowerCase() + )} ${this.getOrganizerStart().format("D")}, ${this.getOrganizerStart().format("YYYY")}`, + }); + + return ` + + + ${emailHead(headerContent)} + +
+ ${emailSchedulingBodyHeader("calendarCircle")} + ${emailScheduledBodyHeaderContent( + this.calEvent.organizer.language.translate("request_reschedule_title_attendee"), + this.calEvent.organizer.language.translate("request_reschedule_subtitle", { + organizer: this.calEvent.organizer.name, + }) + )} + ${emailSchedulingBodyDivider()} + +
+ + + + + + +
+ +
+ + + + + + +
+
+ ${this.getWhat()} + ${this.getWhen()} + ${this.getWho()} + ${this.getLocation()} + ${this.getAdditionalNotes()} +
+
+
+ +
+
+ ${emailSchedulingBodyDivider()} + +
+ + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+
+ ${emailBodyLogo()} + +
+ + + `; + } +} diff --git a/apps/web/lib/emails/templates/common/scheduling-body-head.ts b/apps/web/lib/emails/templates/common/scheduling-body-head.ts index 7f3a6d6690..fc715d2a26 100644 --- a/apps/web/lib/emails/templates/common/scheduling-body-head.ts +++ b/apps/web/lib/emails/templates/common/scheduling-body-head.ts @@ -40,7 +40,7 @@ export const emailSchedulingBodyHeader = (headerType: BodyHeadType): string => { -
+
diff --git a/apps/web/lib/emails/templates/organizer-request-reschedule-email.ts b/apps/web/lib/emails/templates/organizer-request-reschedule-email.ts index 7aa59f0082..f0307545db 100644 --- a/apps/web/lib/emails/templates/organizer-request-reschedule-email.ts +++ b/apps/web/lib/emails/templates/organizer-request-reschedule-email.ts @@ -6,6 +6,7 @@ import utc from "dayjs/plugin/utc"; import { createEvent, DateArray, Person } from "ics"; import { getCancelLink } from "@calcom/lib/CalEventParser"; +import { CalendarEvent } from "@calcom/types/Calendar"; import { emailHead, @@ -22,16 +23,13 @@ dayjs.extend(localizedFormat); dayjs.extend(toArray); export default class OrganizerRequestRescheduledEmail extends OrganizerScheduledEmail { + private metadata: { rescheduleLink: string }; + constructor(calEvent: CalendarEvent, metadata: { rescheduleLink: string }) { + super(calEvent); + this.metadata = metadata; + } protected getNodeMailerPayload(): Record { const toAddresses = [this.calEvent.organizer.email]; - if (this.calEvent.team) { - this.calEvent.team.members.forEach((member) => { - const memberAttendee = this.calEvent.attendees.find((attendee) => attendee.name === member); - if (memberAttendee) { - toAddresses.push(memberAttendee.email); - } - }); - } return { icalEvent: { @@ -86,7 +84,7 @@ export default class OrganizerRequestRescheduledEmail extends OrganizerScheduled } return icsEvent.value; } - // @OVERRIDe + // @OVERRIDE protected getWhen(): string { return `

@@ -108,9 +106,11 @@ export default class OrganizerRequestRescheduledEmail extends OrganizerScheduled protected getTextBody(): string { return ` -${this.calEvent.organizer.language.translate("request_reschedule_title_attendee")} -${this.calEvent.organizer.language.translate("request_reschedule_subtitle", { - organizer: this.calEvent.attendees[0], +${this.calEvent.organizer.language.translate("request_reschedule_title_organizer", { + attendee: this.calEvent.attendees[0].name, +})} +${this.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", { + attendee: this.calEvent.attendees[0].name, })}, ${this.getWhat()} ${this.getWhen()} @@ -142,9 +142,11 @@ ${getCancelLink(this.calEvent)}
${emailSchedulingBodyHeader("calendarCircle")} ${emailScheduledBodyHeaderContent( - this.calEvent.organizer.language.translate("request_reschedule_title_attendee"), - this.calEvent.organizer.language.translate("request_reschedule_subtitle", { - organizer: this.calEvent.attendees[0], + this.calEvent.organizer.language.translate("request_reschedule_title_organizer", { + attendee: this.calEvent.attendees[0].name, + }), + this.calEvent.organizer.language.translate("request_reschedule_subtitle_organizer", { + attendee: this.calEvent.attendees[0].name, }) )} ${emailSchedulingBodyDivider()} @@ -159,7 +161,7 @@ ${getCancelLink(this.calEvent)}
-
+
${this.getWhat()} ${this.getWhen()} @@ -178,33 +180,6 @@ ${getCancelLink(this.calEvent)}
- ${emailSchedulingBodyDivider()} - -
- - - - - - -
- -
- - - - - - -
-
- ${this.getManageLink()} -
-
-
- -
-
${emailBodyLogo()} diff --git a/apps/web/pages/[user]/[type].tsx b/apps/web/pages/[user]/[type].tsx index 9748dfb6c1..4b68205f17 100644 --- a/apps/web/pages/[user]/[type].tsx +++ b/apps/web/pages/[user]/[type].tsx @@ -24,6 +24,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => const userParam = asStringOrNull(context.query.user); const typeParam = asStringOrNull(context.query.type); const dateParam = asStringOrNull(context.query.date); + const rescheduleUid = asStringOrNull(context.query.rescheduleUid); if (!userParam || !typeParam) { throw new Error(`File is not named [type]/[user]`); @@ -214,6 +215,43 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => eventTypeObject.schedule = null; eventTypeObject.availability = []; + type Booking = { + startTime: Date | string; + description: string | null; + attendees?: { + email: string; + name: string; + }[]; + } | null; + // @NOTE: being used several times refactor to exported function + async function getBooking(rescheduleUid: string): Promise { + return await prisma.booking.findFirst({ + where: { + uid: rescheduleUid, + }, + select: { + startTime: true, + description: true, + attendees: { + select: { + email: true, + name: true, + }, + }, + }, + }); + } + + let booking: Booking | null = null; + if (rescheduleUid) { + booking = await getBooking(rescheduleUid); + if (booking) { + // @NOTE: had to do this because Server side cant return [Object objects] + // probably fixable with json.stringify -> json.parse + booking["startTime"] = (booking?.startTime as Date)?.toISOString(); + } + } + return { props: { profile: { @@ -231,6 +269,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => workingHours, trpcState: ssr.dehydrate(), previousPage: context.req.headers.referer ?? null, + booking, }, }; }; diff --git a/apps/web/pages/[user]/book.tsx b/apps/web/pages/[user]/book.tsx index 3e8bd5b6f7..ec5c0f8f53 100644 --- a/apps/web/pages/[user]/book.tsx +++ b/apps/web/pages/[user]/book.tsx @@ -69,6 +69,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { disableGuests: true, users: { select: { + id: true, username: true, name: true, email: true, @@ -112,12 +113,23 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }; })[0]; - async function getBooking() { - return prisma.booking.findFirst({ + type Booking = { + startTime: Date | string; + description: string | null; + attendees: { + email: string; + name: string; + }[]; + } | null; + + // @NOTE: being used several times refactor to exported function + async function getBooking(): Promise { + return await prisma.booking.findFirst({ where: { uid: asStringOrThrow(context.query.rescheduleUid), }, select: { + startTime: true, description: true, attendees: { select: { @@ -129,15 +141,18 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }); } - type Booking = Prisma.PromiseReturnType; let booking: Booking | null = null; - if (context.query.rescheduleUid) { booking = await getBooking(); + if (booking) { + // @NOTE: had to do this because Server side cant return [Object objects] + // probably fixable with json.stringify -> json.parse + booking["startTime"] = (booking?.startTime as Date)?.toISOString(); + } } const t = await getTranslation(context.locale ?? "en", "common"); - + console.log({ booking }); return { props: { locationLabels: getLocationLabels(t), diff --git a/apps/web/pages/api/book/request-reschedule.ts b/apps/web/pages/api/book/request-reschedule.ts index c57147cb05..69b3417a5e 100644 --- a/apps/web/pages/api/book/request-reschedule.ts +++ b/apps/web/pages/api/book/request-reschedule.ts @@ -21,8 +21,8 @@ const rescheduleSchema = z.object({ const handler = async (req: NextApiRequest, res: NextApiResponse) => { const session = await getSession({ req }); const { bookingId, rescheduleReason: cancellationReason } = req.body; - console.log({ bookingId }); - let userOwner: Pick | null; + type PersonAttendee = Pick; + let userOwner: PersonAttendee | null; try { if (session?.user?.id) { userOwner = await prisma.user.findUnique({ @@ -82,15 +82,13 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { // @NOTE: Lets assume all guests are the same language const [firstAttendee] = bookingToReschedule.attendees; const tAttendees = await getTranslation(firstAttendee.locale ?? "en", "common"); - const usersToPeopleType = ( - users: Pick[], - selectedLanguage: TFunction - ): Person[] => { + const usersToPeopleType = (users: PersonAttendee[], selectedLanguage: TFunction): Person[] => { return users?.map((user) => { return { id: user.id || "", email: user.email || "", name: user.name || "", + username: user?.username || "", language: { translate: selectedLanguage, locale: user.locale || "en" }, timeZone: user?.timeZone, }; @@ -105,7 +103,11 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { type: event?.title || "Nameless Event", startTime: bookingToReschedule.startTime.toISOString(), endTime: bookingToReschedule.endTime.toISOString(), - attendees: usersToPeopleType(bookingToReschedule.attendees, tAttendees), + attendees: usersToPeopleType( + // username field doesn't exists on attendee but could be in the future + bookingToReschedule.attendees as unknown as PersonAttendee[], + tAttendees + ), organizer: userOwnerAsPeopleType, }); await calendarEventBuilder.buildEventObjectFromInnerClass(bookingToReschedule.eventTypeId); @@ -117,9 +119,19 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { await calendarEventBuilder.buildLuckyUsers(); } await calendarEventBuilder.buildAttendeesList(); + calendarEventBuilder.setLocation(bookingToReschedule.location); + calendarEventBuilder.setUId(bookingToReschedule.uid); + calendarEventBuilder.setCancellationReason(cancellationReason); console.log({ calendarEventBuilder }); // Send email ================= - await sendRequestRescheduleEmail(calendarEventBuilder.calendarEvent); + const queryParams = new URLSearchParams(); + queryParams.set("rescheduleUid", `${bookingToReschedule.uid}`); + const rescheduleLink = `${process.env.WEBSITE_BASE_URL}/${userOwner.username}/${ + event?.slug + }?${queryParams.toString()}`; + await sendRequestRescheduleEmail(calendarEventBuilder.calendarEvent, { + rescheduleLink, + }); } return res.status(200).json(bookingToReschedule); diff --git a/apps/web/pages/team/[slug]/[type].tsx b/apps/web/pages/team/[slug]/[type].tsx index 8fe83b17f8..1c0ba56f43 100644 --- a/apps/web/pages/team/[slug]/[type].tsx +++ b/apps/web/pages/team/[slug]/[type].tsx @@ -20,6 +20,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => const slugParam = asStringOrNull(context.query.slug); const typeParam = asStringOrNull(context.query.type); const dateParam = asStringOrNull(context.query.date); + const rescheduleUid = asStringOrNull(context.query.rescheduleUid); if (!slugParam || !typeParam) { throw new Error(`File is not named [idOrSlug]/[user]`); @@ -109,6 +110,43 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => eventTypeObject.availability = []; + type Booking = { + startTime: Date | string; + description: string | null; + attendees?: { + email: string; + name: string; + }[]; + } | null; + // @NOTE: being used several times refactor to exported function + async function getBooking(rescheduleUid: string): Promise { + return await prisma.booking.findFirst({ + where: { + uid: rescheduleUid, + }, + select: { + startTime: true, + description: true, + attendees: { + select: { + email: true, + name: true, + }, + }, + }, + }); + } + + let booking: Booking | null = null; + if (rescheduleUid) { + booking = await getBooking(rescheduleUid); + if (booking) { + // @NOTE: had to do this because Server side cant return [Object objects] + // probably fixable with json.stringify -> json.parse + booking["startTime"] = (booking?.startTime as Date)?.toISOString(); + } + } + return { props: { // Team is always pro @@ -126,6 +164,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => eventType: eventTypeObject, workingHours, previousPage: context.req.headers.referer ?? null, + booking, }, }; }; diff --git a/apps/web/pages/team/[slug]/book.tsx b/apps/web/pages/team/[slug]/book.tsx index fc63c0e413..2bb5d4431b 100644 --- a/apps/web/pages/team/[slug]/book.tsx +++ b/apps/web/pages/team/[slug]/book.tsx @@ -56,6 +56,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }, users: { select: { + id: true, avatar: true, name: true, }, @@ -74,12 +75,23 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }; })[0]; - async function getBooking() { + type Booking = { + startTime: Date | string; + description: string | null; + attendees: { + email: string; + name: string; + }[]; + } | null; + + // @NOTE: being used several times refactor to exported function + async function getBooking(): Promise { return prisma.booking.findFirst({ where: { uid: asStringOrThrow(context.query.rescheduleUid), }, select: { + startTime: true, description: true, attendees: { select: { @@ -91,11 +103,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }); } - type Booking = Prisma.PromiseReturnType; let booking: Booking | null = null; - if (context.query.rescheduleUid) { booking = await getBooking(); + if (booking) { + // @NOTE: had to do this because Server side cant return [Object objects] + // probably fixable with json.stringify -> json.parse + booking["startTime"] = (booking?.startTime as Date)?.toISOString(); + } } const t = await getTranslation(context.locale ?? "en", "common"); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 8437863925..df8f89638e 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -66,6 +66,8 @@ "event_has_been_rescheduled": "Updated - Your event has been rescheduled", "request_reschedule_title_attendee": "Request to reschedule your booking", "request_reschedule_subtitle": "{{organizer}} has cancelled the booking and requested you to pick another time.", + "request_reschedule_title_organizer": "You have requested {{attendee}} to reschedule", + "request_reschedule_subtitle_organizer": "You have cancelled the booking and {{attendee}} should be pick a new booking time with you.", "reschedule_reason": "Reason for reschedule", "hi_user_name": "Hi {{name}}", "ics_event_title": "{{eventType}} with {{name}}", diff --git a/packages/core/builders/CalendarEvent/builder.ts b/packages/core/builders/CalendarEvent/builder.ts index 0fc484bf9d..ee8868dfd2 100644 --- a/packages/core/builders/CalendarEvent/builder.ts +++ b/packages/core/builders/CalendarEvent/builder.ts @@ -248,4 +248,8 @@ export class CalendarEventBuilder implements ICalendarEventBuilder { public setDescription(description: CalendarEventClass["description"]) { this.calendarEvent.description = description; } + + public setCancellationReason(cancellationReason: CalendarEventClass["cancellationReason"]) { + this.calendarEvent.cancellationReason = cancellationReason; + } } diff --git a/packages/types/Calendar.d.ts b/packages/types/Calendar.d.ts index ebf9e6a297..692e4df1fb 100644 --- a/packages/types/Calendar.d.ts +++ b/packages/types/Calendar.d.ts @@ -12,6 +12,7 @@ export type Person = { email: string; timeZone: string; language: { translate: TFunction; locale: string }; + username?: string; }; export type EventBusyDate = Record<"start" | "end", Date | string>;