(DEFAULT_PRESET);
+
+ const updateValues = ({
+ preset,
+ startDate,
+ endDate,
+ }: {
+ preset: PresetOption;
+ startDate: Dayjs;
+ endDate?: Dayjs;
+ }) => {
+ setSelectedPreset(preset);
+ setStartDate(startDate);
+ setEndDate(endDate);
+
+ if (startDate && endDate) {
+ updateFilter(column.id, {
+ type: "date_range",
+ data: {
+ startDate: startDate.toDate().toISOString(),
+ endDate: endDate.toDate().toISOString(),
+ preset: preset.value,
+ },
+ });
+ }
+ };
+
+ const updateDateRangeFromPreset = (val: string | null) => {
+ if (val === CUSTOM_PRESET_VALUE) {
+ updateValues({
+ preset: CUSTOM_PRESET,
+ startDate,
+ endDate,
+ });
+ } else {
+ const r = getDateRangeFromPreset(val);
+ updateValues({
+ preset: r.preset,
+ startDate: r.startDate,
+ endDate: r.endDate,
+ });
+ }
+ };
+
+ const updateDateRangeFromPicker = ({
+ startDate,
+ endDate,
+ }: {
+ startDate?: Date | undefined;
+ endDate?: Date | undefined;
+ }) => {
+ updateValues({
+ preset: CUSTOM_PRESET,
+ startDate: startDate ? dayjs(startDate) : getDefaultStartDate(),
+ endDate: endDate ? dayjs(endDate) : undefined,
+ });
+ };
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/features/data-table/components/filters/FilterOptions.tsx b/packages/features/data-table/components/filters/FilterOptions.tsx
index 694d647bc2..77ef8b37c3 100644
--- a/packages/features/data-table/components/filters/FilterOptions.tsx
+++ b/packages/features/data-table/components/filters/FilterOptions.tsx
@@ -1,6 +1,7 @@
"use client";
import type { FilterableColumn } from "../../lib/types";
+import { DateRangeFilter } from "./DateRangeFilter";
import { MultiSelectFilterOptions } from "./MultiSelectFilterOptions";
import { NumberFilterOptions } from "./NumberFilterOptions";
import { SingleSelectFilterOptions } from "./SingleSelectFilterOptions";
@@ -19,6 +20,8 @@ export function FilterOptions({ column }: FilterOptionsProps) {
return ;
} else if (column.type === "number") {
return ;
+ } else if (column.type === "date_range") {
+ return ;
} else {
return null;
}
diff --git a/packages/features/data-table/components/filters/FilterPopover.tsx b/packages/features/data-table/components/filters/FilterPopover.tsx
new file mode 100644
index 0000000000..37655870d6
--- /dev/null
+++ b/packages/features/data-table/components/filters/FilterPopover.tsx
@@ -0,0 +1,73 @@
+// eslint-disable-next-line no-restricted-imports
+import startCase from "lodash/startCase";
+
+import type { IconName } from "@calcom/ui";
+import { Button, Icon, Popover, PopoverContent, PopoverTrigger } from "@calcom/ui";
+
+import { useFilterValue } from "../../hooks";
+import { type FilterableColumn, type FilterValue, ZFilterValue } from "../../lib/types";
+import {
+ isSingleSelectFilterValue,
+ isMultiSelectFilterValue,
+ isTextFilterValue,
+ isNumberFilterValue,
+} from "../../lib/utils";
+import { FilterOptions } from "./FilterOptions";
+
+const FILTER_ICONS: Record = {
+ text: "file-text",
+ number: "binary",
+ multi_select: "layers",
+ single_select: "disc",
+ date_range: "calendar-range",
+};
+
+type FilterPopoverProps = {
+ column: FilterableColumn;
+};
+
+const useSelectedLabels = ({
+ column,
+ filterValue,
+}: {
+ column: FilterableColumn;
+ filterValue?: FilterValue;
+}) => {
+ if (isTextFilterValue(filterValue)) {
+ return { text: filterValue.data.operand, moreCount: 0 };
+ } else if (isNumberFilterValue(filterValue)) {
+ return { text: filterValue.data.operand, moreCount: 0 };
+ } else if (isSingleSelectFilterValue(filterValue)) {
+ const options = (column as Extract).options;
+ const text = options.find((opt) => opt.value === filterValue.data)?.label;
+ return { text, moreCount: 0 };
+ } else if (isMultiSelectFilterValue(filterValue)) {
+ const options = (column as Extract).options;
+ const text = options.find((opt) => opt.value === filterValue.data[0])?.label;
+ return { text, moreCount: filterValue.data.length - 1 };
+ } else {
+ return { text: "", moreCount: 0 };
+ }
+};
+
+export function FilterPopover({ column }: FilterPopoverProps) {
+ const icon = column.icon || FILTER_ICONS[column.type];
+ const filterValue = useFilterValue(column.id, ZFilterValue);
+ const { text, moreCount } = useSelectedLabels({ column, filterValue });
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/features/data-table/components/filters/MultiSelectFilterOptions.tsx b/packages/features/data-table/components/filters/MultiSelectFilterOptions.tsx
index eeeb17a058..5e61c52958 100644
--- a/packages/features/data-table/components/filters/MultiSelectFilterOptions.tsx
+++ b/packages/features/data-table/components/filters/MultiSelectFilterOptions.tsx
@@ -29,10 +29,10 @@ export function MultiSelectFilterOptions({ column }: MultiSelectFilterOptionsPro
return (
-
+
{t("no_options_found")}
- {Array.from(column.options.keys()).map((option) => {
+ {column.options.map((option) => {
if (!option) return null;
const { label: optionLabel, value: optionValue } =
typeof option === "string" ? { label: option, value: option } : option;
diff --git a/packages/features/data-table/components/filters/SingleSelectFilterOptions.tsx b/packages/features/data-table/components/filters/SingleSelectFilterOptions.tsx
index 6d1742daea..dee0f22535 100644
--- a/packages/features/data-table/components/filters/SingleSelectFilterOptions.tsx
+++ b/packages/features/data-table/components/filters/SingleSelectFilterOptions.tsx
@@ -29,10 +29,10 @@ export function SingleSelectFilterOptions({ column }: SingleSelectFilterOptionsP
return (
-
+
{t("no_options_found")}
- {Array.from(column.options.keys()).map((option) => {
+ {column.options.map((option) => {
if (!option) return null;
const { label: optionLabel, value: optionValue } =
typeof option === "string" ? { label: option, value: option } : option;
diff --git a/packages/features/data-table/components/filters/index.tsx b/packages/features/data-table/components/filters/index.tsx
index 852e076565..da543db3bf 100644
--- a/packages/features/data-table/components/filters/index.tsx
+++ b/packages/features/data-table/components/filters/index.tsx
@@ -25,8 +25,10 @@ import {
} from "@calcom/ui";
import { useDataTable } from "../../hooks";
-import type { FilterableColumn, ExternalFilter } from "../../lib/types";
-import { FilterOptions } from "./FilterOptions";
+import type { FilterableColumn } from "../../lib/types";
+import { ClearFiltersButton } from "./ClearFiltersButton";
+import { DateRangeFilter } from "./DateRangeFilter";
+import { FilterPopover } from "./FilterPopover";
interface ColumnVisiblityProps {
table: Table;
@@ -122,16 +124,14 @@ const ColumnVisibilityButton = forwardRef(ColumnVisibilityButtonComponent) as {
table: Table;
- externalFilters?: ExternalFilter[];
}
function AddFilterButtonComponent(
- { table, externalFilters }: AddFilterButtonProps,
+ { table }: AddFilterButtonProps,
ref: React.Ref
) {
const { t } = useLocale();
- const { activeFilters, setActiveFilters, displayedExternalFilters, setDisplayedExternalFilters } =
- useDataTable();
+ const { activeFilters, setActiveFilters } = useDataTable();
const filterableColumns = useFilterableColumns(table);
@@ -148,14 +148,14 @@ function AddFilterButtonComponent(
-
-
+
{t("no_columns_found")}
{filterableColumns.map((column) => {
@@ -169,16 +169,6 @@ function AddFilterButtonComponent(
);
})}
- {(externalFilters || [])
- .filter((filter) => !displayedExternalFilters.includes(filter.key))
- .map((filter, index) => (
- setDisplayedExternalFilters((prev) => [...prev, filter.key])}
- className="px-4 py-2">
- {t(filter.titleKey)}
-
- ))}
@@ -205,9 +195,23 @@ function useFilterableColumns(table: Table) {
type,
};
if (type === "multi_select" || type === "single_select") {
+ const values = column.getFacetedUniqueValues();
+ const options = Array.from(values.keys()).map((option) => {
+ if (typeof option === "string") {
+ return {
+ label: option,
+ value: option,
+ };
+ } else {
+ return {
+ label: option.label as string,
+ value: option.value as string | number,
+ };
+ }
+ });
return {
...base,
- options: column.getFacetedUniqueValues(),
+ options,
};
} else {
return {
@@ -229,18 +233,10 @@ const AddFilterButton = forwardRef(AddFilterButtonComponent) as (
// Add the new ActiveFilters component
interface ActiveFiltersProps {
table: Table;
- externalFilters?: ExternalFilter[];
}
-const filterIcons = {
- text: "file-text",
- number: "binary",
- multi_select: "layers",
- single_select: "disc",
-} as const;
-
-function ActiveFilters({ table, externalFilters }: ActiveFiltersProps) {
- const { activeFilters, displayedExternalFilters } = useDataTable();
+function ActiveFilters({ table }: ActiveFiltersProps) {
+ const { activeFilters } = useDataTable();
const filterableColumns = useFilterableColumns(table);
return (
@@ -248,30 +244,18 @@ function ActiveFilters({ table, externalFilters }: ActiveFiltersProps {
const column = filterableColumns.find((col) => col.id === filter.f);
if (!column) return null;
- const icon = column.icon || filterIcons[column.type];
- return (
-
-
-
-
- {startCase(column.title)}
-
-
-
-
-
-
-
- );
- })}
- {(displayedExternalFilters || []).map((key) => {
- const filter = externalFilters?.find((filter) => filter.key === key);
- if (!filter) return null;
- return {filter.component()};
+ return ;
})}
>
);
}
// Update the export to include ActiveFilters
-export const DataTableFilters = { ColumnVisibilityButton, AddFilterButton, ActiveFilters };
+export const DataTableFilters = {
+ ColumnVisibilityButton,
+ AddFilterButton,
+ ActiveFilters,
+ ClearFiltersButton,
+};
+
+export { DateRangeFilter };
diff --git a/packages/features/data-table/components/index.ts b/packages/features/data-table/components/index.ts
index 1b29221f03..f8f693aa42 100644
--- a/packages/features/data-table/components/index.ts
+++ b/packages/features/data-table/components/index.ts
@@ -1,7 +1,7 @@
export { DataTableToolbar } from "./DataTableToolbar";
export { DataTableSelectionBar } from "./DataTableSelectionBar";
export { DataTablePagination } from "./DataTablePagination";
-export { DataTableFilters } from "./filters";
+export * from "./filters";
export { DataTable } from "./DataTable";
export { DataTableSkeleton } from "./DataTableSkeleton";
export { DataTableWrapper } from "./DataTableWrapper";
diff --git a/packages/features/data-table/hooks/useColumnFilters.ts b/packages/features/data-table/hooks/useColumnFilters.ts
index 31dca6344f..ce9a410370 100644
--- a/packages/features/data-table/hooks/useColumnFilters.ts
+++ b/packages/features/data-table/hooks/useColumnFilters.ts
@@ -1,12 +1,22 @@
-import { useMemo } from "react";
+import { useCallback, useMemo } from "react";
import type { ColumnFilter } from "../lib/types";
import { ZFilterValue } from "../lib/types";
import { isMultiSelectFilterValue } from "../lib/utils";
import { useDataTable } from "./useDataTable";
-export function useColumnFilters(): ColumnFilter[] {
+export function useColumnFilters({ exclude }: { exclude?: string[] } = {}): ColumnFilter[] {
const { activeFilters } = useDataTable();
+
+ // Stringify the exclude array for stable memoization
+ const excludeKey = useMemo(() => JSON.stringify(exclude?.sort()), [exclude]);
+
+ // Now this will only change when the actual contents change
+ const filterExcluded = useCallback(
+ (filter: ColumnFilter) => !exclude?.includes(filter.id),
+ [excludeKey] // Use the stringified key instead of the array
+ );
+
return useMemo(() => {
return (activeFilters || [])
.filter(
@@ -22,6 +32,7 @@ export function useColumnFilters(): ColumnFilter[] {
};
})
.filter((filter): filter is ColumnFilter => filter !== null)
+ .filter(filterExcluded)
.filter((filter) => {
// The empty arrays in `filtersSearchState` keep the filter UI component,
// but we do not send them to the actual query.
@@ -31,5 +42,5 @@ export function useColumnFilters(): ColumnFilter[] {
}
return true;
});
- }, [activeFilters]);
+ }, [activeFilters, filterExcluded]);
}
diff --git a/packages/features/data-table/lib/context.tsx b/packages/features/data-table/lib/context.tsx
index 933ae0cb82..b55f2cff74 100644
--- a/packages/features/data-table/lib/context.tsx
+++ b/packages/features/data-table/lib/context.tsx
@@ -2,7 +2,7 @@
import type { SortingState, OnChangeFn } from "@tanstack/react-table";
import { useQueryState, parseAsArrayOf, parseAsJson } from "nuqs";
-import { createContext, useCallback, useState, type Dispatch, type SetStateAction } from "react";
+import { createContext, useCallback } from "react";
import { z } from "zod";
import { type FilterValue, ZFilterValue, ZSorting } from "./types";
@@ -17,16 +17,12 @@ type ActiveFilter = z.infer;
export type DataTableContextType = {
activeFilters: ActiveFilter[];
setActiveFilters: (filters: ActiveFilter[]) => void;
- clearAll: () => void;
+ clearAll: (exclude?: string[]) => void;
updateFilter: (columnId: string, value: FilterValue) => void;
removeFilter: (columnId: string) => void;
sorting: SortingState;
setSorting: OnChangeFn;
-
- displayedExternalFilters: string[];
- setDisplayedExternalFilters: Dispatch>;
- removeDisplayedExternalFilter: (key: string) => void;
};
export const DataTableContext = createContext(null);
@@ -41,24 +37,28 @@ export function DataTableProvider({ children }: { children: React.ReactNode }) {
parseAsArrayOf(parseAsJson(ZSorting.parse)).withDefault([])
);
- const [displayedExternalFilters, setDisplayedExternalFilters] = useState([]);
-
- const removeDisplayedExternalFilter = useCallback(
- (key: string) => {
- setDisplayedExternalFilters((prev) => prev.filter((f) => f !== key));
+ const clearAll = useCallback(
+ (exclude?: string[]) => {
+ setActiveFilters((prev) => prev.filter((filter) => exclude?.includes(filter.f)));
},
- [setDisplayedExternalFilters]
+ [setActiveFilters]
);
- const clearAll = useCallback(() => {
- setActiveFilters([]);
- setDisplayedExternalFilters([]);
- }, [setActiveFilters, setDisplayedExternalFilters]);
-
const updateFilter = useCallback(
(columnId: string, value: FilterValue) => {
setActiveFilters((prev) => {
- return prev.map((item) => (item.f === columnId ? { ...item, v: value } : item));
+ let added = false;
+ const newFilters = prev.map((item) => {
+ if (item.f === columnId) {
+ added = true;
+ return { ...item, v: value };
+ }
+ return item;
+ });
+ if (!added) {
+ newFilters.push({ f: columnId, v: value });
+ }
+ return newFilters;
});
},
[setActiveFilters]
@@ -81,9 +81,6 @@ export function DataTableProvider({ children }: { children: React.ReactNode }) {
removeFilter,
sorting,
setSorting,
- displayedExternalFilters,
- setDisplayedExternalFilters,
- removeDisplayedExternalFilter,
}}>
{children}
diff --git a/packages/features/data-table/lib/types.ts b/packages/features/data-table/lib/types.ts
index 11c8b844bb..f664e0ec57 100644
--- a/packages/features/data-table/lib/types.ts
+++ b/packages/features/data-table/lib/types.ts
@@ -17,7 +17,7 @@ export type TextFilterOperator = z.infer;
export const ZSingleSelectFilterValue = z.object({
type: z.literal("single_select"),
- data: z.string(),
+ data: z.union([z.string(), z.number()]),
});
export type SingleSelectFilterValue = z.infer;
@@ -60,16 +60,28 @@ export const ZNumberFilterValue = z.object({
export type NumberFilterValue = z.infer;
+export const ZDateRangeFilterValue = z.object({
+ type: z.literal("date_range"),
+ data: z.object({
+ startDate: z.string().nullable(),
+ endDate: z.string().nullable(),
+ preset: z.string(),
+ }),
+});
+
+export type DateRangeFilterValue = z.infer;
+
export const ZFilterValue = z.union([
ZSingleSelectFilterValue,
ZMultiSelectFilterValue,
ZTextFilterValue,
ZNumberFilterValue,
+ ZDateRangeFilterValue,
]);
export type FilterValue = z.infer;
-export type ColumnFilterType = "single_select" | "multi_select" | "text" | "number";
+export type ColumnFilterType = "single_select" | "multi_select" | "text" | "number" | "date_range";
export type ColumnFilterMeta = {
type?: ColumnFilterType;
@@ -83,12 +95,12 @@ export type FilterableColumn = {
| {
type: "single_select";
icon?: IconName;
- options: Map;
+ options: Array<{ label: string; value: string | number }>;
}
| {
type: "multi_select";
icon?: IconName;
- options: Map;
+ options: Array<{ label: string; value: string | number }>;
}
| {
type: "text";
@@ -98,6 +110,10 @@ export type FilterableColumn = {
type: "number";
icon?: IconName;
}
+ | {
+ type: "date_range";
+ icon?: IconName;
+ }
);
export const ZColumnFilter = z.object({
@@ -117,15 +133,11 @@ export type TypedColumnFilter = {
? SingleSelectFilterValue
: T extends "multi_select"
? MultiSelectFilterValue
+ : T extends "date_range"
+ ? DateRangeFilterValue
: never;
};
-export type ExternalFilter = {
- key: string;
- titleKey: string;
- component: () => React.ReactNode;
-};
-
export const ZSorting = z.object({
id: z.string(),
desc: z.boolean(),
diff --git a/packages/features/data-table/lib/utils.ts b/packages/features/data-table/lib/utils.ts
index c9ef419745..3094604408 100644
--- a/packages/features/data-table/lib/utils.ts
+++ b/packages/features/data-table/lib/utils.ts
@@ -6,12 +6,14 @@ import type {
TextFilterValue,
FilterValue,
NumberFilterValue,
+ DateRangeFilterValue,
} from "./types";
import {
ZNumberFilterValue,
ZSingleSelectFilterValue,
ZMultiSelectFilterValue,
ZTextFilterValue,
+ ZDateRangeFilterValue,
} from "./types";
export const textFilter = (cellValue: unknown, filterValue: TextFilterValue) => {
@@ -113,6 +115,10 @@ export const isNumberFilterValue = (filterValue: unknown): filterValue is Number
return ZNumberFilterValue.safeParse(filterValue).success;
};
+export const isDateRangeFilterValue = (filterValue: unknown): filterValue is DateRangeFilterValue => {
+ return ZDateRangeFilterValue.safeParse(filterValue).success;
+};
+
export const dataTableFilter = (cellValue: unknown, filterValue: FilterValue) => {
if (isSingleSelectFilterValue(filterValue)) {
return singleSelectFilter(cellValue, filterValue);
diff --git a/packages/features/insights/components/RoutingFormResponsesTable.tsx b/packages/features/insights/components/RoutingFormResponsesTable.tsx
index d26e4a214e..be7db2fffb 100644
--- a/packages/features/insights/components/RoutingFormResponsesTable.tsx
+++ b/packages/features/insights/components/RoutingFormResponsesTable.tsx
@@ -10,22 +10,27 @@ 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 } from "react";
+import { useMemo, useId, useState } from "react";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
-import type { ExternalFilter } from "@calcom/features/data-table";
import {
DataTableWrapper,
DataTableFilters,
DataTableProvider,
DataTableSkeleton,
useColumnFilters,
+ useFilterValue,
multiSelectFilter,
textFilter,
dataTableFilter,
useDataTable,
+ ZDateRangeFilterValue,
+ ZSingleSelectFilterValue,
+ DateRangeFilter,
+ type FilterableColumn,
} from "@calcom/features/data-table";
import classNames from "@calcom/lib/classNames";
import { useCopy } from "@calcom/lib/hooks/useCopy";
@@ -44,13 +49,9 @@ import {
HoverCardPortal,
} from "@calcom/ui";
-import { useFilterContext } from "../context/provider";
-import { ClearFilters } from "../filters/ClearFilters";
-import { DateSelect } from "../filters/DateSelect";
import { RoutingFormResponsesDownload } from "../filters/Download";
-import { RoutingFormFilterList } from "../filters/RoutingFormFilterList";
-import { TeamAndSelfList } from "../filters/TeamAndSelfList";
-import { UserListInTeam } from "../filters/UserListInTeam";
+import type { OrgTeamsType } from "../filters/OrgTeamsFilter";
+import { OrgTeamsFilter } from "../filters/OrgTeamsFilter";
import {
ZResponseMultipleValues,
ZResponseSingleValue,
@@ -259,57 +260,65 @@ export function RoutingFormResponsesTable() {
);
}
+const createdAtColumn: Extract = {
+ id: "createdAt",
+ title: "createdAt",
+ type: "date_range",
+};
+
export function RoutingFormResponsesTableContent() {
const { t } = useLocale();
- const { filter } = useFilterContext();
const { copyToClipboard } = useCopy();
+ const session = useSession();
+ const currentOrgId = session.data?.user.org?.id;
+ const [orgTeamsType, setOrgTeamsType] = useState(currentOrgId ? "org" : "yours");
+ const [selectedTeamId, setSelectedTeamId] = useState();
- const {
- dateRange,
- selectedTeamId,
- isAll,
- initialConfig,
- selectedRoutingFormId,
- selectedMemberUserId,
- selectedUserId,
- } = filter;
- const initialConfigIsReady = !!(initialConfig?.teamId || initialConfig?.userId || initialConfig?.isAll);
- const [startDate, endDate] = dateRange;
+ const columnFilters = useColumnFilters({ exclude: ["bookingUserId", "formId", "createdAt"] });
- const columnFilters = useColumnFilters();
+ const isAll = orgTeamsType === "org";
+ const teamId = orgTeamsType === "team" ? selectedTeamId : undefined;
+ const userId = orgTeamsType === "yours" ? session.data?.user.id : undefined;
- const teamId = selectedTeamId ?? undefined;
- const userId = selectedUserId ?? undefined;
- const memberUserId = selectedMemberUserId ?? undefined;
- const routingFormId = selectedRoutingFormId ?? undefined;
+ const memberUserId = useFilterValue("bookingUserId", ZSingleSelectFilterValue)?.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 {
data: headers,
isLoading: isHeadersLoading,
isSuccess: isHeadersSuccess,
- } = trpc.viewer.insights.routingFormResponsesHeaders.useQuery(
- {
- userId,
- teamId,
- isAll: isAll ?? false,
- routingFormId,
- },
- {
- enabled: initialConfigIsReady,
- }
- );
+ } = trpc.viewer.insights.routingFormResponsesHeaders.useQuery({
+ userId,
+ teamId,
+ isAll,
+ routingFormId,
+ });
- const { removeDisplayedExternalFilter, sorting, setSorting } = useDataTable();
+ const { data: forms } = trpc.viewer.insights.getRoutingFormsForFilters.useQuery({
+ userId,
+ teamId,
+ isAll,
+ });
+
+ const { data: users } = trpc.viewer.insights.userList.useQuery({
+ teamId: teamId ?? -1,
+ isAll,
+ });
+
+ const { sorting, setSorting } = useDataTable();
const { data, fetchNextPage, isFetching, hasNextPage, isLoading } =
trpc.viewer.insights.routingFormResponses.useInfiniteQuery(
{
teamId,
- startDate: startDate.toISOString(),
- endDate: endDate.toISOString(),
+ startDate,
+ endDate,
userId,
memberUserId,
- isAll: isAll ?? false,
+ isAll,
routingFormId,
columnFilters,
sorting,
@@ -321,7 +330,6 @@ export function RoutingFormResponsesTableContent() {
trpc: {
context: { skipBatch: true },
},
- enabled: initialConfigIsReady,
}
);
@@ -337,6 +345,26 @@ export function RoutingFormResponsesTableContent() {
return [];
}
return [
+ columnHelper.accessor("formId", {
+ id: "formId",
+ header: t("routing_forms"),
+ enableColumnFilter: true,
+ enableSorting: false,
+ meta: {
+ filter: { type: "single_select" },
+ },
+ cell: () => null,
+ }),
+ columnHelper.accessor("bookingUserId", {
+ id: "bookingUserId",
+ header: t("people"),
+ enableColumnFilter: true,
+ enableSorting: false,
+ meta: {
+ filter: { type: "single_select" },
+ },
+ cell: () => null,
+ }),
columnHelper.accessor("bookingAttendees", {
id: "bookingAttendees",
header: t("routing_form_insights_booked_by"),
@@ -500,6 +528,12 @@ export function RoutingFormResponsesTableContent() {
defaultColumn: {
size: 150,
},
+ initialState: {
+ columnVisibility: {
+ formId: false,
+ bookingUserId: false,
+ },
+ },
state: {
sorting,
columnFilters,
@@ -510,41 +544,47 @@ export function RoutingFormResponsesTableContent() {
return new Map();
}
+ const fromArrayToMap = (array: { label: string; value: string | number }[]) => {
+ return new Map(array.map((option) => [{ label: option.label, value: option.value }, 1]));
+ };
+
const fieldHeader = headers.find((h) => h.id === columnId);
if (fieldHeader?.options) {
- return new Map(fieldHeader.options.map((option) => [{ label: option.label, value: option.id }, 1]));
+ return fromArrayToMap(
+ fieldHeader.options
+ .filter((option): option is { id: string; label: string } => option.id !== null)
+ .map((option) => ({
+ label: option.label,
+ value: option.id,
+ }))
+ );
} else if (columnId === "bookingStatusOrder") {
- return new Map(
- Object.keys(statusOrder).map((status) => [
- {
- value: statusOrder[status as BookingStatus],
- label: bookingStatusToText(status as BookingStatus),
- },
- 1,
- ])
+ return fromArrayToMap(
+ Object.keys(statusOrder).map((status) => ({
+ value: statusOrder[status as BookingStatus],
+ label: bookingStatusToText(status as BookingStatus),
+ }))
+ );
+ } else if (columnId === "formId") {
+ return fromArrayToMap(
+ forms?.map((form) => ({
+ label: form.name,
+ value: form.id,
+ })) ?? []
+ );
+ } else if (columnId === "bookingUserId") {
+ return fromArrayToMap(
+ users?.map((user) => ({
+ label: user.name ?? user.email,
+ value: user.id,
+ })) ?? []
);
}
return new Map();
},
});
- const externalFilters = useMemo(
- () => [
- {
- key: "memberUserId",
- titleKey: "people",
- component: () => (
- removeDisplayedExternalFilter("memberUserId")}
- />
- ),
- },
- ],
- [removeDisplayedExternalFilter]
- );
-
- if (isHeadersLoading || ((isFetching || isLoading) && !data)) {
+ if ((isHeadersLoading && !headers) || ((isFetching || isLoading) && !data)) {
return ;
}
@@ -558,16 +598,22 @@ export function RoutingFormResponsesTableContent() {
isFetching={isFetching}
ToolbarLeft={
<>
-
-
-
-
-
+ {
+ setOrgTeamsType(params.type);
+ setSelectedTeamId(params.teamId);
+ }}
+ />
+
+
+
>
}
ToolbarRight={
<>
-
+
>
}>
-
+
);
diff --git a/packages/features/insights/components/RoutingKPICards.tsx b/packages/features/insights/components/RoutingKPICards.tsx
index a3d67bafb7..82b6a3cd88 100644
--- a/packages/features/insights/components/RoutingKPICards.tsx
+++ b/packages/features/insights/components/RoutingKPICards.tsx
@@ -8,26 +8,31 @@ import { useFilterContext } from "../context/provider";
import { valueFormatter } from "../lib";
import { CardInsights } from "./Card";
-export const RoutingKPICards = () => {
+export const RoutingKPICards = ({
+ given,
+}: {
+ given?: { isAll: boolean; teamId: number | undefined; userId: number | undefined };
+}) => {
const { t } = useLocale();
const { filter } = useFilterContext();
+ const userId = given?.userId ?? filter.selectedUserId;
+ const isAll = given?.isAll ?? filter.isAll;
+ const teamId = given?.teamId ?? filter.selectedTeamId;
+
const {
dateRange,
selectedEventTypeId,
- selectedUserId,
selectedMemberUserId,
- isAll,
initialConfig,
selectedRoutingFormId,
selectedBookingStatus,
selectedRoutingFormFilter,
} = filter;
- const initialConfigIsReady = !!(initialConfig?.teamId || initialConfig?.userId || initialConfig?.isAll);
+ const initialConfigIsReady =
+ Boolean(given) || !!(initialConfig?.teamId || initialConfig?.userId || initialConfig?.isAll);
const [startDate, endDate] = dateRange;
- const { selectedTeamId: teamId } = filter;
-
const { data, isSuccess, isPending } = trpc.viewer.insights.routingFormsByStatus.useQuery(
{
startDate: startDate.toISOString(),
@@ -36,7 +41,7 @@ export const RoutingKPICards = () => {
eventTypeId: selectedEventTypeId ?? undefined,
isAll,
routingFormId: selectedRoutingFormId ?? undefined,
- userId: selectedUserId ?? undefined,
+ userId,
memberUserId: selectedMemberUserId ?? undefined,
bookingStatus: selectedBookingStatus ?? undefined,
fieldFilter: selectedRoutingFormFilter ?? undefined,
@@ -72,7 +77,7 @@ export const RoutingKPICards = () => {
return ;
}
- if (!isSuccess || !startDate || !endDate || (!teamId && !selectedUserId)) return null;
+ if (!isSuccess || !startDate || !endDate || (!teamId && !userId)) return null;
return (
<>
diff --git a/packages/features/insights/filters/Download/RoutingFormResponsesDownload.tsx b/packages/features/insights/filters/Download/RoutingFormResponsesDownload.tsx
index e217e7b826..6026dc98cf 100644
--- a/packages/features/insights/filters/Download/RoutingFormResponsesDownload.tsx
+++ b/packages/features/insights/filters/Download/RoutingFormResponsesDownload.tsx
@@ -1,6 +1,6 @@
import { useState } from "react";
-import type { Dayjs } from "@calcom/dayjs";
+import dayjs from "@calcom/dayjs";
import type { ColumnFilter, SortingState } from "@calcom/features/data-table";
import { downloadAsCsv } from "@calcom/lib/csvUtils";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -17,8 +17,8 @@ type Props = {
memberUserId: number | undefined;
routingFormId: string | undefined;
isAll: boolean;
- startDate: Dayjs;
- endDate: Dayjs;
+ startDate: string;
+ endDate: string;
columnFilters: ColumnFilter[];
sorting: SortingState;
};
@@ -52,8 +52,8 @@ export const RoutingFormResponsesDownload = ({
}> => {
const { data, nextCursor } = await utils.viewer.insights.routingFormResponsesForDownload.fetch({
teamId,
- startDate: startDate.toISOString(),
- endDate: endDate.toISOString(),
+ startDate,
+ endDate,
userId,
memberUserId,
isAll: isAll ?? false,
@@ -84,9 +84,9 @@ export const RoutingFormResponsesDownload = ({
}
if (allData.length > 0) {
- const filename = `RoutingFormResponses-${startDate.format("YYYY-MM-DD")}-${endDate.format(
- "YYYY-MM-DD"
- )}.csv`;
+ const filename = `RoutingFormResponses-${dayjs(startDate).format("YYYY-MM-DD")}-${dayjs(
+ endDate
+ ).format("YYYY-MM-DD")}.csv`;
downloadAsCsv(allData as Record[], filename);
}
} catch (error) {
diff --git a/packages/features/insights/filters/OrgTeamsFilter.tsx b/packages/features/insights/filters/OrgTeamsFilter.tsx
new file mode 100644
index 0000000000..9ab6d0a5f3
--- /dev/null
+++ b/packages/features/insights/filters/OrgTeamsFilter.tsx
@@ -0,0 +1,149 @@
+import { useSession } from "next-auth/react";
+import { useState } from "react";
+
+import {
+ FilterCheckboxField,
+ FilterCheckboxFieldsContainer,
+} from "@calcom/features/filters/components/TeamsFilter";
+import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc";
+import { AnimatedPopover, Avatar, Divider, Icon, FilterSearchField } from "@calcom/ui";
+
+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;
+}) => {
+ const { t } = useLocale();
+ const session = useSession();
+ const currentOrgId = session.data?.user.org?.id;
+ const currentUserId = session.data?.user.id;
+ const currentUserName = session.data?.user.name;
+
+ const [query, setQuery] = useState("");
+
+ const { data, isSuccess } = trpc.viewer.insights.teamListForUser.useQuery(undefined, {
+ // Teams don't change that frequently
+ refetchOnWindowFocus: false,
+ trpc: {
+ context: {
+ skipBatch: true,
+ },
+ },
+ });
+
+ const getPopoverProps = () => {
+ if (selectedType === "org") {
+ return { text: t("all"), placeholder: undefined, imageUrl: data?.[0].logoUrl };
+ } else if (selectedType === "yours") {
+ return { text: t("yours"), placeholder: currentUserName, imageUrl: session.data?.user.avatarUrl };
+ } else if (selectedType === "team") {
+ const selectedTeam = data?.find((item) => {
+ return item.id === selectedTeamId;
+ });
+ return {
+ text: `${t("team")}: ${selectedTeam?.name}`,
+ placeholder: selectedTeam?.name,
+ imageUrl: selectedTeam?.logoUrl,
+ };
+ }
+
+ return { text: t("select"), imageUrl: undefined };
+ };
+
+ const { text, placeholder, imageUrl } = getPopoverProps();
+ const isOrgDataAvailable = !!data && data.length > 0 && !!data[0].isOrg;
+
+ const PrefixComponent =
+ selectedType !== undefined && (imageUrl || placeholder) ? (
+
+ ) : null;
+
+ return (
+
+
+ setQuery(e.target.value)}
+ />
+
+
+ {isOrgDataAvailable && (
+ }
+ checked={selectedType === "org"}
+ onChange={(e) => {
+ onSelected({ type: "org", teamId: undefined });
+ }}
+ label={t("all")}
+ />
+ )}
+
+
+ }
+ checked={selectedType === "yours"}
+ onChange={(e) => {
+ if (e.target.checked) {
+ onSelected({ type: "yours", teamId: undefined });
+ } else if (!e.target.checked) {
+ onSelected({ type: currentOrgId ? "org" : "yours", teamId: undefined });
+ }
+ }}
+ label={t("yours")}
+ />
+
+
+ {data
+ ?.filter((team) => !team.isOrg)
+ .filter((team) => team.name?.toLowerCase().includes(query.toLowerCase()))
+ .map((team) => {
+ return (
+ {
+ if (e.target.checked) {
+ onSelected({ type: "team", teamId: team.id });
+ } else if (!e.target.checked) {
+ onSelected({ type: currentOrgId ? "org" : "yours", teamId: undefined });
+ }
+ }}
+ icon={
+
+ }
+ />
+ );
+ })}
+
+
+ );
+};
diff --git a/packages/features/insights/filters/RoutingFormFilterList.tsx b/packages/features/insights/filters/RoutingFormFilterList.tsx
index 9ebd23f2c0..9be2bd2e8e 100644
--- a/packages/features/insights/filters/RoutingFormFilterList.tsx
+++ b/packages/features/insights/filters/RoutingFormFilterList.tsx
@@ -16,38 +16,36 @@ function buildFilterOptions(forms: Form[]) {
}));
}
-export const RoutingFormFilterList = memo(
- ({ showOnlyWhenSelectedInContext = true }: { showOnlyWhenSelectedInContext?: boolean }) => {
- const { t } = useLocale();
- const { filter, setConfigFilters } = useFilterContext();
- const { selectedTeamId, selectedUserId, selectedRoutingFormId, isAll } = filter;
- const { selectedFilter } = filter;
+export const RoutingFormFilterList = memo(() => {
+ const { t } = useLocale();
+ const { filter, setConfigFilters } = useFilterContext();
+ const { selectedTeamId, selectedUserId, selectedRoutingFormId, isAll } = filter;
+ const { selectedFilter } = filter;
- const { data: allForms } = trpc.viewer.insights.getRoutingFormsForFilters.useQuery(
- {
- userId: selectedUserId ?? undefined,
- teamId: selectedTeamId ?? undefined,
- isAll: isAll ?? false,
- },
- {
- enabled: selectedFilter?.includes("routing_forms"),
- }
- );
+ const { data: allForms } = trpc.viewer.insights.getRoutingFormsForFilters.useQuery(
+ {
+ userId: selectedUserId ?? undefined,
+ teamId: selectedTeamId ?? undefined,
+ isAll: isAll ?? false,
+ },
+ {
+ enabled: selectedFilter?.includes("routing_forms"),
+ }
+ );
- if (showOnlyWhenSelectedInContext && !selectedFilter?.includes("routing_forms")) return null;
+ if (!selectedFilter?.includes("routing_forms")) return null;
- const filterOptions = buildFilterOptions(allForms || []);
+ const filterOptions = buildFilterOptions(allForms || []);
- return (
- setConfigFilters({ selectedRoutingFormId: value as string })}
- buttonIcon={}
- />
- );
- }
-);
+ return (
+ setConfigFilters({ selectedRoutingFormId: value as string })}
+ buttonIcon={}
+ />
+ );
+});
RoutingFormFilterList.displayName = "RoutingFormFilterList";
diff --git a/packages/features/insights/filters/UserListInTeam.tsx b/packages/features/insights/filters/UserListInTeam.tsx
index 5ce42026a8..ce96f62187 100644
--- a/packages/features/insights/filters/UserListInTeam.tsx
+++ b/packages/features/insights/filters/UserListInTeam.tsx
@@ -13,13 +13,7 @@ const mapUserToOption = (user: User) => ({
icon: ,
});
-export const UserListInTeam = ({
- showOnlyWhenSelectedInContext = true,
- onClear,
-}: {
- showOnlyWhenSelectedInContext?: boolean;
- onClear?: () => void;
-}) => {
+export const UserListInTeam = ({ onClear }: { onClear?: () => void }) => {
const { t } = useLocale();
const { filter, setConfigFilters } = useFilterContext();
const { selectedFilter, selectedTeamId, selectedMemberUserId, isAll } = filter;
@@ -29,7 +23,7 @@ export const UserListInTeam = ({
isAll: !!isAll,
});
- if (showOnlyWhenSelectedInContext && !selectedFilter?.includes("user")) {
+ if (!selectedFilter?.includes("user")) {
return null;
}
diff --git a/packages/features/insights/server/trpc-router.ts b/packages/features/insights/server/trpc-router.ts
index 0533f73ea7..5be1a133e5 100644
--- a/packages/features/insights/server/trpc-router.ts
+++ b/packages/features/insights/server/trpc-router.ts
@@ -912,38 +912,11 @@ export const insightsRouter = router({
return [];
}
- const membershipConditional: Prisma.MembershipWhereInput = {
- team: {
- slug: { not: null },
- },
- accepted: true,
- userId: user.id,
- OR: [
- {
- role: "ADMIN",
- },
- {
- role: "OWNER",
- },
- ],
- };
-
// Validate if user belongs to org as admin/owner
if (user.organizationId && user.organization.isOrgAdmin) {
- const teamsFromOrg = await ctx.insightsDb.team.findMany({
+ const teamsAndOrg = await ctx.insightsDb.team.findMany({
where: {
- parentId: user.organizationId,
- },
- select: {
- id: true,
- slug: true,
- name: true,
- logoUrl: true,
- },
- });
- const orgTeam = await ctx.insightsDb.team.findUnique({
- where: {
- id: user.organizationId,
+ OR: [{ parentId: user.organizationId }, { id: user.organizationId }],
},
select: {
id: true,
@@ -952,25 +925,18 @@ export const insightsRouter = router({
logoUrl: true,
},
});
+ const teamsFromOrg = teamsAndOrg.filter((team) => team.id !== user.organizationId);
+ const orgTeam = teamsAndOrg.find((team) => team.id === user.organizationId);
if (!orgTeam) {
return [];
}
const result: IResultTeamList[] = [
{
- id: orgTeam.id,
- slug: orgTeam.slug,
- name: orgTeam.name,
- logoUrl: orgTeam.logoUrl,
+ ...orgTeam,
isOrg: true,
},
- ...teamsFromOrg.map(
- (team: Prisma.TeamGetPayload<{ select: { id: true; slug: true; name: true; logoUrl: true } }>) => {
- return {
- ...team,
- };
- }
- ),
+ ...teamsFromOrg,
];
return result;
@@ -978,7 +944,21 @@ export const insightsRouter = router({
// Look if user it's admin/owner in multiple teams
const belongsToTeams = await ctx.insightsDb.membership.findMany({
- where: membershipConditional,
+ where: {
+ team: {
+ slug: { not: null },
+ },
+ accepted: true,
+ userId: user.id,
+ OR: [
+ {
+ role: "ADMIN",
+ },
+ {
+ role: "OWNER",
+ },
+ ],
+ },
include: {
team: {
select: {
diff --git a/packages/ui/components/icon/icon-list.mjs b/packages/ui/components/icon/icon-list.mjs
index 55cbdfa5ed..c3d2ebacc0 100644
--- a/packages/ui/components/icon/icon-list.mjs
+++ b/packages/ui/components/icon/icon-list.mjs
@@ -141,4 +141,6 @@ export const lucideIconList = new Set([
"zap",
"waypoints",
"chevrons-down-up",
+ "sliders-horizontal",
+ "calendar-range",
]);
diff --git a/packages/ui/components/icon/icon-names.ts b/packages/ui/components/icon/icon-names.ts
index 9ffbbd5bcb..2f2e59f172 100644
--- a/packages/ui/components/icon/icon-names.ts
+++ b/packages/ui/components/icon/icon-names.ts
@@ -23,6 +23,7 @@ export type IconName =
| "calendar-check-2"
| "calendar-days"
| "calendar-heart"
+ | "calendar-range"
| "calendar-search"
| "calendar-x-2"
| "calendar"
@@ -114,6 +115,7 @@ export type IconName =
| "shield-check"
| "shield"
| "shuffle"
+ | "sliders-horizontal"
| "sliders-vertical"
| "smartphone"
| "sparkles"
diff --git a/packages/ui/components/popover/AnimatedPopover.tsx b/packages/ui/components/popover/AnimatedPopover.tsx
index 0fb686c315..8854aac3ba 100644
--- a/packages/ui/components/popover/AnimatedPopover.tsx
+++ b/packages/ui/components/popover/AnimatedPopover.tsx
@@ -8,6 +8,7 @@ import { Tooltip } from "../tooltip";
export const AnimatedPopover = ({
text,
+ PrefixComponent,
count,
popoverTriggerClassNames,
children,
@@ -16,6 +17,7 @@ export const AnimatedPopover = ({
prefix,
}: {
text: string;
+ PrefixComponent?: React.ReactNode;
count?: number;
children: React.ReactNode;
popoverTriggerClassNames?: string;
@@ -62,6 +64,7 @@ export const AnimatedPopover = ({
+ {PrefixComponent ? PrefixComponent : null}
{prefix && {prefix} }
{text}
{count && count > 0 && (