"use client";
import type { TFunction } from "i18next";
import { useQueryState } from "nuqs";
import { type ReactNode, useMemo, useRef, useState } from "react";
import posthog from "posthog-js";
import { downloadAsCsv } from "@calcom/lib/csvUtils";
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react";
import classNames from "@calcom/ui/classNames";
import { Avatar } from "@calcom/ui/components/avatar";
import { Badge } from "@calcom/ui/components/badge";
import { Button } from "@calcom/ui/components/button";
import { ToggleGroup, Input } from "@calcom/ui/components/form";
import { HoverCard, HoverCardContent, HoverCardTrigger } from "@calcom/ui/components/hover-card";
import {
TableNew,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@calcom/ui/components/table";
import { Tooltip } from "@calcom/ui/components/tooltip";
import { useInsightsRoutingParameters } from "@calcom/web/modules/insights/hooks/useInsightsRoutingParameters";
import { ChartCard } from "../ChartCard";
interface DownloadButtonProps {
selectedPeriod: "perDay" | "perWeek" | "perMonth";
searchQuery?: string;
}
function DownloadButton({ selectedPeriod, searchQuery }: DownloadButtonProps) {
const [isDownloading, setIsDownloading] = useState(false);
const routingParams = useInsightsRoutingParameters();
const utils = trpc.useContext();
const handleDownload = async (e: React.MouseEvent) => {
e.preventDefault(); // Prevent default form submission
try {
posthog.capture("insights_routing_download_clicked", { teamId: routingParams.selectedTeamId });
const result = await utils.viewer.insights.routedToPerPeriodCsv.fetch({
...routingParams,
period: selectedPeriod,
searchQuery: searchQuery || undefined,
});
if (!result?.data) {
throw new Error("No data received");
}
downloadAsCsv(result.data, result.filename || "routing-data.csv");
} catch (error) {
console.error("Download failed:", error);
} finally {
setIsDownloading(false);
}
};
return (
);
}
interface FormCardProps {
selectedPeriod: "perDay" | "perWeek" | "perMonth";
onPeriodChange: (value: "perDay" | "perWeek" | "perMonth") => void;
searchQuery: string;
onSearchChange: (value: string) => void;
children: ReactNode;
}
function FormCard({ selectedPeriod, onPeriodChange, searchQuery, onSearchChange, children }: FormCardProps) {
const { t } = useLocale();
return (
value && onPeriodChange(value as "perDay" | "perWeek" | "perMonth")}
/>
{children}
);
}
type RoutedToTableRow = RouterOutputs["viewer"]["insights"]["routedToPerPeriod"]["users"]["data"][number];
const getPerformanceBadge = (performance: RoutedToTableRow["performance"], t: TFunction) => {
switch (performance) {
case "above_average":
return (
{t("above_average")}
);
case "below_average":
return (
{t("below_average")}
);
case "median":
return (
{t("median")}
);
case "at_average":
return (
{t("at_average")}
);
default:
return (
{t("no_data")}
);
}
};
export function RoutedToPerPeriod() {
const { t } = useLocale();
const routingParams = useInsightsRoutingParameters();
const [selectedPeriod, setSelectedPeriod] = useState<"perDay" | "perWeek" | "perMonth">("perWeek");
const [searchQuery, setSearchQuery] = useQueryState("search", {
defaultValue: "",
});
const tableContainerRef = useRef(null);
const debouncedSearchQuery = useDebounce(searchQuery, 500);
const { data, isLoading, isError } = trpc.viewer.insights.routedToPerPeriod.useQuery(
{
...routingParams,
period: selectedPeriod,
searchQuery: debouncedSearchQuery || undefined,
},
{
staleTime: 30000,
refetchOnWindowFocus: false,
trpc: {
context: { skipBatch: true },
},
}
);
const flattenedUsers = useMemo(() => {
return data?.users.data || [];
}, [data?.users.data]);
const uniquePeriods = useMemo(() => {
if (!data?.periodStats.data) return [];
// Get all unique periods
const periods = new Set();
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?.periodStats.data]);
const processedData = useMemo(() => {
if (!data?.periodStats.data) return [];
// Create a map for quick lookup of stats
const statsMap = new Map();
data.periodStats.data.forEach((stat) => {
const key = `${stat.userId}-${stat.period_start.toISOString()}`;
statsMap.set(key, stat.total);
});
return flattenedUsers.map((user) => {
const stats = uniquePeriods.reduce(
(acc, period) => {
const key = `${user.id}-${period.toISOString()}`;
acc[period.toISOString()] = statsMap.get(key) ?? 0;
return acc;
},
{} as { [key: string]: number }
);
return {
id: user.id,
name: user.name || "",
avatarUrl: user.avatarUrl,
stats,
performance: user.performance,
totalBookings: user.totalBookings,
};
});
}, [data?.periodStats.data, flattenedUsers, uniquePeriods]);
const isCurrentPeriod = (date: Date, today: Date, selectedPeriod: string): boolean => {
if (selectedPeriod === "perDay") {
return (
date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear()
);
} else if (selectedPeriod === "perWeek") {
const weekStart = new Date(today);
weekStart.setDate(today.getDate() - today.getDay());
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
return date >= weekStart && date <= weekEnd;
} else if (selectedPeriod === "perMonth") {
return date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear();
}
return false;
};
return (
{t("user")}
{uniquePeriods.map((period) => {
const date = period;
const today = new Date();
const isCurrent = isCurrentPeriod(date, today, selectedPeriod);
return (
{date.toLocaleDateString()}
);
})}
{processedData.map((row) => {
return (
{row.name}
{t("total_bookings_per_period")}: {row.totalBookings}
{t("status")}: {getPerformanceBadge(row.performance, t)}
{uniquePeriods.map((period) => {
return (
{row.stats[period.toISOString()] === 0 ? null : (
<>{row.stats[period.toISOString()]}>
)}
);
})}
);
})}
);
}