update rescheduled emails, booking view and availability page view
This commit is contained in:
parent
aed26340f1
commit
41af612355
|
@ -173,7 +173,7 @@ function BookingListItem(booking: BookingItem) {
|
|||
{
|
||||
id: "edit",
|
||||
label: t("edit_booking"),
|
||||
href: "",
|
||||
href: `/reschedule/${booking.uid}`,
|
||||
},
|
||||
{
|
||||
id: "reschedule_request",
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
<div className="mt-4 sm:-mt-2">
|
||||
<p className="text-sm font-medium text-black dark:text-white">{profile.name}</p>
|
||||
<div className="flex gap-2 text-xs font-medium text-gray-600 dark:text-gray-100">
|
||||
<p className="text-sm font-medium text-gray-600 dark:text-white">{profile.name}</p>
|
||||
<div className="flex gap-2 text-xs font-medium text-gray-900 dark:text-gray-100">
|
||||
{eventType.title}
|
||||
<p className="mb-2 text-gray-600 dark:text-white">
|
||||
<InformationCircleIcon className="mr-[10px] -mt-1 inline-block h-4 w-4" />
|
||||
{eventType.description}
|
||||
</p>
|
||||
<div>
|
||||
<ClockIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||
{eventType.length} {t("minutes")}
|
||||
|
@ -177,7 +183,6 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-3 text-gray-600 dark:text-gray-200">{eventType.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="px-4 sm:flex sm:p-4 sm:py-5">
|
||||
|
@ -204,16 +209,20 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
|
|||
truncateAfter={3}
|
||||
/>
|
||||
<h2 className="mt-3 font-medium text-gray-500 dark:text-gray-300">{profile.name}</h2>
|
||||
<h1 className="font-cal mb-4 text-3xl font-semibold text-gray-800 dark:text-white">
|
||||
<h1 className="font-cal mb-4 text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{eventType.title}
|
||||
</h1>
|
||||
<p className="mb-1 -ml-2 px-2 py-1 text-gray-500">
|
||||
<ClockIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||
<p className="mb-2 text-gray-600 dark:text-white">
|
||||
<InformationCircleIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
{eventType.description}
|
||||
</p>
|
||||
<p className="mb-1 -ml-2 px-2 py-1 text-gray-600">
|
||||
<ClockIcon className="mr-1 -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
{eventType.length} {t("minutes")}
|
||||
</p>
|
||||
{eventType.price > 0 && (
|
||||
<p className="mb-1 -ml-2 px-2 py-1 text-gray-500">
|
||||
<CreditCardIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||
<p className="mb-1 -ml-2 px-2 py-1 text-gray-600">
|
||||
<CreditCardIcon className="mr-1 -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={eventType.price / 100.0}
|
||||
|
@ -225,8 +234,6 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
|
|||
)}
|
||||
|
||||
<TimezoneDropdown />
|
||||
|
||||
<p className="mt-3 mb-8 text-gray-600 dark:text-gray-200">{eventType.description}</p>
|
||||
{previousPage === `${BASE_URL}/${profile.slug}` && (
|
||||
<div className="flex h-full flex-col justify-end">
|
||||
<ArrowLeftIcon
|
||||
|
@ -281,8 +288,8 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage
|
|||
function TimezoneDropdown() {
|
||||
return (
|
||||
<Collapsible.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
||||
<Collapsible.Trigger className="min-w-32 mb-1 -ml-2 px-2 py-1 text-left text-gray-500">
|
||||
<GlobeIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||
<Collapsible.Trigger className="min-w-32 mb-1 -ml-2 px-2 py-1 text-left text-gray-600">
|
||||
<GlobeIcon className="mr-1 -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
{timeZone()}
|
||||
{isTimeOptionsOpen ? (
|
||||
<ChevronUpIcon className="ml-1 -mt-1 inline-block h-4 w-4" />
|
||||
|
|
|
@ -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 (
|
||||
<div>
|
||||
|
@ -296,13 +312,17 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag
|
|||
<h1 className="mb-4 text-3xl font-semibold text-gray-800 dark:text-white">
|
||||
{eventType.title}
|
||||
</h1>
|
||||
<p className="mb-2 text-gray-600 dark:text-white">
|
||||
<InformationCircleIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
{eventType.description}
|
||||
</p>
|
||||
<p className="mb-2 text-gray-500">
|
||||
<ClockIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||
<ClockIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
{eventType.length} {t("minutes")}
|
||||
</p>
|
||||
{eventType.price > 0 && (
|
||||
<p className="mb-1 -ml-2 px-2 py-1 text-gray-500">
|
||||
<CreditCardIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||
<CreditCardIcon className="mr-[10px] -mt-1 inline-block h-4 w-4" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={eventType.price / 100.0}
|
||||
|
@ -313,7 +333,7 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag
|
|||
</p>
|
||||
)}
|
||||
<p className="mb-4 text-green-500">
|
||||
<CalendarIcon className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||
<CalendarIcon className="mr-[10px] -mt-1 inline-block h-4 w-4" />
|
||||
{parseDate(date)}
|
||||
</p>
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
<p className="mb-8 text-gray-600 dark:text-white">{eventType.description}</p>
|
||||
{booking?.startTime && rescheduleUid && (
|
||||
<div>
|
||||
{/* Add translation */}
|
||||
<p className="mt-8 mb-2 text-gray-600 dark:text-white">Former time</p>
|
||||
<p className="text-gray-500 line-through dark:text-white">
|
||||
<CalendarIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-400" />
|
||||
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime))}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="sm:w-1/2 sm:pl-8 sm:pr-4">
|
||||
<Form form={bookingForm} handleSubmit={bookEvent}>
|
||||
|
@ -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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -351,9 +384,13 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag
|
|||
<EmailInput
|
||||
{...bookingForm.register("email")}
|
||||
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="you@example.com"
|
||||
type="search" // Disables annoying 1password intrusive popup (non-optimal, I know I know...)
|
||||
disabled={disableInput}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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}
|
||||
/>
|
||||
<span className="text-sm ltr:ml-2 rtl:mr-2 dark:text-gray-500">
|
||||
{locationLabels[location.type]}
|
||||
|
@ -392,6 +430,7 @@ const BookingPage = ({ eventType, booking, profile, locationLabels }: BookingPag
|
|||
placeholder={t("enter_phone_number")}
|
||||
id="phone"
|
||||
required
|
||||
disabled={disableInput}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start space-x-2 rtl:space-x-reverse">
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -20,7 +20,8 @@ function PhoneInput<FormValues>({ control, name, ...rest }: PhoneInputProps<Form
|
|||
name={name}
|
||||
control={control}
|
||||
className={classNames(
|
||||
"border-1 focus-within:border-brand block w-full rounded-sm border border-gray-300 py-px px-3 shadow-sm ring-black focus-within:ring-1 dark:border-black dark:bg-black dark:text-white"
|
||||
"border-1 focus-within:border-brand block w-full rounded-sm border border-gray-300 py-px px-3 shadow-sm ring-black focus-within:ring-1 dark:border-black dark:bg-black dark:text-white",
|
||||
rest.disabled ? "bg-gray-200 dark:text-gray-500" : ""
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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<unknown>[] = [];
|
||||
|
||||
|
@ -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<unknown>[] = [];
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
|
|
@ -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<string, unknown> {
|
||||
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 `
|
||||
<p style="height: 6px"></p>
|
||||
<div style="line-height: 6px;">
|
||||
<p style="color: #494949;">${this.calEvent.organizer.language.translate("when")}</p>
|
||||
<p style="color: #494949; font-weight: 400; line-height: 24px;text-decoration: line-through;">
|
||||
${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"
|
||||
)} <span style="color: #888888">(${this.getTimezone()})</span>
|
||||
</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<!doctype html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
${emailHead(headerContent)}
|
||||
<body style="word-spacing:normal;background-color:#F5F5F5;">
|
||||
<div style="background-color:#F5F5F5;">
|
||||
${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()}
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
${this.getWho()}
|
||||
${this.getLocation()}
|
||||
${this.getAdditionalNotes()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
${emailSchedulingBodyDivider()}
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;text-align:center;color:#3E3E3E;">
|
||||
<a style="padding: 8px 16px;background-color: #292929;color: white;border-radius: 2px;display: inline-block;margin-bottom: 16px;"
|
||||
href="${this.metadata.rescheduleLink}" target="_blank"
|
||||
>
|
||||
Book a new time
|
||||
<img src="https://app.cal.com/emails/linkIcon.png" style="width:16px; margin-left: 5px;filter: brightness(0) invert(1); vertical-align: top;" />
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
${emailBodyLogo()}
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ export const emailSchedulingBodyHeader = (headerType: BodyHeadType): string => {
|
|||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;border-top:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:30px 20px 0 20px;text-align:center;">
|
||||
<td style="border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;border-top:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:30px 30px 0 30px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:558px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
|
|
|
@ -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<string, unknown> {
|
||||
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 `
|
||||
<p style="height: 6px"></p>
|
||||
|
@ -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)}
|
|||
<div style="background-color:#F5F5F5;">
|
||||
${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)}
|
|||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<td align="left" style="font-size:0px;padding:10px 40px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:1;text-align:left;color:#3E3E3E;">
|
||||
${this.getWhat()}
|
||||
${this.getWhen()}
|
||||
|
@ -178,33 +180,6 @@ ${getCancelLink(this.calEvent)}
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
${emailSchedulingBodyDivider()}
|
||||
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" style="width:600px;" width="600" bgcolor="#FFFFFF" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
|
||||
<div style="background:#FFFFFF;background-color:#FFFFFF;margin:0px auto;max-width:600px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="background:#FFFFFF;background-color:#FFFFFF;width:100%;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border-bottom:1px solid #E1E1E1;border-left:1px solid #E1E1E1;border-right:1px solid #E1E1E1;direction:ltr;font-size:0px;padding:0px;text-align:center;">
|
||||
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:598px;" ><![endif]-->
|
||||
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="left" style="font-size:0px;padding:10px 25px;word-break:break-word;">
|
||||
<div style="font-family:Roboto, Helvetica, sans-serif;font-size:16px;font-weight:500;line-height:0px;text-align:left;color:#3E3E3E;">
|
||||
${this.getManageLink()}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
${emailBodyLogo()}
|
||||
<!--[if mso | IE]></td></tr></table><![endif]-->
|
||||
</div>
|
||||
|
|
|
@ -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<Booking> {
|
||||
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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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<Booking> {
|
||||
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<typeof getBooking>;
|
||||
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),
|
||||
|
|
|
@ -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<User, "id" | "email" | "name" | "locale" | "timeZone"> | null;
|
||||
type PersonAttendee = Pick<User, "id" | "email" | "name" | "locale" | "timeZone" | "username">;
|
||||
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<User, "id" | "email" | "name" | "locale" | "timeZone">[],
|
||||
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);
|
||||
|
|
|
@ -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<Booking> {
|
||||
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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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<Booking> {
|
||||
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<typeof getBooking>;
|
||||
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");
|
||||
|
|
|
@ -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}}",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
Loading…
Reference in New Issue
Block a user