diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index f673aa9f64..72ae646473 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -52,6 +52,7 @@ 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"; @@ -60,9 +61,11 @@ import { getCancelEventAction, getEditEventActions, getAfterEventActions, + getReportAction, shouldShowPendingActions, shouldShowEditActions, shouldShowRecurringCancelAction, + shouldShowIndividualReportButton, type BookingActionContext, } from "./bookingActions"; @@ -181,6 +184,13 @@ function BookingListItem(booking: BookingItemProps) { const isPending = booking.status === BookingStatus.PENDING; const isRescheduled = booking.fromReschedule !== null; const isRecurring = booking.recurringEventId !== null; + + const getBookingStatus = (): "upcoming" | "past" | "cancelled" | "rejected" => { + if (isCancelled) return "cancelled"; + if (isRejected) return "rejected"; + if (isBookingInPast) return "past"; + return "upcoming"; + }; const isTabRecurring = booking.listingStatus === "recurring"; const isTabUnconfirmed = booking.listingStatus === "unconfirmed"; const isBookingFromRoutingForm = isBookingReroutable(parsedBooking); @@ -292,6 +302,7 @@ function BookingListItem(booking: BookingItemProps) { const [isOpenReassignDialog, setIsOpenReassignDialog] = useState(false); const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false); const [isOpenAddGuestsDialog, setIsOpenAddGuestsDialog] = useState(false); + const [isOpenReportDialog, setIsOpenReportDialog] = useState(false); const [rerouteDialogIsOpen, setRerouteDialogIsOpen] = useState(false); const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({ onSuccess: () => { @@ -402,6 +413,12 @@ function BookingListItem(booking: BookingItemProps) { (action.id === "view_recordings" && !booking.isRecorded), })) as ActionType[]; + const reportAction = getReportAction(actionContext); + const reportActionWithHandler = { + ...reportAction, + onClick: () => setIsOpenReportDialog(true), + }; + return ( <> + {booking.paid && booking.payment[0] && ( ))} + <> + + + + {reportActionWithHandler.label} + + + )} {shouldShowRecurringCancelAction(actionContext) && } + {shouldShowIndividualReportButton(actionContext) && ( +
+
+ )} {isRejected &&
{t("rejected")}
} {isCancelled && booking.rescheduled && (
@@ -776,6 +833,24 @@ const BookingItemBadges = ({ {booking?.assignmentReason.length > 0 && ( )} + {booking.report && ( + + {(() => { + const reasonKey = `report_reason_${booking.report.reason.toLowerCase()}`; + const reasonText = t(reasonKey); + return booking.report.description + ? `${reasonText}: ${booking.report.description}` + : reasonText; + })()} +
+ }> + + {t("reported")} + + + )} {booking.paid && !booking.payment[0] ? ( {t("error_collecting_card")} diff --git a/apps/web/components/booking/bookingActions.ts b/apps/web/components/booking/bookingActions.ts index 7d50942ebf..96457a65da 100644 --- a/apps/web/components/booking/bookingActions.ts +++ b/apps/web/components/booking/bookingActions.ts @@ -165,6 +165,18 @@ export function getEditEventActions(context: BookingActionContext): ActionType[] return actions.filter(Boolean) as ActionType[]; } +export function getReportAction(context: BookingActionContext): ActionType { + const { booking, t } = context; + + return { + id: "report", + label: t("report_booking"), + icon: "flag", + color: "destructive", + disabled: !!booking.report, + }; +} + export function getAfterEventActions(context: BookingActionContext): ActionType[] { const { booking, cardCharged, attendeeList, t } = context; @@ -205,9 +217,14 @@ export function shouldShowRecurringCancelAction(context: BookingActionContext): return isTabRecurring && isRecurring; } +export function shouldShowIndividualReportButton(context: BookingActionContext): boolean { + const { booking, isPending, isUpcoming, isCancelled, isRejected } = context; + const hasDropdown = shouldShowEditActions(context); + return !booking.report && !hasDropdown && (isCancelled || isRejected || (isPending && isUpcoming)); +} + export function isActionDisabled(actionId: string, context: BookingActionContext): boolean { - const { booking, isBookingInPast, isDisabledRescheduling, isDisabledCancelling, isPending, isConfirmed } = - context; + const { booking, isBookingInPast, isDisabledRescheduling, isDisabledCancelling } = context; switch (actionId) { case "reschedule": @@ -227,7 +244,7 @@ export function isActionDisabled(actionId: string, context: BookingActionContext } export function getActionLabel(actionId: string, context: BookingActionContext): string { - const { booking, isTabRecurring, isRecurring, attendeeList, cardCharged, t } = context; + const { isTabRecurring, isRecurring, attendeeList, cardCharged, t } = context; switch (actionId) { case "reject": diff --git a/apps/web/components/dialog/ReportBookingDialog.tsx b/apps/web/components/dialog/ReportBookingDialog.tsx new file mode 100644 index 0000000000..b926e546ea --- /dev/null +++ b/apps/web/components/dialog/ReportBookingDialog.tsx @@ -0,0 +1,139 @@ +import type { Dispatch, SetStateAction } from "react"; +import { Controller, useForm } from "react-hook-form"; + +import { Dialog } from "@calcom/features/components/controlled-dialog"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { BookingReportReason } from "@calcom/prisma/enums"; +import { trpc } from "@calcom/trpc/react"; +import { Alert } from "@calcom/ui/components/alert"; +import { Button } from "@calcom/ui/components/button"; +import { DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/components/dialog"; +import { Select, Label } from "@calcom/ui/components/form"; +import { TextArea } from "@calcom/ui/components/form"; +import { showToast } from "@calcom/ui/components/toast"; + +type BookingReportStatus = "upcoming" | "past" | "cancelled" | "rejected"; + +interface IReportBookingDialog { + isOpenDialog: boolean; + setIsOpenDialog: Dispatch>; + bookingUid: string; + isRecurring: boolean; + status: BookingReportStatus; +} + +interface FormValues { + reason: BookingReportReason; + description: string; +} + +export const ReportBookingDialog = (props: IReportBookingDialog) => { + const { t } = useLocale(); + const utils = trpc.useUtils(); + const { isOpenDialog, setIsOpenDialog, bookingUid, status } = props; + + const willBeCancelled = status === "upcoming"; + + const { + control, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: { + reason: BookingReportReason.SPAM, + description: "", + }, + }); + + const { mutate: reportBooking, isPending } = trpc.viewer.bookings.reportBooking.useMutation({ + async onSuccess(data) { + showToast(data.message, "success"); + setIsOpenDialog(false); + await utils.viewer.bookings.invalidate(); + }, + onError(error) { + showToast(error.message || t("unexpected_error_try_again"), "error"); + }, + }); + + const onSubmit = (data: FormValues) => { + reportBooking({ + bookingUid, + reason: data.reason, + description: data.description || undefined, + }); + }; + + const reasonOptions = [ + { label: t("report_reason_spam"), value: BookingReportReason.SPAM }, + { label: t("report_reason_dont_know_person"), value: BookingReportReason.DONT_KNOW_PERSON }, + { label: t("report_reason_other"), value: BookingReportReason.OTHER }, + ]; + + return ( + + +
+
+
+ +
+ + ( +