fix: stats on /insights/routing (#18985)
* fix: replace filter implementation on /insights/routing * remove unused functions * fix type error * refactor: extract insights parameters * remove FiltersProvider
This commit is contained in:
@@ -1,35 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { DataTableProvider } from "@calcom/features/data-table";
|
||||
import {
|
||||
FailedBookingsByField,
|
||||
RoutingFormResponsesTable,
|
||||
FailedBookingsByField,
|
||||
RoutedToPerPeriod,
|
||||
} from "@calcom/features/insights/components";
|
||||
import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider";
|
||||
import { InsightsOrgTeamsProvider } from "@calcom/features/insights/context/InsightsOrgTeamsProvider";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
export default function InsightsRoutingFormResponsesPage() {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<FiltersProvider>
|
||||
<div className="mb-4 space-y-4">
|
||||
<RoutingFormResponsesTable />
|
||||
<DataTableProvider>
|
||||
<InsightsOrgTeamsProvider>
|
||||
<div className="mb-4 space-y-4">
|
||||
<RoutingFormResponsesTable />
|
||||
|
||||
<RoutedToPerPeriod />
|
||||
<RoutedToPerPeriod />
|
||||
|
||||
<FailedBookingsByField />
|
||||
<FailedBookingsByField />
|
||||
|
||||
<small className="text-default block text-center">
|
||||
{t("looking_for_more_insights")}{" "}
|
||||
<a
|
||||
className="text-blue-500 hover:underline"
|
||||
href="mailto:updates@cal.com?subject=Feature%20Request%3A%20More%20Analytics&body=Hey%20Cal.com%20Team%2C%20I%20love%20the%20analytics%20page%20but%20I%20am%20looking%20for%20...">
|
||||
{" "}
|
||||
{t("contact_support")}
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</FiltersProvider>
|
||||
<small className="text-default block text-center">
|
||||
{t("looking_for_more_insights")}{" "}
|
||||
<a
|
||||
className="text-blue-500 hover:underline"
|
||||
href="mailto:updates@cal.com?subject=Feature%20Request%3A%20More%20Analytics&body=Hey%20Cal.com%20Team%2C%20I%20love%20the%20analytics%20page%20but%20I%20am%20looking%20for%20...">
|
||||
{" "}
|
||||
{t("contact_support")}
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</InsightsOrgTeamsProvider>
|
||||
</DataTableProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,6 +114,19 @@ export function DataTable<TData, TValue>({
|
||||
...rest.style,
|
||||
}}
|
||||
data-testid={testId ?? "data-table"}>
|
||||
{/*
|
||||
Invalidate left & right properties for <= sm screen size,
|
||||
because we pin columns only for >= sm screen sizes.
|
||||
*/}
|
||||
<style jsx global>{`
|
||||
@media (max-width: 640px) {
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
left: initial !important;
|
||||
right: initial !important;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
<div
|
||||
ref={tableContainerRef}
|
||||
onScroll={onScroll}
|
||||
@@ -124,7 +137,7 @@ export function DataTable<TData, TValue>({
|
||||
)}
|
||||
style={{ gridArea: "body" }}>
|
||||
<TableNew
|
||||
className="grid border-0"
|
||||
className="data-table grid border-0"
|
||||
style={{
|
||||
...columnSizingVars,
|
||||
...(Boolean(enableColumnResizing) && { width: table.getTotalSize() }),
|
||||
@@ -135,7 +148,6 @@ export function DataTable<TData, TValue>({
|
||||
<TableRow key={headerGroup.id} className="hover:bg-subtle flex w-full">
|
||||
{headerGroup.headers.map((header) => {
|
||||
const { column } = header;
|
||||
const meta = column.columnDef.meta;
|
||||
return (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
|
||||
@@ -84,7 +84,7 @@ export function DataTableWrapper<TData, TValue>({
|
||||
className={className}
|
||||
containerClassName={containerClassName}
|
||||
onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}>
|
||||
{(ToolbarLeft || ToolbarRight) && (
|
||||
{(ToolbarLeft || ToolbarRight || children) && (
|
||||
<DataTableToolbar.Root>
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<div className="flex w-full flex-wrap justify-between gap-2">
|
||||
|
||||
@@ -111,6 +111,20 @@ export const numberFilter = (cellValue: unknown, filterValue: NumberFilterValue)
|
||||
return false;
|
||||
};
|
||||
|
||||
export const dateRangeFilter = (cellValue: unknown, filterValue: DateRangeFilterValue) => {
|
||||
if (!(cellValue instanceof Date)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!filterValue.data.startDate || !filterValue.data.endDate) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const cellValueStr = cellValue.toISOString();
|
||||
|
||||
return filterValue.data.startDate <= cellValueStr && filterValue.data.endDate >= cellValueStr;
|
||||
};
|
||||
|
||||
export const isNumberFilterValue = (filterValue: unknown): filterValue is NumberFilterValue => {
|
||||
return ZNumberFilterValue.safeParse(filterValue).success;
|
||||
};
|
||||
@@ -128,6 +142,8 @@ export const dataTableFilter = (cellValue: unknown, filterValue: FilterValue) =>
|
||||
return textFilter(cellValue, filterValue);
|
||||
} else if (isNumberFilterValue(filterValue)) {
|
||||
return numberFilter(cellValue, filterValue);
|
||||
} else if (isDateRangeFilterValue(filterValue)) {
|
||||
return dateRangeFilter(cellValue, filterValue);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { classNames } from "@calcom/lib";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { ToggleGroup } from "@calcom/ui";
|
||||
|
||||
import { useFilterContext } from "../context/provider";
|
||||
import { useInsightsParameters } from "../hooks/useInsightsParameters";
|
||||
import { BarList } from "./tremor/BarList";
|
||||
|
||||
interface FormCardProps {
|
||||
@@ -55,21 +55,13 @@ function FormCard({ formName, fields }: FormCardProps) {
|
||||
}
|
||||
|
||||
export function FailedBookingsByField() {
|
||||
const { filter } = useFilterContext();
|
||||
const { selectedTeamId, selectedUserId, isAll, initialConfig, selectedRoutingFormId } = filter;
|
||||
const initialConfigIsReady = !!(initialConfig?.teamId || initialConfig?.userId || initialConfig?.isAll);
|
||||
|
||||
const { data } = trpc.viewer.insights.failedBookingsByField.useQuery(
|
||||
{
|
||||
userId: selectedUserId ?? undefined,
|
||||
teamId: selectedTeamId ?? undefined,
|
||||
isAll: !!isAll,
|
||||
routingFormId: selectedRoutingFormId ?? undefined,
|
||||
},
|
||||
{
|
||||
enabled: initialConfigIsReady,
|
||||
}
|
||||
);
|
||||
const { userId, teamId, startDate, endDate, isAll, routingFormId } = useInsightsParameters();
|
||||
const { data } = trpc.viewer.insights.failedBookingsByField.useQuery({
|
||||
userId,
|
||||
teamId,
|
||||
isAll,
|
||||
routingFormId,
|
||||
});
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { TFunction } from "next-i18next";
|
||||
import { useQueryState } from "nuqs";
|
||||
import { type ReactNode, useMemo, useRef, useState } from "react";
|
||||
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import { DataTableSkeleton } from "@calcom/features/data-table";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { downloadAsCsv } from "@calcom/lib/csvUtils";
|
||||
@@ -29,14 +28,15 @@ import {
|
||||
TableRow,
|
||||
} from "@calcom/ui/components/table/TableNew";
|
||||
|
||||
import { useFilterContext } from "../context/provider";
|
||||
import { useInsightsParameters } from "../hooks/useInsightsParameters";
|
||||
|
||||
interface DownloadButtonProps {
|
||||
teamId?: number;
|
||||
userId?: number;
|
||||
isAll?: boolean;
|
||||
routingFormId?: string;
|
||||
dateRange: [Dayjs, Dayjs, string | null];
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
selectedPeriod: string;
|
||||
searchQuery?: string;
|
||||
}
|
||||
@@ -46,7 +46,8 @@ function DownloadButton({
|
||||
teamId,
|
||||
isAll,
|
||||
routingFormId,
|
||||
dateRange,
|
||||
startDate,
|
||||
endDate,
|
||||
selectedPeriod,
|
||||
searchQuery,
|
||||
}: DownloadButtonProps) {
|
||||
@@ -59,13 +60,13 @@ function DownloadButton({
|
||||
|
||||
try {
|
||||
const result = await utils.viewer.insights.routedToPerPeriodCsv.fetch({
|
||||
userId: userId ?? undefined,
|
||||
teamId: teamId ?? undefined,
|
||||
startDate: dateRange[0]?.toISOString() ?? "",
|
||||
endDate: dateRange[1]?.toISOString() ?? "",
|
||||
userId,
|
||||
teamId,
|
||||
startDate,
|
||||
endDate,
|
||||
period: selectedPeriod as "perDay" | "perWeek" | "perMonth",
|
||||
isAll: !!isAll,
|
||||
routingFormId: routingFormId ?? undefined,
|
||||
isAll,
|
||||
routingFormId,
|
||||
searchQuery: searchQuery || undefined,
|
||||
});
|
||||
|
||||
@@ -103,7 +104,8 @@ interface FormCardProps {
|
||||
userId?: number;
|
||||
isAll?: boolean;
|
||||
routingFormId?: string;
|
||||
dateRange: [Dayjs, Dayjs, string | null];
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
}
|
||||
|
||||
function FormCard({
|
||||
@@ -116,7 +118,8 @@ function FormCard({
|
||||
userId,
|
||||
isAll,
|
||||
routingFormId,
|
||||
dateRange,
|
||||
startDate,
|
||||
endDate,
|
||||
}: FormCardProps) {
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -150,7 +153,8 @@ function FormCard({
|
||||
teamId={teamId}
|
||||
isAll={isAll}
|
||||
routingFormId={routingFormId}
|
||||
dateRange={dateRange}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
selectedPeriod={selectedPeriod}
|
||||
searchQuery={searchQuery}
|
||||
/>
|
||||
@@ -219,8 +223,7 @@ const getPerformanceBadge = (performance: RoutedToTableRow["performance"], t: TF
|
||||
|
||||
export function RoutedToPerPeriod() {
|
||||
const { t } = useLocale();
|
||||
const { filter } = useFilterContext();
|
||||
const { selectedTeamId, selectedUserId, isAll, selectedRoutingFormId, dateRange } = filter;
|
||||
const { userId, teamId, startDate, endDate, isAll, routingFormId } = useInsightsParameters();
|
||||
const [selectedPeriod, setSelectedPeriod] = useQueryState("selectedPeriod", {
|
||||
defaultValue: "perWeek",
|
||||
});
|
||||
@@ -239,13 +242,13 @@ export function RoutedToPerPeriod() {
|
||||
const { data, fetchNextPage, isFetchingNextPage, hasNextPage, isLoading } =
|
||||
trpc.viewer.insights.routedToPerPeriod.useInfiniteQuery(
|
||||
{
|
||||
userId: selectedUserId ?? undefined,
|
||||
teamId: selectedTeamId ?? undefined,
|
||||
startDate: dateRange[0]?.toISOString() ?? "",
|
||||
endDate: dateRange[1]?.toISOString() ?? "",
|
||||
userId,
|
||||
teamId,
|
||||
startDate,
|
||||
endDate,
|
||||
period: selectedPeriod as "perDay" | "perWeek" | "perMonth",
|
||||
isAll: !!isAll,
|
||||
routingFormId: selectedRoutingFormId ?? undefined,
|
||||
isAll,
|
||||
routingFormId,
|
||||
searchQuery: searchQuery || undefined,
|
||||
limit: 10,
|
||||
},
|
||||
@@ -260,7 +263,6 @@ export function RoutedToPerPeriod() {
|
||||
periodCursor: lastPage.periodStats.nextCursor,
|
||||
};
|
||||
},
|
||||
enabled: !!dateRange[0] && !!dateRange[1],
|
||||
}
|
||||
);
|
||||
|
||||
@@ -334,11 +336,12 @@ export function RoutedToPerPeriod() {
|
||||
onPeriodChange={setSelectedPeriod}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
userId={selectedUserId ?? undefined}
|
||||
teamId={selectedTeamId ?? undefined}
|
||||
userId={userId}
|
||||
teamId={teamId}
|
||||
isAll={isAll}
|
||||
routingFormId={selectedRoutingFormId ?? undefined}
|
||||
dateRange={dateRange}>
|
||||
routingFormId={routingFormId}
|
||||
startDate={startDate}
|
||||
endDate={endDate}>
|
||||
<div className="mt-6">
|
||||
<DataTableSkeleton columns={5} columnWidths={[200, 120, 120, 120, 120]} />
|
||||
</div>
|
||||
@@ -377,11 +380,12 @@ export function RoutedToPerPeriod() {
|
||||
onPeriodChange={setSelectedPeriod}
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
userId={selectedUserId ?? undefined}
|
||||
teamId={selectedTeamId ?? undefined}
|
||||
userId={userId}
|
||||
teamId={teamId}
|
||||
isAll={isAll}
|
||||
routingFormId={selectedRoutingFormId ?? undefined}
|
||||
dateRange={dateRange}>
|
||||
routingFormId={routingFormId}
|
||||
startDate={startDate}
|
||||
endDate={endDate}>
|
||||
<div className="mt-6">
|
||||
<div
|
||||
className="scrollbar-thin border-subtle relaitve relative h-[80dvh] overflow-auto rounded-md border"
|
||||
|
||||
@@ -10,26 +10,17 @@ import {
|
||||
} from "@tanstack/react-table";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import startCase from "lodash/startCase";
|
||||
import { useSession } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { useMemo, useId, useState } from "react";
|
||||
import { useMemo, useId } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import {
|
||||
DataTableWrapper,
|
||||
DataTableFilters,
|
||||
DataTableProvider,
|
||||
DataTableSkeleton,
|
||||
useColumnFilters,
|
||||
useFilterValue,
|
||||
multiSelectFilter,
|
||||
textFilter,
|
||||
dataTableFilter,
|
||||
useDataTable,
|
||||
ZDateRangeFilterValue,
|
||||
ZMultiSelectFilterValue,
|
||||
ZSingleSelectFilterValue,
|
||||
DateRangeFilter,
|
||||
ColumnFilterType,
|
||||
type FilterableColumn,
|
||||
@@ -52,8 +43,8 @@ import {
|
||||
} from "@calcom/ui";
|
||||
|
||||
import { RoutingFormResponsesDownload } from "../filters/Download";
|
||||
import type { OrgTeamsType } from "../filters/OrgTeamsFilter";
|
||||
import { OrgTeamsFilter } from "../filters/OrgTeamsFilter";
|
||||
import { useInsightsParameters } from "../hooks/useInsightsParameters";
|
||||
import {
|
||||
ZResponseMultipleValues,
|
||||
ZResponseSingleValue,
|
||||
@@ -254,41 +245,17 @@ function BookingAtCell({
|
||||
|
||||
export type RoutingFormTableType = ReturnType<typeof useReactTable<RoutingFormTableRow>>;
|
||||
|
||||
export function RoutingFormResponsesTable() {
|
||||
return (
|
||||
<DataTableProvider>
|
||||
<RoutingFormResponsesTableContent />
|
||||
</DataTableProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const createdAtColumn: Extract<FilterableColumn, { type: ColumnFilterType.DATE_RANGE }> = {
|
||||
id: "createdAt",
|
||||
title: "createdAt",
|
||||
type: ColumnFilterType.DATE_RANGE,
|
||||
};
|
||||
|
||||
export function RoutingFormResponsesTableContent() {
|
||||
export function RoutingFormResponsesTable() {
|
||||
const { t } = useLocale();
|
||||
const { copyToClipboard } = useCopy();
|
||||
const session = useSession();
|
||||
const currentOrgId = session.data?.user.org?.id;
|
||||
const [orgTeamsType, setOrgTeamsType] = useState<OrgTeamsType>(currentOrgId ? "org" : "yours");
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<number | undefined>();
|
||||
|
||||
const columnFilters = useColumnFilters({ exclude: ["bookingUserId", "formId", "createdAt"] });
|
||||
|
||||
const isAll = orgTeamsType === "org";
|
||||
const teamId = orgTeamsType === "team" ? selectedTeamId : undefined;
|
||||
const userId = orgTeamsType === "yours" ? session.data?.user.id : undefined;
|
||||
|
||||
const memberUserIds = useFilterValue("bookingUserId", ZMultiSelectFilterValue)?.data as
|
||||
| number[]
|
||||
| undefined;
|
||||
const routingFormId = useFilterValue("formId", ZSingleSelectFilterValue)?.data as string | undefined;
|
||||
const createdAtRange = useFilterValue("createdAt", ZDateRangeFilterValue)?.data;
|
||||
const startDate = createdAtRange?.startDate ?? dayjs().subtract(1, "week").startOf("day").toISOString();
|
||||
const endDate = createdAtRange?.endDate ?? dayjs().endOf("day").toISOString();
|
||||
const { isAll, teamId, userId, memberUserIds, routingFormId, startDate, endDate, columnFilters } =
|
||||
useInsightsParameters();
|
||||
|
||||
const {
|
||||
data: headers,
|
||||
@@ -358,6 +325,10 @@ export function RoutingFormResponsesTableContent() {
|
||||
filter: { type: ColumnFilterType.SINGLE_SELECT },
|
||||
},
|
||||
cell: () => null,
|
||||
filterFn: (row, id, filterValue) => {
|
||||
const cellValue = row.original.formId;
|
||||
return dataTableFilter(cellValue, filterValue);
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("bookingUserId", {
|
||||
id: "bookingUserId",
|
||||
@@ -368,6 +339,10 @@ export function RoutingFormResponsesTableContent() {
|
||||
filter: { type: ColumnFilterType.MULTI_SELECT },
|
||||
},
|
||||
cell: () => null,
|
||||
filterFn: (row, id, filterValue) => {
|
||||
const cellValue = row.original.bookingUserId;
|
||||
return dataTableFilter(cellValue, filterValue);
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("bookingAttendees", {
|
||||
id: "bookingAttendees",
|
||||
@@ -461,7 +436,8 @@ export function RoutingFormResponsesTableContent() {
|
||||
filter: { type: ColumnFilterType.MULTI_SELECT, icon: "circle" },
|
||||
},
|
||||
filterFn: (row, id, filterValue) => {
|
||||
return multiSelectFilter(row.original.bookingStatusOrder, filterValue);
|
||||
const cellValue = row.original.bookingStatusOrder;
|
||||
return dataTableFilter(cellValue, filterValue);
|
||||
},
|
||||
sortingFn: (rowA, rowB) => {
|
||||
const statusA = rowA.original.bookingStatusOrder ?? 6; // put it at the end if bookingStatusOrder is null
|
||||
@@ -497,6 +473,7 @@ export function RoutingFormResponsesTableContent() {
|
||||
columnHelper.accessor("bookingAssignmentReason", {
|
||||
id: "bookingAssignmentReason",
|
||||
header: t("routing_form_insights_assignment_reason"),
|
||||
enableColumnFilter: true,
|
||||
enableSorting: false,
|
||||
meta: {
|
||||
filter: { type: ColumnFilterType.TEXT },
|
||||
@@ -507,18 +484,24 @@ export function RoutingFormResponsesTableContent() {
|
||||
},
|
||||
filterFn: (row, id, filterValue) => {
|
||||
const reason = row.original.bookingAssignmentReason;
|
||||
return textFilter(reason, filterValue);
|
||||
return dataTableFilter(reason, filterValue);
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("createdAt", {
|
||||
id: "createdAt",
|
||||
header: t("routing_form_insights_submitted_at"),
|
||||
// exclude from "Filters" component
|
||||
// because we already have a DateRangeFilter component
|
||||
enableColumnFilter: false,
|
||||
cell: (info) => (
|
||||
<div className="whitespace-nowrap">
|
||||
<Badge variant="gray">{dayjs(info.getValue()).format("MMM D, YYYY HH:mm")}</Badge>
|
||||
</div>
|
||||
),
|
||||
filterFn: (row, id, filterValue) => {
|
||||
const createdAt = row.original.createdAt;
|
||||
return dataTableFilter(createdAt, filterValue);
|
||||
},
|
||||
}),
|
||||
];
|
||||
}, [isHeadersSuccess, headers, t, copyToClipboard]);
|
||||
@@ -588,47 +571,32 @@ export function RoutingFormResponsesTableContent() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1">
|
||||
<DataTableWrapper
|
||||
table={table}
|
||||
isPending={isFetching && !data}
|
||||
hasNextPage={hasNextPage}
|
||||
fetchNextPage={fetchNextPage}
|
||||
isFetching={isFetching}
|
||||
ToolbarLeft={
|
||||
<>
|
||||
<OrgTeamsFilter
|
||||
selectedType={orgTeamsType}
|
||||
selectedTeamId={selectedTeamId}
|
||||
onSelected={(params) => {
|
||||
setOrgTeamsType(params.type);
|
||||
setSelectedTeamId(params.teamId);
|
||||
}}
|
||||
/>
|
||||
<DataTableFilters.AddFilterButton table={table} />
|
||||
<DataTableFilters.ActiveFilters table={table} />
|
||||
<DataTableFilters.ClearFiltersButton exclude={["createdAt"]} />
|
||||
</>
|
||||
}
|
||||
ToolbarRight={
|
||||
<>
|
||||
<DateRangeFilter column={createdAtColumn} />
|
||||
<RoutingFormResponsesDownload
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
teamId={teamId}
|
||||
userId={userId}
|
||||
isAll={isAll}
|
||||
memberUserIds={memberUserIds}
|
||||
routingFormId={routingFormId}
|
||||
columnFilters={columnFilters}
|
||||
sorting={sorting}
|
||||
/>
|
||||
<DataTableFilters.ColumnVisibilityButton table={table} />
|
||||
</>
|
||||
}>
|
||||
<RoutingKPICards given={{ isAll, teamId, userId }} />
|
||||
</DataTableWrapper>
|
||||
</div>
|
||||
<>
|
||||
<div className="flex-1">
|
||||
<DataTableWrapper
|
||||
table={table}
|
||||
isPending={isFetching && !data}
|
||||
hasNextPage={hasNextPage}
|
||||
fetchNextPage={fetchNextPage}
|
||||
isFetching={isFetching}
|
||||
ToolbarLeft={
|
||||
<>
|
||||
<OrgTeamsFilter />
|
||||
<DataTableFilters.AddFilterButton table={table} />
|
||||
<DataTableFilters.ActiveFilters table={table} />
|
||||
<DataTableFilters.ClearFiltersButton exclude={["createdAt"]} />
|
||||
</>
|
||||
}
|
||||
ToolbarRight={
|
||||
<>
|
||||
<DateRangeFilter column={createdAtColumn} />
|
||||
<RoutingFormResponsesDownload sorting={sorting} />
|
||||
<DataTableFilters.ColumnVisibilityButton table={table} />
|
||||
</>
|
||||
}>
|
||||
<RoutingKPICards />
|
||||
</DataTableWrapper>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,81 +4,56 @@ import { Flex, Text, Metric } from "@tremor/react";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
import { useFilterContext } from "../context/provider";
|
||||
import { useInsightsParameters } from "../hooks/useInsightsParameters";
|
||||
import { valueFormatter } from "../lib";
|
||||
import { CardInsights } from "./Card";
|
||||
|
||||
export const RoutingKPICards = ({
|
||||
given,
|
||||
}: {
|
||||
given?: { isAll: boolean; teamId: number | undefined; userId: number | undefined };
|
||||
}) => {
|
||||
export const RoutingKPICards = () => {
|
||||
const { t } = useLocale();
|
||||
const { filter } = useFilterContext();
|
||||
const { teamId, startDate, endDate, userId, memberUserIds, isAll, routingFormId, columnFilters } =
|
||||
useInsightsParameters();
|
||||
|
||||
const userId = given?.userId ?? filter.selectedUserId;
|
||||
const isAll = given?.isAll ?? filter.isAll;
|
||||
const teamId = given?.teamId ?? filter.selectedTeamId;
|
||||
|
||||
const {
|
||||
dateRange,
|
||||
selectedEventTypeId,
|
||||
selectedMemberUserId,
|
||||
initialConfig,
|
||||
selectedRoutingFormId,
|
||||
selectedBookingStatus,
|
||||
selectedRoutingFormFilter,
|
||||
} = filter;
|
||||
const initialConfigIsReady =
|
||||
Boolean(given) || !!(initialConfig?.teamId || initialConfig?.userId || initialConfig?.isAll);
|
||||
const [startDate, endDate] = dateRange;
|
||||
|
||||
const { data, isSuccess, isPending } = trpc.viewer.insights.routingFormsByStatus.useQuery(
|
||||
const { data, isPending } = trpc.viewer.insights.routingFormsByStatus.useQuery(
|
||||
{
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
teamId,
|
||||
eventTypeId: selectedEventTypeId ?? undefined,
|
||||
isAll,
|
||||
routingFormId: selectedRoutingFormId ?? undefined,
|
||||
startDate,
|
||||
endDate,
|
||||
userId,
|
||||
memberUserId: selectedMemberUserId ?? undefined,
|
||||
bookingStatus: selectedBookingStatus ?? undefined,
|
||||
fieldFilter: selectedRoutingFormFilter ?? undefined,
|
||||
memberUserIds,
|
||||
isAll,
|
||||
routingFormId,
|
||||
columnFilters,
|
||||
},
|
||||
{
|
||||
staleTime: 30000,
|
||||
trpc: {
|
||||
context: { skipBatch: true },
|
||||
},
|
||||
enabled: initialConfigIsReady,
|
||||
}
|
||||
);
|
||||
|
||||
const categories: {
|
||||
title: string;
|
||||
index: "active" | "total_responses" | "total_responses_without_booking" | "total_responses_with_booking";
|
||||
index: "total" | "totalWithoutBooking" | "totalWithBooking";
|
||||
}[] = [
|
||||
{
|
||||
title: t("routing_forms_total_responses"),
|
||||
index: "total_responses",
|
||||
index: "total",
|
||||
},
|
||||
{
|
||||
title: t("routing_forms_total_responses_without_booking"),
|
||||
index: "total_responses_without_booking",
|
||||
index: "totalWithoutBooking",
|
||||
},
|
||||
{
|
||||
title: t("routing_forms_total_responses_with_booking"),
|
||||
index: "total_responses_with_booking",
|
||||
index: "totalWithBooking",
|
||||
},
|
||||
];
|
||||
|
||||
if (isPending) {
|
||||
if (isPending || !data) {
|
||||
return <LoadingKPICards categories={categories} />;
|
||||
}
|
||||
|
||||
if (!isSuccess || !startDate || !endDate || (!teamId && !userId)) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid numColsSm={1} numColsLg={3} className="mt-4 gap-x-4 gap-y-4">
|
||||
@@ -86,7 +61,6 @@ export const RoutingKPICards = ({
|
||||
<CardInsights key={item.title}>
|
||||
<Text className="text-default">{item.title}</Text>
|
||||
<Flex className="items-baseline justify-start space-x-3 truncate">
|
||||
{/* @ts-expect-error - theyre actually dynamic fields that we know the index of - but TS doesnt know that */}
|
||||
<Metric className="text-emphasis">{valueFormatter(data[item.index])}</Metric>
|
||||
</Flex>
|
||||
</CardInsights>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useSession } from "next-auth/react";
|
||||
import { createContext, useState } from "react";
|
||||
|
||||
import type { OrgTeamsType } from "../filters/OrgTeamsFilter";
|
||||
|
||||
export type InsightsOrgTeamsContextType = {
|
||||
orgTeamsType: OrgTeamsType;
|
||||
setOrgTeamsType: (type: OrgTeamsType) => void;
|
||||
selectedTeamId: number | undefined;
|
||||
setSelectedTeamId: (id: number | undefined) => void;
|
||||
};
|
||||
|
||||
export const InsightsOrgTeamsContext = createContext<InsightsOrgTeamsContextType | null>(null);
|
||||
|
||||
export function InsightsOrgTeamsProvider({ children }: { children: React.ReactNode }) {
|
||||
const session = useSession();
|
||||
const currentOrgId = session.data?.user.org?.id;
|
||||
const [orgTeamsType, setOrgTeamsType] = useState<OrgTeamsType>(currentOrgId ? "org" : "yours");
|
||||
const [selectedTeamId, setSelectedTeamId] = useState<number | undefined>();
|
||||
|
||||
return (
|
||||
<InsightsOrgTeamsContext.Provider
|
||||
value={{
|
||||
orgTeamsType,
|
||||
setOrgTeamsType,
|
||||
selectedTeamId,
|
||||
setSelectedTeamId,
|
||||
}}>
|
||||
{children}
|
||||
</InsightsOrgTeamsContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import type { ColumnFilter, SortingState } from "@calcom/features/data-table";
|
||||
import type { SortingState } from "@calcom/features/data-table";
|
||||
import { downloadAsCsv } from "@calcom/lib/csvUtils";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
@@ -9,17 +9,11 @@ import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { Button, Dropdown, DropdownItem, DropdownMenuContent, DropdownMenuTrigger } from "@calcom/ui";
|
||||
import { showToast } from "@calcom/ui";
|
||||
|
||||
import { useInsightsParameters } from "../../hooks/useInsightsParameters";
|
||||
|
||||
type RoutingData = RouterOutputs["viewer"]["insights"]["routingFormResponsesForDownload"]["data"][number];
|
||||
|
||||
type Props = {
|
||||
teamId: number | undefined;
|
||||
userId: number | undefined;
|
||||
memberUserIds: number[] | undefined;
|
||||
routingFormId: string | undefined;
|
||||
isAll: boolean;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
columnFilters: ColumnFilter[];
|
||||
sorting: SortingState;
|
||||
};
|
||||
|
||||
@@ -28,18 +22,10 @@ type Batch = {
|
||||
nextCursor: number | undefined;
|
||||
};
|
||||
|
||||
export const RoutingFormResponsesDownload = ({
|
||||
teamId,
|
||||
userId,
|
||||
memberUserIds,
|
||||
routingFormId,
|
||||
isAll,
|
||||
startDate,
|
||||
endDate,
|
||||
columnFilters,
|
||||
sorting,
|
||||
}: Props) => {
|
||||
export const RoutingFormResponsesDownload = ({ sorting }: Props) => {
|
||||
const { t } = useLocale();
|
||||
const { teamId, userId, memberUserIds, routingFormId, isAll, startDate, endDate, columnFilters } =
|
||||
useInsightsParameters();
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
@@ -10,19 +10,14 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { AnimatedPopover, Avatar, Divider, Icon, FilterSearchField } from "@calcom/ui";
|
||||
|
||||
import { useInsightsOrgTeams } from "../hooks/useInsightsOrgTeams";
|
||||
|
||||
export type OrgTeamsType = "org" | "team" | "yours";
|
||||
|
||||
// This is a clone of TeamAndSelfList, but without useFilterContext().
|
||||
// It's meant to be used in the RoutingFormResponsesTable.
|
||||
export const OrgTeamsFilter = ({
|
||||
selectedType,
|
||||
selectedTeamId,
|
||||
onSelected,
|
||||
}: {
|
||||
selectedType: OrgTeamsType;
|
||||
selectedTeamId?: number;
|
||||
onSelected: (params: { type: OrgTeamsType; teamId?: number }) => void;
|
||||
}) => {
|
||||
export const OrgTeamsFilter = () => {
|
||||
const { orgTeamsType, selectedTeamId, setOrgTeamsType, setSelectedTeamId } = useInsightsOrgTeams();
|
||||
const { t } = useLocale();
|
||||
const session = useSession();
|
||||
const currentOrgId = session.data?.user.org?.id;
|
||||
@@ -41,12 +36,17 @@ export const OrgTeamsFilter = ({
|
||||
},
|
||||
});
|
||||
|
||||
const onSelected = (params: { type: OrgTeamsType; teamId?: number }) => {
|
||||
setOrgTeamsType(params.type);
|
||||
setSelectedTeamId(params.teamId);
|
||||
};
|
||||
|
||||
const getPopoverProps = () => {
|
||||
if (selectedType === "org") {
|
||||
if (orgTeamsType === "org") {
|
||||
return { text: t("all"), placeholder: undefined, imageUrl: data?.[0].logoUrl };
|
||||
} else if (selectedType === "yours") {
|
||||
} else if (orgTeamsType === "yours") {
|
||||
return { text: t("yours"), placeholder: currentUserName, imageUrl: session.data?.user.avatarUrl };
|
||||
} else if (selectedType === "team") {
|
||||
} else if (orgTeamsType === "team") {
|
||||
const selectedTeam = data?.find((item) => {
|
||||
return item.id === selectedTeamId;
|
||||
});
|
||||
@@ -64,7 +64,7 @@ export const OrgTeamsFilter = ({
|
||||
const isOrgDataAvailable = !!data && data.length > 0 && !!data[0].isOrg;
|
||||
|
||||
const PrefixComponent =
|
||||
selectedType !== undefined && (imageUrl || placeholder) ? (
|
||||
orgTeamsType !== undefined && (imageUrl || placeholder) ? (
|
||||
<Avatar
|
||||
alt={`${placeholder} logo`}
|
||||
imageSrc={getPlaceholderAvatar(imageUrl, placeholder)}
|
||||
@@ -87,7 +87,7 @@ export const OrgTeamsFilter = ({
|
||||
<FilterCheckboxField
|
||||
id="all"
|
||||
icon={<Icon name="layers" className="h-4 w-4" />}
|
||||
checked={selectedType === "org"}
|
||||
checked={orgTeamsType === "org"}
|
||||
onChange={(e) => {
|
||||
onSelected({ type: "org", teamId: undefined });
|
||||
}}
|
||||
@@ -104,7 +104,7 @@ export const OrgTeamsFilter = ({
|
||||
size="xsm"
|
||||
/>
|
||||
}
|
||||
checked={selectedType === "yours"}
|
||||
checked={orgTeamsType === "yours"}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
onSelected({ type: "yours", teamId: undefined });
|
||||
@@ -125,7 +125,7 @@ export const OrgTeamsFilter = ({
|
||||
key={team.id}
|
||||
id={team.name || ""}
|
||||
label={team.name || ""}
|
||||
checked={selectedTeamId === team.id && selectedType === "team"}
|
||||
checked={selectedTeamId === team.id && orgTeamsType === "team"}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
onSelected({ type: "team", teamId: team.id });
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useContext } from "react";
|
||||
|
||||
import { InsightsOrgTeamsContext } from "../context/InsightsOrgTeamsProvider";
|
||||
|
||||
export function useInsightsOrgTeams() {
|
||||
const context = useContext(InsightsOrgTeamsContext);
|
||||
if (!context) {
|
||||
throw new Error("useInsightsOrgTeams must be used within a InsightsOrgTeamsProvider");
|
||||
}
|
||||
const { orgTeamsType, selectedTeamId, setOrgTeamsType, setSelectedTeamId } = context;
|
||||
const session = useSession();
|
||||
const isAll = orgTeamsType === "org";
|
||||
const teamId = orgTeamsType === "team" ? selectedTeamId : undefined;
|
||||
const userId = orgTeamsType === "yours" ? session.data?.user.id : undefined;
|
||||
|
||||
return {
|
||||
orgTeamsType,
|
||||
setOrgTeamsType,
|
||||
selectedTeamId,
|
||||
setSelectedTeamId,
|
||||
isAll,
|
||||
teamId,
|
||||
userId,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import {
|
||||
useFilterValue,
|
||||
useColumnFilters,
|
||||
ZMultiSelectFilterValue,
|
||||
ZSingleSelectFilterValue,
|
||||
ZDateRangeFilterValue,
|
||||
} from "@calcom/features/data-table";
|
||||
|
||||
import { useInsightsOrgTeams } from "./useInsightsOrgTeams";
|
||||
|
||||
export function useInsightsParameters() {
|
||||
const { isAll, teamId, userId } = useInsightsOrgTeams();
|
||||
|
||||
const memberUserIds = useFilterValue("bookingUserId", ZMultiSelectFilterValue)?.data as
|
||||
| number[]
|
||||
| undefined;
|
||||
const routingFormId = useFilterValue("formId", ZSingleSelectFilterValue)?.data as string | undefined;
|
||||
const createdAtRange = useFilterValue("createdAt", ZDateRangeFilterValue)?.data;
|
||||
const startDate = useMemo(
|
||||
() => createdAtRange?.startDate ?? dayjs().subtract(1, "week").startOf("day").toISOString(),
|
||||
[createdAtRange?.startDate]
|
||||
);
|
||||
const endDate = useMemo(
|
||||
() => createdAtRange?.endDate ?? dayjs().endOf("day").toISOString(),
|
||||
[createdAtRange?.endDate]
|
||||
);
|
||||
const columnFilters = useColumnFilters({ exclude: ["bookingUserId", "formId", "createdAt"] });
|
||||
|
||||
return {
|
||||
isAll,
|
||||
teamId,
|
||||
userId,
|
||||
memberUserIds,
|
||||
routingFormId,
|
||||
startDate,
|
||||
endDate,
|
||||
columnFilters,
|
||||
};
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export const routingFormResponsesInputSchema = z.object({
|
||||
cursor: z.number().optional(),
|
||||
limit: z.number().optional(),
|
||||
columnFilters: z.array(ZColumnFilter),
|
||||
sorting: z.array(ZSorting),
|
||||
sorting: z.array(ZSorting).optional(),
|
||||
});
|
||||
|
||||
export type RoutingFormResponsesInput = z.infer<typeof routingFormResponsesInputSchema>;
|
||||
|
||||
@@ -93,91 +93,48 @@ class RoutingEventsInsights {
|
||||
teamId,
|
||||
startDate,
|
||||
endDate,
|
||||
isAll = false,
|
||||
isAll,
|
||||
organizationId,
|
||||
routingFormId,
|
||||
cursor,
|
||||
limit,
|
||||
userId,
|
||||
memberUserId,
|
||||
searchQuery,
|
||||
bookingStatus,
|
||||
fieldFilter,
|
||||
}: Omit<RoutingFormInsightsFilter, "columnFilters">) {
|
||||
// Get team IDs based on organization if applicable
|
||||
const formsWhereCondition = await this.getWhereForTeamOrAllTeams({
|
||||
userId,
|
||||
memberUserIds,
|
||||
columnFilters,
|
||||
sorting,
|
||||
}: RoutingFormResponsesFilter) {
|
||||
const whereClause = await this.getWhereClauseForRoutingFormResponses({
|
||||
teamId,
|
||||
startDate,
|
||||
endDate,
|
||||
isAll,
|
||||
organizationId,
|
||||
routingFormId,
|
||||
cursor,
|
||||
limit,
|
||||
userId,
|
||||
memberUserIds,
|
||||
columnFilters,
|
||||
sorting,
|
||||
});
|
||||
|
||||
// Base where condition for responses
|
||||
const responsesWhereCondition: Prisma.App_RoutingForms_FormResponseWhereInput = {
|
||||
...(startDate &&
|
||||
endDate && {
|
||||
createdAt: {
|
||||
gte: dayjs(startDate).startOf("day").toDate(),
|
||||
lte: dayjs(endDate).endOf("day").toDate(),
|
||||
},
|
||||
}),
|
||||
...(memberUserId || bookingStatus || searchQuery
|
||||
? {
|
||||
...(bookingStatus === "NO_BOOKING"
|
||||
? { routedToBooking: null }
|
||||
: {
|
||||
routedToBooking: {
|
||||
...(memberUserId && { userId: memberUserId }),
|
||||
...(searchQuery && {
|
||||
user: {
|
||||
OR: [
|
||||
{ email: { contains: searchQuery, mode: "insensitive" } },
|
||||
{ name: { contains: searchQuery, mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
}),
|
||||
...(bookingStatus && { status: bookingStatus }),
|
||||
},
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
...(fieldFilter && {
|
||||
response: {
|
||||
path: [fieldFilter.fieldId, "value"],
|
||||
array_contains: [fieldFilter.optionId],
|
||||
},
|
||||
}),
|
||||
form: formsWhereCondition,
|
||||
};
|
||||
|
||||
// Get total forms count
|
||||
const totalFormsPromise = prisma.app_RoutingForms_Form.count({
|
||||
where: formsWhereCondition,
|
||||
const totalPromise = prisma.routingFormResponse.count({
|
||||
where: whereClause,
|
||||
});
|
||||
|
||||
// Get total responses
|
||||
const totalResponsesPromise = prisma.app_RoutingForms_FormResponse.count({
|
||||
where: responsesWhereCondition,
|
||||
});
|
||||
|
||||
// Get responses without booking
|
||||
const responsesWithoutBookingPromise = prisma.app_RoutingForms_FormResponse.count({
|
||||
const totalWithoutBookingPromise = prisma.routingFormResponse.count({
|
||||
where: {
|
||||
...responsesWhereCondition,
|
||||
routedToBookingUid: null,
|
||||
...whereClause,
|
||||
bookingUid: null,
|
||||
},
|
||||
});
|
||||
|
||||
const [totalForms, totalResponses, responsesWithoutBooking] = await Promise.all([
|
||||
totalFormsPromise,
|
||||
totalResponsesPromise,
|
||||
responsesWithoutBookingPromise,
|
||||
]);
|
||||
const [total, totalWithoutBooking] = await Promise.all([totalPromise, totalWithoutBookingPromise]);
|
||||
|
||||
return {
|
||||
created: totalForms,
|
||||
total_responses: totalResponses,
|
||||
total_responses_without_booking: responsesWithoutBooking,
|
||||
total_responses_with_booking: totalResponses - responsesWithoutBooking,
|
||||
total,
|
||||
totalWithoutBooking,
|
||||
totalWithBooking: total - totalWithoutBooking,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -213,19 +170,16 @@ class RoutingEventsInsights {
|
||||
});
|
||||
}
|
||||
|
||||
static async getRoutingFormPaginatedResponses({
|
||||
static async getWhereClauseForRoutingFormResponses({
|
||||
teamId,
|
||||
startDate,
|
||||
endDate,
|
||||
isAll,
|
||||
organizationId,
|
||||
routingFormId,
|
||||
cursor,
|
||||
limit,
|
||||
userId,
|
||||
memberUserIds,
|
||||
columnFilters,
|
||||
sorting,
|
||||
}: RoutingFormResponsesFilter) {
|
||||
const formsTeamWhereCondition = await this.getWhereForTeamOrAllTeams({
|
||||
userId,
|
||||
@@ -306,6 +260,38 @@ class RoutingEventsInsights {
|
||||
}),
|
||||
};
|
||||
|
||||
return whereClause;
|
||||
}
|
||||
|
||||
static async getRoutingFormPaginatedResponses({
|
||||
teamId,
|
||||
startDate,
|
||||
endDate,
|
||||
isAll,
|
||||
organizationId,
|
||||
routingFormId,
|
||||
cursor,
|
||||
limit,
|
||||
userId,
|
||||
memberUserIds,
|
||||
columnFilters,
|
||||
sorting,
|
||||
}: RoutingFormResponsesFilter) {
|
||||
const whereClause = await this.getWhereClauseForRoutingFormResponses({
|
||||
teamId,
|
||||
startDate,
|
||||
endDate,
|
||||
isAll,
|
||||
organizationId,
|
||||
routingFormId,
|
||||
cursor,
|
||||
limit,
|
||||
userId,
|
||||
memberUserIds,
|
||||
columnFilters,
|
||||
sorting,
|
||||
});
|
||||
|
||||
const totalResponsePromise = prisma.routingFormResponse.count({
|
||||
where: whereClause,
|
||||
});
|
||||
@@ -331,7 +317,7 @@ class RoutingEventsInsights {
|
||||
createdAt: true,
|
||||
},
|
||||
where: whereClause,
|
||||
orderBy: sorting.length > 0 ? makeOrderBy(sorting) : { createdAt: "desc" },
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -1559,35 +1559,22 @@ export const insightsRouter = router({
|
||||
});
|
||||
}),
|
||||
routingFormsByStatus: userBelongsToTeamProcedure
|
||||
.input(
|
||||
rawDataInputSchema.extend({
|
||||
routingFormId: z.string().optional(),
|
||||
bookingStatus: bookingStatusSchema,
|
||||
fieldFilter: z
|
||||
.object({
|
||||
fieldId: z.string(),
|
||||
optionId: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
)
|
||||
.input(routingFormResponsesInputSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { startDate, endDate } = input;
|
||||
|
||||
const stats = await RoutingEventsInsights.getRoutingFormStats({
|
||||
teamId: input.teamId ?? null,
|
||||
startDate,
|
||||
endDate,
|
||||
isAll: input.isAll ?? false,
|
||||
return await RoutingEventsInsights.getRoutingFormStats({
|
||||
teamId: input.teamId,
|
||||
startDate: input.startDate,
|
||||
endDate: input.endDate,
|
||||
isAll: input.isAll,
|
||||
organizationId: ctx.user.organizationId ?? null,
|
||||
routingFormId: input.routingFormId ?? null,
|
||||
userId: input.userId ?? null,
|
||||
memberUserId: input.memberUserId ?? null,
|
||||
bookingStatus: input.bookingStatus ?? null,
|
||||
fieldFilter: input.fieldFilter ?? null,
|
||||
routingFormId: input.routingFormId,
|
||||
cursor: input.cursor,
|
||||
userId: input.userId,
|
||||
memberUserIds: input.memberUserIds,
|
||||
limit: input.limit,
|
||||
columnFilters: input.columnFilters,
|
||||
sorting: input.sorting,
|
||||
});
|
||||
|
||||
return stats;
|
||||
}),
|
||||
routingFormResponses: userBelongsToTeamProcedure
|
||||
.input(routingFormResponsesInputSchema)
|
||||
|
||||
Reference in New Issue
Block a user