fix: virtualised member list for reassign (#17386)
* wip virtual list for reassignemnt * order in alpha and prio order * loading state * add flex shrink so status symbol doesnt move * add gap to each item to make hovering not feel squished * a11y indexing tab group * useInViewObserver hook and move to lib * use correct intersection target * fix type errors * remove type
This commit is contained in:
@@ -3,11 +3,12 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { components } from "react-select";
|
||||
import { z } from "zod";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
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 {
|
||||
@@ -20,7 +21,8 @@ import {
|
||||
DialogFooter,
|
||||
ConfirmationDialogContent,
|
||||
showToast,
|
||||
Select,
|
||||
Input,
|
||||
Icon,
|
||||
RadioGroup as RadioArea,
|
||||
TextAreaField,
|
||||
} from "@calcom/ui";
|
||||
@@ -49,6 +51,12 @@ const formSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
interface TeamMemberOption {
|
||||
label: string;
|
||||
value: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingId }: ReassignDialog) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useUtils();
|
||||
@@ -56,28 +64,39 @@ export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingI
|
||||
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 [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(() => {
|
||||
if (teamMembers.isLoading)
|
||||
return [
|
||||
{
|
||||
label: "Loading...",
|
||||
value: 0,
|
||||
status: "unavailable",
|
||||
},
|
||||
];
|
||||
|
||||
return teamMembers.data?.map((member) => ({
|
||||
label: member.name,
|
||||
return allRows.map((member) => ({
|
||||
label: member.name || member.email,
|
||||
value: member.id,
|
||||
status: member.status,
|
||||
}));
|
||||
}, [teamMembers]);
|
||||
})) as TeamMemberOption[];
|
||||
}, [allRows]);
|
||||
|
||||
const { ref: observerRef } = useInViewObserver(() => {
|
||||
if (hasNextPage && !isFetching) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, document.querySelector('[role="dialog"]'));
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
@@ -138,6 +157,7 @@ export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingI
|
||||
};
|
||||
|
||||
const watchedReassignType = form.watch("reassignType");
|
||||
const watchedTeamMemberId = form.watch("teamMemberId");
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -150,24 +170,20 @@ export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingI
|
||||
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")}>
|
||||
className="mt-1 flex flex-col gap-4">
|
||||
<RadioArea.Item
|
||||
value="round_robin"
|
||||
className={classNames("w-full text-sm")}
|
||||
classNames={{ container: classNames("w-full") }}>
|
||||
className="w-full text-sm"
|
||||
classNames={{ container: "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") }}>
|
||||
<RadioArea.Item value="team_member" className="text-sm" classNames={{ container: "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>
|
||||
@@ -176,31 +192,62 @@ export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingI
|
||||
{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 className="mt-2">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={t("search")}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
<div className="scroll-bar flex h-[150px] flex-col gap-0.5 overflow-y-scroll rounded-md border p-1">
|
||||
{teamMemberOptions.map((member) => (
|
||||
<label
|
||||
key={member.value}
|
||||
tabIndex={watchedTeamMemberId === member.value ? -1 : 0}
|
||||
role="radio"
|
||||
aria-checked={watchedTeamMemberId === member.value}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
form.setValue("teamMemberId", member.value);
|
||||
}
|
||||
}}
|
||||
className={classNames(
|
||||
"hover:bg-subtle focus:bg-subtle focus:ring-emphasis cursor-pointer items-center justify-between gap-0.5 rounded-sm py-2 outline-none focus:ring focus:ring-2",
|
||||
watchedTeamMemberId === member.value && "bg-subtle"
|
||||
)}>
|
||||
<div className="flex flex-1 items-center space-x-3">
|
||||
<input
|
||||
type="radio"
|
||||
className="hidden"
|
||||
checked={watchedTeamMemberId === member.value}
|
||||
onChange={() => form.setValue("teamMemberId", member.value)}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
"h-3 w-3 flex-shrink-0 rounded-full",
|
||||
member.status === "unavailable" ? "bg-red-500" : "bg-green-500"
|
||||
)}
|
||||
/>
|
||||
<span className="text-emphasis w-full text-sm">{member.label}</span>
|
||||
{watchedTeamMemberId === member.value && (
|
||||
<div className="place-self-end pr-2">
|
||||
<Icon name="check" className="text-emphasis h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
<div className="text-default text-center" ref={observerRef}>
|
||||
<Button
|
||||
color="minimal"
|
||||
loading={isFetchingNextPage}
|
||||
disabled={!hasNextPage}
|
||||
onClick={() => fetchNextPage()}>
|
||||
{hasNextPage ? t("load_more_results") : t("no_more_results")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { FiltersContainer } from "@calcom/features/bookings/components/FiltersCo
|
||||
import type { filterQuerySchema } from "@calcom/features/bookings/lib/useFilterQuery";
|
||||
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
@@ -18,7 +19,6 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
||||
import { Alert, Button, EmptyScreen, HorizontalTabs } from "@calcom/ui";
|
||||
|
||||
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
|
||||
import BookingListItem from "@components/booking/BookingListItem";
|
||||
|
||||
@@ -21,6 +21,7 @@ import { parseEventTypeColor } from "@calcom/lib";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
|
||||
import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { useGetTheme } from "@calcom/lib/hooks/useTheme";
|
||||
@@ -60,7 +61,6 @@ import {
|
||||
} from "@calcom/ui";
|
||||
|
||||
import type { AppProps } from "@lib/app-providers";
|
||||
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
|
||||
type GetUserEventGroupsResponse = RouterOutputs["viewer"]["eventTypes"]["getUserEventGroups"];
|
||||
|
||||
@@ -12,13 +12,12 @@ import { Builder, Query, Utils as QbUtils } from "react-awesome-query-builder";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
|
||||
|
||||
import SingleForm, {
|
||||
getServerSidePropsForSingleFormView as getServerSideProps,
|
||||
} from "../../components/SingleForm";
|
||||
@@ -67,7 +66,7 @@ const Result = ({ formId, jsonLogicQuery }: { formId: string; jsonLogicQuery: Js
|
||||
<div className="w-full max-w-[2000px] overflow-x-scroll">
|
||||
{!isPending && (
|
||||
<div className="text-default text-md mx-4 mb-2">
|
||||
{`${numberOfRows} ` + (numberOfRows === 1 ? t("row") : t("rows"))}
|
||||
{`${numberOfRows} ${numberOfRows === 1 ? t("row") : t("rows")}`}
|
||||
</div>
|
||||
)}
|
||||
<table
|
||||
|
||||
@@ -189,4 +189,5 @@ export function SelectWithValidation<
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Select;
|
||||
|
||||
@@ -23,7 +23,7 @@ import { ZPublishInputSchema } from "./publish.schema";
|
||||
import { ZRemoveHostsFromEventTypes } from "./removeHostsFromEventTypes.schema";
|
||||
import { ZRemoveMemberInputSchema } from "./removeMember.schema";
|
||||
import { ZResendInvitationInputSchema } from "./resendInvitation.schema";
|
||||
import { ZGetRoundRobinHostsToReassignInputSchema } from "./roundRobin/getRoundRobinHostsToReasign.schema";
|
||||
import { ZGetRoundRobinHostsInputSchema } from "./roundRobin/getRoundRobinHostsToReasign.schema";
|
||||
import { ZRoundRobinManualReassignInputSchema } from "./roundRobin/roundRobinManualReassign.schema";
|
||||
import { ZRoundRobinReassignInputSchema } from "./roundRobin/roundRobinReassign.schema";
|
||||
import { ZSetInviteExpirationInputSchema } from "./setInviteExpiration.schema";
|
||||
@@ -192,15 +192,13 @@ export const viewerTeamsRouter = router({
|
||||
);
|
||||
return handler(opts);
|
||||
}),
|
||||
getRoundRobinHostsToReassign: authedProcedure
|
||||
.input(ZGetRoundRobinHostsToReassignInputSchema)
|
||||
.query(async (opts) => {
|
||||
const handler = await importHandler(
|
||||
namespaced("getRoundRobinHostsToReassign"),
|
||||
() => import("./roundRobin/getRoundRobinHostsToReasign.handler")
|
||||
);
|
||||
return handler(opts);
|
||||
}),
|
||||
getRoundRobinHostsToReassign: authedProcedure.input(ZGetRoundRobinHostsInputSchema).query(async (opts) => {
|
||||
const handler = await importHandler(
|
||||
namespaced("getRoundRobinHostsToReassign"),
|
||||
() => import("./roundRobin/getRoundRobinHostsToReasign.handler")
|
||||
);
|
||||
return handler(opts);
|
||||
}),
|
||||
checkIfMembershipExists: authedProcedure
|
||||
.input(ZCheckIfMembershipExistsInputSchema)
|
||||
.mutation(async (opts) => {
|
||||
|
||||
+106
-50
@@ -1,10 +1,11 @@
|
||||
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 { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import { userSelect } from "@calcom/prisma";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
import type { TGetRoundRobinHostsToReassignInputSchema } from "./getRoundRobinHostsToReasign.schema";
|
||||
@@ -17,65 +18,122 @@ type GetRoundRobinHostsToReassignOptions = {
|
||||
input: TGetRoundRobinHostsToReassignInputSchema;
|
||||
};
|
||||
|
||||
async function getTeamHostsFromDB({
|
||||
eventTypeId,
|
||||
prisma,
|
||||
searchTerm,
|
||||
cursor,
|
||||
limit = 20,
|
||||
excludeUserId,
|
||||
}: {
|
||||
eventTypeId: number;
|
||||
prisma: PrismaClient;
|
||||
searchTerm?: string;
|
||||
cursor?: number;
|
||||
limit?: number;
|
||||
excludeUserId?: number;
|
||||
}) {
|
||||
const queryWhere = {
|
||||
eventTypeId,
|
||||
isFixed: false,
|
||||
...(excludeUserId && { userId: { not: excludeUserId } }),
|
||||
...(searchTerm && {
|
||||
user: {
|
||||
OR: [
|
||||
{ name: { contains: searchTerm, mode: "insensitive" as const } },
|
||||
{ email: { contains: searchTerm, mode: "insensitive" as const } },
|
||||
],
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const [totalCount, hosts] = await Promise.all([
|
||||
prisma.host.count({ where: queryWhere }),
|
||||
prisma.host.findMany({
|
||||
where: queryWhere,
|
||||
select: {
|
||||
isFixed: true,
|
||||
priority: true,
|
||||
user: {
|
||||
select: {
|
||||
...userSelect.select,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
take: limit + 1, // Take one more to determine if there's a next page
|
||||
...(cursor && { skip: 1, cursor: { userId_eventTypeId: { userId: cursor, eventTypeId } } }),
|
||||
orderBy: [{ user: { name: "asc" } }, { priority: "desc" }],
|
||||
}),
|
||||
]);
|
||||
|
||||
const hasNextPage = hosts.length > limit;
|
||||
const hosts_subset = hasNextPage ? hosts.slice(0, -1) : hosts;
|
||||
|
||||
return {
|
||||
hosts: hosts_subset.map((host) => ({
|
||||
...host.user,
|
||||
isFixed: host.isFixed,
|
||||
priority: host.priority ?? 2,
|
||||
})),
|
||||
totalCount,
|
||||
hasNextPage,
|
||||
nextCursor: hasNextPage ? hosts_subset[hosts_subset.length - 1].user.id : null,
|
||||
};
|
||||
}
|
||||
|
||||
async function getEventTypeFromDB(eventTypeId: number, prisma: PrismaClient) {
|
||||
return prisma.eventType.findUniqueOrThrow({
|
||||
where: { id: eventTypeId },
|
||||
});
|
||||
}
|
||||
|
||||
export const getRoundRobinHostsToReassign = async ({ ctx, input }: GetRoundRobinHostsToReassignOptions) => {
|
||||
const { prisma } = ctx;
|
||||
const { bookingId, limit, cursor, searchTerm } = input;
|
||||
|
||||
const gettingRoundRobinHostsToReassignLogger = logger.getSubLogger({
|
||||
prefix: ["gettingRoundRobinHostsToReassign", `${input.bookingId}`],
|
||||
prefix: ["gettingRoundRobinHostsToReassign", `${bookingId}`],
|
||||
});
|
||||
|
||||
const booking = await prisma.booking.findUniqueOrThrow({
|
||||
where: { id: input.bookingId },
|
||||
where: { id: bookingId },
|
||||
select: {
|
||||
userId: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
eventType: {
|
||||
select: {
|
||||
id: true,
|
||||
timeZone: true,
|
||||
teamId: true,
|
||||
},
|
||||
},
|
||||
eventTypeId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!booking?.eventType) {
|
||||
throw new Error("Booking not found");
|
||||
if (!booking.eventTypeId) {
|
||||
throw new Error("Booking requires a event type to reassign hosts");
|
||||
}
|
||||
|
||||
// Get event type
|
||||
const eventType = await getEventTypesFromDB(booking.eventType.id);
|
||||
|
||||
if (!eventType) {
|
||||
throw new Error("Event type not found");
|
||||
}
|
||||
|
||||
if (!eventType.teamId) {
|
||||
gettingRoundRobinHostsToReassignLogger.warn("Booking with event type that has no teamId", {
|
||||
bookingId: input.bookingId,
|
||||
eventTypeId: eventType.id,
|
||||
teamId: eventType.teamId ?? -1,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
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 { hosts, totalCount, nextCursor } = await getTeamHostsFromDB({
|
||||
eventTypeId: booking.eventTypeId,
|
||||
prisma,
|
||||
searchTerm,
|
||||
cursor,
|
||||
limit,
|
||||
excludeUserId: booking.userId ?? undefined,
|
||||
});
|
||||
|
||||
let availableUsers: IsFixedAwareUser[] = [];
|
||||
try {
|
||||
const eventType = await getEventTypeFromDB(booking.eventTypeId, prisma);
|
||||
availableUsers = await ensureAvailableUsers(
|
||||
{ ...eventType, users: availableEventTypeUsers as IsFixedAwareUser[] },
|
||||
// @ts-expect-error - TODO: We need to make sure nothing in the app needs the return type of getEventTypeFromDB as it fetches everything under the sun
|
||||
{
|
||||
users: hosts as IsFixedAwareUser[],
|
||||
...eventType,
|
||||
},
|
||||
{
|
||||
dateFrom: dayjs(booking.startTime).format(),
|
||||
dateTo: dayjs(booking.endTime).format(),
|
||||
timeZone: eventType.timeZone || "UTC",
|
||||
timeZone: "UTC",
|
||||
},
|
||||
gettingRoundRobinHostsToReassignLogger
|
||||
);
|
||||
@@ -84,25 +142,23 @@ export const getRoundRobinHostsToReassign = async ({ ctx, input }: GetRoundRobin
|
||||
availableUsers = [];
|
||||
} else {
|
||||
gettingRoundRobinHostsToReassignLogger.error(error);
|
||||
// Log error and return empty array to avoid rethrowing the error
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const availableUserIds = new Set(availableUsers.map((u) => u.id));
|
||||
|
||||
const roundRobinHostsToReassign = availableEventTypeUsers.reduce((acc, host) => {
|
||||
const status = availableUserIds.has(host.id) ? "available" : "unavailable";
|
||||
acc.push({
|
||||
id: host.id,
|
||||
name: host.name,
|
||||
email: host.email,
|
||||
status,
|
||||
});
|
||||
return acc;
|
||||
}, [] as { id: number; name: string | null; email: string; status: string }[]);
|
||||
const items = hosts.map((host) => ({
|
||||
id: host.id,
|
||||
name: host.name,
|
||||
email: host.email,
|
||||
status: availableUserIds.has(host.id) ? "available" : "unavailable",
|
||||
}));
|
||||
|
||||
return roundRobinHostsToReassign;
|
||||
return {
|
||||
items,
|
||||
nextCursor,
|
||||
totalCount,
|
||||
};
|
||||
};
|
||||
|
||||
export default getRoundRobinHostsToReassign;
|
||||
|
||||
+7
-5
@@ -1,10 +1,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ZGetRoundRobinHostsToReassignInputSchema = z.object({
|
||||
// New schema for fetching hosts
|
||||
export const ZGetRoundRobinHostsInputSchema = z.object({
|
||||
bookingId: z.number(),
|
||||
exclude: z.enum(["fixedHosts"]).optional(),
|
||||
exclude: z.literal("fixedHosts"),
|
||||
cursor: z.number().optional(), // For pagination
|
||||
limit: z.number().min(1).max(100).optional(),
|
||||
searchTerm: z.string().optional(),
|
||||
});
|
||||
|
||||
export type TGetRoundRobinHostsToReassignInputSchema = z.infer<
|
||||
typeof ZGetRoundRobinHostsToReassignInputSchema
|
||||
>;
|
||||
export type TGetRoundRobinHostsToReassignInputSchema = z.infer<typeof ZGetRoundRobinHostsInputSchema>;
|
||||
|
||||
Reference in New Issue
Block a user