Skip to content

Commit 0a28b58

Browse files
authored
fix: date range computation on /insights (calcom#21436)
* fix: date range computation on /insights * remove log * error handling * update test cases * WIP * fix implementation * support weekStart * clean up * fix date range presets * clean up timeView * fix timeView * clean up * fix event trends * remove unused variables * fix flaky e2e tests * fix date range * update formatting logic
1 parent 3131318 commit 0a28b58

9 files changed

Lines changed: 1187 additions & 361 deletions

File tree

apps/web/playwright/filter-helpers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import type { Page } from "@playwright/test";
22
import { expect } from "@playwright/test";
33

4+
// Helper function to get text within a specific table column
5+
export const getByTableColumnText = (page: Page, columnId: string, text: string) =>
6+
page.locator(`[data-testid="data-table-td-${columnId}"]`).getByText(text);
7+
48
/**
59
* Add a filter from the filter dropdown
610
*/

apps/web/playwright/filter-segment.e2e.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
listSegments,
1111
clearFilters,
1212
openSegmentSubmenu,
13+
getByTableColumnText,
1314
} from "./filter-helpers";
1415
import { test } from "./lib/fixtures";
1516

@@ -58,20 +59,20 @@ test.describe("Filter Segment Functionality", () => {
5859
await test.step("Can apply and save a role filter as a segment", async () => {
5960
await applySelectFilter(page, "role", "admin");
6061

61-
await expect(page.getByText(adminUser.email)).toBeVisible();
62-
await expect(page.getByText(memberUser.email)).toBeHidden();
62+
await expect(getByTableColumnText(page, "member", adminUser.email)).toBeVisible();
63+
await expect(getByTableColumnText(page, "member", memberUser.email)).toBeHidden();
6364

6465
await createFilterSegment(page, segmentName);
6566

6667
await clearFilters(page);
6768

68-
await expect(page.getByText(adminUser.email)).toBeVisible();
69-
await expect(page.getByText(memberUser.email)).toBeVisible();
69+
await expect(getByTableColumnText(page, "member", adminUser.email)).toBeVisible();
70+
await expect(getByTableColumnText(page, "member", memberUser.email)).toBeVisible();
7071

7172
await selectSegment(page, segmentName);
7273

73-
await expect(page.getByText(adminUser.email)).toBeVisible();
74-
await expect(page.getByText(memberUser.email)).toBeHidden();
74+
await expect(getByTableColumnText(page, "member", adminUser.email)).toBeVisible();
75+
await expect(getByTableColumnText(page, "member", memberUser.email)).toBeHidden();
7576
});
7677

7778
await test.step("Can delete a filter segment", async () => {
@@ -119,8 +120,8 @@ test.describe("Filter Segment Functionality", () => {
119120

120121
await selectSegment(page, segmentName);
121122

122-
await expect(page.getByText(adminUser.email)).toBeVisible();
123-
await expect(page.getByText(memberUser.email)).toBeHidden();
123+
await expect(getByTableColumnText(page, "member", adminUser.email)).toBeVisible();
124+
await expect(getByTableColumnText(page, "member", memberUser.email)).toBeHidden();
124125

125126
await deleteSegment(page, segmentName);
126127
});
@@ -167,8 +168,8 @@ test.describe("Filter Segment Functionality", () => {
167168

168169
await clearFilters(page);
169170
await selectSegment(page, segmentName);
170-
await expect(page.getByText(adminUser.email)).toBeVisible();
171-
await expect(page.getByText(memberUser.email)).toBeHidden();
171+
await expect(getByTableColumnText(page, "member", adminUser.email)).toBeVisible();
172+
await expect(getByTableColumnText(page, "member", memberUser.email)).toBeHidden();
172173
});
173174

174175
await test.step("Regular member can see but not modify team segments", async () => {
@@ -193,8 +194,8 @@ test.describe("Filter Segment Functionality", () => {
193194
await expect(dataTable).toBeVisible();
194195

195196
await selectSegment(page, "Team Admin Filter");
196-
await expect(page.getByText(adminUser.email)).toBeVisible();
197-
await expect(page.getByText(memberUser.email)).toBeHidden();
197+
await expect(getByTableColumnText(page, "member", adminUser.email)).toBeVisible();
198+
await expect(getByTableColumnText(page, "member", memberUser.email)).toBeHidden();
198199

199200
await openSegmentSubmenu(page, segmentName);
200201
await expect(

apps/web/playwright/insights-routing-filters.e2e.ts

Lines changed: 35 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
applyTextFilter,
1212
applyNumberFilter,
1313
selectOptionValue,
14+
getByTableColumnText,
1415
} from "./filter-helpers";
1516
import { test } from "./lib/fixtures";
1617

@@ -186,8 +187,8 @@ test.describe("Insights > Routing Filters", () => {
186187

187188
await applySelectFilter(page, "bookingUserId", user1Name);
188189

189-
await expect(page.getByText("John Doe")).toBeVisible();
190-
await expect(page.getByText("Jane Smith")).toBeHidden();
190+
await expect(getByTableColumnText(page, fieldId, "John Doe")).toBeVisible();
191+
await expect(getByTableColumnText(page, fieldId, "Jane Smith")).toBeHidden();
191192

192193
await prisma.booking.delete({ where: { id: booking1.id } });
193194
await prisma.booking.delete({ where: { id: booking2.id } });
@@ -291,26 +292,26 @@ test.describe("Insights > Routing Filters", () => {
291292
await page.getByPlaceholder("Filter attendees by name or email").fill("John");
292293
await page.keyboard.press("Enter");
293294

294-
await expect(page.getByText("Response 1")).toBeVisible();
295-
await expect(page.getByText("Response 2")).toBeHidden();
295+
await expect(getByTableColumnText(page, fieldId, "Response 1")).toBeVisible();
296+
await expect(getByTableColumnText(page, fieldId, "Response 2")).toBeHidden();
296297

297298
await clearFilters(page);
298299
await addFilter(page, "bookingAttendees");
299300
await openFilter(page, "bookingAttendees");
300301
await page.getByPlaceholder("Filter attendees by name or email").fill("jane.smith@example");
301302
await page.keyboard.press("Enter");
302303

303-
await expect(page.getByText("Response 2")).toBeVisible();
304-
await expect(page.getByText("Response 1")).toBeHidden();
304+
await expect(getByTableColumnText(page, fieldId, "Response 2")).toBeVisible();
305+
await expect(getByTableColumnText(page, fieldId, "Response 1")).toBeHidden();
305306

306307
await clearFilters(page);
307308
await addFilter(page, "bookingAttendees");
308309
await openFilter(page, "bookingAttendees");
309310
await page.getByPlaceholder("Filter attendees by name or email").fill("JANE");
310311
await page.keyboard.press("Enter");
311312

312-
await expect(page.getByText("Response 2")).toBeVisible();
313-
await expect(page.getByText("Response 1")).toBeHidden();
313+
await expect(getByTableColumnText(page, fieldId, "Response 2")).toBeVisible();
314+
await expect(getByTableColumnText(page, fieldId, "Response 1")).toBeHidden();
314315

315316
await prisma.booking.delete({ where: { id: booking1.id } });
316317
await prisma.booking.delete({ where: { id: booking2.id } });
@@ -392,26 +393,26 @@ test.describe("Insights > Routing Filters", () => {
392393

393394
await applyTextFilter(page, fieldId, "Contains", "test");
394395

395-
await expect(page.getByText("This is a test description")).toBeVisible();
396-
await expect(page.getByText("Another description for testing")).toBeVisible();
396+
await expect(getByTableColumnText(page, fieldId, "This is a test description")).toBeVisible();
397+
await expect(getByTableColumnText(page, fieldId, "Another description for testing")).toBeVisible();
397398

398399
await clearFilters(page);
399400
await applyTextFilter(page, fieldId, "Is", "This is a test description");
400401

401-
await expect(page.getByText("This is a test description")).toBeVisible();
402-
await expect(page.getByText("Another description for testing")).toBeHidden();
402+
await expect(getByTableColumnText(page, fieldId, "This is a test description")).toBeVisible();
403+
await expect(getByTableColumnText(page, fieldId, "Another description for testing")).toBeHidden();
403404

404405
await clearFilters(page);
405406
await applyTextFilter(page, fieldId, "Is empty");
406407

407-
await expect(page.getByText("This is a test description")).toBeHidden();
408-
await expect(page.getByText("Another description for testing")).toBeHidden();
408+
await expect(getByTableColumnText(page, fieldId, "This is a test description")).toBeHidden();
409+
await expect(getByTableColumnText(page, fieldId, "Another description for testing")).toBeHidden();
409410

410411
await clearFilters(page);
411412
await applyTextFilter(page, fieldId, "Contains", "TEST");
412413

413-
await expect(page.getByText("This is a test description")).toBeVisible();
414-
await expect(page.getByText("Another description for testing")).toBeVisible();
414+
await expect(getByTableColumnText(page, fieldId, "This is a test description")).toBeVisible();
415+
await expect(getByTableColumnText(page, fieldId, "Another description for testing")).toBeVisible();
415416
});
416417

417418
test("NUMBER field: should filter with different number operators", async ({
@@ -489,26 +490,23 @@ test.describe("Insights > Routing Filters", () => {
489490

490491
await applyNumberFilter(page, fieldId, "=", 3);
491492

492-
const getByNumber = (number: number) =>
493-
page.locator(`[data-testid="data-table-td-${fieldId}"]`).getByText(String(number));
494-
495-
await expect(getByNumber(3)).toBeVisible();
496-
await expect(getByNumber(1)).toBeHidden();
497-
await expect(getByNumber(5)).toBeHidden();
493+
await expect(getByTableColumnText(page, fieldId, "3")).toBeVisible();
494+
await expect(getByTableColumnText(page, fieldId, "1")).toBeHidden();
495+
await expect(getByTableColumnText(page, fieldId, "5")).toBeHidden();
498496

499497
await clearFilters(page);
500498
await applyNumberFilter(page, fieldId, ">", 3);
501499

502-
await expect(getByNumber(5)).toBeVisible();
503-
await expect(getByNumber(1)).toBeHidden();
504-
await expect(getByNumber(3)).toBeHidden();
500+
await expect(getByTableColumnText(page, fieldId, "5")).toBeVisible();
501+
await expect(getByTableColumnText(page, fieldId, "1")).toBeHidden();
502+
await expect(getByTableColumnText(page, fieldId, "3")).toBeHidden();
505503

506504
await clearFilters(page);
507505
await applyNumberFilter(page, fieldId, "≤", 3);
508506

509-
await expect(getByNumber(1)).toBeVisible();
510-
await expect(getByNumber(3)).toBeVisible();
511-
await expect(getByNumber(5)).toBeHidden();
507+
await expect(getByTableColumnText(page, fieldId, "1")).toBeVisible();
508+
await expect(getByTableColumnText(page, fieldId, "3")).toBeVisible();
509+
await expect(getByTableColumnText(page, fieldId, "5")).toBeHidden();
512510
});
513511

514512
test("SINGLE_SELECT and MULTI_SELECT fields: should filter correctly", async ({
@@ -620,19 +618,16 @@ test.describe("Insights > Routing Filters", () => {
620618

621619
await page.goto(`/insights/routing`);
622620

623-
const getByColumnValue = (columnId: string, value: string) =>
624-
page.locator(`[data-testid="data-table-td-${columnId}"]`).getByText(value);
625-
626621
await applySelectFilter(page, locationFieldId, "New York");
627-
await expect(getByColumnValue(locationFieldId, "New York")).toBeVisible();
628-
await expect(getByColumnValue(locationFieldId, "London")).toBeHidden();
622+
await expect(getByTableColumnText(page, locationFieldId, "New York")).toBeVisible();
623+
await expect(getByTableColumnText(page, locationFieldId, "London")).toBeHidden();
629624

630625
await clearFilters(page);
631626
await applySelectFilter(page, skillFieldId, "JavaScript");
632627
await applySelectFilter(page, skillFieldId, "TypeScript");
633-
await expect(getByColumnValue(skillFieldId, "JavaScript")).toBeVisible();
634-
await expect(getByColumnValue(skillFieldId, "TypeScript")).toBeVisible();
635-
await expect(getByColumnValue(skillFieldId, "React")).toBeHidden();
628+
await expect(getByTableColumnText(page, skillFieldId, "JavaScript")).toBeVisible();
629+
await expect(getByTableColumnText(page, skillFieldId, "TypeScript")).toBeVisible();
630+
await expect(getByTableColumnText(page, skillFieldId, "React")).toBeHidden();
636631
});
637632

638633
test("createdAt filter: should filter by date range", async ({ page, users, routingForms, prisma }) => {
@@ -698,14 +693,14 @@ test.describe("Insights > Routing Filters", () => {
698693
await page.getByTestId("date-range-options-w").click(); // Last 7 Days
699694
await page.keyboard.press("Escape");
700695

701-
await expect(page.getByText("Recent Response")).toBeVisible();
702-
await expect(page.getByText("Old Response")).toBeHidden();
696+
await expect(getByTableColumnText(page, fieldId, "Recent Response")).toBeVisible();
697+
await expect(getByTableColumnText(page, fieldId, "Old Response")).toBeHidden();
703698

704699
await openFilter(page, "createdAt");
705700
await page.getByTestId("date-range-options-t").click(); // Last 30 Days
706701
await page.keyboard.press("Escape");
707702

708-
await expect(page.getByText("Recent Response")).toBeVisible();
709-
await expect(page.getByText("Old Response")).toBeVisible();
703+
await expect(getByTableColumnText(page, fieldId, "Recent Response")).toBeVisible();
704+
await expect(getByTableColumnText(page, fieldId, "Old Response")).toBeVisible();
710705
});
711706
});

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const PRESET_OPTIONS: PresetOption[] = [
2828
CUSTOM_PRESET,
2929
];
3030

31-
export const getDefaultStartDate = () => dayjs().subtract(1, "week").startOf("day");
31+
export const getDefaultStartDate = () => dayjs().subtract(6, "day").startOf("day");
3232

3333
export const getDefaultEndDate = () => dayjs().endOf("day");
3434

@@ -46,11 +46,11 @@ export const getDateRangeFromPreset = (val: string | null) => {
4646
endDate = dayjs().endOf("day");
4747
break;
4848
case "w": // Last 7 days
49-
startDate = dayjs().subtract(1, "week").startOf("day");
49+
startDate = dayjs().subtract(6, "day").startOf("day");
5050
endDate = dayjs().endOf("day");
5151
break;
5252
case "t": // Last 30 days
53-
startDate = dayjs().subtract(30, "day").startOf("day");
53+
startDate = dayjs().subtract(29, "day").startOf("day");
5454
endDate = dayjs().endOf("day");
5555
break;
5656
case "m": // Month to Date

packages/features/insights/components/BookingStatusLineChart.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Title } from "@tremor/react";
2-
import { useMemo } from "react";
32

43
import { useLocale } from "@calcom/lib/hooks/useLocale";
54
import { trpc } from "@calcom/trpc";
@@ -12,23 +11,14 @@ import { LoadingInsight } from "./LoadingInsights";
1211

1312
export const BookingStatusLineChart = () => {
1413
const { t } = useLocale();
15-
const { isAll, teamId, userId, startDate, endDate, dateRangePreset, eventTypeId } = useInsightsParameters();
16-
17-
const selectedTimeView = useMemo(() => {
18-
if (dateRangePreset === "tdy") return "day";
19-
else if (dateRangePreset === "w") return "week";
20-
else if (dateRangePreset === "m") return "month";
21-
else if (dateRangePreset === "y") return "year";
22-
else return "week";
23-
}, [dateRangePreset]);
14+
const { isAll, teamId, userId, startDate, endDate, eventTypeId } = useInsightsParameters();
2415

2516
const {
2617
data: eventsTimeLine,
2718
isSuccess,
2819
isPending,
2920
} = trpc.viewer.insights.eventsTimeline.useQuery(
3021
{
31-
timeView: selectedTimeView,
3222
startDate,
3323
endDate,
3424
teamId,

packages/features/insights/hooks/useInsightsParameters.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useMemo } from "react";
22

3+
import dayjs from "@calcom/dayjs";
34
import {
45
useFilterValue,
56
useColumnFilters,
@@ -28,14 +29,21 @@ export function useInsightsParameters() {
2829
const eventTypeId = useFilterValue("eventTypeId", ZSingleSelectFilterValue)?.data as number | undefined;
2930
const routingFormId = useFilterValue("formId", ZSingleSelectFilterValue)?.data as string | undefined;
3031
const createdAtRange = useFilterValue("createdAt", ZDateRangeFilterValue)?.data;
31-
const startDate = useMemo(
32-
() => preserveLocalTime(createdAtRange?.startDate ?? getDefaultStartDate().toISOString()),
33-
[createdAtRange?.startDate, preserveLocalTime]
34-
);
35-
const endDate = useMemo(
36-
() => preserveLocalTime(createdAtRange?.endDate ?? getDefaultEndDate().toISOString()),
37-
[createdAtRange?.endDate, preserveLocalTime]
38-
);
32+
// TODO for future: this preserving local time & startOf & endOf should be handled
33+
// from DateRangeFilter out of the box.
34+
// When we do it, we also need to remove those timezone handling logic from the backend side at the same time.
35+
const startDate = useMemo(() => {
36+
const timestamp = dayjs(createdAtRange?.startDate ?? getDefaultStartDate().toISOString())
37+
.startOf("day")
38+
.toISOString();
39+
return preserveLocalTime(timestamp);
40+
}, [createdAtRange?.startDate, preserveLocalTime]);
41+
const endDate = useMemo(() => {
42+
const timestamp = dayjs(createdAtRange?.endDate ?? getDefaultEndDate().toISOString())
43+
.endOf("day")
44+
.toISOString();
45+
return preserveLocalTime(timestamp);
46+
}, [createdAtRange?.endDate, preserveLocalTime]);
3947

4048
const dateRangePreset = useMemo<PresetOptionValue>(() => {
4149
return (createdAtRange?.preset as PresetOptionValue) ?? CUSTOM_PRESET_VALUE;

0 commit comments

Comments
 (0)