import { BookingStatus } from "@prisma/client"; import { useRouter } from "next/router"; import { useState, useMemo } from "react"; import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations"; import dayjs from "@calcom/dayjs"; import classNames from "@calcom/lib/classNames"; import { formatTime } from "@calcom/lib/date-fns"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import { inferQueryInput, inferQueryOutput, trpc } from "@calcom/trpc/react"; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog"; import { Icon } from "@calcom/ui/Icon"; import { TextArea } from "@calcom/ui/form/fields"; import Badge from "@calcom/ui/v2/core/Badge"; import Button from "@calcom/ui/v2/core/Button"; import MeetingTimeInTimezones from "@calcom/ui/v2/core/MeetingTimeInTimezones"; import Tooltip from "@calcom/ui/v2/core/Tooltip"; import showToast from "@calcom/ui/v2/core/notifications"; import useMeQuery from "@lib/hooks/useMeQuery"; import { extractRecurringDates } from "@lib/parseDate"; import { EditLocationDialog } from "@components/dialog/EditLocationDialog"; import { RescheduleDialog } from "@components/dialog/RescheduleDialog"; import TableActions, { ActionType } from "@components/ui/TableActions"; type BookingListingStatus = inferQueryInput<"viewer.bookings">["status"]; type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number]; type BookingItemProps = BookingItem & { listingStatus: BookingListingStatus; recurringBookings: inferQueryOutput<"viewer.bookings">["recurringInfo"]; }; function BookingListItem(booking: BookingItemProps) { // Get user so we can determine 12/24 hour format preferences const query = useMeQuery(); const user = query.data; const { t, i18n } = useLocale(); const utils = trpc.useContext(); const router = useRouter(); const [rejectionReason, setRejectionReason] = useState(""); const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false); const mutation = trpc.useMutation(["viewer.bookings.confirm"], { onSuccess: () => { setRejectionDialogIsOpen(false); showToast(t("booking_confirmation_success"), "success"); utils.invalidateQueries("viewer.bookings"); }, onError: () => { showToast(t("booking_confirmation_failed"), "error"); utils.invalidateQueries("viewer.bookings"); }, }); const isUpcoming = new Date(booking.endTime) >= new Date(); const isPast = new Date(booking.endTime) < new Date(); const isCancelled = booking.status === BookingStatus.CANCELLED; const isConfirmed = booking.status === BookingStatus.ACCEPTED; const isRejected = booking.status === BookingStatus.REJECTED; const isPending = booking.status === BookingStatus.PENDING; const isRecurring = booking.recurringEventId !== null; const isTabRecurring = booking.listingStatus === "recurring"; const isTabUnconfirmed = booking.listingStatus === "unconfirmed"; const bookingConfirm = async (confirm: boolean) => { let body = { bookingId: booking.id, confirmed: confirm, reason: rejectionReason, }; /** * Only pass down the recurring event id when we need to confirm the entire series, which happens in * the "Recurring" tab and "Unconfirmed" tab, to support confirming discretionally in the "Recurring" tab. */ if ((isTabRecurring || isTabUnconfirmed) && isRecurring) { body = Object.assign({}, body, { recurringEventId: booking.recurringEventId }); } mutation.mutate(body); }; const pendingActions: ActionType[] = [ { id: "reject", label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("reject_all") : t("reject"), onClick: () => { setRejectionDialogIsOpen(true); }, icon: Icon.FiSlash, disabled: mutation.isLoading, }, { id: "confirm", label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"), onClick: () => { bookingConfirm(true); }, icon: Icon.FiCheck, disabled: mutation.isLoading, color: "primary", }, ]; let bookedActions: ActionType[] = [ { id: "cancel", label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel"), /* When cancelling we need to let the UI and the API know if the intention is to cancel all remaining bookings or just that booking instance. */ href: `/cancel/${booking.uid}${isTabRecurring && isRecurring ? "?allRemainingBookings=true" : ""}`, icon: Icon.FiX, }, { id: "edit_booking", label: t("edit"), actions: [ { id: "reschedule", icon: Icon.FiClock, label: t("reschedule_booking"), href: `/reschedule/${booking.uid}`, }, { id: "reschedule_request", icon: Icon.FiSend, iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ", label: t("send_reschedule_request"), onClick: () => { setIsOpenRescheduleDialog(true); }, }, { id: "change_location", label: t("edit_location"), onClick: () => { setIsOpenLocationDialog(true); }, icon: Icon.FiMapPin, }, ], }, ]; if (isTabRecurring && isRecurring) { bookedActions = bookedActions.filter((action) => action.id !== "edit_booking"); } 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); const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false); const setLocationMutation = trpc.useMutation("viewer.bookings.editLocation", { onSuccess: () => { showToast(t("location_updated"), "success"); setIsOpenLocationDialog(false); utils.invalidateQueries("viewer.bookings"); }, }); const saveLocation = (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => { let newLocation = newLocationType as string; const eventLocationType = getEventLocationType(newLocationType); if (eventLocationType?.organizerInputType) { newLocation = details[Object.keys(details)[0]]; } setLocationMutation.mutate({ bookingId: booking.id, newLocation }); }; // Extract recurring dates is intensive to run, so use useMemo. // Calculate the booking date(s) and setup recurring event data to show // @FIXME: This is importing the RRULE library which is already heavy. Find out a more optimal way do this. const [recurringStrings, recurringDates] = useMemo(() => { if (booking.recurringBookings !== undefined && booking.eventType.recurringEvent?.freq !== undefined) { return extractRecurringDates(booking, user?.timeZone, i18n); } return [[], []]; // eslint-disable-next-line react-hooks/exhaustive-deps }, [user?.timeZone, i18n.language, booking.recurringBookings]); const location = booking.location || ""; const onClickTableData = () => { router.push({ pathname: "/success", query: { date: booking.startTime, // TODO: Booking when fetched should have id 0 already(for Dynamic Events). type: booking.eventType.id || 0, eventSlug: booking.eventType.slug, username: user?.username || "", name: booking.attendees[0] ? booking.attendees[0].name : undefined, email: booking.attendees[0] ? booking.attendees[0].email : undefined, location: location, eventName: booking.eventType.eventName || "", bookingId: booking.id, recur: booking.recurringEventId, reschedule: isConfirmed, listingStatus: booking.listingStatus, status: booking.status, }, }); }; return ( <> {/* NOTE: Should refactor this dialog component as is being rendered multiple times */}

{t("rejection_reason_description")}

{t("rejection_reason")} (Optional)