diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index bc73c43d61..1baf6e8f33 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -429,12 +429,14 @@ function BookingListItem(booking: BookingItemProps) { setIsOpenDialog={setIsOpenRescheduleDialog} bookingUId={booking.uid} /> - + {isOpenReassignDialog && ( + + )} ({ 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 ( - { - setIsOpenDialog(open); - }}> - - {/* TODO add team member reassignment*/} - - { - form.setValue("reassignType", val as "team_member" | "round_robin"); - }} - className={classNames("mt-1 flex flex-col gap-4")}> - - {t("round_robin")} - {t("round_robin_reassign_description")} - - - {t("team_member_round_robin_reassign")} - {t("team_member_round_robin_reassign_description")} - - + <> + { + setIsOpenDialog(open); + }}> + + {/* TODO add team member reassignment*/} + + { + form.setValue("reassignType", val as "team_member" | "round_robin"); + }} + className={classNames("mt-1 flex flex-col gap-4")}> + + {t("round_robin")} + {t("round_robin_reassign_description")} + + + {t("team_member_round_robin_reassign")} + {t("team_member_round_robin_reassign_description")} + + - {watchedReassignType === "team_member" && ( - - {t("select_team_member")} - option.value === form.getValues("teamMemberId"))} - options={teamMemberOptions} - onChange={(event) => { - form.setValue("teamMemberId", event?.value); - }} - /> - - )} - - - - {t("reassign")} - - - - - + {watchedReassignType === "team_member" && ( + + {t("select_team_member")} + option.value === form.getValues("teamMemberId"))} + options={teamMemberOptions} + onChange={(event) => { + form.setValue("teamMemberId", event?.value); + }} + components={{ + Option: ({ children, ...props }) => { + const status = props.data?.status; + return ( + + + + {children} + + + ); + }, + }} + /> + + )} + + + + {t("reassign")} + + + + + + + + { + const teamMemberId = form.getValues("teamMemberId"); + if (!teamMemberId) { + return; + } + roundRobinManualReassignMutation.mutate({ bookingId, teamMemberId }); + setShowConfirmation(false); + }}> + {t("reassign_unavailable_team_member_description")} + + + > ); }; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index db8b629f57..849be28159 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } + diff --git a/packages/trpc/server/routers/viewer/teams/roundRobin/getRoundRobinHostsToReasign.handler.ts b/packages/trpc/server/routers/viewer/teams/roundRobin/getRoundRobinHostsToReasign.handler.ts index 1ac499a365..bb784b8ee6 100644 --- a/packages/trpc/server/routers/viewer/teams/roundRobin/getRoundRobinHostsToReasign.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/roundRobin/getRoundRobinHostsToReasign.handler.ts @@ -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;
{t("round_robin_reassign_description")}
{t("team_member_round_robin_reassign_description")}