diff --git a/packages/features/insights/components/FailedBookingsByField.tsx b/packages/features/insights/components/FailedBookingsByField.tsx index a41fee435b..b14c27d167 100644 --- a/packages/features/insights/components/FailedBookingsByField.tsx +++ b/packages/features/insights/components/FailedBookingsByField.tsx @@ -56,11 +56,12 @@ function FormCard({ formName, fields }: FormCardProps) { export function FailedBookingsByField() { const { filter } = useFilterContext(); - const { selectedTeamId, isAll, initialConfig, selectedRoutingFormId } = filter; + 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, diff --git a/packages/features/insights/components/RoutedToPerPeriod.tsx b/packages/features/insights/components/RoutedToPerPeriod.tsx index c73c39a0b3..8160b2f5ec 100644 --- a/packages/features/insights/components/RoutedToPerPeriod.tsx +++ b/packages/features/insights/components/RoutedToPerPeriod.tsx @@ -33,6 +33,7 @@ import { useFilterContext } from "../context/provider"; interface DownloadButtonProps { teamId?: number; + userId?: number; isAll?: boolean; routingFormId?: string; dateRange: [Dayjs, Dayjs, string | null]; @@ -41,6 +42,7 @@ interface DownloadButtonProps { } function DownloadButton({ + userId, teamId, isAll, routingFormId, @@ -57,6 +59,7 @@ 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() ?? "", @@ -97,6 +100,7 @@ interface FormCardProps { onSearchChange: (value: string) => void; children: ReactNode; teamId?: number; + userId?: number; isAll?: boolean; routingFormId?: string; dateRange: [Dayjs, Dayjs, string | null]; @@ -109,6 +113,7 @@ function FormCard({ onSearchChange, children, teamId, + userId, isAll, routingFormId, dateRange, @@ -141,6 +146,7 @@ function FormCard({ /> (null); const { copyToClipboard } = useCopy(); - const { dateRange, selectedTeamId, isAll, initialConfig, selectedRoutingFormId, selectedMemberUserId } = - filter; + const { + dateRange, + selectedTeamId, + isAll, + initialConfig, + selectedRoutingFormId, + selectedMemberUserId, + selectedUserId, + selectedBookingStatus, + selectedRoutingFormFilter, + } = filter; const initialConfigIsReady = !!(initialConfig?.teamId || initialConfig?.userId || initialConfig?.isAll); const [startDate, endDate] = dateRange; @@ -276,6 +285,7 @@ export function RoutingFormResponsesTableContent({ const { data: headers, isLoading: isHeadersLoading } = trpc.viewer.insights.routingFormResponsesHeaders.useQuery( { + userId: selectedUserId ?? undefined, teamId: selectedTeamId ?? undefined, isAll: isAll ?? false, routingFormId: selectedRoutingFormId ?? undefined, @@ -291,7 +301,8 @@ export function RoutingFormResponsesTableContent({ teamId: selectedTeamId, startDate: startDate.toISOString(), endDate: endDate.toISOString(), - userId: selectedMemberUserId ?? undefined, + userId: selectedUserId ?? undefined, + memberUserId: selectedMemberUserId ?? undefined, isAll: isAll ?? false, routingFormId: selectedRoutingFormId ?? undefined, columnFilters, diff --git a/packages/features/insights/components/RoutingKPICards.tsx b/packages/features/insights/components/RoutingKPICards.tsx index 3734e57e77..a3d67bafb7 100644 --- a/packages/features/insights/components/RoutingKPICards.tsx +++ b/packages/features/insights/components/RoutingKPICards.tsx @@ -36,7 +36,8 @@ export const RoutingKPICards = () => { eventTypeId: selectedEventTypeId ?? undefined, isAll, routingFormId: selectedRoutingFormId ?? undefined, - userId: selectedMemberUserId ?? undefined, + userId: selectedUserId ?? undefined, + memberUserId: selectedMemberUserId ?? undefined, bookingStatus: selectedBookingStatus ?? undefined, fieldFilter: selectedRoutingFormFilter ?? undefined, }, diff --git a/packages/features/insights/context/FiltersProvider.tsx b/packages/features/insights/context/FiltersProvider.tsx index 7920175660..440edd31cd 100644 --- a/packages/features/insights/context/FiltersProvider.tsx +++ b/packages/features/insights/context/FiltersProvider.tsx @@ -96,6 +96,7 @@ export function FiltersProvider({ children }: { children: React.ReactNode }) { // Use the custom hook const { mostPopularForm } = useDefaultRoutingForm({ + userId: userIdParsed, teamId: teamIdParsed, isAll: safe.success ? !!safe.data.teamId : false, routingFormId: routingFormIdParsed, diff --git a/packages/features/insights/filters/Download/RoutingDownload.tsx b/packages/features/insights/filters/Download/RoutingDownload.tsx index b4980c76a3..45684b91ae 100644 --- a/packages/features/insights/filters/Download/RoutingDownload.tsx +++ b/packages/features/insights/filters/Download/RoutingDownload.tsx @@ -29,6 +29,7 @@ const RoutingDownload = () => { endDate: filter.dateRange[1].toISOString(), teamId: filter.selectedTeamId ?? undefined, userId: filter.selectedUserId ?? undefined, + memberUserId: filter.selectedMemberUserId ?? undefined, routingFormId: filter.selectedRoutingFormId ?? undefined, bookingStatus: filter.selectedBookingStatus ?? undefined, fieldFilter: filter.selectedRoutingFormFilter ?? undefined, diff --git a/packages/features/insights/filters/FilterType.tsx b/packages/features/insights/filters/FilterType.tsx index e01a9b3086..ecac07c677 100644 --- a/packages/features/insights/filters/FilterType.tsx +++ b/packages/features/insights/filters/FilterType.tsx @@ -34,6 +34,7 @@ export const FilterType = ({ showRoutingFilters = false }: { showRoutingFilters? // Query routing form field options when showRoutingFilters is true const { data: routingFormFieldOptions } = trpc.viewer.insights.getRoutingFormFieldOptions.useQuery( { + userId: selectedUserId ?? -1, teamId: selectedTeamId ?? -1, isAll: !!isAll, routingFormId: selectedRoutingFormId ?? undefined, diff --git a/packages/features/insights/filters/RoutingFormFieldFilter.tsx b/packages/features/insights/filters/RoutingFormFieldFilter.tsx index b7c4707500..c9e67e7e16 100644 --- a/packages/features/insights/filters/RoutingFormFieldFilter.tsx +++ b/packages/features/insights/filters/RoutingFormFieldFilter.tsx @@ -9,10 +9,11 @@ import { useFilterContext } from "../context/provider"; export const RoutingFormFieldFilter = ({ fieldId }: { fieldId: string }) => { const { t } = useLocale(); const { filter, setConfigFilters } = useFilterContext(); - const { selectedTeamId, isAll, selectedRoutingFormId, selectedRoutingFormFilter } = filter; + const { selectedTeamId, selectedUserId, isAll, selectedRoutingFormId, selectedRoutingFormFilter } = filter; const { data: fieldOptions } = trpc.viewer.insights.getRoutingFormFieldOptions.useQuery( { + userId: selectedUserId ?? undefined, teamId: selectedTeamId ?? undefined, isAll: !!isAll, routingFormId: selectedRoutingFormId ?? undefined, diff --git a/packages/features/insights/filters/RoutingFormFilterList.tsx b/packages/features/insights/filters/RoutingFormFilterList.tsx index b70203366a..9ebd23f2c0 100644 --- a/packages/features/insights/filters/RoutingFormFilterList.tsx +++ b/packages/features/insights/filters/RoutingFormFilterList.tsx @@ -20,11 +20,12 @@ export const RoutingFormFilterList = memo( ({ showOnlyWhenSelectedInContext = true }: { showOnlyWhenSelectedInContext?: boolean }) => { const { t } = useLocale(); const { filter, setConfigFilters } = useFilterContext(); - const { selectedTeamId, selectedRoutingFormId, isAll } = filter; + 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, }, diff --git a/packages/features/insights/filters/TeamAndSelfList.tsx b/packages/features/insights/filters/TeamAndSelfList.tsx index 241a33c33b..9c20b4c928 100644 --- a/packages/features/insights/filters/TeamAndSelfList.tsx +++ b/packages/features/insights/filters/TeamAndSelfList.tsx @@ -100,6 +100,7 @@ export const TeamAndSelfList = ({ selectedTeamId: data[0].id, selectedUserId: null, selectedTeamName: null, + selectedRoutingFormId: null, isAll: true, }); }} @@ -127,12 +128,14 @@ export const TeamAndSelfList = ({ selectedEventTypeId: null, selectedMemberUserId: null, selectedFilter: null, + selectedRoutingFormId: null, }); } else if (!e.target.checked) { setConfigFilters({ selectedTeamId: isOrgDataAvailable ? data[0].id : null, selectedTeamName: null, - isAll: true, + selectedRoutingFormId: null, + isAll: isOrgDataAvailable, }); } }} @@ -155,7 +158,9 @@ export const TeamAndSelfList = ({ onChange={(e) => { if (e.target.checked) { setConfigFilters({ + selectedRoutingFormId: null, selectedUserId: session.data?.user.id, + selectedMemberUserId: null, selectedTeamId: null, isAll: false, }); @@ -164,6 +169,7 @@ export const TeamAndSelfList = ({ selectedTeamId: isOrgDataAvailable ? data[0].id : null, selectedUserId: null, selectedTeamName: null, + selectedRoutingFormId: null, isAll: isOrgDataAvailable, }); } diff --git a/packages/features/insights/hooks/useDefaultRoutingForm.ts b/packages/features/insights/hooks/useDefaultRoutingForm.ts index 8cf93c624a..cddd864aa2 100644 --- a/packages/features/insights/hooks/useDefaultRoutingForm.ts +++ b/packages/features/insights/hooks/useDefaultRoutingForm.ts @@ -5,11 +5,13 @@ import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { trpc } from "@calcom/trpc"; export function useDefaultRoutingForm({ + userId, teamId, isAll, routingFormId, onRoutingFormChange, }: { + userId: number | null | undefined; teamId: number | null | undefined; isAll: boolean; routingFormId: string | null | undefined; @@ -24,6 +26,7 @@ export function useDefaultRoutingForm({ // Query to get routing forms list const { data: routingForms } = trpc.viewer.insights.getRoutingFormsForFilters.useQuery( { + userId: userId ?? undefined, teamId: teamId ?? undefined, isAll, }, diff --git a/packages/features/insights/server/routing-events.ts b/packages/features/insights/server/routing-events.ts index b2c0bbb0b5..7eff6b4e29 100644 --- a/packages/features/insights/server/routing-events.ts +++ b/packages/features/insights/server/routing-events.ts @@ -11,6 +11,7 @@ import { readonlyPrisma as prisma } from "@calcom/prisma"; import type { BookingStatus } from "@calcom/prisma/enums"; type RoutingFormInsightsTeamFilter = { + userId?: number | null; teamId?: number | null; isAll: boolean; organizationId?: number | null; @@ -20,7 +21,7 @@ type RoutingFormInsightsTeamFilter = { type RoutingFormInsightsFilter = RoutingFormInsightsTeamFilter & { startDate?: string; endDate?: string; - userId?: number | null; + memberUserId?: number | null; searchQuery?: string | null; bookingStatus?: BookingStatus | "NO_BOOKING" | null; fieldFilter?: { @@ -32,6 +33,7 @@ type RoutingFormInsightsFilter = RoutingFormInsightsTeamFilter & { class RoutingEventsInsights { private static async getWhereForTeamOrAllTeams({ + userId, teamId, isAll, organizationId, @@ -55,23 +57,21 @@ class RoutingEventsInsights { // Base where condition for forms const formsWhereCondition: Prisma.App_RoutingForms_FormWhereInput = { - ...(teamIds.length > 0 && { - teamId: { - in: teamIds, - }, - }), + ...(teamIds.length > 0 + ? { + teamId: { + in: teamIds, + }, + } + : { + userId: userId ?? -1, + teamId: null, + }), ...(routingFormId && { id: routingFormId, }), }; - if (teamIds.length === 0 && !routingFormId) { - if (!organizationId) { - throw new Error("Organization ID is required"); - } - formsWhereCondition.teamId = organizationId; - } - return formsWhereCondition; } @@ -83,12 +83,14 @@ class RoutingEventsInsights { organizationId, routingFormId, userId, + memberUserId, searchQuery, bookingStatus, fieldFilter, }: Omit) { // Get team IDs based on organization if applicable const formsWhereCondition = await this.getWhereForTeamOrAllTeams({ + userId, teamId, isAll, organizationId, @@ -104,13 +106,13 @@ class RoutingEventsInsights { lte: dayjs(endDate).endOf("day").toDate(), }, }), - ...(userId || bookingStatus || searchQuery + ...(memberUserId || bookingStatus || searchQuery ? { ...(bookingStatus === "NO_BOOKING" ? { routedToBooking: null } : { routedToBooking: { - ...(userId && { userId }), + ...(memberUserId && { userId: memberUserId }), ...(searchQuery && { user: { OR: [ @@ -166,16 +168,23 @@ class RoutingEventsInsights { } static async getRoutingFormsForFilters({ + userId, teamId, isAll, organizationId, }: { + userId?: number; teamId?: number; isAll: boolean; organizationId?: number | undefined; routingFormId?: string | undefined; }) { - const formsWhereCondition = await this.getWhereForTeamOrAllTeams({ teamId, isAll, organizationId }); + const formsWhereCondition = await this.getWhereForTeamOrAllTeams({ + userId, + teamId, + isAll, + organizationId, + }); return await prisma.app_RoutingForms_Form.findMany({ where: formsWhereCondition, select: { @@ -200,9 +209,11 @@ class RoutingEventsInsights { cursor, limit, userId, + memberUserId, columnFilters, }: Omit & { cursor?: number; limit?: number }) { const formsTeamWhereCondition = await this.getWhereForTeamOrAllTeams({ + userId, teamId, isAll, organizationId, @@ -218,8 +229,8 @@ class RoutingEventsInsights { ); let bookingWhereInput: Prisma.BookingWhereInput = {}; - if (userId) { - bookingWhereInput.userId = userId; + if (memberUserId) { + bookingWhereInput.userId = memberUserId; } if (bookingStatusFilter) { bookingWhereInput = { @@ -329,12 +340,14 @@ class RoutingEventsInsights { } static async getRoutingFormFieldOptions({ + userId, teamId, isAll, routingFormId, organizationId, }: RoutingFormInsightsTeamFilter) { const formsWhereCondition = await this.getWhereForTeamOrAllTeams({ + userId, teamId, isAll, routingFormId, @@ -355,12 +368,14 @@ class RoutingEventsInsights { } static async getFailedBookingsByRoutingFormGroup({ + userId, teamId, isAll, routingFormId, organizationId, }: RoutingFormInsightsTeamFilter) { const formsWhereCondition = await this.getWhereForTeamOrAllTeams({ + userId, teamId, isAll, organizationId, @@ -374,6 +389,10 @@ class RoutingEventsInsights { // @ts-expect-error it doest exist but TS isnt smart enough when its unmber or int filter teamConditions.push(`f."teamId" IN (${formsWhereCondition.teamId.in.join(",")})`); } + // @ts-expect-error it doest exist but TS isnt smart enough when its unmber or int filter + if (!formsWhereCondition.teamId?.in && userId) { + teamConditions.push(`f.userId = '${userId}'`); + } if (routingFormId) { teamConditions.push(`f.id = '${routingFormId}'`); } @@ -486,12 +505,14 @@ class RoutingEventsInsights { } static async getRoutingFormHeaders({ + userId, teamId, isAll, organizationId, routingFormId, }: RoutingFormInsightsTeamFilter) { const formsWhereCondition = await this.getWhereForTeamOrAllTeams({ + userId, teamId, isAll, organizationId, @@ -528,12 +549,14 @@ class RoutingEventsInsights { organizationId, routingFormId, userId, + memberUserId, bookingStatus, fieldFilter, take, skip, }: Omit & { take?: number; skip?: number }) { const formsWhereCondition = await this.getWhereForTeamOrAllTeams({ + userId, teamId, isAll, organizationId, @@ -587,13 +610,13 @@ class RoutingEventsInsights { lte: dayjs(endDate).endOf("day").toDate(), }, }), - ...(userId || bookingStatus + ...(memberUserId || bookingStatus ? { ...(bookingStatus === "NO_BOOKING" ? { routedToBooking: null } : { routedToBooking: { - ...(userId && { userId }), + ...(memberUserId && { userId: memberUserId }), ...(bookingStatus && { status: bookingStatus }), }, }), @@ -698,6 +721,7 @@ class RoutingEventsInsights { } static async routedToPerPeriod({ + userId, teamId, isAll, organizationId, @@ -730,6 +754,7 @@ class RoutingEventsInsights { // Build the team conditions for the WHERE clause const formsWhereCondition = await this.getWhereForTeamOrAllTeams({ + userId, teamId, isAll, organizationId, @@ -743,6 +768,10 @@ class RoutingEventsInsights { // @ts-expect-error same as above teamConditions.push(`f."teamId" IN (${formsWhereCondition.teamId.in.join(",")})`); } + // @ts-expect-error it does exist but TS isn't smart enough when it's number or int filter + if (!formsWhereCondition.teamId?.in && userId) { + teamConditions.push(`f.userId = '${userId}'`); + } if (routingFormId) { teamConditions.push(`f.id = '${routingFormId}'`); } @@ -953,6 +982,7 @@ class RoutingEventsInsights { } static async routedToPerPeriodCsv({ + userId, teamId, isAll, organizationId, @@ -979,6 +1009,7 @@ class RoutingEventsInsights { // Build the team conditions for the WHERE clause const formsWhereCondition = await this.getWhereForTeamOrAllTeams({ + userId, teamId, isAll, organizationId, @@ -992,6 +1023,10 @@ class RoutingEventsInsights { // @ts-expect-error same as above teamConditions.push(`f."teamId" IN (${formsWhereCondition.teamId.in.join(",")})`); } + // @ts-expect-error it does exist but TS isn't smart enough when it's number or int filter + if (!formsWhereCondition.teamId?.in && userId) { + teamConditions.push(`f.userId = '${userId}'`); + } if (routingFormId) { teamConditions.push(`f.id = '${routingFormId}'`); } diff --git a/packages/features/insights/server/trpc-router.ts b/packages/features/insights/server/trpc-router.ts index f36dd56425..ecc4dedd17 100644 --- a/packages/features/insights/server/trpc-router.ts +++ b/packages/features/insights/server/trpc-router.ts @@ -1552,10 +1552,11 @@ export const insightsRouter = router({ }), getRoutingFormsForFilters: userBelongsToTeamProcedure - .input(z.object({ teamId: z.number().optional(), isAll: z.boolean() })) + .input(z.object({ userId: z.number().optional(), teamId: z.number().optional(), isAll: z.boolean() })) .query(async ({ ctx, input }) => { - const { teamId, isAll } = input; + const { userId, teamId, isAll } = input; return await RoutingEventsInsights.getRoutingFormsForFilters({ + userId, teamId, isAll, organizationId: ctx.user.organizationId ?? undefined, @@ -1585,6 +1586,7 @@ export const insightsRouter = router({ 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, }); @@ -1611,13 +1613,19 @@ export const insightsRouter = router({ routingFormId: input.routingFormId ?? null, cursor: input.cursor, userId: input.userId ?? null, + memberUserId: input.memberUserId ?? null, limit: input.limit, columnFilters: input.columnFilters, }); }), getRoutingFormFieldOptions: userBelongsToTeamProcedure .input( - z.object({ teamId: z.number().optional(), isAll: z.boolean(), routingFormId: z.string().optional() }) + z.object({ + userId: z.number().optional(), + teamId: z.number().optional(), + isAll: z.boolean(), + routingFormId: z.string().optional(), + }) ) .query(async ({ input, ctx }) => { const options = await RoutingEventsInsights.getRoutingFormFieldOptions({ @@ -1628,7 +1636,12 @@ export const insightsRouter = router({ }), failedBookingsByField: userBelongsToTeamProcedure .input( - z.object({ teamId: z.number().optional(), isAll: z.boolean(), routingFormId: z.string().optional() }) + z.object({ + userId: z.number().optional(), + teamId: z.number().optional(), + isAll: z.boolean(), + routingFormId: z.string().optional(), + }) ) .query(async ({ ctx, input }) => { return await RoutingEventsInsights.getFailedBookingsByRoutingFormGroup({ @@ -1639,6 +1652,7 @@ export const insightsRouter = router({ routingFormResponsesHeaders: userBelongsToTeamProcedure .input( z.object({ + userId: z.number().optional(), teamId: z.number().optional(), isAll: z.boolean(), routingFormId: z.string().optional(), @@ -1646,6 +1660,7 @@ export const insightsRouter = router({ ) .query(async ({ ctx, input }) => { const headers = await RoutingEventsInsights.getRoutingFormHeaders({ + userId: input.userId ?? null, teamId: input.teamId ?? null, isAll: input.isAll, organizationId: ctx.user.organizationId ?? null, @@ -1669,8 +1684,18 @@ export const insightsRouter = router({ }) ) .query(async ({ ctx, input }) => { - const { teamId, startDate, endDate, userId, isAll, routingFormId, bookingStatus, fieldFilter, cursor } = - input; + const { + teamId, + startDate, + endDate, + userId, + memberUserId, + isAll, + routingFormId, + bookingStatus, + fieldFilter, + cursor, + } = input; if (!teamId && !userId) { return { data: [], hasMore: false, nextCursor: null }; @@ -1682,6 +1707,7 @@ export const insightsRouter = router({ startDate, endDate, userId, + memberUserId, isAll: isAll ?? false, organizationId: ctx.user.organizationId, routingFormId, @@ -1719,9 +1745,11 @@ export const insightsRouter = router({ }) ) .query(async ({ ctx, input }) => { - const { teamId, startDate, endDate, period, cursor, limit, isAll, routingFormId, searchQuery } = input; + const { teamId, userId, startDate, endDate, period, cursor, limit, isAll, routingFormId, searchQuery } = + input; return await RoutingEventsInsights.routedToPerPeriod({ + userId: userId ?? null, teamId: teamId ?? null, startDate, endDate, @@ -1747,6 +1775,7 @@ export const insightsRouter = router({ const { startDate, endDate } = input; try { const csvData = await RoutingEventsInsights.routedToPerPeriodCsv({ + userId: input.userId ?? null, teamId: input.teamId ?? null, startDate, endDate,