diff --git a/backend/plugins/operation_api/src/modules/cycle/utils.ts b/backend/plugins/operation_api/src/modules/cycle/utils.ts index f68a15c996..906eb75044 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 { sendTRPCMessage } from 'erxes-api-shared/utils'; +import { tz } from 'moment-timezone'; import { Types } from 'mongoose'; import { IModels } from '~/connectionResolvers'; @@ -130,6 +131,17 @@ export const getCycleProgressChart = async ( return []; } + const timezone = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'configs', + action: 'getConfig', + input: { + code: 'TIMEZONE', + }, + defaultValue: 'UTC', + }); + const [totalScopeResult] = await models.Task.aggregate([ { $match: { ...filter }, @@ -177,10 +189,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 +222,7 @@ export const getCycleProgressChart = async ( { $project: { _id: 0, - date: { $dateToString: { format: '%Y-%m-%d', date: '$_id' } }, + date: '$_id', started: 1, completed: 1, }, @@ -238,16 +250,12 @@ export const getCycleProgressChart = async ( chartData: [], }; - const start = startOfDay(new Date(cycle.startDate)); - const end = startOfDay(new Date(cycle.endDate)); + const start = tz(new Date(cycle.startDate), timezone); + const end = tz(new Date(cycle.endDate), timezone); - const days = differenceInCalendarDays(end, start) + 1; + const days = end.diff(start, 'days') + 1; - chartData.chartData = fillMissingDays( - chartDataAggregation, - cycle.startDate, - days, - ); + 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 a47296212f..0f1b1da1ab 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 { Moment } from 'moment-timezone'; export const fillUntilTargetDate = ( data: { date: string; started: number; completed: number }[], @@ -24,7 +25,7 @@ export const fillUntilTargetDate = ( export const fillMissingDays = ( data: { date: string; started: number; completed: number }[], - baseDate: Date, + baseDate: Moment, totalDays = 7, ) => { const filledData: { date: string; started: number; completed: number }[] = []; @@ -32,8 +33,8 @@ export const fillMissingDays = ( const mapDateToData = new Map(data.map((item) => [item.date, item])); for (let i = 0; i < totalDays; i++) { - const date = addDays(baseDate, i); - const key = format(date, 'yyyy-MM-dd'); + const date = baseDate.clone().add(i, 'days'); + const key = date.format('YYYY-MM-DD'); const item = mapDateToData.get(key); if (item) { diff --git a/backend/plugins/operation_api/src/worker/dailyCheckCycles.ts b/backend/plugins/operation_api/src/worker/dailyCheckCycles.ts index a0eaaa8d09..ffb0da47c2 100644 --- a/backend/plugins/operation_api/src/worker/dailyCheckCycles.ts +++ b/backend/plugins/operation_api/src/worker/dailyCheckCycles.ts @@ -1,10 +1,11 @@ import { Job } from 'bullmq'; -import { endOfDay } from 'date-fns'; // эсвэл өөр utility import { getEnv, getSaasOrganizations, + sendTRPCMessage, sendWorkerQueue, } from 'erxes-api-shared/utils'; +import { tz } from 'moment-timezone'; import { generateModels } from '~/connectionResolvers'; export const dailyCheckCycles = async () => { @@ -14,35 +15,51 @@ export const dailyCheckCycles = async () => { const orgs = await getSaasOrganizations(); for (const org of orgs) { - if (org.enabledcycles) { - sendWorkerQueue('operations', 'checkCycle').add('checkCycle', { - subdomain: org.subdomain, - }); - } + sendWorkerQueue('operations', 'checkCycle').add('checkCycle', { + subdomain: org.subdomain, + timezone: org.timezone, + }); } return 'success'; } else { + const timezone = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'configs', + action: 'getConfig', + input: { + code: 'TIMEZONE', + }, + defaultValue: 'UTC', + }); + sendWorkerQueue('operations', 'checkCycle').add('checkCycle', { subdomain: 'os', + timezone, }); return 'success'; } }; export const checkCycle = async (job: Job) => { - const { subdomain } = job?.data ?? {}; + const { subdomain, timezone = 'UTC' } = job?.data ?? {}; + + const tzToday = tz(new Date(), timezone); + + if (tzToday.hour() !== 0) { + return; + } const models = await generateModels(subdomain); - const today = new Date(); + const utcStart = tzToday.startOf('day').toDate(); + const utcEnd = tzToday.endOf('day').toDate(); const endCycles = await models.Cycle.find({ isActive: true, isCompleted: false, - endDate: { - $lte: endOfDay(today), - }, + endDate: { $gte: utcStart, $lte: utcEnd }, }); if (endCycles?.length) { @@ -54,9 +71,7 @@ export const checkCycle = async (job: Job) => { const startCycles = await models.Cycle.find({ isActive: false, isCompleted: false, - startDate: { - $lte: endOfDay(today), - }, + startDate: { $gte: utcStart, $lte: utcEnd }, }); if (startCycles?.length) { diff --git a/backend/plugins/operation_api/src/worker/index.ts b/backend/plugins/operation_api/src/worker/index.ts index 239fa85ffc..e426175130 100644 --- a/backend/plugins/operation_api/src/worker/index.ts +++ b/backend/plugins/operation_api/src/worker/index.ts @@ -10,7 +10,7 @@ export const initMQWorkers = async (redis: any) => { await myQueue.upsertJobScheduler( 'operations-daily-cycles-check', { - pattern: '0 0 * * *', + pattern: '0 * * * *', tz: 'UTC', }, { diff --git a/frontend/core-ui/src/modules/app/components/SettingsRoutes.tsx b/frontend/core-ui/src/modules/app/components/SettingsRoutes.tsx index 5bcb8d7d95..995f3e5a69 100644 --- a/frontend/core-ui/src/modules/app/components/SettingsRoutes.tsx +++ b/frontend/core-ui/src/modules/app/components/SettingsRoutes.tsx @@ -32,11 +32,11 @@ const SettingsMailConfig = lazy(() => default: module.MailConfigPage, })), ); -// const GeneralSettings = lazy(() => -// import('~/pages/settings/workspace/GeneralSettingsPage').then((module) => ({ -// default: module.GeneralSettingsPage, -// })), -// ); +const GeneralSettings = lazy(() => + import('~/pages/settings/workspace/GeneralSettingsPage').then((module) => ({ + default: module.GeneralSettingsPage, + })), +); const TeamMemberSettings = lazy(() => import('~/pages/settings/workspace/TeamMemberPage').then((module) => ({ default: module.TeamMemberPage, @@ -123,10 +123,10 @@ export function SettingsRoutes() { path={SettingsWorkspacePath.MailConfig} element={} /> - {/* } - /> */} + /> } diff --git a/frontend/core-ui/src/modules/settings/constants/data.ts b/frontend/core-ui/src/modules/settings/constants/data.ts index fa0ca57122..0967362026 100644 --- a/frontend/core-ui/src/modules/settings/constants/data.ts +++ b/frontend/core-ui/src/modules/settings/constants/data.ts @@ -4,6 +4,7 @@ import { TSettingPath, } from '@/types/paths/SettingsPath'; import { + IconAdjustmentsAlt, IconChessKnight, IconFile, IconMail, @@ -120,6 +121,11 @@ export const SETTINGS_PATH_DATA: { [key: string]: TSettingPath[] } = { // }, ], nav: [ + { + name: 'General', + icon: IconAdjustmentsAlt, + path: SettingsWorkspacePath.General, + }, { name: 'Team member', icon: IconUsersGroup, @@ -130,11 +136,6 @@ export const SETTINGS_PATH_DATA: { [key: string]: TSettingPath[] } = { icon: IconUserCog, path: SettingsWorkspacePath.Permissions, }, - // { - // name: 'General', - // icon: IconAdjustmentsAlt, - // path: SettingsWorkspacePath.General, - // }, { name: 'File upload', icon: IconFile, diff --git a/frontend/core-ui/src/modules/settings/general/components/GeneralSettings.tsx b/frontend/core-ui/src/modules/settings/general/components/GeneralSettings.tsx index 3390db129b..adbc100e49 100644 --- a/frontend/core-ui/src/modules/settings/general/components/GeneralSettings.tsx +++ b/frontend/core-ui/src/modules/settings/general/components/GeneralSettings.tsx @@ -1,16 +1,17 @@ -import { useEffect } from 'react'; -import { useSwitchLanguage } from '~/i18n'; -import { SubmitHandler } from 'react-hook-form'; -import { Button, Form, Spinner, useToast } from 'erxes-ui'; import { useConfig } from '@/settings/file-upload/hook/useConfigs'; -import { SelectCurrency } from '@/settings/general/components/SelectCurrency'; -import { TGeneralSettingsProps } from '@/settings/general/types'; import { TConfig } from '@/settings/file-upload/types'; import { GeneralSettingsSkeleton } from '@/settings/general/components/GeneralSettingsSkeleton'; -import { useGeneralSettingsForms } from '@/settings/general/hooks/useGeneralSettingsForms'; import SelectControl from '@/settings/general/components/SelectControl'; -import { LANGUAGES } from '@/settings/general/constants/data'; +import { SelectCurrency } from '@/settings/general/components/SelectCurrency'; import { SelectMainCurrency } from '@/settings/general/components/SelectMainCurrency'; +import { SelectTimezone } from '@/settings/general/components/SelectTimezone'; +import { LANGUAGES } from '@/settings/general/constants/data'; +import { useGeneralSettingsForms } from '@/settings/general/hooks/useGeneralSettingsForms'; +import { TGeneralSettingsProps } from '@/settings/general/types'; +import { Button, Form, Spinner, useToast } from 'erxes-ui'; +import { useEffect } from 'react'; +import { SubmitHandler } from 'react-hook-form'; +import { useSwitchLanguage } from '~/i18n'; const GeneralSettings = () => { const { languages } = useSwitchLanguage(); @@ -23,19 +24,22 @@ const GeneralSettings = () => { const { configs, updateConfig, loading, isLoading } = useConfig(); const updateCurrency = (data: TGeneralSettingsProps) => { - const updatedConfigs = configs.reduce( - (acc: Record, config: TConfig) => { - const key = config.code as keyof TGeneralSettingsProps; - acc[config.code] = key in data ? data[key] : config.value; + const updatedConfigs = { + // start with all existing configs + ...configs.reduce((acc: Record, config: TConfig) => { + acc[config.code] = config.value; return acc; - }, - {} as Record, - ); + }, {} as Record), + // override/add with new data + ...data, + }; + updateConfig(updatedConfigs); }; const submitHandler: SubmitHandler = (data) => { updateCurrency(data); + handleLanguage(data.languageCode).then(() => { toast({ title: 'Updated successfully', @@ -52,8 +56,13 @@ const GeneralSettings = () => { const mainCurrency = configs?.find( (data: any) => data.code === 'mainCurrency', ); + + const timezone = configs?.find((data: any) => data.code === 'TIMEZONE'); + methods.setValue('dealCurrency', currencies?.value); methods.setValue('mainCurrency', mainCurrency?.value); + + timezone && methods.setValue('TIMEZONE', timezone?.value); } }, [configs, methods]); @@ -78,6 +87,7 @@ const GeneralSettings = () => { /> +