fix: provide timeZone to /insights from the server side (#21580)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
+1
-1
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user