diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index b11f673cb7c4b6..109e123bf90004 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -1,3 +1,7 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/* eslint-disable prettier/prettier */ +import Image from "next/image"; import Link from "next/link"; import { useState, useEffect, useRef } from "react"; import { Controller, useFieldArray, useForm } from "react-hook-form"; @@ -40,6 +44,17 @@ import { Tooltip } from "@calcom/ui/components/tooltip"; import assignmentReasonBadgeTitleMap from "@lib/booking/assignmentReasonBadgeTitleMap"; +import { useBookingItemState } from "@components/booking/hooks/useBookingItemState"; +import { AddGuestsDialog } from "@components/dialog/AddGuestsDialog"; +import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog"; +import { EditLocationDialog } from "@components/dialog/EditLocationDialog"; +import { ReassignDialog } from "@components/dialog/ReassignDialog"; +import { ReportBookingDialog } from "@components/dialog/ReportBookingDialog"; +import { RerouteDialog } from "@components/dialog/RerouteDialog"; +import { RescheduleDialog } from "@components/dialog/RescheduleDialog"; + +import { BookingActionsDropdown } from "./BookingActionsDropdown"; +import { useBookingActionsStoreContext, BookingActionsStoreProvider } from "./BookingActionsStoreProvider"; import { WrongAssignmentDialog } from "../dialog/WrongAssignmentDialog"; import { buildBookingLink } from "../../modules/bookings/lib/buildBookingLink"; import { useBookingDetailsSheetStore } from "../../modules/bookings/store/bookingDetailsSheetStore"; @@ -146,6 +161,14 @@ function BookingListItem(booking: BookingItemProps) { t, i18n: { language }, } = useLocale(); + const utils = trpc.useUtils(); + + // Use our centralized state hook + const { dialogState, openDialog, closeDialog, rejectionReason, setRejectionReason } = useBookingItemState(); + + const cardCharged = booking?.payment[0]?.success; + const [rejectionReason, setRejectionReason] = useState(""); + const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false); // Get selected booking UID from store // The provider should always be available when BookingListItem is rendered (bookingsV3Enabled is true) @@ -162,6 +185,21 @@ function BookingListItem(booking: BookingItemProps) { } }, [isSelected]); + const mutation = trpc.viewer.bookings.confirm.useMutation({ + onSuccess: (data) => { + if (data?.status === BookingStatus.REJECTED) { + closeDialog("rejection"); + 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 attendeeList = booking.attendees.map((attendee) => ({ ...attendee, noShow: attendee.noShow || false, @@ -239,6 +277,20 @@ function BookingListItem(booking: BookingItemProps) { t, } as BookingActionContext; + const basePendingActions = getPendingActions(actionContext); + const pendingActions: ActionType[] = basePendingActions.map((action) => ({ + ...action, + onClick: + action.id === "reject" + ? () => openDialog("rejection") + : action.id === "confirm" + ? () => bookingConfirm(true) + : undefined, + disabled: action.disabled || mutation.isPending, + })) as ActionType[]; + + const cancelEventAction = getCancelEventAction(actionContext); + const RequestSentMessage = () => { return ( @@ -272,6 +324,51 @@ function BookingListItem(booking: BookingItemProps) { const showPendingPayment = paymentAppData.enabled && booking.payment.length && !booking.paid; + const baseEditEventActions = getEditEventActions(actionContext); + const editEventActions: ActionType[] = baseEditEventActions.map((action) => ({ + ...action, + onClick: + action.id === "reschedule_request" + ? () => openDialog("reschedule") + : action.id === "reroute" + ? () => openDialog("reroute") + : action.id === "change_location" + ? () => openDialog("editLocation") + : action.id === "add_members" + ? () => openDialog("addGuests") + : action.id === "reassign" + ? () => openDialog("reassign") + : undefined, + })) as ActionType[]; + + const baseAfterEventActions = getAfterEventActions(actionContext); + const afterEventActions: ActionType[] = baseAfterEventActions.map((action) => ({ + ...action, + onClick: + action.id === "view_recordings" + ? () => openDialog("viewRecordings") + : action.id === "meeting_session_details" + ? () => openDialog("meetingSessionDetails") + : action.id === "charge_card" + ? () => openDialog("chargeCard") + : action.id === "no_show" + ? () => { + if (attendeeList.length === 1) { + const attendee = attendeeList[0]; + noShowMutation.mutate({ + bookingUid: booking.uid, + attendees: [{ email: attendee.email, noShow: !attendee.noShow }], + }); + return; + } + openDialog("noShowAttendees"); + } + : undefined, + disabled: + action.disabled || + (action.id === "no_show" && !(isBookingInPast || isOngoing)) || + (action.id === "view_recordings" && !booking.isRecorded), + })) as ActionType[]; const setIsOpenReportDialog = useBookingActionsStoreContext((state) => state.setIsOpenReportDialog); const setIsCancelDialogOpen = useBookingActionsStoreContext((state) => state.setIsCancelDialogOpen); const isOpenWrongAssignmentDialog = useBookingActionsStoreContext( @@ -288,6 +385,92 @@ function BookingListItem(booking: BookingItemProps) { }; return ( + <> + (open ? openDialog("reschedule") : closeDialog("reschedule"))} + bookingUId={booking.uid} + /> + {dialogState.reassign && ( + (open ? openDialog("reassign") : closeDialog("reassign"))} + bookingId={booking.id} + teamId={booking.eventType?.team?.id || 0} + bookingFromRoutingForm={isBookingFromRoutingForm} + /> + )} + (open ? openDialog("editLocation") : closeDialog("editLocation"))} + teamId={booking.eventType?.team?.id} + /> + (open ? openDialog("addGuests") : closeDialog("addGuests"))} + bookingId={booking.id} + /> + + {booking.paid && booking.payment[0] && ( + (open ? openDialog("chargeCard") : closeDialog("chargeCard"))} + bookingId={booking.id} + paymentAmount={booking.payment[0].amount} + paymentCurrency={booking.payment[0].currency} + /> + )} + {isCalVideoLocation && ( + (open ? openDialog("viewRecordings") : closeDialog("viewRecordings"))} + timeFormat={userTimeFormat ?? null} + /> + )} + {isCalVideoLocation && dialogState.meetingSessionDetails && ( + + open ? openDialog("meetingSessionDetails") : closeDialog("meetingSessionDetails") + } + timeFormat={userTimeFormat ?? null} + /> + )} + {dialogState.noShowAttendees && ( + (open ? openDialog("noShowAttendees") : closeDialog("noShowAttendees"))} + isOpen={dialogState.noShowAttendees} + /> + )} + (open ? openDialog("rejection") : closeDialog("rejection"))}> + + +
+ + {t("rejection_reason")} + (Optional) + + } + value={rejectionReason} + onChange={(e) => setRejectionReason(e.target.value)} + />
+ + {isBookingFromRoutingForm && ( + closeDialog("reroute")} + booking={{ ...parsedBooking, eventType: parsedBooking.eventType }} + /> + )} + = now && !booking.recurringInfo?.bookings[BookingStatus.CANCELLED] - .map((date) => date.toString()) + .map((date: { toString: () => any }) => date.toString()) .includes(recurringDate.toString()) ); }).length; @@ -671,7 +863,7 @@ const RecurringBookingsTooltip = ({ const pastOrCancelled = aDate < now || booking.recurringInfo?.bookings[BookingStatus.CANCELLED] - .map((date) => date.toString()) + .map((date: { toString: () => any }) => date.toString()) .includes(aDate.toString()); return (

diff --git a/apps/web/components/booking/hooks/useBookingItemState.ts b/apps/web/components/booking/hooks/useBookingItemState.ts new file mode 100644 index 00000000000000..2299e23f69c1f7 --- /dev/null +++ b/apps/web/components/booking/hooks/useBookingItemState.ts @@ -0,0 +1,77 @@ +import { useState } from "react"; + +export type DialogState = { + reschedule: boolean; + reassign: boolean; + editLocation: boolean; + addGuests: boolean; + chargeCard: boolean; + viewRecordings: boolean; + meetingSessionDetails: boolean; + noShowAttendees: boolean; + rejection: boolean; + reroute: boolean; +}; + +export function useBookingItemState() { + const [rejectionReason, setRejectionReason] = useState(""); + const [dialogState, setDialogState] = useState({ + reschedule: false, + reassign: false, + editLocation: false, + addGuests: false, + chargeCard: false, + viewRecordings: false, + meetingSessionDetails: false, + noShowAttendees: false, + rejection: false, + reroute: false, + }); + + const openDialog = (dialog: keyof DialogState) => { + setDialogState((prevState) => ({ + ...prevState, + [dialog]: true, + })); + }; + + const closeDialog = (dialog: keyof DialogState) => { + setDialogState((prevState) => ({ + ...prevState, + [dialog]: false, + })); + }; + + const toggleDialog = (dialog: keyof DialogState) => { + setDialogState((prevState) => ({ + ...prevState, + [dialog]: !prevState[dialog], + })); + }; + + // Helper function to reset all dialogs (useful when one action should close others) + const resetDialogs = () => { + setDialogState({ + reschedule: false, + reassign: false, + editLocation: false, + addGuests: false, + chargeCard: false, + viewRecordings: false, + meetingSessionDetails: false, + noShowAttendees: false, + rejection: false, + reroute: false, + }); + }; + + return { + dialogState, + openDialog, + closeDialog, + toggleDialog, + resetDialogs, + rejectionReason, + setRejectionReason, + }; +} diff --git a/apps/web/components/dialog/ChargeCardDialog.tsx b/apps/web/components/dialog/ChargeCardDialog.tsx index 4ce3de8e4d29b8..3849595dcc5493 100644 --- a/apps/web/components/dialog/ChargeCardDialog.tsx +++ b/apps/web/components/dialog/ChargeCardDialog.tsx @@ -1,4 +1,3 @@ -import type { Dispatch, SetStateAction } from "react"; import { useState } from "react"; import { Dialog } from "@calcom/features/components/controlled-dialog"; @@ -9,15 +8,15 @@ import { DialogContent, DialogFooter, DialogHeader, DialogClose } from "@calcom/ import { Icon } from "@calcom/ui/components/icon"; import { showToast } from "@calcom/ui/components/toast"; -interface IRescheduleDialog { +interface IChargeCardDialog { isOpenDialog: boolean; - setIsOpenDialog: Dispatch>; + setIsOpenDialog: (isOpen: boolean) => void; bookingId: number; paymentAmount: number; paymentCurrency: string; } -export const ChargeCardDialog = (props: IRescheduleDialog) => { +export const ChargeCardDialog = (props: IChargeCardDialog) => { const { t } = useLocale(); const utils = trpc.useUtils(); const { isOpenDialog, setIsOpenDialog, bookingId } = props; diff --git a/packages/platform/atoms/src/components/ui/dialog.tsx b/packages/platform/atoms/src/components/ui/dialog.tsx index 245742a5ffe05a..f4439b550935a7 100644 --- a/packages/platform/atoms/src/components/ui/dialog.tsx +++ b/packages/platform/atoms/src/components/ui/dialog.tsx @@ -51,7 +51,7 @@ DialogOverlay.displayName = DialogPrimitives.Overlay.displayName; const DialogContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef + React.ComponentPropsWithoutRef & { enableOverflow?: boolean } >(({ className, children, ...props }, ref) => ( <>