fix: provide timeZone to /insights from the server side (#21580)

This commit is contained in:
Eunjae Lee
2025-06-12 15:26:44 +00:00
committed by GitHub
parent 5bb2a904ce
commit 6cccb1725f
10 changed files with 102 additions and 98 deletions
@@ -1,4 +1,10 @@
import { _generateMetadata } from "app/_utils";
import { cookies, headers } from "next/headers";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import prisma from "@calcom/prisma";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import InsightsPage from "~/insights/insights-view";
@@ -11,6 +17,17 @@ export const generateMetadata = async () =>
"/insights"
);
export default async function Page() {
return <InsightsPage />;
}
const ServerPage = async () => {
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const { timeZone } = await prisma.user.findUniqueOrThrow({
where: { id: session?.user.id ?? -1 },
select: {
timeZone: true,
},
});
return <InsightsPage timeZone={timeZone} />;
};
export default ServerPage;
+2 -2
View File
@@ -28,9 +28,9 @@ import { useInsightsBookings } from "@calcom/features/insights/hooks/useInsights
import { useInsightsOrgTeams } from "@calcom/features/insights/hooks/useInsightsOrgTeams";
import { useLocale } from "@calcom/lib/hooks/useLocale";
export default function InsightsPage() {
export default function InsightsPage({ timeZone }: { timeZone: string }) {
return (
<DataTableProvider>
<DataTableProvider timeZone={timeZone}>
<InsightsOrgTeamsProvider>
<InsightsPageContent />
</InsightsOrgTeamsProvider>
@@ -64,6 +64,8 @@ export type DataTableContextType = {
searchTerm: string;
setSearchTerm: (searchTerm: string | null) => void;
timeZone?: string;
};
export const DataTableContext = createContext<DataTableContextType | null>(null);
@@ -75,6 +77,7 @@ interface DataTableProviderProps {
ctaContainerClassName?: string;
defaultPageSize?: number;
segments?: FilterSegmentOutput[];
timeZone?: string;
preferredSegmentId?: number | null;
}
@@ -85,6 +88,7 @@ export function DataTableProvider({
defaultPageSize = DEFAULT_PAGE_SIZE,
ctaContainerClassName = CTA_CONTAINER_CLASS_NAME,
segments: providedSegments,
timeZone,
preferredSegmentId,
}: DataTableProviderProps) {
const filterToOpen = useRef<string | undefined>(undefined);
@@ -245,6 +249,7 @@ export function DataTableProvider({
isSegmentEnabled,
searchTerm,
setSearchTerm: setDebouncedSearchTerm,
timeZone,
}}>
{children}
</DataTableContext.Provider>
@@ -0,0 +1,28 @@
import { useMemo } from "react";
import dayjs from "@calcom/dayjs";
import { preserveLocalTime } from "../lib/preserveLocalTime";
import { useDataTable } from "./useDataTable";
/**
* Converts a timestamp to maintain the same local time in a different timezone.
*
* For example, if it's midnight (00:00) in Paris time:
* - Input : "2025-05-22T22:00:00.000Z" (Midnight/00:00 in Paris)
* - Output: "2025-05-22T15:00:00.000Z" (Midnight/00:00 in Seoul)
*
* This ensures that times like midnight (00:00) or end of day (23:59)
* remain at those exact local times when converting between timezones.
* The output timestamp is based on the timezone in the user's profile settings.
*/
export function useChangeTimeZoneWithPreservedLocalTime(isoString: string) {
const { timeZone: profileTimeZone } = useDataTable();
return useMemo(() => {
const currentTimeZone = dayjs.tz.guess();
if (!profileTimeZone || currentTimeZone === profileTimeZone) {
return isoString;
}
return preserveLocalTime(isoString, currentTimeZone, profileTimeZone);
}, [isoString, profileTimeZone]);
}
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
import dayjs from "@calcom/dayjs";
import { preserveLocalTime } from "../useUserTimePreferences";
import { preserveLocalTime } from "../preserveLocalTime";
describe("preserveLocalTime", () => {
it("should preserve midnight (00:00) when converting from Paris to Seoul", () => {
@@ -0,0 +1,31 @@
import dayjs from "@calcom/dayjs";
/**
* Converts a timestamp to maintain the same local time in a different timezone.
*
* For example, if it's midnight (00:00) in Paris time:
* - Input : "2025-05-22T22:00:00.000Z" (Midnight/00:00 in Paris)
* - Output: "2025-05-22T15:00:00.000Z" (Midnight/00:00 in Seoul)
*
* This ensures that times like midnight (00:00) or end of day (23:59)
* remain at those exact local times when converting between timezones.
* The output timestamp is based on the timezone in the user's profile settings.
*/
export const preserveLocalTime = (isoString: string, originalTimeZone: string, targetTimeZone: string) => {
// Parse the input time
const time = dayjs(isoString).tz(originalTimeZone);
// Get the wall clock time components
const hours = time.hour();
const minutes = time.minute();
const seconds = time.second();
const milliseconds = time.millisecond();
// Create a new date in target timezone with same wall clock time
return dayjs
.tz(time.format("YYYY-MM-DD"), targetTimeZone)
.hour(hours)
.minute(minutes)
.second(seconds)
.millisecond(milliseconds)
.toISOString();
};
@@ -8,19 +8,18 @@ import {
ZSingleSelectFilterValue,
ZDateRangeFilterValue,
} from "@calcom/features/data-table";
import { useChangeTimeZoneWithPreservedLocalTime } from "@calcom/features/data-table/hooks/useChangeTimeZoneWithPreservedLocalTime";
import {
getDefaultStartDate,
getDefaultEndDate,
CUSTOM_PRESET_VALUE,
type PresetOptionValue,
} from "@calcom/features/data-table/lib/dateRange";
import { useUserTimePreferences } from "@calcom/trpc/react/hooks/useUserTimePreferences";
import { useInsightsOrgTeams } from "./useInsightsOrgTeams";
export function useInsightsParameters() {
const { isAll, teamId, userId } = useInsightsOrgTeams();
const { preserveLocalTime } = useUserTimePreferences();
const memberUserIds = useFilterValue("bookingUserId", ZMultiSelectFilterValue)?.data as
| number[]
@@ -32,18 +31,20 @@ export function useInsightsParameters() {
// 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 = useMemo(() => {
const timestamp = dayjs(createdAtRange?.startDate ?? getDefaultStartDate().toISOString())
.startOf("day")
.toISOString();
return preserveLocalTime(timestamp);
}, [createdAtRange?.startDate, preserveLocalTime]);
const endDate = useMemo(() => {
const timestamp = dayjs(createdAtRange?.endDate ?? getDefaultEndDate().toISOString())
.endOf("day")
.toISOString();
return preserveLocalTime(timestamp);
}, [createdAtRange?.endDate, preserveLocalTime]);
const startDate = useChangeTimeZoneWithPreservedLocalTime(
useMemo(() => {
return dayjs(createdAtRange?.startDate ?? getDefaultStartDate().toISOString())
.startOf("day")
.toISOString();
}, [createdAtRange?.startDate])
);
const endDate = useChangeTimeZoneWithPreservedLocalTime(
useMemo(() => {
return dayjs(createdAtRange?.endDate ?? getDefaultEndDate().toISOString())
.endOf("day")
.toISOString();
}, [createdAtRange?.endDate])
);
const dateRangePreset = useMemo<PresetOptionValue>(() => {
return (createdAtRange?.preset as PresetOptionValue) ?? CUSTOM_PRESET_VALUE;
@@ -1,55 +0,0 @@
import { useMemo, useCallback } from "react";
import dayjs from "@calcom/dayjs";
import { trpc } from "../trpc";
export const preserveLocalTime = (isoString: string, originalTimeZone: string, targetTimeZone: string) => {
// Parse the input time
const time = dayjs(isoString).tz(originalTimeZone);
// Get the wall clock time components
const hours = time.hour();
const minutes = time.minute();
const seconds = time.second();
const milliseconds = time.millisecond();
// Create a new date in target timezone with same wall clock time
return dayjs
.tz(time.format("YYYY-MM-DD"), targetTimeZone)
.hour(hours)
.minute(minutes)
.second(seconds)
.millisecond(milliseconds)
.toISOString();
};
export function useUserTimePreferences() {
const { data } = trpc.viewer.me.getUserTimePreferences.useQuery(undefined, {
staleTime: 5 * 60 * 1000,
});
const timeFormat = useMemo(() => data?.timeFormat ?? 12, [data?.timeFormat]);
const timeZone = useMemo(() => data?.timeZone ?? dayjs.tz.guess(), [data?.timeZone]);
/**
* Converts a timestamp to maintain the same local time in a different timezone.
*
* For example, if it's midnight (00:00) in Paris time:
* - Input : "2025-05-22T22:00:00.000Z" (Midnight/00:00 in Paris)
* - Output: "2025-05-22T15:00:00.000Z" (Midnight/00:00 in Seoul)
*
* This ensures that times like midnight (00:00) or end of day (23:59)
* remain at those exact local times when converting between timezones.
* The output timestamp is based on the timezone in the user's profile settings.
*/
const _preserveLocalTime = useCallback(
(timestamp: string) => preserveLocalTime(timestamp, dayjs.tz.guess(), timeZone),
[timeZone]
);
return {
timeFormat,
timeZone,
preserveLocalTime: _preserveLocalTime,
};
}
@@ -18,10 +18,6 @@ export const meRouter = router({
return handler({ ctx });
}),
get,
getUserTimePreferences: authedProcedure.query(async ({ ctx }) => {
const handler = (await import("./getUserTimePreferences.handler")).getUserTimePreferencesHandler;
return handler({ ctx });
}),
getUserTopBanners: authedProcedure.query(async ({ ctx }) => {
const handler = (await import("./getUserTopBanners.handler")).getUserTopBannersHandler;
return handler({ ctx });
@@ -1,19 +0,0 @@
import type { Session } from "next-auth";
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
type GetUserTimePreferencesOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
session: Session;
};
};
export const getUserTimePreferencesHandler = async ({ ctx }: GetUserTimePreferencesOptions) => {
const { user } = ctx;
return {
timeFormat: user.timeFormat ?? 12,
timeZone: user.timeZone,
};
};