From ad973002255f7720e6a78726f46e65112e99d7a4 Mon Sep 17 00:00:00 2001 From: batmnkh2344 Date: Mon, 22 Sep 2025 11:14:05 +0800 Subject: [PATCH 1/3] chore: pass timezone through headers --- .../src/utils/apollo/utils.ts | 8 ++++--- .../src/utils/headers/index.ts | 5 +++-- .../src/utils/headers/timezone.ts | 5 +++++ .../cycle/graphql/resolvers/queries/cycle.ts | 8 +++---- .../operation_api/src/modules/cycle/utils.ts | 21 +++++++++++-------- .../src/modules/project/utils/charUtils.ts | 4 +++- .../providers/apollo-provider/apolloClient.ts | 1 + 7 files changed, 33 insertions(+), 19 deletions(-) create mode 100644 backend/erxes-api-shared/src/utils/headers/timezone.ts diff --git a/backend/erxes-api-shared/src/utils/apollo/utils.ts b/backend/erxes-api-shared/src/utils/apollo/utils.ts index eacf427f53..e8eff23be9 100644 --- a/backend/erxes-api-shared/src/utils/apollo/utils.ts +++ b/backend/erxes-api-shared/src/utils/apollo/utils.ts @@ -1,9 +1,9 @@ -import { IMainContext } from '../../core-types'; -import { extractUserFromHeader } from '../headers'; -import { getSubdomain } from '../utils'; import { ExpressContextFunctionArgument } from '@apollo/server/dist/esm/express4'; import { Request as ApiRequest, Response as ApiResponse } from 'express'; import { nanoid } from 'nanoid'; +import { IMainContext } from '../../core-types'; +import { extractUserFromHeader, getTimezone } from '../headers'; +import { getSubdomain } from '../utils'; export const generateApolloContext = ( @@ -22,6 +22,7 @@ export const generateApolloContext = return {}; } const user: any = extractUserFromHeader(req.headers); + const timezone: any = getTimezone(req.headers); const subdomain = getSubdomain(req); @@ -39,6 +40,7 @@ export const generateApolloContext = requestInfo: { secure: req.secure, cookies: req.cookies, + timezone, }, }; diff --git a/backend/erxes-api-shared/src/utils/headers/index.ts b/backend/erxes-api-shared/src/utils/headers/index.ts index 4df33a412c..849a32a36a 100644 --- a/backend/erxes-api-shared/src/utils/headers/index.ts +++ b/backend/erxes-api-shared/src/utils/headers/index.ts @@ -1,4 +1,5 @@ -export * from './sanitize'; -export * from './user'; export * from './get-hostname'; +export * from './sanitize'; export * from './subdomain'; +export * from './timezone'; +export * from './user'; diff --git a/backend/erxes-api-shared/src/utils/headers/timezone.ts b/backend/erxes-api-shared/src/utils/headers/timezone.ts new file mode 100644 index 0000000000..6a6b1f8ea7 --- /dev/null +++ b/backend/erxes-api-shared/src/utils/headers/timezone.ts @@ -0,0 +1,5 @@ +import { IncomingHttpHeaders } from 'http'; + +export const getTimezone = (req: IncomingHttpHeaders) => { + return req['x-timezone'] || req['timezone'] || 'UTC'; +}; diff --git a/backend/plugins/operation_api/src/modules/cycle/graphql/resolvers/queries/cycle.ts b/backend/plugins/operation_api/src/modules/cycle/graphql/resolvers/queries/cycle.ts index d081feea19..7c4378bd1c 100644 --- a/backend/plugins/operation_api/src/modules/cycle/graphql/resolvers/queries/cycle.ts +++ b/backend/plugins/operation_api/src/modules/cycle/graphql/resolvers/queries/cycle.ts @@ -1,7 +1,7 @@ -import { cursorPaginate } from 'erxes-api-shared/utils'; -import { IContext } from '~/connectionResolvers'; import { ICycleDocument } from '@/cycle/types'; import { STATUS_TYPES } from '@/status/constants/types'; +import { cursorPaginate } from 'erxes-api-shared/utils'; +import { IContext } from '~/connectionResolvers'; import { getCycleProgressByMember, @@ -80,9 +80,9 @@ export const cycleQueries = { getCycleProgressChart: async ( _parent: undefined, { _id, assigneeId }, - { models }: IContext, + { models, requestInfo: { timezone } }: IContext, ) => { - return getCycleProgressChart(_id, assigneeId, models); + return getCycleProgressChart(_id, assigneeId, models, timezone); }, getCycleProgressByMember: async ( diff --git a/backend/plugins/operation_api/src/modules/cycle/utils.ts b/backend/plugins/operation_api/src/modules/cycle/utils.ts index f68a15c996..e091974294 100644 --- a/backend/plugins/operation_api/src/modules/cycle/utils.ts +++ b/backend/plugins/operation_api/src/modules/cycle/utils.ts @@ -1,6 +1,7 @@ import { fillMissingDays } from '@/project/utils/charUtils'; import { STATUS_TYPES } from '@/status/constants/types'; -import { differenceInCalendarDays, startOfDay } from 'date-fns'; +import { differenceInCalendarDays } from 'date-fns'; +import { toZonedTime } from 'date-fns-tz'; import { Types } from 'mongoose'; import { IModels } from '~/connectionResolvers'; @@ -115,6 +116,7 @@ export const getCycleProgressChart = async ( cycleId: string, assigneeId: string | undefined, models: IModels, + timezone: string, ) => { const filter: { cycleId: Types.ObjectId; assigneeId?: string } = { cycleId: new Types.ObjectId(cycleId), @@ -177,10 +179,10 @@ export const getCycleProgressChart = async ( { $addFields: { dayDate: { - $dateFromParts: { - year: { $year: '$statusChangedDate' }, - month: { $month: '$statusChangedDate' }, - day: { $dayOfMonth: '$statusChangedDate' }, + $dateToString: { + format: '%Y-%m-%d', + date: '$statusChangedDate', + timezone, }, }, isStarted: { $eq: ['$statusType', STATUS_TYPES.STARTED] }, @@ -210,7 +212,7 @@ export const getCycleProgressChart = async ( { $project: { _id: 0, - date: { $dateToString: { format: '%Y-%m-%d', date: '$_id' } }, + date: '$_id', started: 1, completed: 1, }, @@ -238,15 +240,16 @@ export const getCycleProgressChart = async ( chartData: [], }; - const start = startOfDay(new Date(cycle.startDate)); - const end = startOfDay(new Date(cycle.endDate)); + const start = toZonedTime(cycle.startDate, timezone); + const end = toZonedTime(cycle.endDate, timezone); const days = differenceInCalendarDays(end, start) + 1; chartData.chartData = fillMissingDays( chartDataAggregation, - cycle.startDate, + start, days, + timezone, ); return chartData; diff --git a/backend/plugins/operation_api/src/modules/project/utils/charUtils.ts b/backend/plugins/operation_api/src/modules/project/utils/charUtils.ts index a47296212f..c9517429f0 100644 --- a/backend/plugins/operation_api/src/modules/project/utils/charUtils.ts +++ b/backend/plugins/operation_api/src/modules/project/utils/charUtils.ts @@ -1,4 +1,5 @@ import { addDays, differenceInCalendarDays, format } from 'date-fns'; +import { toZonedTime } from 'date-fns-tz'; export const fillUntilTargetDate = ( data: { date: string; started: number; completed: number }[], @@ -26,6 +27,7 @@ export const fillMissingDays = ( data: { date: string; started: number; completed: number }[], baseDate: Date, totalDays = 7, + timezone: string, ) => { const filledData: { date: string; started: number; completed: number }[] = []; @@ -33,7 +35,7 @@ export const fillMissingDays = ( for (let i = 0; i < totalDays; i++) { const date = addDays(baseDate, i); - const key = format(date, 'yyyy-MM-dd'); + const key = format(toZonedTime(date, timezone), 'yyyy-MM-dd'); const item = mapDateToData.get(key); if (item) { diff --git a/frontend/core-ui/src/providers/apollo-provider/apolloClient.ts b/frontend/core-ui/src/providers/apollo-provider/apolloClient.ts index f4410484ad..8e51df3716 100644 --- a/frontend/core-ui/src/providers/apollo-provider/apolloClient.ts +++ b/frontend/core-ui/src/providers/apollo-provider/apolloClient.ts @@ -35,6 +35,7 @@ const authLink = setContext((_, { headers }) => { headers: { ...headers, sessioncode: sessionStorage.getItem('sessioncode') || '', + 'x-timezone': Intl.DateTimeFormat().resolvedOptions().timeZone, }, }; }); From 1edb77a8b0bd1977495601d1734dae97cf0aae8e Mon Sep 17 00:00:00 2001 From: batmnkh2344 Date: Mon, 22 Sep 2025 13:10:00 +0800 Subject: [PATCH 2/3] chore: fill line active dot --- .../src/modules/cycle/components/detail/CycleProgressChart.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/plugins/operation_ui/src/modules/cycle/components/detail/CycleProgressChart.tsx b/frontend/plugins/operation_ui/src/modules/cycle/components/detail/CycleProgressChart.tsx index df0e16de32..c00d45982b 100644 --- a/frontend/plugins/operation_ui/src/modules/cycle/components/detail/CycleProgressChart.tsx +++ b/frontend/plugins/operation_ui/src/modules/cycle/components/detail/CycleProgressChart.tsx @@ -107,6 +107,7 @@ export const CycleProgressChart = ({ dot={false} connectNulls={true} strokeLinecap="round" + activeDot={{ fill: STATUS_COLORS.started, r: 4 }} /> From b1a61902e0ae5eda4f3416ca742a29ab7b1a0a11 Mon Sep 17 00:00:00 2001 From: batmnkh2344 Date: Mon, 22 Sep 2025 13:44:30 +0800 Subject: [PATCH 3/3] chore: review changes --- backend/erxes-api-shared/src/utils/apollo/utils.ts | 2 +- backend/erxes-api-shared/src/utils/headers/timezone.ts | 10 ++++++++-- .../plugins/operation_api/src/modules/cycle/utils.ts | 7 +------ .../src/modules/project/utils/charUtils.ts | 4 +--- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/backend/erxes-api-shared/src/utils/apollo/utils.ts b/backend/erxes-api-shared/src/utils/apollo/utils.ts index e8eff23be9..24ab9432c0 100644 --- a/backend/erxes-api-shared/src/utils/apollo/utils.ts +++ b/backend/erxes-api-shared/src/utils/apollo/utils.ts @@ -22,7 +22,7 @@ export const generateApolloContext = return {}; } const user: any = extractUserFromHeader(req.headers); - const timezone: any = getTimezone(req.headers); + const timezone: string = getTimezone(req.headers); const subdomain = getSubdomain(req); diff --git a/backend/erxes-api-shared/src/utils/headers/timezone.ts b/backend/erxes-api-shared/src/utils/headers/timezone.ts index 6a6b1f8ea7..8adff327a7 100644 --- a/backend/erxes-api-shared/src/utils/headers/timezone.ts +++ b/backend/erxes-api-shared/src/utils/headers/timezone.ts @@ -1,5 +1,11 @@ import { IncomingHttpHeaders } from 'http'; -export const getTimezone = (req: IncomingHttpHeaders) => { - return req['x-timezone'] || req['timezone'] || 'UTC'; +export const getTimezone = (req: IncomingHttpHeaders): string => { + const timezone = req['x-timezone'] || req['timezone'] || 'UTC'; + + if (Array.isArray(timezone)) { + return timezone[0]; + } + + return timezone; }; diff --git a/backend/plugins/operation_api/src/modules/cycle/utils.ts b/backend/plugins/operation_api/src/modules/cycle/utils.ts index e091974294..9b82684bf3 100644 --- a/backend/plugins/operation_api/src/modules/cycle/utils.ts +++ b/backend/plugins/operation_api/src/modules/cycle/utils.ts @@ -245,12 +245,7 @@ export const getCycleProgressChart = async ( const days = differenceInCalendarDays(end, start) + 1; - chartData.chartData = fillMissingDays( - chartDataAggregation, - start, - days, - timezone, - ); + chartData.chartData = fillMissingDays(chartDataAggregation, start, days); return chartData; }; diff --git a/backend/plugins/operation_api/src/modules/project/utils/charUtils.ts b/backend/plugins/operation_api/src/modules/project/utils/charUtils.ts index c9517429f0..a47296212f 100644 --- a/backend/plugins/operation_api/src/modules/project/utils/charUtils.ts +++ b/backend/plugins/operation_api/src/modules/project/utils/charUtils.ts @@ -1,5 +1,4 @@ import { addDays, differenceInCalendarDays, format } from 'date-fns'; -import { toZonedTime } from 'date-fns-tz'; export const fillUntilTargetDate = ( data: { date: string; started: number; completed: number }[], @@ -27,7 +26,6 @@ export const fillMissingDays = ( data: { date: string; started: number; completed: number }[], baseDate: Date, totalDays = 7, - timezone: string, ) => { const filledData: { date: string; started: number; completed: number }[] = []; @@ -35,7 +33,7 @@ export const fillMissingDays = ( for (let i = 0; i < totalDays; i++) { const date = addDays(baseDate, i); - const key = format(toZonedTime(date, timezone), 'yyyy-MM-dd'); + const key = format(date, 'yyyy-MM-dd'); const item = mapDateToData.get(key); if (item) {