Skip to content
181 changes: 44 additions & 137 deletions packages/features/insights/components/routing/RoutedToPerPeriod.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { type ReactNode, useMemo, useRef, useState } from "react";

import { DataTableSkeleton } from "@calcom/features/data-table";
import { downloadAsCsv } from "@calcom/lib/csvUtils";
import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver";
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc";
import type { RouterOutputs } from "@calcom/trpc/react";
Expand All @@ -26,46 +26,26 @@ import {
} from "@calcom/ui/components/table";
import { Tooltip } from "@calcom/ui/components/tooltip";

import { useInsightsParameters } from "../../hooks/useInsightsParameters";
import { useInsightsRoutingParameters } from "../../hooks/useInsightsRoutingParameters";
import { ChartCard } from "../ChartCard";

interface DownloadButtonProps {
teamId?: number;
userId?: number;
isAll?: boolean;
routingFormId?: string;
startDate: string;
endDate: string;
selectedPeriod: string;
selectedPeriod: "perDay" | "perWeek" | "perMonth";
searchQuery?: string;
}

function DownloadButton({
userId,
teamId,
isAll,
routingFormId,
startDate,
endDate,
selectedPeriod,
searchQuery,
}: DownloadButtonProps) {
function DownloadButton({ selectedPeriod, searchQuery }: DownloadButtonProps) {
const [isDownloading, setIsDownloading] = useState(false);
const routingParams = useInsightsRoutingParameters();
const utils = trpc.useContext();
const { t } = useLocale();

const handleDownload = async (e: React.MouseEvent) => {
e.preventDefault(); // Prevent default form submission

try {
const result = await utils.viewer.insights.routedToPerPeriodCsv.fetch({
userId,
teamId,
startDate,
endDate,
period: selectedPeriod as "perDay" | "perWeek" | "perMonth",
isAll,
routingFormId,
...routingParams,
period: selectedPeriod,
searchQuery: searchQuery || undefined,
});

Expand Down Expand Up @@ -94,32 +74,14 @@ function DownloadButton({
}

interface FormCardProps {
selectedPeriod: string;
onPeriodChange: (value: string) => void;
selectedPeriod: "perDay" | "perWeek" | "perMonth";
onPeriodChange: (value: "perDay" | "perWeek" | "perMonth") => void;
searchQuery: string;
onSearchChange: (value: string) => void;
children: ReactNode;
teamId?: number;
userId?: number;
isAll?: boolean;
routingFormId?: string;
startDate: string;
endDate: string;
}

function FormCard({
selectedPeriod,
onPeriodChange,
searchQuery,
onSearchChange,
children,
teamId,
userId,
isAll,
routingFormId,
startDate,
endDate,
}: FormCardProps) {
function FormCard({ selectedPeriod, onPeriodChange, searchQuery, onSearchChange, children }: FormCardProps) {
const { t } = useLocale();

return (
Expand All @@ -135,7 +97,7 @@ function FormCard({
]}
className="w-fit"
value={selectedPeriod}
onValueChange={(value) => value && onPeriodChange(value)}
onValueChange={(value) => value && onPeriodChange(value as "perDay" | "perWeek" | "perMonth")}
/>
<div className="flex gap-2">
<div className="w-64">
Expand All @@ -147,16 +109,7 @@ function FormCard({
className="w-full"
/>
</div>
<DownloadButton
userId={userId}
teamId={teamId}
isAll={isAll}
routingFormId={routingFormId}
startDate={startDate}
endDate={endDate}
selectedPeriod={selectedPeriod}
searchQuery={searchQuery}
/>
<DownloadButton selectedPeriod={selectedPeriod} searchQuery={searchQuery} />
</div>
</div>
{children}
Expand Down Expand Up @@ -215,87 +168,56 @@ const getPerformanceBadge = (performance: RoutedToTableRow["performance"], t: TF

export function RoutedToPerPeriod() {
const { t } = useLocale();
const { userId, teamId, startDate, endDate, isAll, routingFormId } = useInsightsParameters();
const [selectedPeriod, setSelectedPeriod] = useQueryState("selectedPeriod", {
defaultValue: "perWeek",
});
const routingParams = useInsightsRoutingParameters();
const [selectedPeriod, setSelectedPeriod] = useState<"perDay" | "perWeek" | "perMonth">("perWeek");
const [searchQuery, setSearchQuery] = useQueryState("search", {
defaultValue: "",
});

const { ref: loadMoreRef } = useInViewObserver(() => {
if (hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
});

const tableContainerRef = useRef<HTMLDivElement>(null);

const { data, fetchNextPage, isFetchingNextPage, hasNextPage, isLoading } =
trpc.viewer.insights.routedToPerPeriod.useInfiniteQuery(
{
userId,
teamId,
startDate,
endDate,
period: selectedPeriod as "perDay" | "perWeek" | "perMonth",
isAll,
routingFormId,
searchQuery: searchQuery || undefined,
limit: 10,
const debouncedSearchQuery = useDebounce(searchQuery, 500);

const { data, isLoading } = trpc.viewer.insights.routedToPerPeriod.useQuery(
{
...routingParams,
period: selectedPeriod,
searchQuery: debouncedSearchQuery || undefined,
},
{
staleTime: 30000,
refetchOnWindowFocus: false,
trpc: {
context: { skipBatch: true },
},
{
getNextPageParam: (lastPage) => {
if (!lastPage.users.nextCursor && !lastPage.periodStats.nextCursor) {
return undefined;
}

return {
userCursor: lastPage.users.nextCursor,
periodCursor: lastPage.periodStats.nextCursor,
};
},
}
);
}
);

const flattenedUsers = useMemo(() => {
const userMap = new Map();
data?.pages.forEach((page) => {
page.users.data.forEach((user) => {
if (!userMap.has(user.id)) {
userMap.set(user.id, user);
}
});
});
return Array.from(userMap.values());
}, [data?.pages]);
return data?.users.data || [];
}, [data?.users.data]);

const uniquePeriods = useMemo(() => {
if (!data?.pages) return [];
if (!data?.periodStats.data) return [];

// Get all unique periods from all pages
// Get all unique periods
const periods = new Set<string>();
data.pages.forEach((page) => {
page.periodStats.data.forEach((stat) => {
periods.add(stat.period_start.toISOString());
});
data.periodStats.data.forEach((stat) => {
periods.add(stat.period_start.toISOString());
});

return Array.from(periods)
.map((dateStr) => new Date(dateStr))
.sort((a, b) => a.getTime() - b.getTime());
}, [data?.pages]);
}, [data?.periodStats.data]);

const processedData = useMemo(() => {
if (!data?.pages) return [];
if (!data?.periodStats.data) return [];

// Create a map for quick lookup of stats
const statsMap = new Map<string, number>();
data.pages.forEach((page) => {
page.periodStats.data.forEach((stat) => {
const key = `${stat.userId}-${stat.period_start.toISOString()}`;
statsMap.set(key, stat.total);
});
data.periodStats.data.forEach((stat) => {
const key = `${stat.userId}-${stat.period_start.toISOString()}`;
statsMap.set(key, stat.total);
});

return flattenedUsers.map((user) => {
Expand All @@ -314,7 +236,7 @@ export function RoutedToPerPeriod() {
totalBookings: user.totalBookings,
};
});
}, [data?.pages, flattenedUsers, uniquePeriods]);
}, [data?.periodStats.data, flattenedUsers, uniquePeriods]);

if (isLoading) {
return (
Expand All @@ -327,13 +249,7 @@ export function RoutedToPerPeriod() {
selectedPeriod={selectedPeriod}
onPeriodChange={setSelectedPeriod}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
userId={userId}
teamId={teamId}
isAll={isAll}
routingFormId={routingFormId}
startDate={startDate}
endDate={endDate}>
onSearchChange={setSearchQuery}>
<div className="mt-6">
<DataTableSkeleton columns={5} columnWidths={[200, 120, 120, 120, 120]} />
</div>
Expand Down Expand Up @@ -368,13 +284,7 @@ export function RoutedToPerPeriod() {
selectedPeriod={selectedPeriod}
onPeriodChange={setSelectedPeriod}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
userId={userId}
teamId={teamId}
isAll={isAll}
routingFormId={routingFormId}
startDate={startDate}
endDate={endDate}>
onSearchChange={setSearchQuery}>
<div className="mt-6">
<div
className="scrollbar-thin border-subtle relative overflow-auto rounded-md border"
Expand Down Expand Up @@ -405,10 +315,7 @@ export function RoutedToPerPeriod() {
<TableBody className="relative">
{processedData.map((row, index) => {
return (
<TableRow
key={row.id}
ref={index === processedData.length - 1 ? loadMoreRef : undefined}
className="divide-muted divide-x">
<TableRow key={row.id} className="divide-muted divide-x">
<TableCell className="bg-default w-[200px]">
<HoverCard>
<HoverCardTrigger asChild>
Expand Down
11 changes: 11 additions & 0 deletions packages/features/insights/server/raw-data.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ export const routingRepositoryBaseInputSchema = z.object({
columnFilters: z.array(ZColumnFilter).optional(),
});

export const routedToPerPeriodInputSchema = routingRepositoryBaseInputSchema.extend({
period: z.enum(["perDay", "perWeek", "perMonth"]),
limit: z.number().int().min(1).max(100).default(10),
searchQuery: z.string().trim().min(1).optional(),
});

export const routedToPerPeriodCsvInputSchema = routingRepositoryBaseInputSchema.extend({
period: z.enum(["perDay", "perWeek", "perMonth"]),
searchQuery: z.string().trim().min(1).optional(),
});

export const bookingRepositoryBaseInputSchema = z.object({
scope: z.union([z.literal("user"), z.literal("team"), z.literal("org")]),
selectedTeamId: z.number().optional(),
Expand Down
Loading
Loading