import { useAutoAnimate } from "@formkit/auto-animate/react"; import { zodResolver } from "@hookform/resolvers/zod"; import type { Dispatch, SetStateAction } from "react"; import { useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { Dialog } from "@calcom/features/components/controlled-dialog"; import { ErrorCode } from "@calcom/lib/errorCodes"; import { useDebounce } from "@calcom/lib/hooks/useDebounce"; import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import classNames from "@calcom/ui/classNames"; import { Button } from "@calcom/ui/components/button"; import { DialogContent, DialogFooter, DialogClose, ConfirmationDialogContent, } from "@calcom/ui/components/dialog"; import { TextAreaField, Form, Label, Input } from "@calcom/ui/components/form"; import { Icon } from "@calcom/ui/components/icon"; import { RadioAreaGroup as RadioArea } from "@calcom/ui/components/radio"; import { showToast } from "@calcom/ui/components/toast"; enum ReassignType { ROUND_ROBIN = "round_robin", TEAM_MEMBER = "team_member", } type ReassignDialog = { isOpenDialog: boolean; setIsOpenDialog: Dispatch>; teamId: number; bookingId: number; bookingFromRoutingForm: boolean; }; type FormValues = { reassignType: ReassignType.ROUND_ROBIN | ReassignType.TEAM_MEMBER; teamMemberId?: number; reassignReason?: string; }; const formSchema = z.object({ reassignType: z.nativeEnum(ReassignType), teamMemberId: z.number().optional(), reassignReason: z .string() .optional() .refine((val) => !val || val.length >= 10, { message: "Reassign reason must be at least 10 characters long if provided", }), }); interface TeamMemberOption { label: string; value: number; status: string; } export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingId, bookingFromRoutingForm, }: ReassignDialog) => { const { t } = useLocale(); const utils = trpc.useUtils(); const [animationParentRef] = useAutoAnimate({ duration: 150, easing: "ease-in-out", }); const [searchTerm, setSearchTerm] = useState(""); const debouncedSearch = useDebounce(searchTerm, 500); const { data, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage } = trpc.viewer.teams.getRoundRobinHostsToReassign.useInfiniteQuery( { bookingId, exclude: "fixedHosts", limit: 10, searchTerm: debouncedSearch, }, { getNextPageParam: (lastPage) => lastPage.nextCursor, } ); const allRows = useMemo(() => { return data?.pages.flatMap((page) => page.items) ?? []; }, [data]); const teamMemberOptions = useMemo(() => { return allRows.map((member) => ({ label: member.name || member.email, value: member.id, status: member.status, })) as TeamMemberOption[]; }, [allRows]); const { ref: observerRef } = useInViewObserver(() => { if (hasNextPage && !isFetching) { fetchNextPage(); } }, document.querySelector('[role="dialog"]')); const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { reassignType: bookingFromRoutingForm ? ReassignType.TEAM_MEMBER : ReassignType.ROUND_ROBIN, }, }); const roundRobinReassignMutation = trpc.viewer.teams.roundRobinReassign.useMutation({ onSuccess: async (data) => { await utils.viewer.bookings.get.invalidate(); setIsOpenDialog(false); showToast(t("booking_reassigned_to_host", { host: data?.reassignedTo.name }), "success"); }, onError: async (error) => { if (error.message.includes(ErrorCode.NoAvailableUsersFound)) { showToast(t("no_available_hosts"), "error"); } else { showToast(t("unexpected_error_try_again"), "error"); } }, }); const roundRobinManualReassignMutation = trpc.viewer.teams.roundRobinManualReassign.useMutation({ onSuccess: async () => { await utils.viewer.bookings.get.invalidate(); setIsOpenDialog(false); showToast(t("booking_reassigned"), "success"); }, onError: async (error) => { if (error.message.includes(ErrorCode.NoAvailableUsersFound)) { showToast(t("no_available_hosts"), "error"); } else { showToast(t(error.message), "error"); } }, }); const [confirmationModal, setConfirmationModal] = useState<{ show: boolean; membersStatus: "unavailable" | "available" | null; }>({ show: false, membersStatus: null, }); const handleSubmit = (values: FormValues) => { if (values.reassignType === ReassignType.ROUND_ROBIN) { roundRobinReassignMutation.mutate({ teamId, bookingId }); } else { if (values.teamMemberId) { const selectedMember = teamMemberOptions?.find((member) => member.value === values.teamMemberId); if (selectedMember) { setConfirmationModal({ show: true, membersStatus: selectedMember.status as "available" | "unavailable", }); } } } }; const watchedReassignType = form.watch("reassignType"); const watchedTeamMemberId = form.watch("teamMemberId"); return ( <> { setIsOpenDialog(open); }}>
{ const reassignType: ReassignType = z.nativeEnum(ReassignType).parse(val); form.setValue("reassignType", reassignType); }} defaultValue={bookingFromRoutingForm ? ReassignType.TEAM_MEMBER : ReassignType.ROUND_ROBIN} className="mt-1 flex flex-col gap-4"> {!bookingFromRoutingForm ? ( {t("round_robin")}

{t("round_robin_reassign_description")}

) : null} {t("team_member_round_robin_reassign")}

{t("team_member_round_robin_reassign_description")}

{watchedReassignType === ReassignType.TEAM_MEMBER && (
setSearchTerm(e.target.value)} />
{teamMemberOptions.map((member) => (
)}
setConfirmationModal({ ...confirmationModal, show: open })}> { const teamMemberId = form.getValues("teamMemberId"); if (!teamMemberId) { return; } roundRobinManualReassignMutation.mutate({ bookingId, teamMemberId, reassignReason: form.getValues("reassignReason"), }); setConfirmationModal({ show: false, membersStatus: null, }); }}>

{confirmationModal?.membersStatus === "unavailable" ? t("reassign_unavailable_team_member_description") : t("reassign_available_team_member_description")}

form.setValue("reassignReason", e.target.value)} required={confirmationModal?.membersStatus === "unavailable"} />
); };