Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions apps/web/modules/insights/insights-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,15 @@ export default function InsightsPage({ timeZone }: { timeZone: string }) {
);
}

const timestampColumn: Extract<FilterableColumn, { type: ColumnFilterType.DATE_RANGE }> = {
id: "timestamp",
title: "timestamp",
type: ColumnFilterType.DATE_RANGE,
};
function getDateColumn(
dateTarget: DateTarget
): Extract<FilterableColumn, { type: ColumnFilterType.DATE_RANGE }> {
return {
id: dateTarget,
title: dateTarget,
type: ColumnFilterType.DATE_RANGE,
};
}

function InsightsPageContent() {
const { t } = useLocale();
Expand All @@ -70,11 +74,11 @@ function InsightsPageContent() {
<DataTableFilters.AddFilterButton table={table} hideWhenFilterApplied />
<DataTableFilters.ActiveFilters table={table} />
<DataTableFilters.AddFilterButton table={table} variant="sm" showWhenFilterApplied />
<DataTableFilters.ClearFiltersButton exclude={["timestamp"]} />
<DataTableFilters.ClearFiltersButton exclude={[dateTarget]} />
<div className="grow" />
<Download />
<ButtonGroup combined>
<DateRangeFilter column={timestampColumn} />
<DateRangeFilter column={getDateColumn(dateTarget as DateTarget)} />
<DateTargetSelector value={dateTarget as DateTarget} onChange={setDateTarget} />
</ButtonGroup>
<TimezoneBadge />
Expand Down
12 changes: 6 additions & 6 deletions packages/features/insights/hooks/useInsightsBookingParameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,25 @@ export function useInsightsBookingParameters() {
parse: (value) => (value === "createdAt" ? "createdAt" : "startTime"),
});

const timestampRange = useFilterValue("timestamp", ZDateRangeFilterValue)?.data;
const dateRange = useFilterValue(dateTarget, ZDateRangeFilterValue)?.data;
// TODO for future: this preserving local time & startOf & endOf should be handled
// from DateRangeFilter out of the box.
// When we do it, we also need to remove those timezone handling logic from the backend side at the same time.
const startDate = useChangeTimeZoneWithPreservedLocalTime(
useMemo(() => {
return dayjs(timestampRange?.startDate ?? getDefaultStartDate().toISOString())
return dayjs(dateRange?.startDate ?? getDefaultStartDate().toISOString())
.startOf("day")
.toISOString();
}, [timestampRange?.startDate])
}, [dateRange?.startDate])
);
const endDate = useChangeTimeZoneWithPreservedLocalTime(
useMemo(() => {
return dayjs(timestampRange?.endDate ?? getDefaultEndDate().toISOString())
return dayjs(dateRange?.endDate ?? getDefaultEndDate().toISOString())
.endOf("day")
.toISOString();
}, [timestampRange?.endDate])
}, [dateRange?.endDate])
);
const columnFilters = useColumnFilters({ exclude: ["timestamp"] });
const columnFilters = useColumnFilters();

return {
scope,
Expand Down
23 changes: 13 additions & 10 deletions packages/features/insights/hooks/useInsightsParameters.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useQueryState } from "nuqs";
import { useMemo } from "react";

import dayjs from "@calcom/dayjs";
Expand All @@ -20,39 +21,41 @@ import { useInsightsOrgTeams } from "./useInsightsOrgTeams";

export function useInsightsParameters() {
const { isAll, teamId, userId, scope, selectedTeamId } = useInsightsOrgTeams();
const [dateTarget] = useQueryState("dateTarget", {
defaultValue: "startTime" as const,
});

const memberUserIds = useFilterValue("bookingUserId", ZMultiSelectFilterValue)?.data as
| number[]
| undefined;
const memberUserId = useFilterValue("bookingUserId", ZSingleSelectFilterValue)?.data as number | undefined;
const eventTypeId = useFilterValue("eventTypeId", ZSingleSelectFilterValue)?.data as number | undefined;
const routingFormId = useFilterValue("formId", ZSingleSelectFilterValue)?.data as string | undefined;
const createdAtRange = useFilterValue("createdAt", ZDateRangeFilterValue)?.data;

const dateRange = useFilterValue(dateTarget, ZDateRangeFilterValue)?.data;
// TODO for future: this preserving local time & startOf & endOf should be handled
// from DateRangeFilter out of the box.
// When we do it, we also need to remove those timezone handling logic from the backend side at the same time.
const startDate = useChangeTimeZoneWithPreservedLocalTime(
useMemo(() => {
return dayjs(createdAtRange?.startDate ?? getDefaultStartDate().toISOString())
return dayjs(dateRange?.startDate ?? getDefaultStartDate().toISOString())
.startOf("day")
.toISOString();
}, [createdAtRange?.startDate])
}, [dateRange?.startDate])
);
const endDate = useChangeTimeZoneWithPreservedLocalTime(
useMemo(() => {
return dayjs(createdAtRange?.endDate ?? getDefaultEndDate().toISOString())
return dayjs(dateRange?.endDate ?? getDefaultEndDate().toISOString())
.endOf("day")
.toISOString();
}, [createdAtRange?.endDate])
}, [dateRange?.endDate])
);

const dateRangePreset = useMemo<PresetOptionValue>(() => {
return (createdAtRange?.preset as PresetOptionValue) ?? CUSTOM_PRESET_VALUE;
}, [createdAtRange?.preset]);
return (dateRange?.preset as PresetOptionValue) ?? CUSTOM_PRESET_VALUE;
}, [dateRange?.preset]);

const columnFilters = useColumnFilters({
exclude: ["createdAt"],
});
const columnFilters = useColumnFilters();

return {
isAll,
Expand Down
12 changes: 0 additions & 12 deletions packages/features/insights/server/raw-data.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,6 @@ export type InsightsRoutingServiceInput = {
selectedTeamId?: number;

columnFilters?: ColumnFilter[];
startDate: string;
endDate: string;
};

export type InsightsRoutingServicePaginatedInput = InsightsRoutingServiceInput & {
Expand All @@ -70,8 +68,6 @@ export const insightsRoutingServiceInputSchema = z.object({
scope: z.union([z.literal("user"), z.literal("team"), z.literal("org")]),
selectedTeamId: z.number().optional(),
columnFilters: z.array(ZColumnFilter).optional(),
startDate: z.string(),
endDate: z.string(),
}) satisfies z.ZodType<InsightsRoutingServiceInput>;

export const insightsRoutingServicePaginatedInputSchema = z.object({
Expand All @@ -84,8 +80,6 @@ export const insightsRoutingServicePaginatedInputSchema = z.object({
export const routingRepositoryBaseInputSchema = z.object({
scope: z.union([z.literal("user"), z.literal("team"), z.literal("org")]),
selectedTeamId: z.number().optional(),
startDate: z.string(),
endDate: z.string(),
columnFilters: z.array(ZColumnFilter).optional(),
});

Expand All @@ -103,12 +97,6 @@ export const routedToPerPeriodCsvInputSchema = routingRepositoryBaseInputSchema.
export const bookingRepositoryBaseInputSchema = z.object({
scope: z.union([z.literal("user"), z.literal("team"), z.literal("org")]),
selectedTeamId: z.number().optional(),
startDate: z.string(),
endDate: z.string(),
timeZone: z.string(),
columnFilters: z.array(ZColumnFilter).optional(),
dateTarget: z
.union([z.literal("startTime"), z.literal("createdAt")])
.optional()
.default("startTime"),
});
106 changes: 88 additions & 18 deletions packages/features/insights/server/trpc-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import type { Prisma } from "@prisma/client";
import { z } from "zod";

import dayjs from "@calcom/dayjs";
import type { ColumnFilter } from "@calcom/features/data-table/lib/types";
import { ColumnFilterType } from "@calcom/features/data-table/lib/types";
import {
insightsRoutingServiceInputSchema,
insightsRoutingServicePaginatedInputSchema,
Expand Down Expand Up @@ -311,11 +313,33 @@ export interface IResultTeamList {
/**
* Helper function to create InsightsBookingService with standardized parameters
*/
function extractDateRangeFromFilters(columnFilters?: ColumnFilter[]) {
if (!columnFilters) return { startDate: null, endDate: null };

for (const filter of columnFilters) {
if (
(filter.id === "startTime" || filter.id === "createdAt") &&
filter.value.type === ColumnFilterType.DATE_RANGE
) {
const dateFilter = filter.value as Extract<
ColumnFilter["value"],
{ type: ColumnFilterType.DATE_RANGE }
>;
return {
startDate: dateFilter.data.startDate,
endDate: dateFilter.data.endDate,
};
}
}

return { startDate: null, endDate: null };
}

function createInsightsBookingService(
ctx: { user: { id: number; organizationId: number | null } },
input: z.infer<typeof bookingRepositoryBaseInputSchema>
) {
const { scope, selectedTeamId, startDate, endDate, columnFilters, dateTarget } = input;
const { scope, selectedTeamId, columnFilters } = input;
return getInsightsBookingService({
options: {
scope,
Expand All @@ -325,11 +349,6 @@ function createInsightsBookingService(
},
filters: {
...(columnFilters && { columnFilters }),
dateRange: {
target: dateTarget,
startDate,
endDate,
},
},
});
}
Expand All @@ -338,6 +357,7 @@ function createInsightsRoutingService(
ctx: { insightsDb: PrismaClient; user: { id: number; organizationId: number | null } },
input: z.infer<typeof routingRepositoryBaseInputSchema>
) {
const { startDate, endDate } = extractDateRangeFromFilters(input.columnFilters);
return getInsightsRoutingService({
options: {
scope: input.scope,
Expand All @@ -346,8 +366,8 @@ function createInsightsRoutingService(
orgId: ctx.user.organizationId,
},
filters: {
startDate: input.startDate,
endDate: input.endDate,
startDate: startDate || "",
endDate: endDate || "",
columnFilters: input.columnFilters,
},
});
Expand All @@ -364,10 +384,43 @@ export const insightsRouter = router({

// Calculate previous period dates and create service for previous period
const previousPeriodDates = currentPeriodService.calculatePreviousPeriodDates();

const { startDate: currentStartDate, endDate: currentEndDate } = extractDateRangeFromFilters(
input.columnFilters
);
if (!currentStartDate || !currentEndDate) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Date range is required" });
}

// Create column filters for previous period
const previousPeriodColumnFilters =
input.columnFilters?.map((filter) => {
if (
(filter.id === "startTime" || filter.id === "createdAt") &&
filter.value.type === ColumnFilterType.DATE_RANGE
) {
const originalValue = filter.value as Extract<
ColumnFilter["value"],
{ type: ColumnFilterType.DATE_RANGE }
>;
return {
...filter,
value: {
type: ColumnFilterType.DATE_RANGE,
data: {
startDate: previousPeriodDates.startDate,
endDate: previousPeriodDates.endDate,
preset: originalValue.data.preset,
},
},
} as ColumnFilter;
}
return filter;
}) || [];

const previousPeriodService = createInsightsBookingService(ctx, {
...input,
startDate: previousPeriodDates.startDate,
endDate: previousPeriodDates.endDate,
columnFilters: previousPeriodColumnFilters,
});

// Get previous period stats
Expand Down Expand Up @@ -455,7 +508,12 @@ export const insightsRouter = router({
};
}),
eventTrends: insightsPbacProcedure.input(bookingRepositoryBaseInputSchema).query(async ({ ctx, input }) => {
const { startDate, endDate, timeZone } = input;
const { timeZone, columnFilters } = input;
const { startDate, endDate } = extractDateRangeFromFilters(columnFilters);

if (!startDate || !endDate) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Date range is required" });
}

// Calculate timeView and dateRanges
const timeView = getTimeView(startDate, endDate);
Expand Down Expand Up @@ -491,7 +549,12 @@ export const insightsRouter = router({
averageEventDuration: insightsPbacProcedure
.input(bookingRepositoryBaseInputSchema)
.query(async ({ ctx, input }) => {
const { startDate, endDate, timeZone } = input;
const { timeZone, columnFilters } = input;
const { startDate, endDate } = extractDateRangeFromFilters(columnFilters);

if (!startDate || !endDate) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Date range is required" });
}

const insightsBookingService = createInsightsBookingService(ctx, input);

Expand Down Expand Up @@ -958,9 +1021,10 @@ export const insightsRouter = router({
});

const csvString = objectToCsv(csvData);
const downloadAs = `routed-to-${period}-${dayjs(rest.startDate).format("YYYY-MM-DD")}-${dayjs(
rest.endDate
).format("YYYY-MM-DD")}.csv`;
const { startDate, endDate } = extractDateRangeFromFilters(rest.columnFilters);
const downloadAs = `routed-to-${period}-${dayjs(startDate || new Date()).format(
"YYYY-MM-DD"
)}-${dayjs(endDate || new Date()).format("YYYY-MM-DD")}.csv`;

return { data: csvString, filename: downloadAs };
} catch (e) {
Expand All @@ -981,10 +1045,16 @@ export const insightsRouter = router({
getRoutingFunnelData: insightsPbacProcedure
.input(routingRepositoryBaseInputSchema)
.query(async ({ ctx, input }) => {
const timeView = getTimeView(input.startDate, input.endDate);
const { startDate, endDate } = extractDateRangeFromFilters(input.columnFilters);

if (!startDate || !endDate) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Date range is required" });
}

const timeView = getTimeView(startDate, endDate);
const dateRanges = getDateRanges({
startDate: input.startDate,
endDate: input.endDate,
startDate,
endDate,
timeZone: ctx.user.timeZone,
timeView,
weekStart: ctx.user.weekStart,
Expand Down
Loading