feat: add date and time columns to insights CSV exports (#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 6356657bd0c503f41349c8d1463bb4dd427b1a2c. * default columns are formatted as iso * address feedback --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
isValidRoutingFormFieldType,
|
||||
} from "@calcom/app-store/routing-forms/lib/FieldTypes";
|
||||
import { zodFields as routingFormFieldsSchema } from "@calcom/app-store/routing-forms/zod";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import type { InsightsRoutingBaseService } from "@calcom/lib/server/service/InsightsRoutingBaseService";
|
||||
import { readonlyPrisma as prisma } from "@calcom/prisma";
|
||||
@@ -71,6 +72,7 @@ class RoutingEventsInsights {
|
||||
teamId: {
|
||||
in: teamIds,
|
||||
},
|
||||
...(routingFormId && { id: routingFormId }),
|
||||
}
|
||||
: {
|
||||
userId: userId ?? -1,
|
||||
@@ -119,9 +121,11 @@ class RoutingEventsInsights {
|
||||
static async getRoutingFormPaginatedResponsesForDownload({
|
||||
headersPromise,
|
||||
dataPromise,
|
||||
timeZone,
|
||||
}: {
|
||||
headersPromise: ReturnType<typeof RoutingEventsInsights.getRoutingFormHeaders>;
|
||||
dataPromise: ReturnType<typeof InsightsRoutingBaseService.prototype.getTableData>;
|
||||
timeZone: string;
|
||||
}) {
|
||||
const [headers, data] = await Promise.all([headersPromise, dataPromise]);
|
||||
|
||||
@@ -157,11 +161,31 @@ class RoutingEventsInsights {
|
||||
"Response ID": item.id,
|
||||
"Form Name": item.formName,
|
||||
"Submitted At": item.createdAt.toISOString(),
|
||||
"Submitted At_date": dayjs(item.createdAt).tz(timeZone).format("YYYY-MM-DD"),
|
||||
"Submitted At_time": dayjs(item.createdAt).tz(timeZone).format("HH:mm:ss"),
|
||||
"Has Booking": item.bookingUid !== null,
|
||||
"Booking Status": item.bookingStatus || "NO_BOOKING",
|
||||
"Booking Created At": item.bookingCreatedAt?.toISOString() || "",
|
||||
"Booking Created At_date": item.bookingCreatedAt
|
||||
? dayjs(item.bookingCreatedAt).tz(timeZone).format("YYYY-MM-DD")
|
||||
: "",
|
||||
"Booking Created At_time": item.bookingCreatedAt
|
||||
? dayjs(item.bookingCreatedAt).tz(timeZone).format("HH:mm:ss")
|
||||
: "",
|
||||
"Booking Start Time": item.bookingStartTime?.toISOString() || "",
|
||||
"Booking Start Time_date": item.bookingStartTime
|
||||
? dayjs(item.bookingStartTime).tz(timeZone).format("YYYY-MM-DD")
|
||||
: "",
|
||||
"Booking Start Time_time": item.bookingStartTime
|
||||
? dayjs(item.bookingStartTime).tz(timeZone).format("HH:mm:ss")
|
||||
: "",
|
||||
"Booking End Time": item.bookingEndTime?.toISOString() || "",
|
||||
"Booking End Time_date": item.bookingEndTime
|
||||
? dayjs(item.bookingEndTime).tz(timeZone).format("YYYY-MM-DD")
|
||||
: "",
|
||||
"Booking End Time_time": item.bookingEndTime
|
||||
? dayjs(item.bookingEndTime).tz(timeZone).format("HH:mm:ss")
|
||||
: "",
|
||||
"Assignment Reason": item.bookingAssignmentReason || "",
|
||||
"Routed To Name": item.bookingUserName || "",
|
||||
"Routed To Email": item.bookingUserEmail || "",
|
||||
|
||||
@@ -822,7 +822,7 @@ export const insightsRouter = router({
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { limit, offset } = input;
|
||||
const { limit, offset, timeZone } = input;
|
||||
|
||||
const insightsBookingService = createInsightsBookingService(ctx, input);
|
||||
|
||||
@@ -830,6 +830,7 @@ export const insightsRouter = router({
|
||||
return await insightsBookingService.getCsvData({
|
||||
limit: limit ?? 100,
|
||||
offset: offset ?? 0,
|
||||
timeZone,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||||
@@ -886,6 +887,7 @@ export const insightsRouter = router({
|
||||
return await RoutingEventsInsights.getRoutingFormPaginatedResponsesForDownload({
|
||||
headersPromise,
|
||||
dataPromise,
|
||||
timeZone: ctx.user.timeZone,
|
||||
});
|
||||
}),
|
||||
getRoutingFormFieldOptions: userBelongsToTeamProcedure
|
||||
|
||||
@@ -479,7 +479,17 @@ export class InsightsBookingBaseService {
|
||||
});
|
||||
}
|
||||
|
||||
async getCsvData({ limit = 100, offset = 0 }: { limit?: number; offset?: number }) {
|
||||
async getCsvData({
|
||||
limit = 100,
|
||||
offset = 0,
|
||||
timeZone,
|
||||
}: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
timeZone: string;
|
||||
}) {
|
||||
const DATE_FORMAT = "YYYY-MM-DD";
|
||||
const TIME_FORMAT = "HH:mm:ss";
|
||||
const baseConditions = await this.getBaseConditions();
|
||||
|
||||
// Get total count first
|
||||
@@ -628,8 +638,20 @@ export class InsightsBookingBaseService {
|
||||
})
|
||||
);
|
||||
|
||||
// 6. Combine booking data with attendee data
|
||||
// 6. Combine booking data with attendee data and add ISO timestamp columns
|
||||
const data = csvData.map((bookingTimeStatus) => {
|
||||
const dateAndTime = {
|
||||
createdAt: bookingTimeStatus.createdAt.toISOString(),
|
||||
createdAt_date: dayjs(bookingTimeStatus.createdAt).tz(timeZone).format(DATE_FORMAT),
|
||||
createdAt_time: dayjs(bookingTimeStatus.createdAt).tz(timeZone).format(TIME_FORMAT),
|
||||
startTime: bookingTimeStatus.startTime.toISOString(),
|
||||
startTime_date: dayjs(bookingTimeStatus.startTime).tz(timeZone).format(DATE_FORMAT),
|
||||
startTime_time: dayjs(bookingTimeStatus.startTime).tz(timeZone).format(TIME_FORMAT),
|
||||
endTime: bookingTimeStatus.endTime.toISOString(),
|
||||
endTime_date: dayjs(bookingTimeStatus.endTime).tz(timeZone).format(DATE_FORMAT),
|
||||
endTime_time: dayjs(bookingTimeStatus.endTime).tz(timeZone).format(TIME_FORMAT),
|
||||
};
|
||||
|
||||
if (!bookingTimeStatus.uid) {
|
||||
// should not be reached because we filtered above
|
||||
const nullAttendeeFields: Record<string, null> = {};
|
||||
@@ -639,6 +661,7 @@ export class InsightsBookingBaseService {
|
||||
|
||||
return {
|
||||
...bookingTimeStatus,
|
||||
...dateAndTime,
|
||||
noShowGuests: null,
|
||||
noShowGuestsCount: 0,
|
||||
...nullAttendeeFields,
|
||||
@@ -655,6 +678,7 @@ export class InsightsBookingBaseService {
|
||||
|
||||
return {
|
||||
...bookingTimeStatus,
|
||||
...dateAndTime,
|
||||
noShowGuests: null,
|
||||
noShowGuestsCount: 0,
|
||||
...nullAttendeeFields,
|
||||
@@ -663,6 +687,7 @@ export class InsightsBookingBaseService {
|
||||
|
||||
return {
|
||||
...bookingTimeStatus,
|
||||
...dateAndTime,
|
||||
noShowGuests: attendeeData.noShowGuests,
|
||||
noShowGuestsCount: attendeeData.noShowGuestsCount,
|
||||
...Object.fromEntries(Object.entries(attendeeData).filter(([key]) => key.startsWith("attendee"))),
|
||||
|
||||
Reference in New Issue
Block a user