chore: remove dead code regarding insights (#19022)
This commit is contained in:
@@ -1,88 +0,0 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useFilterContext } from "@calcom/features/insights/context/provider";
|
||||
import { downloadAsCsv } from "@calcom/lib/csvUtils";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { Button, Dropdown, DropdownItem, DropdownMenuContent, DropdownMenuTrigger } from "@calcom/ui";
|
||||
import { showToast } from "@calcom/ui";
|
||||
|
||||
type RawRoutingData = RouterOutputs["viewer"]["insights"]["rawRoutingData"]["data"][number] | undefined;
|
||||
|
||||
interface BatchResult {
|
||||
data: RawRoutingData[];
|
||||
nextCursor: number | null;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
const RoutingDownload = () => {
|
||||
const { filter } = useFilterContext();
|
||||
const { t } = useLocale();
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const fetchBatch = async (cursor: number | null = null) => {
|
||||
const result = await utils.viewer.insights.rawRoutingData.fetch({
|
||||
startDate: filter.dateRange[0].toISOString(),
|
||||
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,
|
||||
isAll: !!filter.isAll,
|
||||
cursor: cursor ?? undefined,
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const handleDownloadClick = async () => {
|
||||
try {
|
||||
setIsDownloading(true);
|
||||
let allData: RawRoutingData[] = [];
|
||||
let hasMore = true;
|
||||
let cursor: number | null = null;
|
||||
|
||||
// Fetch data in batches until there's no more data
|
||||
while (hasMore) {
|
||||
const result: BatchResult = await fetchBatch(cursor);
|
||||
allData = [...allData, ...result.data];
|
||||
hasMore = result.hasMore;
|
||||
cursor = result.nextCursor;
|
||||
}
|
||||
|
||||
if (allData.length > 0) {
|
||||
const filename = `RoutingInsights-${filter.dateRange[0].format(
|
||||
"YYYY-MM-DD"
|
||||
)}-${filter.dateRange[1].format("YYYY-MM-DD")}.csv`;
|
||||
downloadAsCsv(allData as Record<string, unknown>[], filename);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(t("error_downloading_data"), "error");
|
||||
} finally {
|
||||
setIsDownloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
EndIcon="file-down"
|
||||
color="secondary"
|
||||
className="self-end sm:self-baseline"
|
||||
loading={isDownloading}>
|
||||
{t("download")}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownItem onClick={handleDownloadClick}>{t("as_csv")}</DropdownItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
export { RoutingDownload };
|
||||
@@ -1,3 +1,2 @@
|
||||
export { Download } from "./Download";
|
||||
export { RoutingDownload } from "./RoutingDownload";
|
||||
export { RoutingFormResponsesDownload } from "./RoutingFormResponsesDownload";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import type { IconName } from "@calcom/ui";
|
||||
import {
|
||||
Dropdown,
|
||||
@@ -23,31 +22,10 @@ type Option = {
|
||||
StartIcon: IconName;
|
||||
};
|
||||
|
||||
export const FilterType = ({ showRoutingFilters = false }: { showRoutingFilters?: boolean }) => {
|
||||
export const FilterType = () => {
|
||||
const { t } = useLocale();
|
||||
const { filter, setConfigFilters } = useFilterContext();
|
||||
const { selectedFilter, selectedUserId, selectedTeamId, selectedRoutingFormId, isAll, initialConfig } =
|
||||
filter;
|
||||
const initialConfigIsReady = !!(initialConfig?.teamId || initialConfig?.userId || initialConfig?.isAll);
|
||||
|
||||
// Dynamically load filters if showRoutingFilters is set to true
|
||||
// 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,
|
||||
},
|
||||
{
|
||||
enabled: showRoutingFilters && initialConfigIsReady,
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
const { selectedFilter, selectedUserId } = filter;
|
||||
|
||||
const filterOptions = useMemo(() => {
|
||||
let options: Option[] = [
|
||||
@@ -58,38 +36,11 @@ export const FilterType = ({ showRoutingFilters = false }: { showRoutingFilters?
|
||||
},
|
||||
];
|
||||
|
||||
// Add routing forms filter options
|
||||
if (showRoutingFilters) {
|
||||
options.push({
|
||||
label: t("routing_forms"),
|
||||
value: "routing_forms" as FilterType,
|
||||
StartIcon: "calendar-check-2" as IconName,
|
||||
});
|
||||
|
||||
options.push({
|
||||
label: t("booking_status"),
|
||||
value: "booking_status" as FilterType,
|
||||
StartIcon: "circle" as IconName,
|
||||
});
|
||||
|
||||
// Add dynamic routing form field options
|
||||
if (routingFormFieldOptions?.length) {
|
||||
options = [
|
||||
...options,
|
||||
...routingFormFieldOptions.map((option) => ({
|
||||
label: option.label,
|
||||
value: `rf_${option.id}` as FilterType,
|
||||
StartIcon: "layers" as IconName,
|
||||
})),
|
||||
];
|
||||
}
|
||||
} else {
|
||||
options.push({
|
||||
label: t("event_type"),
|
||||
value: "event-type",
|
||||
StartIcon: "link",
|
||||
});
|
||||
}
|
||||
options.push({
|
||||
label: t("event_type"),
|
||||
value: "event-type",
|
||||
StartIcon: "link",
|
||||
});
|
||||
|
||||
if (selectedUserId) {
|
||||
// remove user option from filterOptions
|
||||
@@ -97,7 +48,7 @@ export const FilterType = ({ showRoutingFilters = false }: { showRoutingFilters?
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [t, showRoutingFilters, routingFormFieldOptions, selectedUserId]);
|
||||
}, [t, selectedUserId]);
|
||||
|
||||
return (
|
||||
<Dropdown>
|
||||
|
||||
@@ -2,13 +2,10 @@ import { useFilterContext } from "@calcom/features/insights/context/provider";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Icon, Tooltip } from "@calcom/ui";
|
||||
|
||||
import { BookingStatusFilter } from "./BookingStatusFilter";
|
||||
import { DateSelect } from "./DateSelect";
|
||||
import { Download, RoutingDownload } from "./Download";
|
||||
import { Download } from "./Download";
|
||||
import { EventTypeList } from "./EventTypeList";
|
||||
import { FilterType } from "./FilterType";
|
||||
import { RoutingFormFieldFilter } from "./RoutingFormFieldFilter";
|
||||
import { RoutingFormFilterList } from "./RoutingFormFilterList";
|
||||
import { TeamAndSelfList } from "./TeamAndSelfList";
|
||||
import { UserListInTeam } from "./UserListInTeam";
|
||||
|
||||
@@ -37,7 +34,7 @@ const ClearFilters = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Filters = ({ showRoutingFilters = false }: { showRoutingFilters?: boolean }) => {
|
||||
export const Filters = () => {
|
||||
const { filter } = useFilterContext();
|
||||
const { selectedFilter } = filter;
|
||||
|
||||
@@ -51,23 +48,13 @@ export const Filters = ({ showRoutingFilters = false }: { showRoutingFilters?: b
|
||||
return (
|
||||
<div className="ml-auto mt-6 flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:justify-between">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:flex-nowrap sm:justify-start">
|
||||
<TeamAndSelfList omitOrg={showRoutingFilters} />
|
||||
<TeamAndSelfList omitOrg={false} />
|
||||
|
||||
<UserListInTeam />
|
||||
|
||||
<EventTypeList />
|
||||
|
||||
{showRoutingFilters ? (
|
||||
<>
|
||||
<RoutingFormFilterList />
|
||||
<BookingStatusFilter />
|
||||
{routingFormFieldIds.map((fieldId) => {
|
||||
if (fieldId) return <RoutingFormFieldFilter fieldId={fieldId} />;
|
||||
})}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<FilterType showRoutingFilters={showRoutingFilters} />
|
||||
<FilterType />
|
||||
|
||||
<ClearFilters />
|
||||
</div>
|
||||
@@ -96,7 +83,7 @@ export const Filters = ({ showRoutingFilters = false }: { showRoutingFilters?: b
|
||||
</Tooltip>
|
||||
</ButtonGroup> */}
|
||||
<div className="flex flex-col-reverse sm:flex-row sm:flex-nowrap sm:justify-between">
|
||||
{showRoutingFilters ? <RoutingDownload /> : <Download />}
|
||||
<Download />
|
||||
<DateSelect className="me-2 ms-2" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,10 +4,7 @@ import mapKeys from "lodash/mapKeys";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import startCase from "lodash/startCase";
|
||||
|
||||
import {
|
||||
zodFields as routingFormFieldsSchema,
|
||||
routingFormResponseInDbSchema,
|
||||
} from "@calcom/app-store/routing-forms/zod";
|
||||
import { zodFields as routingFormFieldsSchema } from "@calcom/app-store/routing-forms/zod";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import type { ColumnFilter, TypedColumnFilter } from "@calcom/features/data-table";
|
||||
import { ColumnFilterType } from "@calcom/features/data-table";
|
||||
@@ -632,185 +629,6 @@ class RoutingEventsInsights {
|
||||
return headers;
|
||||
}
|
||||
|
||||
static async getRawData({
|
||||
teamId,
|
||||
startDate,
|
||||
endDate,
|
||||
isAll,
|
||||
organizationId,
|
||||
routingFormId,
|
||||
userId,
|
||||
memberUserId,
|
||||
bookingStatus,
|
||||
fieldFilter,
|
||||
take,
|
||||
skip,
|
||||
}: Omit<RoutingFormInsightsFilter, "columnFilters"> & { take?: number; skip?: number }) {
|
||||
const formsWhereCondition = await this.getWhereForTeamOrAllTeams({
|
||||
userId,
|
||||
teamId,
|
||||
isAll,
|
||||
organizationId,
|
||||
routingFormId,
|
||||
});
|
||||
|
||||
// First get all forms and their fields to build a mapping
|
||||
const forms = await prisma.app_RoutingForms_Form.findMany({
|
||||
where: formsWhereCondition,
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Create a mapping of form ID to fields
|
||||
type FormFieldOption = {
|
||||
label: string;
|
||||
type: string;
|
||||
options: Record<string, string>;
|
||||
};
|
||||
|
||||
type FormFieldsMap = Record<string, Record<string, FormFieldOption>>;
|
||||
|
||||
const formFieldsMap = forms.reduce((acc, form) => {
|
||||
const fields = routingFormFieldsSchema.parse(form.fields);
|
||||
acc[form.id] =
|
||||
fields?.reduce((fieldMap: Record<string, FormFieldOption>, field) => {
|
||||
fieldMap[field.id] = {
|
||||
label: field.label,
|
||||
type: field.type,
|
||||
options:
|
||||
field.options?.reduce((optMap, opt) => {
|
||||
if (opt.id !== null) {
|
||||
optMap[opt.id] = opt.label;
|
||||
}
|
||||
return optMap;
|
||||
}, {} as Record<string, string>) ?? {},
|
||||
};
|
||||
return fieldMap;
|
||||
}, {}) || {};
|
||||
return acc;
|
||||
}, {} as FormFieldsMap);
|
||||
|
||||
const responsesWhereCondition: Prisma.App_RoutingForms_FormResponseWhereInput = {
|
||||
...(startDate &&
|
||||
endDate && {
|
||||
createdAt: {
|
||||
gte: dayjs(startDate).startOf("day").toDate(),
|
||||
lte: dayjs(endDate).endOf("day").toDate(),
|
||||
},
|
||||
}),
|
||||
...(memberUserId || bookingStatus
|
||||
? {
|
||||
...(bookingStatus === "NO_BOOKING"
|
||||
? { routedToBooking: null }
|
||||
: {
|
||||
routedToBooking: {
|
||||
...(memberUserId && { userId: memberUserId }),
|
||||
...(bookingStatus && { status: bookingStatus }),
|
||||
},
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
...(fieldFilter && {
|
||||
response: {
|
||||
path: [fieldFilter.fieldId, "value"],
|
||||
array_contains: [fieldFilter.optionId],
|
||||
},
|
||||
}),
|
||||
form: formsWhereCondition,
|
||||
};
|
||||
|
||||
const responses = await prisma.app_RoutingForms_FormResponse.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
response: true,
|
||||
createdAt: true,
|
||||
form: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
routedToBooking: {
|
||||
select: {
|
||||
uid: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
attendees: {
|
||||
select: {
|
||||
email: true,
|
||||
name: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
assignmentReason: {
|
||||
select: {
|
||||
reasonString: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
where: responsesWhereCondition,
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
take: take,
|
||||
skip: skip,
|
||||
});
|
||||
|
||||
// Transform the data into a flat structure suitable for CSV
|
||||
return responses.map((response) => {
|
||||
const parsedResponse = routingFormResponseInDbSchema.parse(response.response);
|
||||
const formFields = formFieldsMap[response.form.id];
|
||||
|
||||
const flatResponse: Record<string, unknown> = {
|
||||
"Response ID": response.id,
|
||||
"Form Name": response.form.name,
|
||||
"Submitted At": response.createdAt.toISOString(),
|
||||
"Has Booking": !!response.routedToBooking,
|
||||
"Booking Status": response.routedToBooking?.status || "NO_BOOKING",
|
||||
"Booking Created At": response.routedToBooking?.createdAt?.toISOString() || "",
|
||||
"Booking Start Time": response.routedToBooking?.startTime?.toISOString() || "",
|
||||
"Booking End Time": response.routedToBooking?.endTime?.toISOString() || "",
|
||||
"Attendee Name": response.routedToBooking?.attendees[0]?.name || "",
|
||||
"Attendee Email": response.routedToBooking?.attendees[0]?.email || "",
|
||||
"Attendee Timezone": response.routedToBooking?.attendees[0]?.timeZone || "",
|
||||
"Assignment Reason": response.routedToBooking?.assignmentReason[0].reasonString || "",
|
||||
"Routed To Name": response.routedToBooking?.user?.name || "",
|
||||
"Routed To Email": response.routedToBooking?.user?.email || "",
|
||||
};
|
||||
|
||||
// Add form fields as columns with their labels
|
||||
Object.entries(parsedResponse).forEach(([fieldId, field]) => {
|
||||
const fieldInfo = formFields[fieldId];
|
||||
if (fieldInfo) {
|
||||
const fieldLabel = fieldInfo.label;
|
||||
if (Array.isArray(field.value)) {
|
||||
// For multi-select fields, map the IDs to labels
|
||||
const values = field.value.map((val) => fieldInfo.options[val] || val);
|
||||
flatResponse[fieldLabel] = values.join(", ");
|
||||
} else {
|
||||
// For single-select fields, map the ID to label
|
||||
flatResponse[fieldLabel] = fieldInfo.options[field.value] || field.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return flatResponse;
|
||||
});
|
||||
}
|
||||
|
||||
static async routedToPerPeriod({
|
||||
userId,
|
||||
teamId,
|
||||
|
||||
@@ -1663,66 +1663,6 @@ export const insightsRouter = router({
|
||||
|
||||
return headers || [];
|
||||
}),
|
||||
rawRoutingData: userBelongsToTeamProcedure
|
||||
.input(
|
||||
rawDataInputSchema.extend({
|
||||
routingFormId: z.string().optional(),
|
||||
bookingStatus: bookingStatusSchema,
|
||||
fieldFilter: z
|
||||
.object({
|
||||
fieldId: z.string(),
|
||||
optionId: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
cursor: z.number().optional(),
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const {
|
||||
teamId,
|
||||
startDate,
|
||||
endDate,
|
||||
userId,
|
||||
memberUserId,
|
||||
isAll,
|
||||
routingFormId,
|
||||
bookingStatus,
|
||||
fieldFilter,
|
||||
cursor,
|
||||
} = input;
|
||||
|
||||
if (!teamId && !userId) {
|
||||
return { data: [], hasMore: false, nextCursor: null };
|
||||
}
|
||||
|
||||
try {
|
||||
const csvData = await RoutingEventsInsights.getRawData({
|
||||
teamId,
|
||||
startDate,
|
||||
endDate,
|
||||
userId,
|
||||
memberUserId,
|
||||
isAll: isAll ?? false,
|
||||
organizationId: ctx.user.organizationId,
|
||||
routingFormId,
|
||||
bookingStatus,
|
||||
fieldFilter,
|
||||
take: BATCH_SIZE,
|
||||
skip: cursor || 0,
|
||||
});
|
||||
|
||||
const hasMore = csvData.length === BATCH_SIZE;
|
||||
const nextCursor = hasMore ? (cursor || 0) + BATCH_SIZE : null;
|
||||
|
||||
return {
|
||||
data: csvData,
|
||||
hasMore,
|
||||
nextCursor,
|
||||
};
|
||||
} catch (e) {
|
||||
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||||
}
|
||||
}),
|
||||
routedToPerPeriod: userBelongsToTeamProcedure
|
||||
.input(
|
||||
rawDataInputSchema.extend({
|
||||
|
||||
Reference in New Issue
Block a user