diff --git a/apps/api b/apps/api index a1dcfa59..378cbf8f 160000 --- a/apps/api +++ b/apps/api @@ -1 +1 @@ -Subproject commit a1dcfa59bc43d3f71af62ae438f96a667e807913 +Subproject commit 378cbf8f3a67ea7877296f1da02edb2b6e3efbce diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index 8036bb4b..62c370e1 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -1,18 +1,21 @@ import { BanIcon, CheckIcon, ClockIcon, XIcon } from "@heroicons/react/outline"; +import { PaperAirplaneIcon } from "@heroicons/react/outline"; import { BookingStatus } from "@prisma/client"; import dayjs from "dayjs"; import { useState } from "react"; import { useMutation } from "react-query"; +import classNames from "@calcom/lib/classNames"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; import Button from "@calcom/ui/Button"; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog"; import { TextArea } from "@calcom/ui/form/fields"; import { HttpError } from "@lib/core/http/error"; -import { useLocale } from "@lib/hooks/useLocale"; import { inferQueryOutput, trpc } from "@lib/trpc"; import { useMeQuery } from "@components/Shell"; +import { RescheduleDialog } from "@components/dialog/RescheduleDialog"; import TableActions, { ActionType } from "@components/ui/TableActions"; type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number]; @@ -80,15 +83,42 @@ function BookingListItem(booking: BookingItem) { { id: "reschedule", label: t("reschedule"), - href: `/reschedule/${booking.uid}`, icon: ClockIcon, + actions: [ + { + id: "edit", + label: t("reschedule_booking"), + href: `/reschedule/${booking.uid}`, + }, + { + id: "reschedule_request", + label: t("send_reschedule_request"), + onClick: () => setIsOpenRescheduleDialog(true), + }, + ], }, ]; - const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY"); + const RequestSentMessage = () => { + return ( +
+ +

{t("reschedule_request_sent")}

+
+ ); + }; + const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY"); + const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false); return ( <> + + + {/* NOTE: Should refactor this dialog component as is being rendered multiple times */} @@ -146,7 +176,10 @@ function BookingListItem(booking: BookingItem) {
+ className={classNames( + "max-w-56 truncate text-sm font-medium leading-6 text-neutral-900 md:max-w-max", + isCancelled ? "line-through" : "" + )}> {booking.eventType?.team && {booking.eventType.team.name}: } {booking.title} {!!booking?.eventType?.price && !booking.paid && ( @@ -161,11 +194,17 @@ function BookingListItem(booking: BookingItem) { "{booking.description}"
)} + {booking.attendees.length !== 0 && (
{booking.attendees[0].email}
)} + {isCancelled && booking.rescheduled && ( +
+ +
+ )} @@ -180,6 +219,11 @@ function BookingListItem(booking: BookingItem) { )} ) : null} + {isCancelled && booking.rescheduled && ( +
+ +
+ )} diff --git a/apps/web/components/booking/pages/AvailabilityPage.tsx b/apps/web/components/booking/pages/AvailabilityPage.tsx index cd2c2813..c561bad5 100644 --- a/apps/web/components/booking/pages/AvailabilityPage.tsx +++ b/apps/web/components/booking/pages/AvailabilityPage.tsx @@ -1,11 +1,13 @@ // Get router variables import { ArrowLeftIcon, + CalendarIcon, ChevronDownIcon, ChevronUpIcon, ClockIcon, CreditCardIcon, GlobeIcon, + InformationCircleIcon, } from "@heroicons/react/solid"; import * as Collapsible from "@radix-ui/react-collapsible"; import { useContracts } from "contexts/contractsContext"; @@ -18,14 +20,15 @@ import { FormattedNumber, IntlProvider } from "react-intl"; import { useEmbedStyles, useIsEmbed, useIsBackgroundTransparent, sdkActionManager } from "@calcom/embed-core"; import classNames from "@calcom/lib/classNames"; +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 { parseDate } from "@lib/parseDate"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; import { detectBrowserTimeFormat } from "@lib/timeFormat"; @@ -45,12 +48,12 @@ 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 isEmbed = useIsEmbed(); const { rescheduleUid } = router.query; const { isReady, Theme } = useTheme(profile.theme); - const { t } = useLocale(); + const { t, i18n } = useLocale(); const { contracts } = useContracts(); const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker"); let isBackgroundTransparent = useIsBackgroundTransparent(); @@ -179,15 +182,21 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage />

{profile.name}

-
+
{eventType.title} + {eventType?.description && ( +

+ + {eventType.description} +

+ )}
- + {eventType.length} {t("minutes")}
{eventType.price > 0 && ( -
- +
+
-

{eventType.description}

@@ -226,17 +234,23 @@ const AvailabilityPage = ({ profile, plan, eventType, workingHours, previousPage size={10} truncateAfter={3} /> -

{profile.name}

-

+

{profile.name}

+

{eventType.title}

-

- + {eventType?.description && ( +

+ + {eventType.description} +

+ )} +

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

{eventType.price > 0 && ( -

- +

+ - -

{eventType.description}

{previousPage === `${BASE_URL}/${profile.slug}` && (
Go Back

)} + {booking?.startTime && rescheduleUid && ( +
+

+ {t("former_time")} +

+

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

+
+ )}
- - + + {timeZone()} {isTimeOptionsOpen ? ( diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index dd36b04f..fe317dd0 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -1,4 +1,10 @@ -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 { useContracts } from "contexts/contractsContext"; import dayjs from "dayjs"; @@ -12,7 +18,7 @@ import { FormattedNumber, IntlProvider } from "react-intl"; import { ReactMultiEmail } from "react-multi-email"; import { useMutation } from "react-query"; -import { useIsEmbed, useEmbedStyles, useIsBackgroundTransparent } from "@calcom/embed-core"; +import { useIsEmbed, useIsBackgroundTransparent } from "@calcom/embed-core"; import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { HttpError } from "@calcom/lib/http-error"; @@ -26,10 +32,9 @@ import { ensureArray } from "@lib/ensureArray"; import useTheme from "@lib/hooks/useTheme"; import { LocationType } from "@lib/location"; import createBooking from "@lib/mutations/bookings/create-booking"; -import { parseZone } from "@lib/parseZone"; +import { parseDate } from "@lib/parseDate"; import slugify from "@lib/slugify"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; -import { detectBrowserTimeFormat } from "@lib/timeFormat"; import CustomBranding from "@components/CustomBranding"; import AvatarGroup from "@components/ui/AvatarGroup"; @@ -147,6 +152,10 @@ const BookingPage = ({ const locationInfo = (type: LocationType) => locations.find((location) => location.type === type); const loggedInIsOwner = eventType?.users[0]?.name === session?.user?.name; + const guestListEmails = !isDynamicGroupBooking + ? booking?.attendees.slice(1).map((attendee) => attendee.email) + : []; + const defaultValues = () => { if (!rescheduleUid) { return { @@ -173,7 +182,8 @@ const BookingPage = ({ return { name: primaryAttendee.name || "", email: primaryAttendee.email || "", - guests: !isDynamicGroupBooking ? booking.attendees.slice(1).map((attendee) => attendee.email) : [], + guests: guestListEmails, + notes: booking.description || "", }; }; @@ -212,14 +222,6 @@ const BookingPage = ({ } }; - const parseDate = (date: string | null) => { - if (!date) return "No date"; - const parsedZone = parseZone(date); - if (!parsedZone?.isValid()) return "Invalid date"; - const formattedTime = parsedZone?.format(detectBrowserTimeFormat); - return formattedTime + ", " + dayjs(date).toDate().toLocaleString(i18n.language, { dateStyle: "full" }); - }; - const bookEvent = (booking: BookingFormValues) => { telemetry.withJitsu((jitsu) => jitsu.track( @@ -273,6 +275,8 @@ const BookingPage = ({ }); }; + const disableInput = !!rescheduleUid; + return (
@@ -322,16 +326,22 @@ const BookingPage = ({

{profile.name}

-

+

{eventType.title}

-

- + {eventType?.description && ( +

+ + {eventType.description} +

+ )} +

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

{eventType.price > 0 && ( -

- +

+ )}

- - {parseDate(date)} + + {parseDate(date, i18n)}

{eventTypeDetail.isWeb3Active && eventType.metadata.smartContractAddress && (

{t("requires_ownership_of_a_token") + " " + eventType.metadata.smartContractAddress}

)} -

{eventType.description}

+ {booking?.startTime && rescheduleUid && ( +
+

+ {t("former_time")} +

+

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

+
+ )}
@@ -365,8 +385,12 @@ const BookingPage = ({ 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} />
@@ -380,9 +404,13 @@ const BookingPage = ({
@@ -399,6 +427,7 @@ const BookingPage = ({ {...bookingForm.register("locationType", { required: true })} value={location.type} defaultChecked={selectedLocation === location.type} + disabled={disableInput} /> {locationLabels[location.type]} @@ -421,6 +450,7 @@ const BookingPage = ({ placeholder={t("enter_phone_number")} id="phone" required + disabled={disableInput} /> @@ -443,8 +473,12 @@ const BookingPage = ({ })} 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 && ( @@ -456,6 +490,7 @@ const BookingPage = ({ 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={disableInput} /> )} {input.type === EventTypeCustomInputType.NUMBER && ( @@ -507,32 +542,49 @@ const BookingPage = ({ className="mb-1 block text-sm font-medium text-gray-700 dark:text-white"> {t("guests")} - ( - void - ) => { - return ( -
- {email} - removeEmail(index)}> - × - -
- ); - }} - /> - )} - /> + {!disableInput && ( + ( + void + ) => { + return ( +
+ {email} + {!disableInput && ( + removeEmail(index)}> + × + + )} +
+ ); + }} + /> + )} + /> + )} + {/* Custom code when guest emails should not be editable */} + {disableInput && guestListEmails && guestListEmails.length > 0 && ( +
+ {/* // @TODO: user owners are appearing as guest here when should be only user input */} + {guestListEmails.map((email, index) => { + return ( +
+ {email} +
+ ); + })} +
+ )} )} @@ -546,9 +598,14 @@ const BookingPage = ({