import { useRouter } from "next/router"; import { useState } from "react"; import type { EventLocationType } from "@calcom/app-store/locations"; import { getEventLocationType } from "@calcom/app-store/locations"; import dayjs from "@calcom/dayjs"; // TODO: Use browser locale, implement Intl in Dayjs maybe? import "@calcom/dayjs/locales"; import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog"; import classNames from "@calcom/lib/classNames"; import { formatTime } from "@calcom/lib/date-fns"; import getPaymentAppData from "@calcom/lib/getPaymentAppData"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { getEveryFreqFor } from "@calcom/lib/recurringStrings"; import { BookingStatus } from "@calcom/prisma/enums"; import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; import type { ActionType } from "@calcom/ui"; import { Badge, Button, Dialog, DialogClose, DialogContent, DialogFooter, MeetingTimeInTimezones, showToast, Tooltip, TableActions, TextAreaField, } from "@calcom/ui"; import { Check, Clock, MapPin, RefreshCcw, Send, Ban, X, CreditCard } from "@calcom/ui/components/icon"; import useMeQuery from "@lib/hooks/useMeQuery"; import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog"; import { EditLocationDialog } from "@components/dialog/EditLocationDialog"; import { RescheduleDialog } from "@components/dialog/RescheduleDialog"; type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"]; type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number]; type BookingItemProps = BookingItem & { listingStatus: BookingListingStatus; recurringInfo: RouterOutputs["viewer"]["bookings"]["get"]["recurringInfo"][number] | undefined; }; 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: { language }, } = useLocale(); const utils = trpc.useContext(); const router = useRouter(); const [rejectionReason, setRejectionReason] = useState(""); const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false); const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false); const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState(false); const cardCharged = booking?.payment[0]?.success; const mutation = trpc.viewer.bookings.confirm.useMutation({ onSuccess: (data) => { if (data?.status === BookingStatus.REJECTED) { setRejectionDialogIsOpen(false); showToast(t("booking_rejection_success"), "success"); } else { showToast(t("booking_confirmation_success"), "success"); } utils.viewer.bookings.invalidate(); }, onError: () => { showToast(t("booking_confirmation_failed"), "error"); utils.viewer.bookings.invalidate(); }, }); 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 paymentAppData = getPaymentAppData(booking.eventType); 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 getSeatReferenceUid = () => { if (!booking.seatsReferences[0]) { return undefined; } return booking.seatsReferences[0].referenceUid; }; const pendingActions: ActionType[] = [ { id: "reject", label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("reject_all") : t("reject"), onClick: () => { setRejectionDialogIsOpen(true); }, icon: Ban, disabled: mutation.isLoading, }, // For bookings with payment, only confirm if the booking is paid for ...((isPending && !booking?.eventType?.price) || (!!booking?.eventType?.price && booking.paid) ? [ { id: "confirm", label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"), onClick: () => { bookingConfirm(true); }, icon: Check, disabled: mutation.isLoading, }, ] : []), ]; const showRecordingActions: ActionType[] = [ { id: "view_recordings", label: t("view_recordings"), onClick: () => { setViewRecordingsDialogIsOpen(true); }, disabled: mutation.isLoading, }, ]; 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: `/booking/${booking.uid}?cancel=true${ isTabRecurring && isRecurring ? "&allRemainingBookings=true" : "" }${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""} `, icon: X, }, { id: "edit_booking", label: t("edit"), actions: [ { id: "reschedule", icon: Clock, label: t("reschedule_booking"), href: `/reschedule/${booking.uid}${ booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : "" }`, }, { id: "reschedule_request", icon: Send, 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: MapPin, }, ], }, ]; const chargeCardActions: ActionType[] = [ { id: "charge_card", label: cardCharged ? t("no_show_fee_charged") : t("collect_no_show_fee"), disabled: cardCharged, onClick: () => { setChargeCardDialogIsOpen(true); }, icon: CreditCard, }, ]; if (isTabRecurring && isRecurring) { bookedActions = bookedActions.filter((action) => action.id !== "edit_booking"); } if (isPast && isPending && !isConfirmed) { bookedActions = bookedActions.filter((action) => action.id !== "cancel"); } const RequestSentMessage = () => { return ( {t("reschedule_request_sent")} ); }; const startTime = dayjs(booking.startTime) .locale(language) .format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY"); const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false); const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false); const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({ onSuccess: () => { showToast(t("location_updated"), "success"); setIsOpenLocationDialog(false); utils.viewer.bookings.invalidate(); }, }); 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 }); }; // Getting accepted recurring dates to show const recurringDates = booking.recurringInfo?.bookings[BookingStatus.ACCEPTED] .concat(booking.recurringInfo?.bookings[BookingStatus.CANCELLED]) .concat(booking.recurringInfo?.bookings[BookingStatus.PENDING]) .sort((date1: Date, date2: Date) => date1.getTime() - date2.getTime()); const onClickTableData = () => { router.push({ pathname: `/booking/${booking.uid}`, query: { allRemainingBookings: isTabRecurring, email: booking.attendees[0] ? booking.attendees[0].email : undefined, }, }); }; const title = booking.title; // To be used after we run query on legacy bookings // const showRecordingsButtons = booking.isRecorded && isPast && isConfirmed; const showRecordingsButtons = (booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed; return ( <> {booking.paid && booking.payment[0] && ( )} {showRecordingsButtons && ( )} {/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
{t("rejection_reason")} (Optional) } value={rejectionReason} onChange={(e) => setRejectionReason(e.target.value)} />
{startTime}
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "} {formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
{isPending && ( {t("unconfirmed")} )} {booking.eventType?.team && ( {booking.eventType.team.name} )} {booking.paid && !booking.payment[0] ? ( {t("error_collecting_card")} ) : booking.paid ? ( {booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")} ) : null} {recurringDates !== undefined && (
)}
{/* Time and Badges for mobile */}
{startTime}
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "} {formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
{isPending && ( {t("unconfirmed")} )} {booking.eventType?.team && ( {booking.eventType.team.name} )} {!!booking?.eventType?.price && !booking.paid && ( {t("pending_payment")} )} {recurringDates !== undefined && (
)}
{title} {paymentAppData.enabled && !booking.paid && booking.payment.length && ( {t("pending_payment")} )}
{booking.description && (
"{booking.description}"
)} {booking.attendees.length !== 0 && ( )} {isCancelled && booking.rescheduled && (
)}
{isUpcoming && !isCancelled ? ( <> {isPending && user?.id === booking.user?.id && } {isConfirmed && } {isRejected &&
{t("rejected")}
} ) : null} {isPast && isPending && !isConfirmed ? : null} {showRecordingsButtons && } {isCancelled && booking.rescheduled && (
)} {booking.status === "ACCEPTED" && booking.paid && booking.payment[0]?.paymentOption === "HOLD" && (
)} ); } interface RecurringBookingsTooltipProps { booking: BookingItemProps; recurringDates: Date[]; } const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookingsTooltipProps) => { // Get user so we can determine 12/24 hour format preferences const query = useMeQuery(); const user = query.data; const { t, i18n: { language }, } = useLocale(); const now = new Date(); const recurringCount = recurringDates.filter((recurringDate) => { return ( recurringDate >= now && !booking.recurringInfo?.bookings[BookingStatus.CANCELLED] .map((date) => date.toDateString()) .includes(recurringDate.toDateString()) ); }).length; return ( (booking.recurringInfo && booking.eventType?.recurringEvent?.freq && (booking.listingStatus === "recurring" || booking.listingStatus === "unconfirmed" || booking.listingStatus === "cancelled") && (
{ const pastOrCancelled = aDate < now || booking.recurringInfo?.bookings[BookingStatus.CANCELLED] .map((date) => date.toDateString()) .includes(aDate.toDateString()); return (

{formatTime(aDate, user?.timeFormat, user?.timeZone)} {" - "} {dayjs(aDate).locale(language).format("D MMMM YYYY")}

); })}>

{booking.status === BookingStatus.ACCEPTED ? `${t("event_remaining", { count: recurringCount, })}` : getEveryFreqFor({ t, recurringEvent: booking.eventType.recurringEvent, recurringCount: booking.recurringInfo.count, })}

)) || null ); }; interface UserProps { id: number; name: string | null; email: string; } const FirstAttendee = ({ user, currentEmail, }: { user: UserProps; currentEmail: string | null | undefined; }) => { const { t } = useLocale(); return user.email === currentEmail ? (
{t("you")}
) : ( e.stopPropagation()}> {user.name} ); }; type AttendeeProps = { name?: string; email: string; }; const Attendee = ({ email, name }: AttendeeProps) => { return ( e.stopPropagation()}> {name || email} ); }; const DisplayAttendees = ({ attendees, user, currentEmail, }: { attendees: AttendeeProps[]; user: UserProps | null; currentEmail?: string | null; }) => { const { t } = useLocale(); return (
{user && } {attendees.length > 1 ? :  {t("and")} } {attendees.length > 1 && ( <>
 {t("and")} 
{attendees.length > 2 ? ( (

))}>
{t("plus_more", { count: attendees.length - 1 })}
) : ( )} )}
); }; export default BookingListItem;