fix: eventTypeId filter on Download at /insights (#20463)
* fix: eventTypeId filter on Download at /insights * show toast for error
This commit is contained in:
@@ -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 = () => {
|
||||
<Button
|
||||
EndIcon="file-down"
|
||||
color="secondary"
|
||||
{...(isPending && { loading: isPending })}
|
||||
loading={isDownloading}
|
||||
className="h-full self-end sm:self-baseline">
|
||||
{t("download")}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownItem onClick={() => handleDownloadClick(data)}>{t("as_csv")}</DropdownItem>
|
||||
<DropdownItem onClick={handleDownloadClick}>{t("as_csv")}</DropdownItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user