feat: add availability shown in rr reassign (#17287)

* (feat) add availability status to select

* add logic for confirmation modal on unavailable host

* fix type error

* fix type error

* more type fixes

* fix types
This commit is contained in:
sean-brydon
2024-10-24 12:38:03 +01:00
committed by GitHub
parent d0a1e90199
commit 0949bbf167
4 changed files with 178 additions and 84 deletions
@@ -429,12 +429,14 @@ function BookingListItem(booking: BookingItemProps) {
setIsOpenDialog={setIsOpenRescheduleDialog}
bookingUId={booking.uid}
/>
<ReassignDialog
isOpenDialog={isOpenReassignDialog}
setIsOpenDialog={setIsOpenReassignDialog}
bookingId={booking.id}
teamId={booking.eventType?.team?.id || 0}
/>
{isOpenReassignDialog && (
<ReassignDialog
isOpenDialog={isOpenReassignDialog}
setIsOpenDialog={setIsOpenReassignDialog}
bookingId={booking.id}
teamId={booking.eventType?.team?.id || 0}
/>
)}
<EditLocationDialog
booking={booking}
saveLocation={saveLocation}
+106 -57
View File
@@ -1,7 +1,8 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { Dispatch, SetStateAction } from "react";
import { useMemo } from "react";
import { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { components } from "react-select";
import { classNames } from "@calcom/lib";
import { ErrorCode } from "@calcom/lib/errorCodes";
@@ -15,6 +16,7 @@ import {
DialogClose,
DialogContent,
DialogFooter,
ConfirmationDialogContent,
showToast,
Select,
RadioGroup as RadioArea,
@@ -51,12 +53,14 @@ export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingI
{
label: "Loading...",
value: 0,
status: "unavailable",
},
];
return teamMembers.data?.map((member) => ({
label: member.name,
value: member.id,
status: member.status,
}));
}, [teamMembers]);
@@ -96,12 +100,19 @@ export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingI
},
});
const [showConfirmation, setShowConfirmation] = useState(false);
const handleSubmit = (values: FormValues) => {
if (values.reassignType === "round_robin") {
roundRobinReassignMutation.mutate({ teamId, bookingId });
} else {
if (values.teamMemberId) {
roundRobinManualReassignMutation.mutate({ bookingId, teamMemberId: values.teamMemberId });
const selectedMember = teamMemberOptions?.find((member) => member.value === values.teamMemberId);
if (selectedMember && selectedMember.status === "unavailable") {
setShowConfirmation(true);
} else {
roundRobinManualReassignMutation.mutate({ bookingId, teamMemberId: values.teamMemberId });
}
}
}
};
@@ -109,61 +120,99 @@ export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingI
const watchedReassignType = form.watch("reassignType");
return (
<Dialog
open={isOpenDialog}
onOpenChange={(open) => {
setIsOpenDialog(open);
}}>
<DialogContent
title={t("reassign_round_robin_host")}
description={t("reassign_to_another_rr_host")}
enableOverflow>
{/* TODO add team member reassignment*/}
<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>
<>
<Dialog
open={isOpenDialog}
onOpenChange={(open) => {
setIsOpenDialog(open);
}}>
<DialogContent
title={t("reassign_round_robin_host")}
description={t("reassign_to_another_rr_host")}
enableOverflow>
{/* TODO add team member reassignment*/}
<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>
{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);
}}
components={{
Option: ({ children, ...props }) => {
const status = props.data?.status;
return (
<components.Option {...props}>
<div className="flex items-center gap-2">
<div
className={classNames(
"h-2 w-2 rounded-full",
status === "available" ? "bg-green-500" : "bg-red-500"
)}
/>
{children}
</div>
</components.Option>
);
},
}}
/>
</div>
)}
<DialogFooter>
<DialogClose />
<Button
type="submit"
data-testid="rejection-confirm"
loading={roundRobinReassignMutation.isPending || roundRobinManualReassignMutation.isPending}>
{t("reassign")}
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
<Dialog open={showConfirmation} onOpenChange={setShowConfirmation}>
<ConfirmationDialogContent
variety="warning"
title={t("confirm_reassign_unavailable")}
confirmBtnText={t("yes_reassign")}
cancelBtnText={t("cancel")}
onConfirm={() => {
const teamMemberId = form.getValues("teamMemberId");
if (!teamMemberId) {
return;
}
roundRobinManualReassignMutation.mutate({ bookingId, teamMemberId });
setShowConfirmation(false);
}}>
{t("reassign_unavailable_team_member_description")}
</ConfirmationDialogContent>
</Dialog>
</>
);
};
@@ -2694,5 +2694,9 @@
"add_new_field": "Add new field",
"you_dont_have_access_to_reroute_this_booking": "You don't have access to reroute this booking",
"form_response_not_found": "Form response not found",
"confirm_reassign_unavailable": "Manual Reassign to unavailable host",
"reassign_unavailable_team_member_description": "Are you sure you want to reassign this booking to an unavailable host?",
"yes_reassign": "Yes, reassign",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1,4 +1,10 @@
import dayjs from "@calcom/dayjs";
import { ensureAvailableUsers } from "@calcom/features/bookings/lib/handleNewBooking/ensureAvailableUsers";
import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB";
import type { IsFixedAwareUser } from "@calcom/features/bookings/lib/handleNewBooking/types";
import logger from "@calcom/lib/logger";
import type { PrismaClient } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import type { TGetRoundRobinHostsToReassignInputSchema } from "./getRoundRobinHostsToReasign.schema";
@@ -11,21 +17,32 @@ type GetRoundRobinHostsToReassignOptions = {
input: TGetRoundRobinHostsToReassignInputSchema;
};
async function isTeamAdmin(prisma: PrismaClient, userId: number, teamId: number) {
const membership = await prisma.membership.findFirst({
where: { userId, teamId, role: { in: [MembershipRole.ADMIN, MembershipRole.OWNER] } },
});
return membership?.role ?? false;
}
export const getRoundRobinHostsToReassign = async ({ ctx, input }: GetRoundRobinHostsToReassignOptions) => {
const { prisma } = ctx;
const { isOrgAdmin } = ctx.user.organization;
const hasPermsToView = !ctx.user.organization.isPrivate || isOrgAdmin;
if (!hasPermsToView) {
return [];
}
const gettingRoundRobinHostsToReassignLogger = logger.getSubLogger({
prefix: ["gettingRoundRobinHostsToReassign", `${input.bookingId}`],
});
const booking = await prisma.booking.findUniqueOrThrow({
where: { id: input.bookingId },
select: {
userId: true,
startTime: true,
endTime: true,
eventType: {
select: {
id: true,
timeZone: true,
teamId: true,
},
},
},
@@ -35,27 +52,49 @@ export const getRoundRobinHostsToReassign = async ({ ctx, input }: GetRoundRobin
throw new Error("Booking not found");
}
// If input.exclude is "fixedHosts", exclude fixed hosts from the list
const excludeFixedHosts = input.exclude === "fixedHosts";
// Get event type
const eventType = await getEventTypesFromDB(booking.eventType.id);
const eventTypeHosts = await prisma.host.findMany({
where: {
eventTypeId: booking.eventType.id,
isFixed: excludeFixedHosts ? false : undefined,
if (!eventType || !eventType.teamId) {
throw new Error("Event type not found");
}
const availableEventTypeUsers = eventType.hosts
.filter((h) => !h.isFixed && h.user.id !== booking.userId)
.map((host) => ({
...host.user,
isFixed: host.isFixed,
priority: host?.priority ?? 2,
}));
const availableUsers = await ensureAvailableUsers(
{ ...eventType, users: availableEventTypeUsers as IsFixedAwareUser[] },
{
dateFrom: dayjs(booking.startTime).format(),
dateTo: dayjs(booking.endTime).format(),
timeZone: eventType.timeZone || "UTC",
},
select: {
user: {
select: {
id: true,
name: true,
},
},
},
});
gettingRoundRobinHostsToReassignLogger
);
const hosts = eventTypeHosts.map((host) => host.user);
const availableUserIds = new Set(availableUsers.map((u) => u.id));
return hosts;
const canViewReassignBusyUsers = isOrgAdmin || (await isTeamAdmin(prisma, ctx.user.id, eventType.teamId));
const roundRobinHostsToReassign = availableEventTypeUsers.reduce((acc, host) => {
const status = availableUserIds.has(host.id) ? "available" : "unavailable";
if (canViewReassignBusyUsers || status === "available") {
acc.push({
id: host.id,
name: host.name,
email: host.email,
status,
});
}
return acc;
}, [] as { id: number; name: string | null; email: string; status: string }[]);
return roundRobinHostsToReassign;
};
export default getRoundRobinHostsToReassign;