import { BanIcon, CheckIcon, ClockIcon, LocationMarkerIcon, PaperAirplaneIcon, PencilAltIcon, XIcon, } from "@heroicons/react/outline"; import { RefreshIcon } from "@heroicons/react/solid"; import { BookingStatus } from "@prisma/client"; import dayjs from "dayjs"; import { useRouter } from "next/router"; import { useState } from "react"; import { useMutation } from "react-query"; import { parseRecurringEvent } from "@calcom/lib"; import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import showToast from "@calcom/lib/notification"; import { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import Button from "@calcom/ui/Button"; import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog"; import { Tooltip } from "@calcom/ui/Tooltip"; import { TextArea } from "@calcom/ui/form/fields"; import { HttpError } from "@lib/core/http/error"; import useMeQuery from "@lib/hooks/useMeQuery"; import { linkValueToString } from "@lib/linkValueToString"; import { LocationType } from "@lib/location"; import { parseRecurringDates } from "@lib/parseDate"; import { inferQueryInput, inferQueryOutput, trpc } from "@lib/trpc"; 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; recurringCount?: number; }; 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 = useMutation( async (confirm: boolean) => { let body = { id: booking.id, confirmed: confirm, language: i18n.language, reason: rejectionReason, }; /** * Only pass down the recurring event id when we need to confirm the entire series, which happens in * the "Recurring" tab, to support confirming discretionally in the "Recurring" tab. */ if (booking.listingStatus === "recurring" && booking.recurringEventId !== null) { body = Object.assign({}, body, { recurringEventId: booking.recurringEventId }); } const res = await fetch("/api/book/confirm", { method: "PATCH", body: JSON.stringify(body), headers: { "Content-Type": "application/json", }, }); if (!res.ok) { throw new HttpError({ statusCode: res.status }); } setRejectionDialogIsOpen(false); }, { async onSettled() { await utils.invalidateQueries(["viewer.bookings"]); }, } ); const isUpcoming = 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 pendingActions: ActionType[] = [ { id: "reject", label: booking.listingStatus === "recurring" && booking.recurringEventId !== null ? t("reject_all") : t("reject"), onClick: () => { setRejectionDialogIsOpen(true); }, icon: BanIcon, disabled: mutation.isLoading, }, { id: "confirm", label: booking.listingStatus === "recurring" && booking.recurringEventId !== null ? t("confirm_all") : t("confirm"), onClick: () => { mutation.mutate(true); }, icon: CheckIcon, disabled: mutation.isLoading, color: "primary", }, ]; let bookedActions: ActionType[] = [ { id: "cancel", label: booking.listingStatus === "recurring" && booking.recurringEventId !== null ? t("cancel_all_remaining") : t("cancel"), href: `/cancel/${booking.uid}`, icon: XIcon, }, { id: "edit_booking", label: t("edit_booking"), icon: PencilAltIcon, actions: [ { id: "reschedule", icon: ClockIcon, label: t("reschedule_booking"), href: `/reschedule/${booking.uid}`, }, { id: "reschedule_request", icon: PaperAirplaneIcon, iconClassName: "rotate-45 w-[18px] -ml-[2px]", label: t("send_reschedule_request"), onClick: () => { setIsOpenRescheduleDialog(true); }, }, { id: "change_location", label: t("edit_location"), onClick: () => { setIsOpenLocationDialog(true); }, icon: LocationMarkerIcon, }, ], }, ]; if (booking.listingStatus === "recurring" && booking.recurringEventId !== null) { 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: LocationType, details: { [key: string]: string }) => { let newLocation = newLocationType as string; if ( newLocationType === LocationType.InPerson || newLocationType === LocationType.Link || newLocationType === LocationType.UserPhone ) { newLocation = details[Object.keys(details)[0]]; } setLocationMutation.mutate({ bookingId: booking.id, newLocation }); }; // Calculate the booking date(s) and setup recurring event data to show let recurringStrings: string[] = []; let recurringDates: Date[] = []; const today = new Date(); if (booking.recurringCount && booking.eventType.recurringEvent?.freq !== undefined) { [recurringStrings, recurringDates] = parseRecurringDates( { startDate: booking.startTime, recurringEvent: parseRecurringEvent(booking.eventType.recurringEvent), recurringCount: booking.recurringCount, }, i18n ); if (booking.status === BookingStatus.PENDING) { // Only take into consideration next up instances if booking is confirmed recurringDates = recurringDates.filter((aDate) => aDate >= today); recurringStrings = recurringDates.map((_, key) => recurringStrings[key]); } } let location = booking.location || ""; if (location.includes("integration")) { if (booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED) { location = t("web_conference"); } else if (isConfirmed) { location = linkValueToString(booking.location, t); } else { location = t("web_conferencing_details_to_follow"); } } const onClick = () => { 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, user: 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)