diff --git a/packages/features/insights/components/RoutingFormResponsesTable.tsx b/packages/features/insights/components/RoutingFormResponsesTable.tsx index cc62b9d828..acb7f46185 100644 --- a/packages/features/insights/components/RoutingFormResponsesTable.tsx +++ b/packages/features/insights/components/RoutingFormResponsesTable.tsx @@ -1,6 +1,5 @@ "use client"; -import { keepPreviousData } from "@tanstack/react-query"; import { useReactTable, getCoreRowModel, @@ -55,34 +54,25 @@ export function RoutingFormResponsesTable() { const getInsightsFacetedUniqueValues = useInsightsFacetedUniqueValues({ headers, userId, teamId, isAll }); - const { sorting, updateFilter } = useDataTable(); + const { sorting, limit, offset, updateFilter } = useDataTable(); - const { data, fetchNextPage, isFetching, isPending, hasNextPage, isLoading } = - trpc.viewer.insights.routingFormResponses.useInfiniteQuery( - { - teamId, - startDate, - endDate, - userId, - memberUserIds, - isAll, - routingFormId, - columnFilters, - sorting, - limit: 30, - }, - { - getNextPageParam: (lastPage) => lastPage.nextCursor, - placeholderData: keepPreviousData, - trpc: { - context: { skipBatch: true }, - }, - } - ); + const { data, isFetching, isPending, isLoading } = trpc.viewer.insights.routingFormResponses.useQuery({ + teamId, + startDate, + endDate, + userId, + memberUserIds, + isAll, + routingFormId, + columnFilters, + sorting, + limit, + offset, + }); const processedData = useMemo(() => { - if (!isHeadersSuccess) return []; - return (data?.pages?.flatMap((page) => page.data) ?? []) as RoutingFormTableRow[]; + if (!isHeadersSuccess || !data) return []; + return data.data as RoutingFormTableRow[]; }, [data, isHeadersSuccess]); const columns = useInsightsColumns({ headers, isHeadersSuccess }); @@ -118,7 +108,7 @@ export function RoutingFormResponsesTable() { } }, [table, getInsightsFacetedUniqueValues, routingFormId]); - if ((isHeadersLoading && !headers) || ((isFetching || isLoading) && !data)) { + if (isHeadersLoading && !headers) { return ; } @@ -128,10 +118,9 @@ export function RoutingFormResponsesTable() { } ToolbarLeft={ <> diff --git a/packages/features/insights/filters/Download/RoutingFormResponsesDownload.tsx b/packages/features/insights/filters/Download/RoutingFormResponsesDownload.tsx index d5b6f29fd3..a54199be71 100644 --- a/packages/features/insights/filters/Download/RoutingFormResponsesDownload.tsx +++ b/packages/features/insights/filters/Download/RoutingFormResponsesDownload.tsx @@ -17,10 +17,7 @@ type Props = { sorting: SortingState; }; -type Batch = { - data: RoutingData[]; - nextCursor: number | undefined; -}; +const BATCH_SIZE = 100; // Increased batch size for downloads export const RoutingFormResponsesDownload = ({ sorting }: Props) => { const { t } = useLocale(); @@ -31,12 +28,12 @@ export const RoutingFormResponsesDownload = ({ sorting }: Props) => { const utils = trpc.useUtils(); const fetchBatch = async ( - cursor: number | undefined = undefined + offset: number ): Promise<{ data: RoutingData[]; - nextCursor: number | undefined; + total: number; }> => { - const { data, nextCursor } = await utils.viewer.insights.routingFormResponsesForDownload.fetch({ + const result = await utils.viewer.insights.routingFormResponsesForDownload.fetch({ teamId, startDate, endDate, @@ -46,27 +43,28 @@ export const RoutingFormResponsesDownload = ({ sorting }: Props) => { routingFormId, columnFilters, sorting, - cursor, + limit: BATCH_SIZE, + offset, }); - return { - data, - nextCursor, - }; + return result; }; const handleDownloadClick = async () => { try { setIsDownloading(true); let allData: RoutingData[] = []; - let hasMore = true; - let cursor: number | undefined = undefined; + let offset = 0; - // Fetch data in batches until there's no more data - while (hasMore) { - const result: Batch = await fetchBatch(cursor); + // Get first batch to get total count + const firstBatch = await fetchBatch(0); + allData = [...firstBatch.data]; + const totalRecords = firstBatch.total; + + // Continue fetching remaining batches + while (allData.length < totalRecords) { + offset += BATCH_SIZE; + const result = await fetchBatch(offset); allData = [...allData, ...result.data]; - hasMore = result.nextCursor !== undefined; - cursor = result.nextCursor; } if (allData.length > 0) { diff --git a/packages/features/insights/server/raw-data.schema.ts b/packages/features/insights/server/raw-data.schema.ts index 9c720a332a..d0bbcdb3aa 100644 --- a/packages/features/insights/server/raw-data.schema.ts +++ b/packages/features/insights/server/raw-data.schema.ts @@ -23,7 +23,7 @@ export const rawDataInputSchema = z.object({ eventTypeId: z.coerce.number().optional().nullable(), }) satisfies z.ZodType; -export type RoutingFormResponsesInput = { +export type RoutingFormStatsInput = { startDate: string; endDate: string; teamId?: number; @@ -31,13 +31,16 @@ export type RoutingFormResponsesInput = { memberUserIds?: number[]; isAll: boolean; routingFormId?: string; - cursor?: number; - limit?: number; columnFilters: ColumnFilter[]; sorting?: Sorting[]; }; -export const routingFormResponsesInputSchema = z.object({ +export type RoutingFormResponsesInput = RoutingFormStatsInput & { + offset?: number; + limit?: number; +}; + +export const routingFormStatsInputSchema = z.object({ startDate: z.string(), endDate: z.string(), teamId: z.coerce.number().optional(), @@ -45,8 +48,12 @@ export const routingFormResponsesInputSchema = z.object({ memberUserIds: z.number().array().optional(), isAll: z.coerce.boolean(), routingFormId: z.string().optional(), - cursor: z.number().optional(), - limit: z.number().optional(), columnFilters: z.array(ZColumnFilter), sorting: z.array(ZSorting).optional(), +}) satisfies z.ZodType; + +export const routingFormResponsesInputSchema = z.object({ + ...routingFormStatsInputSchema.shape, + offset: z.number().optional(), + limit: z.number().max(100).optional(), }) satisfies z.ZodType; diff --git a/packages/features/insights/server/routing-events.ts b/packages/features/insights/server/routing-events.ts index 2e55e1ec97..4f7deb382f 100644 --- a/packages/features/insights/server/routing-events.ts +++ b/packages/features/insights/server/routing-events.ts @@ -12,10 +12,12 @@ import { zodFields as routingFormFieldsSchema } from "@calcom/app-store/routing- import dayjs from "@calcom/dayjs"; import { ColumnFilterType } from "@calcom/features/data-table"; import { makeWhereClause, makeOrderBy } from "@calcom/features/data-table/lib/server"; -import type { ColumnFilter, TypedColumnFilter } from "@calcom/features/data-table/lib/types"; -import type { RoutingFormResponsesInput } from "@calcom/features/insights/server/raw-data.schema"; +import type { TypedColumnFilter } from "@calcom/features/data-table/lib/types"; +import type { + RoutingFormResponsesInput, + RoutingFormStatsInput, +} from "@calcom/features/insights/server/raw-data.schema"; import { readonlyPrisma as prisma } from "@calcom/prisma"; -import type { BookingStatus } from "@calcom/prisma/enums"; import { type ResponseValue, ZResponse } from "../lib/types"; @@ -27,17 +29,8 @@ type RoutingFormInsightsTeamFilter = { routingFormId?: string | null; }; -type RoutingFormInsightsFilter = RoutingFormInsightsTeamFilter & { - startDate?: string; - endDate?: string; - memberUserId?: number | null; - searchQuery?: string | null; - bookingStatus?: BookingStatus | "NO_BOOKING" | null; - fieldFilter?: { - fieldId: string; - optionId: string; - } | null; - columnFilters: ColumnFilter[]; +type RoutingFormStatsFilter = RoutingFormStatsInput & { + organizationId: number | null; }; type RoutingFormResponsesFilter = RoutingFormResponsesInput & { @@ -114,13 +107,11 @@ class RoutingEventsInsights { isAll, organizationId, routingFormId, - cursor, - limit, userId, memberUserIds, columnFilters, sorting, - }: RoutingFormResponsesFilter) { + }: RoutingFormStatsFilter) { const whereClause = await this.getWhereClauseForRoutingFormResponses({ teamId, startDate, @@ -128,8 +119,6 @@ class RoutingEventsInsights { isAll, organizationId, routingFormId, - cursor, - limit, userId, memberUserIds, columnFilters, @@ -288,8 +277,8 @@ class RoutingEventsInsights { isAll, organizationId, routingFormId, - cursor, limit, + offset, userId, memberUserIds, columnFilters, @@ -302,8 +291,6 @@ class RoutingEventsInsights { isAll, organizationId, routingFormId, - cursor, - limit, userId, memberUserIds, columnFilters, @@ -336,16 +323,14 @@ class RoutingEventsInsights { }, where: whereClause, orderBy: sorting && sorting.length > 0 ? makeOrderBy(sorting) : { createdAt: "desc" }, - take: limit ? limit + 1 : undefined, // Get one extra item to check if there are more pages - cursor: cursor ? { id: cursor } : undefined, + take: limit, + skip: offset, }); const [totalResponses, responses] = await Promise.all([totalResponsePromise, responsesPromise]); - const hasNextPage = responses.length > (limit ?? 0); - const responsesToReturn = responses.slice(0, limit ? limit : responses.length); type Response = Omit< - (typeof responsesToReturn)[number], + (typeof responses)[number], "response" | "responseLowercase" | "bookingAttendees" > & { response: Record; @@ -355,8 +340,7 @@ class RoutingEventsInsights { return { total: totalResponses, - data: responsesToReturn as Response[], - nextCursor: hasNextPage ? responsesToReturn[responsesToReturn.length - 1].id : undefined, + data: responses as Response[], }; } @@ -367,8 +351,8 @@ class RoutingEventsInsights { isAll, organizationId, routingFormId, - cursor, limit, + offset, userId, memberUserIds, columnFilters, @@ -388,10 +372,10 @@ class RoutingEventsInsights { isAll, organizationId, routingFormId, - cursor, userId, memberUserIds, limit, + offset, columnFilters, sorting, }); @@ -444,7 +428,7 @@ class RoutingEventsInsights { return { data: dataWithFlatResponse, - nextCursor: data.nextCursor, + total: data.total, }; } diff --git a/packages/features/insights/server/trpc-router.ts b/packages/features/insights/server/trpc-router.ts index 5f1280fa28..76d50b5d2b 100644 --- a/packages/features/insights/server/trpc-router.ts +++ b/packages/features/insights/server/trpc-router.ts @@ -6,6 +6,7 @@ import dayjs from "@calcom/dayjs"; import { rawDataInputSchema, routingFormResponsesInputSchema, + routingFormStatsInputSchema, } from "@calcom/features/insights/server/raw-data.schema"; import { randomString } from "@calcom/lib/random"; import type { readonlyPrisma } from "@calcom/prisma"; @@ -1574,7 +1575,7 @@ export const insightsRouter = router({ }); }), routingFormsByStatus: userBelongsToTeamProcedure - .input(routingFormResponsesInputSchema) + .input(routingFormStatsInputSchema) .query(async ({ ctx, input }) => { return await RoutingEventsInsights.getRoutingFormStats({ teamId: input.teamId, @@ -1583,10 +1584,8 @@ export const insightsRouter = router({ isAll: input.isAll, organizationId: ctx.user.organizationId ?? null, routingFormId: input.routingFormId, - cursor: input.cursor, userId: ctx.user.id, memberUserIds: input.memberUserIds, - limit: input.limit, columnFilters: input.columnFilters, sorting: input.sorting, }); @@ -1601,10 +1600,10 @@ export const insightsRouter = router({ isAll: input.isAll, organizationId: ctx.user.organizationId ?? null, routingFormId: input.routingFormId, - cursor: input.cursor, userId: ctx.user.id, memberUserIds: input.memberUserIds, limit: input.limit, + offset: input.offset, columnFilters: input.columnFilters, sorting: input.sorting, }); @@ -1619,10 +1618,10 @@ export const insightsRouter = router({ isAll: input.isAll, organizationId: ctx.user.organizationId ?? null, routingFormId: input.routingFormId, - cursor: input.cursor, userId: ctx.user.id, memberUserIds: input.memberUserIds, limit: input.limit ?? BATCH_SIZE, + offset: input.offset, columnFilters: input.columnFilters, sorting: input.sorting, });