From f039e116d7c8be12a8a31533ae2be6c86658cd7f Mon Sep 17 00:00:00 2001 From: Eunjae Lee Date: Tue, 1 Apr 2025 00:00:15 +0200 Subject: [PATCH] fix: eventTypeId filter on Download at /insights (#20463) * fix: eventTypeId filter on Download at /insights * show toast for error --- .../insights/filters/Download/Download.tsx | 63 ++--- packages/features/insights/server/events.ts | 17 +- .../features/insights/server/trpc-router.ts | 238 +++++++++++------- 3 files changed, 182 insertions(+), 136 deletions(-) diff --git a/packages/features/insights/filters/Download/Download.tsx b/packages/features/insights/filters/Download/Download.tsx index d44dfe2616..0f31b47c43 100644 --- a/packages/features/insights/filters/Download/Download.tsx +++ b/packages/features/insights/filters/Download/Download.tsx @@ -1,41 +1,46 @@ +import { useState } from "react"; + import { downloadAsCsv } from "@calcom/lib/csvUtils"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import type { RouterOutputs } from "@calcom/trpc"; import { trpc } from "@calcom/trpc"; -import { Dropdown, DropdownItem, DropdownMenuContent, DropdownMenuTrigger } from "@calcom/ui/components/dropdown"; import { Button } from "@calcom/ui/components/button"; +import { + Dropdown, + DropdownItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@calcom/ui/components/dropdown"; +import { showToast } from "@calcom/ui/components/toast"; import { useInsightsParameters } from "../../hooks/useInsightsParameters"; const Download = () => { const { t } = useLocale(); - const { startDate, endDate, teamId, userId, eventTypeId, memberUserId } = useInsightsParameters(); + const { startDate, endDate, teamId, userId, eventTypeId, memberUserId, isAll } = useInsightsParameters(); + const [isDownloading, setIsDownloading] = useState(false); + const utils = trpc.useUtils(); - const { data, isPending } = trpc.viewer.insights.rawData.useQuery( - { - startDate, - endDate, - teamId, - userId, - eventTypeId, - memberUserId, - }, - { - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: false, - staleTime: Infinity, - trpc: { - context: { skipBatch: true }, - }, + const handleDownloadClick = async () => { + try { + setIsDownloading(true); + const data = await utils.viewer.insights.rawData.fetch({ + startDate, + endDate, + teamId, + userId, + eventTypeId, + memberUserId, + isAll, + }); + + if (!data) return; + const { data: csvRaw, filename } = data; + downloadAsCsv(csvRaw, filename); + } catch (error) { + showToast(t("unexpected_error_try_again"), "error"); + } finally { + setIsDownloading(false); } - ); - - type RawData = RouterOutputs["viewer"]["insights"]["rawData"] | undefined; - const handleDownloadClick = async (data: RawData) => { - if (!data) return; - const { data: csvRaw, filename } = data; - downloadAsCsv(csvRaw, filename); }; return ( @@ -44,13 +49,13 @@ const Download = () => { - handleDownloadClick(data)}>{t("as_csv")} + {t("as_csv")} ); diff --git a/packages/features/insights/server/events.ts b/packages/features/insights/server/events.ts index 434f4ecd62..5bd5bd55f0 100644 --- a/packages/features/insights/server/events.ts +++ b/packages/features/insights/server/events.ts @@ -381,7 +381,7 @@ class EventsInsights { } ) => { // Obtain the where conditional - const whereConditional = await this.obtainWhereConditional(props); + const whereConditional = await this.obtainWhereConditionalForDownload(props); const csvData = await prisma.bookingTimeStatus.findMany({ select: { @@ -452,7 +452,9 @@ class EventsInsights { }; /* - * This is meant to be used for all functions inside insights router, ideally we should have a view that have all of this data + * This is meant to be used for all functions inside insights router, + * but it's currently used only for CSV download. + * Ideally we should have a view that have all of this data * The order where will be from the most specific to the least specific * starting from the top will be: * - memberUserId @@ -466,7 +468,7 @@ class EventsInsights { * @param props * @returns */ - static obtainWhereConditional = async ( + static obtainWhereConditionalForDownload = async ( props: RawDataInput & { organizationId: number | null; isOrgAdminOrOwner: boolean | null } ) => { const { @@ -492,14 +494,7 @@ class EventsInsights { } if (eventTypeId) { - whereConditional["OR"] = [ - { - eventTypeId, - }, - { - eventParentId: eventTypeId, - }, - ]; + whereConditional["eventTypeId"] = eventTypeId; } if (memberUserId) { whereConditional["userId"] = memberUserId; diff --git a/packages/features/insights/server/trpc-router.ts b/packages/features/insights/server/trpc-router.ts index 76d50b5d2b..68080c63ff 100644 --- a/packages/features/insights/server/trpc-router.ts +++ b/packages/features/insights/server/trpc-router.ts @@ -1080,103 +1080,19 @@ export const insightsRouter = router({ const { prisma, user } = ctx; const { teamId, userId, isAll } = input; - if (!teamId && !userId) { - return []; - } - - const membershipWhereConditional: Prisma.MembershipWhereInput = {}; - - let childrenTeamIds: number[] = []; - - if (isAll && teamId && user.organizationId && user.isOwnerAdminOfParentTeam) { - const childTeams = await prisma.team.findMany({ - where: { - parentId: user.organizationId, - }, - select: { - id: true, - }, - }); - if (childTeams.length > 0) { - childrenTeamIds = childTeams.map((team) => team.id); - } - membershipWhereConditional["teamId"] = { - in: [user.organizationId, ...childrenTeamIds], - }; - } - - if (teamId && !isAll) { - membershipWhereConditional["teamId"] = teamId; - membershipWhereConditional["userId"] = user.id; - } - if (userId) { - membershipWhereConditional["userId"] = userId; - } - - // I'm not using unique here since when userId comes from input we should look for every - // event type that user owns - const membership = await prisma.membership.findFirst({ - where: membershipWhereConditional, - }); - - if (!membership && !user.isOwnerAdminOfParentTeam) { - throw new Error("User is not part of a team/org"); - } - - const eventTypeWhereConditional: Prisma.EventTypeWhereInput = {}; - if (isAll && childrenTeamIds.length > 0 && user.organizationId && user.isOwnerAdminOfParentTeam) { - eventTypeWhereConditional["teamId"] = { - in: [user.organizationId, ...childrenTeamIds], - }; - } - if (teamId && !isAll) { - eventTypeWhereConditional["teamId"] = teamId; - } - if (userId) { - eventTypeWhereConditional["userId"] = userId; - } - let eventTypeResult: Prisma.EventTypeGetPayload<{ - select: { - id: true; - slug: true; - teamId: true; - title: true; - team: { - select: { - name: true; - }; - }; - }; - }>[] = []; - - let isMember = membership?.role === "MEMBER"; - if (user.isOwnerAdminOfParentTeam) { - isMember = false; - } - if (isMember) { - eventTypeWhereConditional["OR"] = [ - { userId: user.id }, - { users: { some: { id: user.id } } }, - // @TODO this is not working as expected - // hosts: { some: { id: user.id } }, - ]; - } - eventTypeResult = await prisma.eventType.findMany({ - select: { - id: true, - slug: true, - title: true, - teamId: true, - team: { - select: { - name: true, - }, - }, + const eventTypeList = await getEventTypeList({ + prisma, + teamId, + userId, + isAll, + user: { + id: user.id, + organizationId: user.organizationId, + isOwnerAdminOfParentTeam: user.isOwnerAdminOfParentTeam, }, - where: eventTypeWhereConditional, }); - return eventTypeResult; + return eventTypeList; }), recentRatings: userBelongsToTeamProcedure.input(rawDataInputSchema).query(async ({ ctx, input }) => { const { teamId, startDate, endDate, eventTypeId, isAll, userId, memberUserId } = input; @@ -1534,7 +1450,22 @@ export const insightsRouter = router({ return result; }), rawData: userBelongsToTeamProcedure.input(rawDataInputSchema).query(async ({ ctx, input }) => { - const { startDate, endDate, teamId, userId, memberUserId, isAll, eventTypeId } = input; + const { startDate, endDate, teamId, userId, memberUserId, isAll } = input; + + const eventTypeList = await getEventTypeList({ + prisma: ctx.prisma, + teamId, + userId, + isAll, + user: { + id: ctx.user.id, + organizationId: ctx.user.organizationId, + isOwnerAdminOfParentTeam: ctx.user.isOwnerAdminOfParentTeam, + }, + }); + + // use eventTypeId filter only if it's accessible by this user + const eventTypeId = eventTypeList.find((eventType) => eventType.id === input.eventTypeId)?.id; const isOrgAdminOrOwner = ctx.user.isOwnerAdminOfParentTeam; try { @@ -1560,7 +1491,6 @@ export const insightsRouter = router({ } catch (e) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } - return { data: "", filename: "" }; }), getRoutingFormsForFilters: userBelongsToTeamProcedure @@ -1758,3 +1688,119 @@ export const insightsRouter = router({ } }), }); + +async function getEventTypeList({ + prisma, + teamId, + userId, + isAll, + user, +}: { + prisma: typeof readonlyPrisma; + teamId: number | null | undefined; + userId: number | null | undefined; + isAll: boolean | undefined; + user: { + id: number; + organizationId: number | null; + isOwnerAdminOfParentTeam: boolean; + }; +}) { + if (!teamId && !userId) { + return []; + } + + const membershipWhereConditional: Prisma.MembershipWhereInput = {}; + + let childrenTeamIds: number[] = []; + + if (isAll && teamId && user.organizationId && user.isOwnerAdminOfParentTeam) { + const childTeams = await prisma.team.findMany({ + where: { + parentId: user.organizationId, + }, + select: { + id: true, + }, + }); + if (childTeams.length > 0) { + childrenTeamIds = childTeams.map((team) => team.id); + } + membershipWhereConditional["teamId"] = { + in: [user.organizationId, ...childrenTeamIds], + }; + } + + if (teamId && !isAll) { + membershipWhereConditional["teamId"] = teamId; + membershipWhereConditional["userId"] = user.id; + } + if (userId) { + membershipWhereConditional["userId"] = userId; + } + + // I'm not using unique here since when userId comes from input we should look for every + // event type that user owns + const membership = await prisma.membership.findFirst({ + where: membershipWhereConditional, + }); + + if (!membership && !user.isOwnerAdminOfParentTeam) { + throw new Error("User is not part of a team/org"); + } + + const eventTypeWhereConditional: Prisma.EventTypeWhereInput = {}; + if (isAll && childrenTeamIds.length > 0 && user.organizationId && user.isOwnerAdminOfParentTeam) { + eventTypeWhereConditional["teamId"] = { + in: [user.organizationId, ...childrenTeamIds], + }; + } + if (teamId && !isAll) { + eventTypeWhereConditional["teamId"] = teamId; + } + if (userId) { + eventTypeWhereConditional["userId"] = userId; + } + let eventTypeResult: Prisma.EventTypeGetPayload<{ + select: { + id: true; + slug: true; + teamId: true; + title: true; + team: { + select: { + name: true; + }; + }; + }; + }>[] = []; + + let isMember = membership?.role === "MEMBER"; + if (user.isOwnerAdminOfParentTeam) { + isMember = false; + } + if (isMember) { + eventTypeWhereConditional["OR"] = [ + { userId: user.id }, + { users: { some: { id: user.id } } }, + // @TODO this is not working as expected + // hosts: { some: { id: user.id } }, + ]; + } + eventTypeResult = await prisma.eventType.findMany({ + select: { + id: true, + slug: true, + title: true, + teamId: true, + team: { + select: { + name: true, + }, + }, + }, + where: eventTypeWhereConditional, + }); + + return eventTypeResult; +}