Skip to content

Commit bdc3cb9

Browse files
feat: allow to choose dateTarget for /insights (startTime by default) (calcom#23752)
* feat: allow to choose dateTarget for /insights (startTime by default) * feat: add timestamp selector for insights date filtering - Add TimestampFilter component with Start Time/Created At options - Extend useInsightsBookingParameters hook with timestamp selection - Update all insight components to use dateTarget parameter - Add i18n translations for new UI strings - Position selector next to DateRangeFilter as requested Addresses user request to add select box next to date range filter allowing users to choose between startTime (default) and createdAt for displaying booking metrics on the Insights page. Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * refactor: rename TimestampFilter to DateTargetSelector with nuqs URL state - Rename TimestampFilter component to DateTargetSelector - Implement nuqs hook in InsightsPageContent for URL state management - Update useInsightsBookingParameters to return dateTarget from URL state - Add dateTarget field to insightsRoutingServiceInputSchema and related types - Simplify individual insight components to use insightsBookingParams directly - Remove manual timestampTarget destructuring from all components - Update all tRPC routing service calls to include dateTarget parameter - All TypeScript checks now pass successfully Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * update styles * fix inconsistency * feat: replace Select with Command component and rename filter ID - Replace Select with Command + Popover for compact width and wider dropdown - Add descriptive option labels with translations - Change filter ID from 'createdAt' to 'timestamp' across all components - Maintain URL state management with nuqs - Fix ESLint warning for missing dependency in useEffect Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: revert routing components to use createdAt filter ID - Keep timestamp filter ID change scoped only to main insights page - Routing components should continue using createdAt as filter ID - Only insights-view.tsx and related booking hooks use timestamp Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * update styles * fix trpc router * refactor timestamp column for insights booking service * fix * update text * rename and clean up * fix endDate in DateRangeFilter * fix type errors * fix startTime filter and type errors * provide default date range * add completed to getMembersStatsWithCount * add unit tests * fix type error * address feedback --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 616e4c4 commit bdc3cb9

20 files changed

Lines changed: 884 additions & 154 deletions

File tree

apps/web/modules/insights/insights-view.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
"use client";
22

3+
import { useState, useCallback } from "react";
4+
35
import {
46
DataTableProvider,
57
DataTableFilters,
68
DateRangeFilter,
79
ColumnFilterType,
810
type FilterableColumn,
911
} from "@calcom/features/data-table";
12+
import { useDataTable } from "@calcom/features/data-table/hooks/useDataTable";
1013
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
1114
import {
1215
AverageEventDurationChart,
@@ -27,11 +30,13 @@ import {
2730
TimezoneBadge,
2831
} from "@calcom/features/insights/components/booking";
2932
import { InsightsOrgTeamsProvider } from "@calcom/features/insights/context/InsightsOrgTeamsProvider";
33+
import { DateTargetSelector, type DateTarget } from "@calcom/features/insights/filters/DateTargetSelector";
3034
import { Download } from "@calcom/features/insights/filters/Download";
3135
import { OrgTeamsFilter } from "@calcom/features/insights/filters/OrgTeamsFilter";
3236
import { useInsightsBookings } from "@calcom/features/insights/hooks/useInsightsBookings";
3337
import { useInsightsOrgTeams } from "@calcom/features/insights/hooks/useInsightsOrgTeams";
3438
import { useLocale } from "@calcom/lib/hooks/useLocale";
39+
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
3540

3641
export default function InsightsPage({ timeZone }: { timeZone: string }) {
3742
return (
@@ -49,10 +54,26 @@ const createdAtColumn: Extract<FilterableColumn, { type: ColumnFilterType.DATE_R
4954
type: ColumnFilterType.DATE_RANGE,
5055
};
5156

57+
const startTimeColumn: Extract<FilterableColumn, { type: ColumnFilterType.DATE_RANGE }> = {
58+
id: "startTime",
59+
title: "startTime",
60+
type: ColumnFilterType.DATE_RANGE,
61+
};
62+
5263
function InsightsPageContent() {
5364
const { t } = useLocale();
5465
const { table } = useInsightsBookings();
5566
const { isAll, teamId, userId } = useInsightsOrgTeams();
67+
const { removeFilter } = useDataTable();
68+
const [dateTarget, _setDateTarget] = useState<"startTime" | "createdAt">("startTime");
69+
70+
const setDateTarget = useCallback(
71+
(target: "startTime" | "createdAt") => {
72+
_setDateTarget(target);
73+
removeFilter(target === "startTime" ? "createdAt" : "startTime");
74+
},
75+
[_setDateTarget, removeFilter]
76+
);
5677

5778
return (
5879
<>
@@ -63,10 +84,16 @@ function InsightsPageContent() {
6384
<DataTableFilters.AddFilterButton table={table} hideWhenFilterApplied />
6485
<DataTableFilters.ActiveFilters table={table} />
6586
<DataTableFilters.AddFilterButton table={table} variant="sm" showWhenFilterApplied />
66-
<DataTableFilters.ClearFiltersButton exclude={["createdAt"]} />
87+
<DataTableFilters.ClearFiltersButton exclude={["startTime", "createdAt"]} />
6788
<div className="grow" />
6889
<Download />
69-
<DateRangeFilter column={createdAtColumn} />
90+
<ButtonGroup combined>
91+
<DateRangeFilter
92+
column={dateTarget === "startTime" ? startTimeColumn : createdAtColumn}
93+
options={{ convertToTimeZone: true }}
94+
/>
95+
<DateTargetSelector value={dateTarget as DateTarget} onChange={setDateTarget} />
96+
</ButtonGroup>
7097
<TimezoneBadge />
7198
</div>
7299

apps/web/public/static/locales/en/common.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3689,6 +3689,10 @@
36893689
"before_scheduled_start_time": "Before scheduled start time",
36903690
"cancel_booking_acknowledge_no_show_fee": "I acknowledge that by cancelling the booking within {{timeValue}} {{timeUnit}} of the start time I will be charged the no show fee of {{amount, currency}}",
36913691
"contact_organizer": "If you have any questions, please contact the organizer.",
3692+
"booking_time_option": "Booking time",
3693+
"booking_time_option_description": "When the booking is scheduled (start to end)",
3694+
"created_at_option": "Created at",
3695+
"created_at_option_description": "When the booking was originally created",
36923696
"call_details": "Call Details",
36933697
"call_id": "Call ID",
36943698
"call_information": "Call Information",

packages/features/data-table/components/filters/DateRangeFilter.tsx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from "react";
44

55
import dayjs from "@calcom/dayjs";
66
import { useLocale } from "@calcom/lib/hooks/useLocale";
7+
import { CURRENT_TIMEZONE } from "@calcom/lib/timezoneConstants";
78
import classNames from "@calcom/ui/classNames";
89
import { Badge } from "@calcom/ui/components/badge";
910
import { Button, buttonClasses } from "@calcom/ui/components/button";
@@ -29,6 +30,7 @@ import {
2930
getDateRangeFromPreset,
3031
type PresetOption,
3132
} from "../../lib/dateRange";
33+
import { preserveLocalTime } from "../../lib/preserveLocalTime";
3234
import type { FilterableColumn, DateRangeFilterOptions } from "../../lib/types";
3335
import { ZDateRangeFilterValue, ColumnFilterType } from "../../lib/types";
3436
import { useFilterPopoverOpen } from "./useFilterPopoverOpen";
@@ -48,9 +50,8 @@ export const DateRangeFilter = ({
4850
}: DateRangeFilterProps) => {
4951
const { open, onOpenChange } = useFilterPopoverOpen(column.id);
5052
const filterValue = useFilterValue(column.id, ZDateRangeFilterValue);
51-
const { updateFilter, removeFilter } = useDataTable();
53+
const { updateFilter, removeFilter, timeZone: givenTimeZone } = useDataTable();
5254
const range = options?.range ?? "past";
53-
const endOfDay = options?.endOfDay ?? false;
5455
const forceCustom = range === "custom";
5556
const forcePast = range === "past";
5657

@@ -70,6 +71,19 @@ export const DateRangeFilter = ({
7071
: DEFAULT_PRESET
7172
);
7273

74+
const convertTimestamp = useCallback(
75+
(timestamp: string) => {
76+
if (!options?.convertToTimeZone) {
77+
return timestamp;
78+
}
79+
if (!givenTimeZone || CURRENT_TIMEZONE === givenTimeZone) {
80+
return timestamp;
81+
}
82+
return preserveLocalTime(timestamp, CURRENT_TIMEZONE, givenTimeZone);
83+
},
84+
[options?.convertToTimeZone, givenTimeZone]
85+
);
86+
7387
const updateValues = useCallback(
7488
({ preset, startDate, endDate }: { preset: PresetOption; startDate?: Dayjs; endDate?: Dayjs }) => {
7589
setSelectedPreset(preset);
@@ -80,14 +94,14 @@ export const DateRangeFilter = ({
8094
updateFilter(column.id, {
8195
type: ColumnFilterType.DATE_RANGE,
8296
data: {
83-
startDate: startDate.toDate().toISOString(),
84-
endDate: (endOfDay ? endDate.endOf("day") : endDate).toDate().toISOString(),
97+
startDate: convertTimestamp(startDate.toDate().toISOString()),
98+
endDate: convertTimestamp(endDate.toDate().toISOString()),
8599
preset: preset.value,
86100
},
87101
});
88102
}
89103
},
90-
[column.id, endOfDay]
104+
[column.id, updateFilter, convertTimestamp]
91105
);
92106

93107
useEffect(() => {
@@ -126,10 +140,12 @@ export const DateRangeFilter = ({
126140
startDate?: Date | undefined;
127141
endDate?: Date | undefined;
128142
}) => {
143+
// DateRangePicker returns the beginning of the day,
144+
// so we need to update `endDate` to the end of the day.
129145
updateValues({
130146
preset: CUSTOM_PRESET,
131147
startDate: startDate ? dayjs(startDate) : getDefaultStartDate(),
132-
endDate: endDate ? dayjs(endDate) : undefined,
148+
endDate: endDate ? dayjs(endDate).add(1, "day").subtract(1, "millisecond") : undefined,
133149
});
134150
};
135151

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,20 @@
11
import { useMemo } from "react";
22

3-
import dayjs from "@calcom/dayjs";
3+
import { CURRENT_TIMEZONE } from "@calcom/lib/timezoneConstants";
44

55
import { preserveLocalTime } from "../lib/preserveLocalTime";
66
import { useDataTable } from "./useDataTable";
77

88
/**
99
* Converts a timestamp to maintain the same local time in a different timezone.
10-
*
11-
* For example, if it's midnight (00:00) in Paris time:
12-
* - Input : "2025-05-22T22:00:00.000Z" (Midnight/00:00 in Paris)
13-
* - Output: "2025-05-22T15:00:00.000Z" (Midnight/00:00 in Seoul)
14-
*
15-
* This ensures that times like midnight (00:00) or end of day (23:59)
16-
* remain at those exact local times when converting between timezones.
17-
* The output timestamp is based on the timezone in the user's profile settings.
10+
* Fore more info, read packages/features/data-table/lib/preserveLocalTime.ts
1811
*/
1912
export function useChangeTimeZoneWithPreservedLocalTime(isoString: string) {
2013
const { timeZone: profileTimeZone } = useDataTable();
2114
return useMemo(() => {
22-
const currentTimeZone = dayjs.tz.guess();
23-
if (!profileTimeZone || currentTimeZone === profileTimeZone) {
15+
if (!profileTimeZone || CURRENT_TIMEZONE === profileTimeZone) {
2416
return isoString;
2517
}
26-
return preserveLocalTime(isoString, currentTimeZone, profileTimeZone);
18+
return preserveLocalTime(isoString, CURRENT_TIMEZONE, profileTimeZone);
2719
}, [isoString, profileTimeZone]);
2820
}

packages/features/data-table/lib/preserveLocalTime.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ import dayjs from "@calcom/dayjs";
1010
* This ensures that times like midnight (00:00) or end of day (23:59)
1111
* remain at those exact local times when converting between timezones.
1212
* The output timestamp is based on the timezone in the user's profile settings.
13+
*
14+
* For example, the profile timezone is Asia/Seoul,
15+
* but the current user is in Europe/Paris.
16+
* `Date` pickers will normally emit timestamps in the user's local timezone. (00:00:00 ~ 23:59:59 in Paris time)
17+
* but what we really want is to fetch the data based on the user's profile timezone. (00:00:00 ~ 23:59:59 in Seoul time)
18+
* That's why we need to convert the timestamp to the user's profile timezone.
1319
*/
1420
export const preserveLocalTime = (isoString: string, originalTimeZone: string, targetTimeZone: string) => {
1521
// Parse the input time

packages/features/data-table/lib/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export const ZFilterValue = z.union([
116116

117117
export type DateRangeFilterOptions = {
118118
range?: "past" | "custom";
119-
endOfDay?: boolean;
119+
convertToTimeZone?: boolean;
120120
};
121121

122122
export type TextFilterOptions = {

packages/features/insights/components/booking/EventTrendsChart.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ type EventTrendsData = RouterOutputs["viewer"]["insights"]["eventTrends"][number
3636
const CustomTooltip = ({
3737
active,
3838
payload,
39-
label,
39+
label: _label,
4040
}: {
4141
active?: boolean;
4242
payload?: Array<{

packages/features/insights/components/booking/LeastCompletedBookings.tsx

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { useMemo } from "react";
44

55
import dayjs from "@calcom/dayjs";
6-
import { useChangeTimeZoneWithPreservedLocalTime } from "@calcom/features/data-table/hooks/useChangeTimeZoneWithPreservedLocalTime";
76
import { useLocale } from "@calcom/lib/hooks/useLocale";
87
import { trpc } from "@calcom/trpc";
98

@@ -16,18 +15,6 @@ export const LeastCompletedTeamMembersTable = () => {
1615
const { t } = useLocale();
1716
let insightsBookingParams = useInsightsBookingParameters();
1817

19-
const currentTime = useChangeTimeZoneWithPreservedLocalTime(
20-
useMemo(() => {
21-
return dayjs().toISOString();
22-
}, [])
23-
);
24-
25-
// booking with endDate < now is "accepted" booking
26-
insightsBookingParams = {
27-
...insightsBookingParams,
28-
endDate: currentTime,
29-
};
30-
3118
const { data, isSuccess, isPending } = trpc.viewer.insights.membersWithLeastCompletedBookings.useQuery(
3219
insightsBookingParams,
3320
{

packages/features/insights/components/booking/MostCompletedBookings.tsx

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { useMemo } from "react";
44

55
import dayjs from "@calcom/dayjs";
6-
import { useChangeTimeZoneWithPreservedLocalTime } from "@calcom/features/data-table/hooks/useChangeTimeZoneWithPreservedLocalTime";
76
import { useLocale } from "@calcom/lib/hooks/useLocale";
87
import { trpc } from "@calcom/trpc";
98

@@ -16,18 +15,6 @@ export const MostCompletedTeamMembersTable = () => {
1615
const { t } = useLocale();
1716
let insightsBookingParams = useInsightsBookingParameters();
1817

19-
const currentTime = useChangeTimeZoneWithPreservedLocalTime(
20-
useMemo(() => {
21-
return dayjs().toISOString();
22-
}, [])
23-
);
24-
25-
// booking with endDate < now is "accepted" booking
26-
insightsBookingParams = {
27-
...insightsBookingParams,
28-
endDate: currentTime,
29-
};
30-
3118
const { data, isSuccess, isPending } = trpc.viewer.insights.membersWithMostCompletedBookings.useQuery(
3219
insightsBookingParams,
3320
{

packages/features/insights/components/booking/TimezoneBadge.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { useDataTable } from "@calcom/features/data-table";
66
import NoSSR from "@calcom/lib/components/NoSSR";
77
import { useLocale } from "@calcom/lib/hooks/useLocale";
88
import { CURRENT_TIMEZONE } from "@calcom/lib/timezoneConstants";
9-
import { Badge } from "@calcom/ui/components/badge";
109
import { Icon } from "@calcom/ui/components/icon";
1110
import { Tooltip } from "@calcom/ui/components/tooltip";
1211

@@ -41,9 +40,7 @@ const TimezoneBadgeContent = () => {
4140

4241
return (
4342
<Tooltip content={timezoneData.tooltipContent}>
44-
<Badge variant="gray" size="sm" data-testid="timezone-mismatch-badge">
45-
<Icon name="info" />
46-
</Badge>
43+
<Icon name="info" data-testid="timezone-mismatch-badge" className="text-subtle" />
4744
</Tooltip>
4845
);
4946
};

0 commit comments

Comments
 (0)