Repository: calcom/cal.com
Pull request #20356: fix: add UID filter to /insights/routing
Author: login: eunjae-lee
Labels: name: routing-forms
description: area: routing forms, routing, forms
name: insights
description: area: insights, analytics
name: core
description: area: core, team members only
name: consumer
name: ready-for-e2e
Created: 2025-03-25T16:45:31Z | Merged: 2025-03-26T09:52:40Z

## What does this PR do?

This PR adds UID filter to /insights/routing

- Fixes CAL-5285

## Visual Demo (For contributors especially)

![Screenshot 2025-03-25 at 17 42 06](https://github.com/user-attachments/assets/4ae23192-9190-48c0-b621-37ea0b56253c)


## Mandatory Tasks (DO NOT REMOVE)

- [x] I have self-reviewed the code (A decent size PR without self-review might be rejected).
- [x] N/A - I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). If N/A, write N/A here and check the checkbox.
- [x] I confirm automated tests are in place that prove my fix is effective or that my feature works.




Diff:
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 5fb157e96b581b..daa2e4be7ddb41 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -3037,5 +3037,6 @@
   "no_team_members": "You don't have team members yet",
   "could_not_find_slug_to_publish_org": "Could not find slug to publish the organization",
   "picklist": "Picklist",
+  "name_or_email": "Name or Email",
   "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
-}
\ No newline at end of file
+}
diff --git a/packages/features/data-table/components/filters/TextFilterOptions.tsx b/packages/features/data-table/components/filters/TextFilterOptions.tsx
index 9a0bc5140d9d36..c05ffc83e6f8d1 100644
--- a/packages/features/data-table/components/filters/TextFilterOptions.tsx
+++ b/packages/features/data-table/components/filters/TextFilterOptions.tsx
@@ -3,8 +3,8 @@
 import { useForm, Controller } from "react-hook-form";
 
 import { useLocale } from "@calcom/lib/hooks/useLocale";
-import { Select } from "@calcom/ui/components/form";
 import { Button } from "@calcom/ui/components/button";
+import { Select } from "@calcom/ui/components/form";
 import { Form } from "@calcom/ui/components/form";
 import { Input } from "@calcom/ui/components/form";
 
@@ -19,7 +19,7 @@ export type TextFilterOptionsProps = {
 
 export function TextFilterOptions({ column }: TextFilterOptionsProps) {
   const { t } = useLocale();
-  const textFilterOperatorOptions = useTextFilterOperatorOptions();
+  const textFilterOperatorOptions = useTextFilterOperatorOptions(column.textOptions?.allowedOperators);
   const filterValue = useFilterValue(column.id, ZTextFilterValue);
   const { updateFilter, removeFilter } = useDataTable();
 
@@ -63,7 +63,13 @@ export function TextFilterOptions({ column }: TextFilterOptionsProps) {
                     }
                   }}
                 />
-                {value?.requiresOperand && <Input className="mt-2" {...form.register("operand")} />}
+                {value?.requiresOperand && (
+                  <Input
+                    className="mt-2"
+                    {...form.register("operand")}
+                    placeholder={column.textOptions?.placeholder}
+                  />
+                )}
               </>
             )}
           />
diff --git a/packages/features/data-table/components/filters/utils.ts b/packages/features/data-table/components/filters/utils.ts
index 00099c28f51112..a9a6ca59d437cb 100644
--- a/packages/features/data-table/components/filters/utils.ts
+++ b/packages/features/data-table/components/filters/utils.ts
@@ -1,5 +1,6 @@
 import { useLocale } from "@calcom/lib/hooks/useLocale";
 
+import { type TextFilterOperator, textFilterOperators } from "../../lib/types";
 import type { TextFilterOperatorOption, NumberFilterOperatorOption } from "./types";
 
 export const numberFilterOperatorOptions: NumberFilterOperatorOption[] = [
@@ -11,9 +12,12 @@ export const numberFilterOperatorOptions: NumberFilterOperatorOption[] = [
   { value: "lte", label: "≤" },
 ];
 
-export const useTextFilterOperatorOptions = (): TextFilterOperatorOption[] => {
+export const useTextFilterOperatorOptions = (
+  allowedOperators?: TextFilterOperator[]
+): TextFilterOperatorOption[] => {
   const { t } = useLocale();
-  return [
+  const operators = allowedOperators ?? textFilterOperators;
+  const options = [
     { value: "equals", label: t("filter_operator_is"), requiresOperand: true },
     { value: "notEquals", label: t("filter_operator_is_not"), requiresOperand: true },
     { value: "contains", label: t("filter_operator_contains"), requiresOperand: true },
@@ -23,4 +27,7 @@ export const useTextFilterOperatorOptions = (): TextFilterOperatorOption[] => {
     { value: "isEmpty", label: t("filter_operator_is_empty"), requiresOperand: false },
     { value: "isNotEmpty", label: t("filter_operator_not_empty"), requiresOperand: false },
   ];
+  return options.filter((opt): opt is TextFilterOperatorOption =>
+    operators.includes(opt.value as TextFilterOperator)
+  );
 };
diff --git a/packages/features/data-table/lib/server.ts b/packages/features/data-table/lib/server.ts
index 4bee912079fbb0..1baa98979ea4b1 100644
--- a/packages/features/data-table/lib/server.ts
+++ b/packages/features/data-table/lib/server.ts
@@ -6,7 +6,7 @@ import {
   isNumberFilterValue,
 } from "./utils";
 
-type makeWhereClauseProps = {
+type MakeWhereClauseProps = {
   columnName: string;
   filterValue: FilterValue;
   json?: true | { path: string[] };
@@ -20,7 +20,7 @@ export function makeOrderBy(sorting: SortingState) {
   }));
 }
 
-export function makeWhereClause(props: makeWhereClauseProps) {
+export function makeWhereClause(props: MakeWhereClauseProps) {
   const { columnName, filterValue } = props;
   const isJson = props.json === true || (typeof props.json === "object" && props.json.path?.length > 0);
   const jsonPath = isJson && typeof props.json === "object" ? props.json.path : undefined;
diff --git a/packages/features/data-table/lib/types.ts b/packages/features/data-table/lib/types.ts
index 0d7811acb6353c..b611e9e8b3a60f 100644
--- a/packages/features/data-table/lib/types.ts
+++ b/packages/features/data-table/lib/types.ts
@@ -10,7 +10,7 @@ export enum ColumnFilterType {
   DATE_RANGE = "dr",
 }
 
-const textFilterOperators = [
+export const textFilterOperators = [
   "equals",
   "notEquals",
   "contains",
@@ -113,14 +113,24 @@ export type DateRangeFilterOptions = {
   range: "past" | "custom";
 };
 
+export type TextFilterOptions = {
+  allowedOperators?: TextFilterOperator[];
+  placeholder?: string;
+};
+
 export type ColumnFilterMeta =
   | {
       type: ColumnFilterType.DATE_RANGE;
       icon?: IconName;
-      dateRangeOptions: DateRangeFilterOptions;
+      dateRangeOptions?: DateRangeFilterOptions;
+    }
+  | {
+      type: ColumnFilterType.TEXT;
+      icon?: IconName;
+      textOptions?: TextFilterOptions;
     }
   | {
-      type?: Exclude<ColumnFilterType, ColumnFilterType.DATE_RANGE>;
+      type?: Exclude<ColumnFilterType, ColumnFilterType.DATE_RANGE | ColumnFilterType.TEXT>;
       icon?: IconName;
     };
 
@@ -139,6 +149,7 @@ export type FilterableColumn = {
     }
   | {
       type: ColumnFilterType.TEXT;
+      textOptions?: TextFilterOptions;
     }
   | {
       type: ColumnFilterType.NUMBER;
diff --git a/packages/features/insights/components/RoutingKPICards.tsx b/packages/features/insights/components/RoutingKPICards.tsx
index 1d8bab4d582d49..a912dd3cb63fce 100644
--- a/packages/features/insights/components/RoutingKPICards.tsx
+++ b/packages/features/insights/components/RoutingKPICards.tsx
@@ -50,10 +50,14 @@ export const RoutingKPICards = () => {
     },
   ];
 
-  if (isPending || !data) {
+  if (isPending) {
     return <LoadingKPICards categories={categories} />;
   }
 
+  if (!data) {
+    return null;
+  }
+
   return (
     <>
       <Grid numColsSm={1} numColsLg={3} className="mt-4 gap-x-4 gap-y-4">
diff --git a/packages/features/insights/hooks/useInsightsColumns.tsx b/packages/features/insights/hooks/useInsightsColumns.tsx
index d663eb71534882..6bfcc86753fcb2 100644
--- a/packages/features/insights/hooks/useInsightsColumns.tsx
+++ b/packages/features/insights/hooks/useInsightsColumns.tsx
@@ -67,8 +67,16 @@ export const useInsightsColumns = ({
         id: "bookingUid",
         header: t("uid"),
         size: 100,
-        enableColumnFilter: false,
+        enableColumnFilter: true,
         enableSorting: false,
+        meta: {
+          filter: {
+            type: ColumnFilterType.TEXT,
+            textOptions: {
+              allowedOperators: ["equals"],
+            },
+          },
+        },
         cell: (info) => {
           const bookingUid = info.getValue();
           if (!bookingUid) return null;
@@ -95,6 +103,14 @@ export const useInsightsColumns = ({
         size: 200,
         enableColumnFilter: false,
         enableSorting: false,
+        meta: {
+          filter: {
+            type: ColumnFilterType.TEXT,
+            textOptions: {
+              placeholder: t("name_or_email"),
+            },
+          },
+        },
         cell: (info) => {
           return <BookedByCell attendees={info.getValue()} rowId={info.row.original.id} />;
         },
diff --git a/packages/features/insights/server/routing-events.ts b/packages/features/insights/server/routing-events.ts
index 433916194d3acb..a2b8693dab1135 100644
--- a/packages/features/insights/server/routing-events.ts
+++ b/packages/features/insights/server/routing-events.ts
@@ -126,6 +126,12 @@ class RoutingEventsInsights {
       sorting,
     });
 
+    if (whereClause.bookingUid) {
+      // If bookingUid filter is applied, total count should be either 0 or 1.
+      // So this metrics doesn't provide any value.
+      return null;
+    }
+
     const totalPromise = prisma.routingFormResponse.count({
       where: whereClause,
     });
@@ -217,9 +223,15 @@ class RoutingEventsInsights {
     const assignmentReasonValue = bookingAssignmentReason
       ? getLowercasedFilterValue(bookingAssignmentReason)
       : undefined;
+    const bookingUid = columnFilters.find((filter) => filter.id === "bookingUid") as
+      | TypedColumnFilter<ColumnFilterType.TEXT>
+      | undefined;
 
     const responseFilters = columnFilters.filter(
-      (filter) => filter.id !== "bookingStatusOrder" && filter.id !== "bookingAssignmentReason"
+      (filter) =>
+        filter.id !== "bookingStatusOrder" &&
+        filter.id !== "bookingAssignmentReason" &&
+        filter.id !== "bookingUid"
     );
 
     const whereClause: Prisma.RoutingFormResponseWhereInput = {
@@ -256,6 +268,13 @@ class RoutingEventsInsights {
           },
         }),
 
+      // bookingUid
+      ...(bookingUid &&
+        makeWhereClause({
+          columnName: "bookingUid",
+          filterValue: bookingUid.value,
+        })),
+
       // AND clause
       ...(responseFilters.length > 0 && {
         AND: responseFilters.map((fieldFilter) => {


Reviews:
user: login: eunjae-lee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T16:51:28Z
user: login: TusharBhatt1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T18:37:41Z
user: login: eunjae-lee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T20:27:59Z
user: login: sean-brydon
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-26T09:29:16Z

Review comments:
diff_hunk: @@ -113,14 +113,24 @@ export type DateRangeFilterOptions = {
   range: "past" | "custom";
 };
 
+export type TextFilterOptions = {
+  allowedOperators?: TextFilterOperator[];
+  placeholder?: string;
+};
+
 export type ColumnFilterMeta =
   | {
       type: ColumnFilterType.DATE_RANGE;
       icon?: IconName;
-      dateRangeOptions: DateRangeFilterOptions;
+      dateRangeOptions?: DateRangeFilterOptions;
+    }
+  | {
+      type: ColumnFilterType.TEXT;
+      icon?: IconName;
+      textOptions?: TextFilterOptions;
path: packages/features/data-table/lib/types.ts
user: login: eunjae-lee
body: supporting textOptions for TEXT type
created_at: 2025-03-25T16:50:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -50,10 +50,14 @@ export const RoutingKPICards = () => {
     },
   ];
 
-  if (isPending || !data) {
+  if (isPending) {
     return <LoadingKPICards categories={categories} />;
   }
 
+  if (!data) {
path: packages/features/insights/components/RoutingKPICards.tsx
user: login: eunjae-lee
body: now, the backend can return null when we need to hide this component.
created_at: 2025-03-25T16:50:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,5 +1,6 @@
 import { useLocale } from "@calcom/lib/hooks/useLocale";
 
+import { type TextFilterOperator, textFilterOperators } from "../../lib/types";
path: packages/features/data-table/components/filters/utils.ts
user: login: TusharBhatt1
body: I guess we can use absolute import here
created_at: 2025-03-25T18:37:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,5 +1,6 @@
 import { useLocale } from "@calcom/lib/hooks/useLocale";
 
+import { type TextFilterOperator, textFilterOperators } from "../../lib/types";
path: packages/features/data-table/components/filters/utils.ts
user: login: eunjae-lee
body: We decided to avoid absolute import within the same module for the better TS performance. For example, this file belongs to `packages/features/data-table` and it's importing `../../lib/types` which is still within the same `packages/features/data-table`. 
created_at: 2025-03-25T20:27:58Z
author_association: CONTRIBUTOR

PR comments:
user: login: linear[bot]
created_at: 2025-03-25T16:45:34Z
body: <p><a href="https://linear.app/calcom/issue/CAL-5285/add-uid-filters-to-insightsrouting">CAL-5285 Add UID filters to /insights/routing</a></p>
author_association: NONE
user: login: vercel[bot]
created_at: 2025-03-25T16:46:16Z
body: [vc]: #y80JENROQFvlmyW5PUjkUATIkBjg9s7zLdlr9w1KIX4=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJjYWxjb20td2ViLWNhbmFyeSIsInJvb3REaXJlY3RvcnkiOiJhcHBzL3dlYiIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9jYWwvY2FsY29tLXdlYi1jYW5hcnkvM1p3b25pZHJQcmVCbXNlcHZNdEYxNnRZOVJ4YyIsInByZXZpZXdVcmwiOiJjYWxjb20td2ViLWNhbmFyeS1naXQtZXVuamFlLWNhbC01Mjg1LWFkZC1tb3JlLWZpbHRlLTg1ZTc2NS1jYWwudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJJR05PUkVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9fSx7Im5hbWUiOiJjYWwiLCJyb290RGlyZWN0b3J5IjoiYXBwcy93ZWIiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vY2FsL2NhbC9BMW9SOHV2V1hBV1dLMUs5b1B6aEdYdkZrU2RHIiwicHJldmlld1VybCI6ImNhbC1naXQtZXVuamFlLWNhbC01Mjg1LWFkZC1tb3JlLWZpbHRlcnMtb24taW5zaWdodHNyb3V0aW5nLWNhbC52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IklHTk9SRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)


<details><summary>2 Skipped Deployments</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **cal** | ⬜️ Ignored ([Inspect](https://vercel.com/cal/cal/A1oR8uvWXAWWK1K9oPzhGXvFkSdG)) | [Visit Preview](https://cal-git-eunjae-cal-5285-add-more-filters-on-insightsrouting-cal.vercel.app) |  | Mar 26, 2025 9:33am |
| **calcom-web-canary** | ⬜️ Ignored ([Inspect](https://vercel.com/cal/calcom-web-canary/3ZwonidrPreBmsepvMtF16tY9Rxc)) | [Visit Preview](https://calcom-web-canary-git-eunjae-cal-5285-add-more-filte-85e765-cal.vercel.app) |  | Mar 26, 2025 9:33am |
</details>



author_association: NONE
user: login: graphite-app[bot]
created_at: 2025-03-25T17:05:11Z
body: ### Graphite Automations
<details><summary><a href="https://app.graphite.dev/automations/?org=calcom&ruleId=qoTwHtX8MnP39HKxIt4V&ref=gt-automation-github-comment">"Add consumer team as reviewer"</a> took an action on this PR • (03/25/25)</summary><p>1 reviewer was added to this PR based on Keith Williams's automation.</p></details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-26T10:01:51Z
body: <!-- GENERATED-E2E-RESULTS -->
## E2E results are ready!
- [Workflow #54096.1 latest results](https://calcom.github.io/test-results/reports/eunjae/cal-5285-add-more-filters-on-insightsrouting/14079911359/1)
author_association: CONTRIBUTOR