feat: round robin handover to specific host (#17215)
* frontend dialog * handler + fe calling handler * reafctors + start work on fix hosts tests * restore rr * update tests + fixing types * use a getRRHostsToReasign handler + error handle + i18n * typefixes * assert type * fix: adds missing translation * Update roundRobinManualReassignment.ts * Update roundRobinManualReassignment.ts * Update apps/web/components/dialog/ReassignDialog.tsx Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> * Update roundRobinManualReassignment.ts --------- Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
This commit is contained in:
13 files changed
+1104
-79
No files matched your search
@@ -1,9 +1,24 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, Dialog, DialogClose, DialogContent, DialogFooter, showToast } from "@calcom/ui";
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Label,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
showToast,
|
||||
Select,
|
||||
RadioGroup as RadioArea,
|
||||
} from "@calcom/ui";
|
||||
|
||||
type ReassignDialog = {
|
||||
isOpenDialog: boolean;
|
||||
@@ -12,9 +27,44 @@ type ReassignDialog = {
|
||||
bookingId: number;
|
||||
};
|
||||
|
||||
type FormValues = {
|
||||
reassignType: "round_robin" | "team_member";
|
||||
teamMemberId?: number;
|
||||
};
|
||||
|
||||
export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingId }: ReassignDialog) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
const [animationParentRef] = useAutoAnimate<HTMLFormElement>({
|
||||
duration: 150,
|
||||
easing: "ease-in-out",
|
||||
});
|
||||
// Were using legacy list members here because we don't currently have an easy way to paginate a select via infinite scroll
|
||||
const teamMembers = trpc.viewer.teams.getRoundRobinHostsToReassign.useQuery({
|
||||
bookingId,
|
||||
exclude: "fixedHosts",
|
||||
});
|
||||
|
||||
const teamMemberOptions = useMemo(() => {
|
||||
if (teamMembers.isLoading)
|
||||
return [
|
||||
{
|
||||
label: "Loading...",
|
||||
value: 0,
|
||||
},
|
||||
];
|
||||
|
||||
return teamMembers.data?.map((member) => ({
|
||||
label: member.name,
|
||||
value: member.id,
|
||||
}));
|
||||
}, [teamMembers]);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
defaultValues: {
|
||||
reassignType: "round_robin",
|
||||
},
|
||||
});
|
||||
|
||||
const roundRobinReassignMutation = trpc.viewer.teams.roundRobinReassign.useMutation({
|
||||
onSuccess: async () => {
|
||||
@@ -31,6 +81,33 @@ export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingI
|
||||
},
|
||||
});
|
||||
|
||||
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 handleSubmit = (values: FormValues) => {
|
||||
if (values.reassignType === "round_robin") {
|
||||
roundRobinReassignMutation.mutate({ teamId, bookingId });
|
||||
} else {
|
||||
if (values.teamMemberId) {
|
||||
roundRobinManualReassignMutation.mutate({ bookingId, teamMemberId: values.teamMemberId });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const watchedReassignType = form.watch("reassignType");
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpenDialog}
|
||||
@@ -39,17 +116,50 @@ export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingI
|
||||
}}>
|
||||
<DialogContent title={t("reassign_round_robin_host")} description={t("reassign_to_another_rr_host")}>
|
||||
{/* TODO add team member reassignment*/}
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
<Button
|
||||
data-testid="rejection-confirm"
|
||||
loading={roundRobinReassignMutation.isPending}
|
||||
onClick={() => {
|
||||
roundRobinReassignMutation.mutate({ teamId, bookingId });
|
||||
}}>
|
||||
{t("reassign")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
<Form form={form} handleSubmit={handleSubmit} ref={animationParentRef}>
|
||||
<RadioArea.Group
|
||||
onValueChange={(val) => {
|
||||
form.setValue("reassignType", val as "team_member" | "round_robin");
|
||||
}}
|
||||
className={classNames("mt-1 flex flex-col gap-4")}>
|
||||
<RadioArea.Item
|
||||
value="round_robin"
|
||||
className={classNames("w-full text-sm")}
|
||||
classNames={{ container: classNames("w-full") }}>
|
||||
<strong className="mb-1 block">{t("round_robin")}</strong>
|
||||
<p>{t("round_robin_reassign_description")}</p>
|
||||
</RadioArea.Item>
|
||||
<RadioArea.Item
|
||||
value="team_member"
|
||||
className={classNames("text-sm")}
|
||||
classNames={{ container: classNames("w-full") }}>
|
||||
<strong className="mb-1 block">{t("team_member_round_robin_reassign")}</strong>
|
||||
<p>{t("team_member_round_robin_reassign_description")}</p>
|
||||
</RadioArea.Item>
|
||||
</RadioArea.Group>
|
||||
|
||||
{watchedReassignType === "team_member" && (
|
||||
<div className="mb-2">
|
||||
<Label className="text-emphasis mt-6">{t("select_team_member")}</Label>
|
||||
<Select
|
||||
value={teamMemberOptions?.find((option) => option.value === form.getValues("teamMemberId"))}
|
||||
options={teamMemberOptions}
|
||||
onChange={(event) => {
|
||||
form.setValue("teamMemberId", event?.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
<Button
|
||||
type="submit"
|
||||
data-testid="rejection-confirm"
|
||||
loading={roundRobinReassignMutation.isPending || roundRobinManualReassignMutation.isPending}>
|
||||
{t("reassign")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -2673,6 +2673,11 @@
|
||||
"choose_hosts_schedule": "Choose host schedules",
|
||||
"hosts_schedule_description": "You can select a different schedule for each host. Each host's default schedule is chosen by default.",
|
||||
"no_hosts_description": "No hosts assigned, add hosts in assignment tab.",
|
||||
"team_member_round_robin_reassign": "Manually reassign round robin host",
|
||||
"team_member_round_robin_reassign_description": "Manually reassign round robin host to another available host",
|
||||
"round_robin_reassign_description": "Reassign round robin host to another available host",
|
||||
"invalid_round_robin_host": "New user is not a valid round-robin host for this event type",
|
||||
"user_is_round_robin_fixed": "New user is a fixed host for this event type",
|
||||
"reschedule_with_same_timeslot": "Reschedule with same timeslot",
|
||||
"reschedule_with_same_timeslot_of_new_event": "Reschedule with same timeslot of the new event",
|
||||
"reschedule_to_the_new_event_with_different_timeslot": "Reschedule to the new event with different timeslot",
|
||||
@@ -2680,4 +2685,4 @@
|
||||
"we_dont_have_id_for_the_new_event": "We don't have ID for the new event. Please go to the Routing Form and save it again.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "Rerouted booking successfully. Redirecting to booking page",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user