Skip to content

Commit b407be9

Browse files
feat: add date and time columns to insights CSV exports (calcom#23771)
* feat: add ISO timestamp columns to booking CSV exports - Add createdAt_iso, startTime_iso, endTime_iso columns to getCsvData() method - Use .toISOString() format for new columns: 2025-09-09T13:03:55+02:00 - Keep existing timestamp columns unchanged to avoid breaking changes - Addresses user request for parseable timestamp format in CSV downloads Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * feat: add ISO timestamp columns to routing CSV exports - Add ISO format columns for createdAt, bookingCreatedAt, bookingStartTime, bookingEndTime - Preserve original timestamp format to avoid breaking changes - Complete implementation for both /insights and /insights/routing pages Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * feat: replace ISO columns with timezone-aware date/time columns - Replace _iso columns with separate _date and _time columns - Use user timezone for proper date/time conversion - Add timezone parameter to CSV export methods - Maintain backward compatibility with original timestamp format - Date format: YYYY-MM-DD, Time format: HH:mm:ss Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix: resolve TypeScript type compatibility issues - Fix WhereForTeamOrAllTeams type compatibility in routing-events.ts - Restructure conditional object creation to ensure required properties - Clean up merge conflict remnants from previous rebase Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * clean up * fix: use user profile timezone for CSV exports instead of browser timezone - Modified rawData endpoint to use ctx.user.timeZone instead of input.timeZone - Removed timeZone field from bookingRepositoryBaseInputSchema - Updated useInsightsBookingParameters to remove timeZone property - Fixed RecentNoShowGuestsChart to use useDataTable for timezone access - Resolves timezone discrepancy where CSV exports showed incorrect time values Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * Revert "fix: use user profile timezone for CSV exports instead of browser timezone" This reverts commit 6356657. * default columns are formatted as iso * address feedback --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 4e19204 commit b407be9

3 files changed

Lines changed: 54 additions & 3 deletions

File tree

packages/features/insights/server/routing-events.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
isValidRoutingFormFieldType,
99
} from "@calcom/app-store/routing-forms/lib/FieldTypes";
1010
import { zodFields as routingFormFieldsSchema } from "@calcom/app-store/routing-forms/zod";
11+
import dayjs from "@calcom/dayjs";
1112
import { WEBAPP_URL } from "@calcom/lib/constants";
1213
import type { InsightsRoutingBaseService } from "@calcom/lib/server/service/InsightsRoutingBaseService";
1314
import { readonlyPrisma as prisma } from "@calcom/prisma";
@@ -71,6 +72,7 @@ class RoutingEventsInsights {
7172
teamId: {
7273
in: teamIds,
7374
},
75+
...(routingFormId && { id: routingFormId }),
7476
}
7577
: {
7678
userId: userId ?? -1,
@@ -119,9 +121,11 @@ class RoutingEventsInsights {
119121
static async getRoutingFormPaginatedResponsesForDownload({
120122
headersPromise,
121123
dataPromise,
124+
timeZone,
122125
}: {
123126
headersPromise: ReturnType<typeof RoutingEventsInsights.getRoutingFormHeaders>;
124127
dataPromise: ReturnType<typeof InsightsRoutingBaseService.prototype.getTableData>;
128+
timeZone: string;
125129
}) {
126130
const [headers, data] = await Promise.all([headersPromise, dataPromise]);
127131

@@ -157,11 +161,31 @@ class RoutingEventsInsights {
157161
"Response ID": item.id,
158162
"Form Name": item.formName,
159163
"Submitted At": item.createdAt.toISOString(),
164+
"Submitted At_date": dayjs(item.createdAt).tz(timeZone).format("YYYY-MM-DD"),
165+
"Submitted At_time": dayjs(item.createdAt).tz(timeZone).format("HH:mm:ss"),
160166
"Has Booking": item.bookingUid !== null,
161167
"Booking Status": item.bookingStatus || "NO_BOOKING",
162168
"Booking Created At": item.bookingCreatedAt?.toISOString() || "",
169+
"Booking Created At_date": item.bookingCreatedAt
170+
? dayjs(item.bookingCreatedAt).tz(timeZone).format("YYYY-MM-DD")
171+
: "",
172+
"Booking Created At_time": item.bookingCreatedAt
173+
? dayjs(item.bookingCreatedAt).tz(timeZone).format("HH:mm:ss")
174+
: "",
163175
"Booking Start Time": item.bookingStartTime?.toISOString() || "",
176+
"Booking Start Time_date": item.bookingStartTime
177+
? dayjs(item.bookingStartTime).tz(timeZone).format("YYYY-MM-DD")
178+
: "",
179+
"Booking Start Time_time": item.bookingStartTime
180+
? dayjs(item.bookingStartTime).tz(timeZone).format("HH:mm:ss")
181+
: "",
164182
"Booking End Time": item.bookingEndTime?.toISOString() || "",
183+
"Booking End Time_date": item.bookingEndTime
184+
? dayjs(item.bookingEndTime).tz(timeZone).format("YYYY-MM-DD")
185+
: "",
186+
"Booking End Time_time": item.bookingEndTime
187+
? dayjs(item.bookingEndTime).tz(timeZone).format("HH:mm:ss")
188+
: "",
165189
"Assignment Reason": item.bookingAssignmentReason || "",
166190
"Routed To Name": item.bookingUserName || "",
167191
"Routed To Email": item.bookingUserEmail || "",

packages/features/insights/server/trpc-router.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -822,14 +822,15 @@ export const insightsRouter = router({
822822
})
823823
)
824824
.query(async ({ ctx, input }) => {
825-
const { limit, offset } = input;
825+
const { limit, offset, timeZone } = input;
826826

827827
const insightsBookingService = createInsightsBookingService(ctx, input);
828828

829829
try {
830830
return await insightsBookingService.getCsvData({
831831
limit: limit ?? 100,
832832
offset: offset ?? 0,
833+
timeZone,
833834
});
834835
} catch (e) {
835836
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
@@ -886,6 +887,7 @@ export const insightsRouter = router({
886887
return await RoutingEventsInsights.getRoutingFormPaginatedResponsesForDownload({
887888
headersPromise,
888889
dataPromise,
890+
timeZone: ctx.user.timeZone,
889891
});
890892
}),
891893
getRoutingFormFieldOptions: userBelongsToTeamProcedure

packages/lib/server/service/InsightsBookingBaseService.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,17 @@ export class InsightsBookingBaseService {
479479
});
480480
}
481481

482-
async getCsvData({ limit = 100, offset = 0 }: { limit?: number; offset?: number }) {
482+
async getCsvData({
483+
limit = 100,
484+
offset = 0,
485+
timeZone,
486+
}: {
487+
limit?: number;
488+
offset?: number;
489+
timeZone: string;
490+
}) {
491+
const DATE_FORMAT = "YYYY-MM-DD";
492+
const TIME_FORMAT = "HH:mm:ss";
483493
const baseConditions = await this.getBaseConditions();
484494

485495
// Get total count first
@@ -628,8 +638,20 @@ export class InsightsBookingBaseService {
628638
})
629639
);
630640

631-
// 6. Combine booking data with attendee data
641+
// 6. Combine booking data with attendee data and add ISO timestamp columns
632642
const data = csvData.map((bookingTimeStatus) => {
643+
const dateAndTime = {
644+
createdAt: bookingTimeStatus.createdAt.toISOString(),
645+
createdAt_date: dayjs(bookingTimeStatus.createdAt).tz(timeZone).format(DATE_FORMAT),
646+
createdAt_time: dayjs(bookingTimeStatus.createdAt).tz(timeZone).format(TIME_FORMAT),
647+
startTime: bookingTimeStatus.startTime.toISOString(),
648+
startTime_date: dayjs(bookingTimeStatus.startTime).tz(timeZone).format(DATE_FORMAT),
649+
startTime_time: dayjs(bookingTimeStatus.startTime).tz(timeZone).format(TIME_FORMAT),
650+
endTime: bookingTimeStatus.endTime.toISOString(),
651+
endTime_date: dayjs(bookingTimeStatus.endTime).tz(timeZone).format(DATE_FORMAT),
652+
endTime_time: dayjs(bookingTimeStatus.endTime).tz(timeZone).format(TIME_FORMAT),
653+
};
654+
633655
if (!bookingTimeStatus.uid) {
634656
// should not be reached because we filtered above
635657
const nullAttendeeFields: Record<string, null> = {};
@@ -639,6 +661,7 @@ export class InsightsBookingBaseService {
639661

640662
return {
641663
...bookingTimeStatus,
664+
...dateAndTime,
642665
noShowGuests: null,
643666
noShowGuestsCount: 0,
644667
...nullAttendeeFields,
@@ -655,6 +678,7 @@ export class InsightsBookingBaseService {
655678

656679
return {
657680
...bookingTimeStatus,
681+
...dateAndTime,
658682
noShowGuests: null,
659683
noShowGuestsCount: 0,
660684
...nullAttendeeFields,
@@ -663,6 +687,7 @@ export class InsightsBookingBaseService {
663687

664688
return {
665689
...bookingTimeStatus,
690+
...dateAndTime,
666691
noShowGuests: attendeeData.noShowGuests,
667692
noShowGuestsCount: attendeeData.noShowGuestsCount,
668693
...Object.fromEntries(Object.entries(attendeeData).filter(([key]) => key.startsWith("attendee"))),

0 commit comments

Comments
 (0)