diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx
index c617ae115f..b11f673cb7 100644
--- a/apps/web/components/booking/BookingListItem.tsx
+++ b/apps/web/components/booking/BookingListItem.tsx
@@ -40,6 +40,7 @@ import { Tooltip } from "@calcom/ui/components/tooltip";
import assignmentReasonBadgeTitleMap from "@lib/booking/assignmentReasonBadgeTitleMap";
+import { WrongAssignmentDialog } from "../dialog/WrongAssignmentDialog";
import { buildBookingLink } from "../../modules/bookings/lib/buildBookingLink";
import { useBookingDetailsSheetStore } from "../../modules/bookings/store/bookingDetailsSheetStore";
import type { BookingAttendee } from "../../modules/bookings/types";
@@ -273,6 +274,12 @@ function BookingListItem(booking: BookingItemProps) {
const setIsOpenReportDialog = useBookingActionsStoreContext((state) => state.setIsOpenReportDialog);
const setIsCancelDialogOpen = useBookingActionsStoreContext((state) => state.setIsCancelDialogOpen);
+ const isOpenWrongAssignmentDialog = useBookingActionsStoreContext(
+ (state) => state.isOpenWrongAssignmentDialog
+ );
+ const setIsOpenWrongAssignmentDialog = useBookingActionsStoreContext(
+ (state) => state.setIsOpenWrongAssignmentDialog
+ );
const reportAction = getReportAction(actionContext);
const reportActionWithHandler = {
@@ -512,7 +519,22 @@ function BookingListItem(booking: BookingItemProps) {
userTimeFormat={userTimeFormat}
userTimeZone={userTimeZone}
isRescheduled={isRescheduled}
+ onAssignmentReasonClick={
+ isBookingFromRoutingForm ? () => setIsOpenWrongAssignmentDialog(true) : undefined
+ }
/>
+ {isBookingFromRoutingForm && (
+
+ )}
);
}
@@ -525,6 +547,7 @@ const BookingItemBadges = ({
userTimeFormat,
userTimeZone,
isRescheduled,
+ onAssignmentReasonClick,
}: {
booking: BookingItemProps;
isPending: boolean;
@@ -533,6 +556,7 @@ const BookingItemBadges = ({
userTimeFormat: number | null | undefined;
userTimeZone: string | undefined;
isRescheduled: boolean;
+ onAssignmentReasonClick?: () => void;
}) => {
const { t } = useLocale();
@@ -561,7 +585,10 @@ const BookingItemBadges = ({
)}
{booking?.assignmentReason.length > 0 && (
-
+
)}
{booking.report && (
{
+const AssignmentReasonTooltip = ({
+ assignmentReason,
+ onClick,
+}: {
+ assignmentReason: AssignmentReason;
+ onClick?: () => void;
+}) => {
const { t } = useLocale();
const badgeTitle = assignmentReasonBadgeTitleMap(assignmentReason.reasonEnum);
return (
{assignmentReason.reasonString}
}>
-
+
{t(badgeTitle)}
diff --git a/apps/web/components/booking/actions/BookingActionsDropdown.tsx b/apps/web/components/booking/actions/BookingActionsDropdown.tsx
index c883685481..651dc48816 100644
--- a/apps/web/components/booking/actions/BookingActionsDropdown.tsx
+++ b/apps/web/components/booking/actions/BookingActionsDropdown.tsx
@@ -31,6 +31,7 @@ import { RejectionReasonDialog } from "@components/dialog/RejectionReasonDialog"
import { ReportBookingDialog } from "@components/dialog/ReportBookingDialog";
import { RerouteDialog } from "@components/dialog/RerouteDialog";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
+import { WrongAssignmentDialog } from "@components/dialog/WrongAssignmentDialog";
import { useBookingConfirmation } from "../hooks/useBookingConfirmation";
import type { BookingItemProps } from "../types";
@@ -118,6 +119,12 @@ export function BookingActionsDropdown({
const setIsOpenAddGuestsDialog = useBookingActionsStoreContext((state) => state.setIsOpenAddGuestsDialog);
const isOpenReportDialog = useBookingActionsStoreContext((state) => state.isOpenReportDialog);
const setIsOpenReportDialog = useBookingActionsStoreContext((state) => state.setIsOpenReportDialog);
+ const isOpenWrongAssignmentDialog = useBookingActionsStoreContext(
+ (state) => state.isOpenWrongAssignmentDialog
+ );
+ const setIsOpenWrongAssignmentDialog = useBookingActionsStoreContext(
+ (state) => state.setIsOpenWrongAssignmentDialog
+ );
const rerouteDialogIsOpen = useBookingActionsStoreContext((state) => state.rerouteDialogIsOpen);
const setRerouteDialogIsOpen = useBookingActionsStoreContext((state) => state.setRerouteDialogIsOpen);
const isCancelDialogOpen = useBookingActionsStoreContext((state) => state.isCancelDialogOpen);
@@ -440,6 +447,18 @@ export function BookingActionsDropdown({
isRecurring={isRecurring}
status={getBookingStatus()}
/>
+ {isBookingFromRoutingForm && (
+
+ )}
{booking.paid && booking.payment[0] && (
+ {isBookingFromRoutingForm && (
+
+ {
+ e.stopPropagation();
+ setIsOpenWrongAssignmentDialog(true);
+ }}
+ data-testid="report_wrong_assignment">
+ {t("report_wrong_assignment")}
+
+
+ )}
>
>;
@@ -31,6 +32,7 @@ export type BookingActionsStore = {
setIsOpenReportDialog: React.Dispatch>;
setRerouteDialogIsOpen: React.Dispatch>;
setIsCancelDialogOpen: React.Dispatch>;
+ setIsOpenWrongAssignmentDialog: React.Dispatch>;
};
export const createBookingActionsStore = () => {
@@ -48,6 +50,7 @@ export const createBookingActionsStore = () => {
isOpenReportDialog: false,
rerouteDialogIsOpen: false,
isCancelDialogOpen: false,
+ isOpenWrongAssignmentDialog: false,
// Dialog setters
setRejectionDialogIsOpen: (isOpen) =>
@@ -101,5 +104,10 @@ export const createBookingActionsStore = () => {
set((state) => ({
isCancelDialogOpen: typeof isOpen === "function" ? isOpen(state.isCancelDialogOpen) : isOpen,
})),
+ setIsOpenWrongAssignmentDialog: (isOpen) =>
+ set((state) => ({
+ isOpenWrongAssignmentDialog:
+ typeof isOpen === "function" ? isOpen(state.isOpenWrongAssignmentDialog) : isOpen,
+ })),
}));
};
diff --git a/apps/web/components/dialog/WrongAssignmentDialog.tsx b/apps/web/components/dialog/WrongAssignmentDialog.tsx
new file mode 100644
index 0000000000..bb780e87dd
--- /dev/null
+++ b/apps/web/components/dialog/WrongAssignmentDialog.tsx
@@ -0,0 +1,330 @@
+import { Dialog } from "@calcom/features/components/controlled-dialog";
+import { useCopy } from "@calcom/lib/hooks/useCopy";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+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 { Label, Select, TextArea } from "@calcom/ui/components/form";
+import { Icon } from "@calcom/ui/components/icon";
+import { showToast } from "@calcom/ui/components/toast";
+import type { Dispatch, SetStateAction } from "react";
+import type { Control, ControllerRenderProps } from "react-hook-form";
+import { Controller, useForm } from "react-hook-form";
+
+interface IWrongAssignmentDialog {
+ isOpenDialog: boolean;
+ setIsOpenDialog: Dispatch>;
+ bookingUid: string;
+ routingReason: string | null;
+ guestEmail: string;
+ hostEmail: string;
+ hostName: string | null;
+ teamId: number | null;
+}
+
+interface FormValues {
+ correctAssignee: string;
+ additionalNotes: string;
+}
+
+interface TeamMemberOption {
+ label: string;
+ value: string;
+ email: string;
+}
+
+interface RoutingInfoSectionProps {
+ routingReason: string | null;
+ noRoutingReasonText: string;
+ routingReasonLabel: string;
+ guestEmail: string;
+ whoBookedItLabel: string;
+ hostEmail: string;
+ hostName: string | null;
+ whoReceivedItLabel: string;
+ copyToClipboard: (text: string) => void;
+ isCopied: boolean;
+}
+
+function RoutingInfoSection(props: RoutingInfoSectionProps): JSX.Element {
+ const {
+ routingReason,
+ noRoutingReasonText,
+ routingReasonLabel,
+ guestEmail,
+ whoBookedItLabel,
+ hostEmail,
+ hostName,
+ whoReceivedItLabel,
+ copyToClipboard,
+ isCopied,
+ } = props;
+
+ let copyIconName: "check" | "copy" = "copy";
+ if (isCopied) {
+ copyIconName = "check";
+ }
+
+ let hostDisplay = hostEmail;
+ if (hostName) {
+ hostDisplay = `${hostName} (${hostEmail})`;
+ }
+
+ const handleCopyClick = (): void => {
+ copyToClipboard(guestEmail);
+ };
+
+ return (
+
+
+
+
+ {routingReason || noRoutingReasonText}
+
+
+
+
+
+
+ {guestEmail}
+
+
+
+
+
+
+
{hostDisplay}
+
+
+ );
+}
+
+interface AssigneeSectionProps {
+ teamId: number | null;
+ teamMemberOptions: TeamMemberOption[];
+ control: Control;
+ whoShouldHaveReceivedItLabel: string;
+ optionalLabel: string;
+ selectTeamMemberPlaceholder: string;
+ enterEmailPlaceholder: string;
+}
+
+interface AdditionalNotesSectionProps {
+ control: Control;
+ additionalNotesLabel: string;
+ placeholder: string;
+ fieldRequiredText: string;
+ errorMessage: string | undefined;
+}
+
+function AdditionalNotesSection(props: AdditionalNotesSectionProps): JSX.Element {
+ const { control, additionalNotesLabel, placeholder, fieldRequiredText, errorMessage } = props;
+
+ const renderTextArea = ({
+ field,
+ }: {
+ field: ControllerRenderProps;
+ }): JSX.Element => ;
+
+ return (
+
+
+
+ {errorMessage &&
{errorMessage}
}
+
+ );
+}
+
+function AssigneeSection(props: AssigneeSectionProps): JSX.Element {
+ const {
+ teamId,
+ teamMemberOptions,
+ control,
+ whoShouldHaveReceivedItLabel,
+ optionalLabel,
+ selectTeamMemberPlaceholder,
+ enterEmailPlaceholder,
+ } = props;
+
+ const showTeamSelector = teamId && teamMemberOptions.length > 0;
+
+ const renderTeamSelect = ({
+ field,
+ }: {
+ field: ControllerRenderProps;
+ }): JSX.Element => {
+ const handleChange = (option: TeamMemberOption | null): void => {
+ if (option) field.onChange(option.value);
+ };
+ return (
+