"use client"; import { keepPreviousData } from "@tanstack/react-query"; import { useReactTable, getCoreRowModel, getFilteredRowModel, getSortedRowModel, createColumnHelper, } from "@tanstack/react-table"; import Link from "next/link"; import { useRef, useMemo, useId } from "react"; import dayjs from "@calcom/dayjs"; import classNames from "@calcom/lib/classNames"; import { useCopy } from "@calcom/lib/hooks/useCopy"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { BookingStatus } from "@calcom/prisma/enums"; import { trpc, type RouterOutputs } from "@calcom/trpc"; import { DataTable, useFetchMoreOnBottomReached, Badge, Avatar, Icon, HoverCard, HoverCardContent, HoverCardTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@calcom/ui"; import type { BadgeProps } from "@calcom/ui/components/badge/Badge"; import { useFilterContext } from "../context/provider"; type RoutingFormResponse = RouterOutputs["viewer"]["insights"]["routingFormResponses"]["data"][number]; type RoutingFormTableRow = { id: number; formName: string; createdAt: Date; routedToBooking: RoutingFormResponse["routedToBooking"]; [key: string]: any; }; function CellWithOverflowX({ children, className }: { children: React.ReactNode; className?: string }) { return (
{ // Only add shadow if the scroll width is greater than 200px if (!el) return; const nextElement = el.nextElementSibling; if (!nextElement) return; if (el.scrollWidth > 200) { nextElement.classList.remove("hidden"); } else { nextElement.classList.add("hidden"); } }}> {children}
); } // Upper case the first letter of each word and replace underscores with spaces function bookingStatusToText(status: BookingStatus) { return status .toLowerCase() .replace(/_/g, " ") .replace(/\b\w/g, (c) => c.toUpperCase()); } function BookedByCell({ attendees, rowId, }: { attendees: { email: string; timeZone: string }[] | undefined; rowId: number; }) { const cellId = useId(); if (!attendees || attendees.length === 0) return
; return (
{attendees.map((attendee) => ( {attendee.email} ))}
); } function ResponseValueCell({ value, rowId }: { value: string[]; rowId: number }) { const cellId = useId(); if (value.length === 0) return
; return ( {value.length > 2 ? ( <> {value.slice(0, 2).map((v: string, i: number) => ( {v} ))} +{value.length - 2}
{value.slice(2).map((v: string, i: number) => ( {v} ))}
) : ( value.map((v: string, i: number) => ( {v} )) )}
); } function BookingStatusBadge({ booking }: { booking: RoutingFormResponse["routedToBooking"] }) { let badgeVariant: BadgeProps["variant"] = "success"; if (!booking) return null; switch (booking.status) { case BookingStatus.REJECTED: case BookingStatus.AWAITING_HOST: case BookingStatus.PENDING: case BookingStatus.CANCELLED: badgeVariant = "warning"; break; } return {bookingStatusToText(booking.status)}; } function BookingAtCell({ booking, rowId, copyToClipboard, t, }: { booking: RoutingFormResponse["routedToBooking"]; rowId: number; copyToClipboard: (text: string) => void; t: (key: string) => string; }) { const cellId = useId(); if (!booking || !booking.user) { return
; } return (
{dayjs(booking.createdAt).format("MMM D, YYYY HH:mm")}

{booking.user.name}

{booking.user.email}

Status:
); } export type RoutingFormTableType = ReturnType>; export function RoutingFormResponsesTable({ children, }: { children?: React.ReactNode | ((table: RoutingFormTableType) => React.ReactNode); }) { const { t } = useLocale(); const { filter } = useFilterContext(); const tableContainerRef = useRef(null); const { copyToClipboard, isCopied } = useCopy(); const { dateRange, selectedTeamId, isAll, initialConfig, selectedRoutingFormId, selectedMemberUserId, selectedBookingStatus, selectedRoutingFormFilter, } = filter; const initialConfigIsReady = !!(initialConfig?.teamId || initialConfig?.userId || initialConfig?.isAll); const [startDate, endDate] = dateRange; const { data: headers, isLoading: isHeadersLoading } = trpc.viewer.insights.routingFormResponsesHeaders.useQuery( { teamId: selectedTeamId ?? undefined, isAll: isAll ?? false, routingFormId: selectedRoutingFormId ?? undefined, }, { enabled: initialConfigIsReady, } ); const { data, fetchNextPage, isFetching, hasNextPage, isLoading } = trpc.viewer.insights.routingFormResponses.useInfiniteQuery( { teamId: selectedTeamId, startDate: startDate.toISOString(), endDate: endDate.toISOString(), userId: selectedMemberUserId ?? undefined, isAll: isAll ?? false, routingFormId: selectedRoutingFormId ?? undefined, bookingStatus: selectedBookingStatus ?? undefined, fieldFilter: selectedRoutingFormFilter ?? undefined, limit: 10, }, { getNextPageParam: (lastPage) => lastPage.nextCursor, placeholderData: keepPreviousData, trpc: { context: { skipBatch: true }, }, enabled: initialConfigIsReady, } ); const flatData = useMemo(() => data?.pages?.flatMap((page) => page.data) ?? [], [data]); const totalDBRowCount = data?.pages?.[0]?.total ?? 0; const totalFetched = flatData.length; const processedData = useMemo(() => { if (isHeadersLoading) return []; return flatData.map((response) => { const row: RoutingFormTableRow = { id: response.id, formName: response.form.name, formId: response.form.id, createdAt: response.createdAt, routedToBooking: response.routedToBooking, }; Object.entries(response.response).forEach(([fieldId, field]) => { const header = headers?.find((h) => h.id === fieldId); if (header?.options) { if (Array.isArray(field.value)) { // Map the IDs to their corresponding labels for array values const labels = field.value.map((id) => { const option = header.options?.find((opt) => opt.id === id); return option?.label ?? id; }); row[fieldId] = labels; } else { // Handle single value case const option = header.options?.find((opt) => opt.id === field.value); row[fieldId] = option?.label ?? field.value; } } else { row[fieldId] = field.value; } }); return row; }); }, [flatData, headers, isHeadersLoading]); const columnHelper = createColumnHelper(); const columns = useMemo( () => [ columnHelper.accessor("bookedAttendees", { id: "bookedBy", header: t("routing_form_insights_booked_by"), size: 200, cell: (info) => { const row = info.row.original; return ; }, }), ...(headers?.map((header) => { return columnHelper.accessor(header.id, { id: header.id, header: header.label, size: 200, cell: (info) => { let value = info.getValue(); value = Array.isArray(value) ? value : [value]; return (
); }, }); }) ?? []), columnHelper.accessor("routedToBooking", { id: "bookingStatus", header: t("routing_form_insights_booking_status"), size: 250, cell: (info) => (
), }), columnHelper.accessor("routedToBooking", { id: "bookingAt", header: t("routing_form_insights_booking_at"), size: 250, cell: (info) => (
), }), columnHelper.accessor("createdAt", { id: "submittedAt", header: t("routing_form_insights_submitted_at"), size: 250, cell: (info) => (
{dayjs(info.getValue()).format("MMM D, YYYY HH:mm")}
), }), ], [headers, t, copyToClipboard] ); const table = useReactTable({ data: processedData, columns, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getSortedRowModel: getSortedRowModel(), defaultColumn: { size: 200, }, }); const fetchMoreOnBottomReached = useFetchMoreOnBottomReached( tableContainerRef, fetchNextPage, isFetching, totalFetched, totalDBRowCount ); if (isHeadersLoading || ((isFetching || isLoading) && !data)) { return (
{[...Array(4)].map((_, index) => (
))} {[...Array(10)].map((_, rowIndex) => ( {[...Array(4)].map((_, colIndex) => (
))} ))}
); } return (
{ if (hasNextPage) { fetchMoreOnBottomReached(e.target as HTMLDivElement); } }} isPending={isFetching && !data}> {typeof children === "function" ? children(table) : children}
); }