feat: add paid, userEmail, userName, and rating filters to insights page (#23299)

* feat: add paid, userEmail, and userName filters to insights page

- Add paid filter as MULTI_SELECT with 'Paid'/'Free' options
- Add userEmail filter as TEXT for searching user emails
- Add userName filter as TEXT for searching user names
- Update DummyTableRow type to include new filter columns
- Add backend filtering logic in buildColumnFilterCondition method
- Use existing columns from BookingTimeStatusDenormalized view
- Follow established patterns from /bookings page implementation

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: change paid filter from MULTI_SELECT to SINGLE_SELECT

- Updated useInsightsBookings.ts to use ColumnFilterType.SINGLE_SELECT for paid filter
- Modified buildColumnFilterCondition to handle single select with isSingleSelectFilterValue
- Simplified backend logic to handle single boolean value instead of array

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix paid filter

* feat: add rating filter to insights page

- Add rating field to DummyTableRow type
- Add rating column with SINGLE_SELECT filter type
- Add rating options 1-5 in faceted unique values
- Add rating filtering logic in backend service
- Rating filter allows filtering bookings by 1-5 star ratings

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* feat: change rating filter to NUMBER type

- Update rating filter from SINGLE_SELECT to NUMBER in useInsightsBookings.ts
- Remove rating options from useInsightsBookingFacetedUniqueValues.ts since NUMBER filters don't need predefined options
- Add isNumberFilterValue import and implement number filter logic in InsightsBookingBaseService.ts
- Support eq, gt, gte, lt, lte operators for rating number filter

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: use makeSqlCondition utility for filter logic

- Import makeSqlCondition from @calcom/features/data-table/lib/server
- Replace manual SQL condition building with makeSqlCondition utility for userEmail, userName, and rating filters
- Maintain existing paid filter logic for boolean conversion
- Follow established pattern from InsightsRoutingBaseService
- Cleaner, more maintainable code with consistent filter handling

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* add missing texts

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2025-08-27 16:00:10 +00:00
1 parent d0e59f1824
commit b7a7797293
4 files changed
+100 -2

No files matched your search

@@ -3491,5 +3491,8 @@
"meeting_link": "Meeting link",
"my_bookings": "My Bookings",
"phone": "Phone",
"free": "Free",
"user_email": "User Email",
"user_name": "User Name",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -2,6 +2,7 @@ import type { Table } from "@tanstack/react-table";
import { useCallback } from "react";
import { convertFacetedValuesToMap, type FacetedValue } from "@calcom/features/data-table";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { BookingStatus } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc";
@@ -24,6 +25,7 @@ export const useInsightsBookingFacetedUniqueValues = ({
teamId: number | undefined;
isAll: boolean;
}) => {
const { t } = useLocale();
const { data: users } = trpc.viewer.insights.userList.useQuery(
{
teamId,
@@ -68,9 +70,14 @@ export const useInsightsBookingFacetedUniqueValues = ({
label: eventType.teamId ? `${eventType.title} (${eventType.team?.name})` : eventType.title,
})) ?? []
);
} else if (columnId === "paid") {
return convertFacetedValuesToMap([
{ value: "true", label: t("paid") },
{ value: "false", label: t("free") },
]);
}
return new Map<FacetedValue, number>();
},
[users, eventTypes]
[users, eventTypes, t]
);
};
@@ -12,6 +12,10 @@ type DummyTableRow = {
userId: number | null;
eventTypeId: number | null;
status: BookingStatus;
paid: boolean;
userEmail: string | null;
userName: string | null;
rating: number | null;
};
const emptyData: DummyTableRow[] = [];
@@ -67,6 +71,58 @@ export const useInsightsBookings = () => {
},
cell: () => null,
}),
columnHelper.accessor("paid", {
id: "paid",
header: t("paid"),
size: 150,
meta: {
filter: {
type: ColumnFilterType.SINGLE_SELECT,
},
},
enableColumnFilter: true,
enableSorting: false,
cell: () => null,
}),
columnHelper.accessor("userEmail", {
id: "userEmail",
header: t("user_email"),
size: 200,
meta: {
filter: {
type: ColumnFilterType.TEXT,
},
},
enableColumnFilter: true,
enableSorting: false,
cell: () => null,
}),
columnHelper.accessor("userName", {
id: "userName",
header: t("user_name"),
size: 200,
meta: {
filter: {
type: ColumnFilterType.TEXT,
},
},
enableColumnFilter: true,
enableSorting: false,
cell: () => null,
}),
columnHelper.accessor("rating", {
id: "rating",
header: t("rating"),
size: 150,
meta: {
filter: {
type: ColumnFilterType.NUMBER,
},
},
enableColumnFilter: true,
enableSorting: false,
cell: () => null,
}),
];
}, [t]);
@@ -3,9 +3,15 @@ import md5 from "md5";
import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { makeSqlCondition } from "@calcom/features/data-table/lib/server";
import { ZColumnFilter } from "@calcom/features/data-table/lib/types";
import type { ColumnFilter } from "@calcom/features/data-table/lib/types";
import { isSingleSelectFilterValue, isMultiSelectFilterValue } from "@calcom/features/data-table/lib/utils";
import {
isSingleSelectFilterValue,
isMultiSelectFilterValue,
isTextFilterValue,
isNumberFilterValue,
} from "@calcom/features/data-table/lib/utils";
import type { DateRange } from "@calcom/features/insights/server/insightsDateUtils";
import type { readonlyPrisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
@@ -319,6 +325,32 @@ export class InsightsBookingBaseService {
return Prisma.sql`"status" IN (${Prisma.join(statusValues)})`;
}
if (id === "paid" && isSingleSelectFilterValue(value)) {
const paidValue = value.data === "true";
return Prisma.sql`"paid" = ${paidValue}`;
}
if (id === "userEmail" && isTextFilterValue(value)) {
const condition = makeSqlCondition(value);
if (condition) {
return Prisma.sql`"userEmail" ${condition}`;
}
}
if (id === "userName" && isTextFilterValue(value)) {
const condition = makeSqlCondition(value);
if (condition) {
return Prisma.sql`"userName" ${condition}`;
}
}
if (id === "rating" && isNumberFilterValue(value)) {
const condition = makeSqlCondition(value);
if (condition) {
return Prisma.sql`"rating" ${condition}`;
}
}
return null;
}