Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 22 additions & 14 deletions backend/plugins/operation_api/src/modules/cycle/utils.ts
Original file line number Diff line number Diff line change
@@ -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';
Comment on lines +3 to +4
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix Moment Timezone import to avoid compilation failure

moment-timezone doesn’t expose tz as a named export, so the build breaks (TypeScript reports “Module 'moment-timezone' has no exported member 'tz'”). Switch to the default moment import and call moment.tz(...) instead on Line 253 and Line 254.

Apply this diff:

-import { sendTRPCMessage } from 'erxes-api-shared/utils';
-import { tz } from 'moment-timezone';
+import { sendTRPCMessage } from 'erxes-api-shared/utils';
+import moment from 'moment-timezone';
@@
-  const start = tz(new Date(cycle.startDate), timezone);
-  const end = tz(new Date(cycle.endDate), timezone);
+  const start = moment.tz(new Date(cycle.startDate), timezone);
+  const end = moment.tz(new Date(cycle.endDate), timezone);

Also applies to: 253-259

🤖 Prompt for AI Agents
In backend/plugins/operation_api/src/modules/cycle/utils.ts around lines 3-4 and
253-259, the file currently imports tz as a named export from moment-timezone
which TypeScript rejects; replace that import with the default moment import
(e.g., import moment from 'moment-timezone') and update all occurrences where
tz(...) is used on lines 253-259 to call moment.tz(...), ensuring any type
annotations/import adjustments are updated accordingly.

import { Types } from 'mongoose';
import { IModels } from '~/connectionResolvers';

Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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] },
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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;
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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 }[],
Expand All @@ -24,16 +25,16 @@ export const fillUntilTargetDate = (

export const fillMissingDays = (
data: { date: string; started: number; completed: number }[],
baseDate: Date,
baseDate: Moment,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify all callers of fillMissingDays have been updated.

The function signature changed from baseDate: Date to baseDate: Moment, which is a breaking change. Ensure that all call sites have been updated to pass a Moment instance instead of a Date object.

Run the following script to find all usages of fillMissingDays:


🏁 Script executed:

#!/bin/bash
# Description: Find all usages of fillMissingDays to verify they pass Moment instances

# Search for function calls with context
rg -n -C5 'fillMissingDays\s*\(' --type=ts

Length of output: 6253


Convert baseDate/startDate to Moment in fillMissingDays calls
In project/graphql/resolvers/queries/project.ts (e.g., lines 600, 614, 642, 661), you’re still passing raw Date objects (baseDate/startDate) into fillMissingDays(baseDate: Moment, …). Wrap those dates with moment() or tz() so they match the updated signature.

🤖 Prompt for AI Agents
In backend/plugins/operation_api/src/modules/project/utils/charUtils.ts around
line 28: the helper signature now expects Moment for baseDate but callers in
project/graphql/resolvers/queries/project.ts (around lines 600, 614, 642, 661)
still pass raw Date objects; update those calls to wrap the Date values with
moment() (or moment.tz(...) if timezone context is required) so you pass a
Moment instance into fillMissingDays; ensure any existing timezone handling is
preserved when converting.

totalDays = 7,
) => {
const filledData: { date: string; started: number; completed: number }[] = [];

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) {
Expand Down
43 changes: 29 additions & 14 deletions backend/plugins/operation_api/src/worker/dailyCheckCycles.ts
Original file line number Diff line number Diff line change
@@ -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';
Comment on lines +5 to 9
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Import moment-timezone correctly in the worker

moment-timezone lacks a named tz export, so this file won’t compile. Use the default moment import and call moment.tz(...) on Line 48. Update downstream references accordingly (Lines 56-57).

Apply this diff:

-import {
-  getEnv,
-  getSaasOrganizations,
-  sendTRPCMessage,
-  sendWorkerQueue,
-} from 'erxes-api-shared/utils';
-import { tz } from 'moment-timezone';
+import {
+  getEnv,
+  getSaasOrganizations,
+  sendTRPCMessage,
+  sendWorkerQueue,
+} from 'erxes-api-shared/utils';
+import moment from 'moment-timezone';
@@
-  const tzToday = tz(new Date(), timezone);
+  const tzToday = moment.tz(new Date(), timezone);
@@
-  const utcStart = tzToday.startOf('day').toDate();
-  const utcEnd = tzToday.endOf('day').toDate();
+  const utcStart = tzToday.clone().startOf('day').toDate();
+  const utcEnd = tzToday.clone().endOf('day').toDate();

Also applies to: 48-58

🤖 Prompt for AI Agents
In backend/plugins/operation_api/src/worker/dailyCheckCycles.ts around lines 5
to 9 and 48 to 58, the file imports tz as a named export from moment-timezone
which does not exist; replace the named import with a default moment import
(import moment from 'moment-timezone') and update code that calls tz(...) to use
moment.tz(...), adjusting any downstream variables or references on lines 56-57
to use the values returned by moment.tz(...) accordingly.


export const dailyCheckCycles = async () => {
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion backend/plugins/operation_api/src/worker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const initMQWorkers = async (redis: any) => {
await myQueue.upsertJobScheduler(
'operations-daily-cycles-check',
{
pattern: '0 0 * * *',
pattern: '0 * * * *',
tz: 'UTC',
Comment on lines +13 to 14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify the worker has timezone-aware guards to prevent excessive executions.

The cron schedule changed from daily at midnight (0 0 * * *) to hourly (0 * * * *), a 24x frequency increase. Per the PR objectives, the worker should "run only at midnight in the configured timezone." Ensure the dailyCheckCycles worker implementation includes proper guards to execute only once per day per organization based on their configured timezone, preventing unnecessary executions during the other 23 hours.

Run the following script to examine the worker implementation:


🏁 Script executed:

#!/bin/bash
# Description: Verify dailyCheckCycles has timezone-aware execution guards

# Find the dailyCheckCycles implementation
ast-grep --pattern $'export $_ dailyCheckCycles = $_'

# Search for timezone checking logic
rg -n -C10 'dailyCheckCycles' backend/plugins/operation_api/src/worker/dailyCheckCycles.ts

Length of output: 649


Add timezone-based execution guard in dailyCheckCycles
dailyCheckCycles is now scheduled hourly but currently enqueues jobs for every org each run and never uses the imported tz. Refactor to use moment-timezone to check tz(Date.now(), org.timezone).hour() === 0 (and minute === 0) so jobs only enqueue at local midnight.
backend/plugins/operation_api/src/worker/dailyCheckCycles.ts:14–21

},
{
Expand Down
14 changes: 7 additions & 7 deletions frontend/core-ui/src/modules/app/components/SettingsRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -123,10 +123,10 @@ export function SettingsRoutes() {
path={SettingsWorkspacePath.MailConfig}
element={<SettingsMailConfig />}
/>
{/* <Route
<Route
path={SettingsWorkspacePath.General}
element={<GeneralSettings />}
/> */}
/>
<Route
path={SettingsWorkspacePath.TeamMember}
element={<TeamMemberSettings />}
Expand Down
11 changes: 6 additions & 5 deletions frontend/core-ui/src/modules/settings/constants/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
TSettingPath,
} from '@/types/paths/SettingsPath';
import {
IconAdjustmentsAlt,
IconChessKnight,
IconFile,
IconMail,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -23,19 +24,22 @@ const GeneralSettings = () => {
const { configs, updateConfig, loading, isLoading } = useConfig();

const updateCurrency = (data: TGeneralSettingsProps) => {
const updatedConfigs = configs.reduce(
(acc: Record<string, any>, 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<string, any>, config: TConfig) => {
acc[config.code] = config.value;
return acc;
},
{} as Record<string, any>,
);
}, {} as Record<string, any>),
// override/add with new data
...data,
};

updateConfig(updatedConfigs);
};

const submitHandler: SubmitHandler<TGeneralSettingsProps> = (data) => {
updateCurrency(data);

handleLanguage(data.languageCode).then(() => {
toast({
title: 'Updated successfully',
Expand All @@ -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);
}
Comment on lines +60 to 66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Resolve ESLint failure when hydrating TIMEZONE

Linting fails on Line 65 (@typescript-eslint/no-unused-expressions). Replace the short-circuit expression with an explicit conditional.

Apply this diff:

-      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);
+      const timezone = configs?.find((data: any) => data.code === 'TIMEZONE');
+
+      methods.setValue('dealCurrency', currencies?.value);
+      methods.setValue('mainCurrency', mainCurrency?.value);
+
+      if (timezone) {
+        methods.setValue('TIMEZONE', timezone.value);
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
}
const timezone = configs?.find((data: any) => data.code === 'TIMEZONE');
methods.setValue('dealCurrency', currencies?.value);
methods.setValue('mainCurrency', mainCurrency?.value);
if (timezone) {
methods.setValue('TIMEZONE', timezone.value);
}
}
🧰 Tools
🪛 ESLint

[error] 65-65: Expected an assignment or function call and instead saw an expression.

(@typescript-eslint/no-unused-expressions)

🤖 Prompt for AI Agents
In frontend/core-ui/src/modules/settings/general/components/GeneralSettings.tsx
around lines 60 to 66, the short-circuit expression "timezone &&
methods.setValue('TIMEZONE', timezone?.value)" triggers an ESLint
no-unused-expressions error; replace it with an explicit conditional that checks
for timezone and then calls methods.setValue with timezone.value (i.e., use an
if (timezone) { methods.setValue('TIMEZONE', timezone.value); } block) to
eliminate the unused-expression pattern and ensure proper typing.

}, [configs, methods]);

Expand All @@ -78,6 +87,7 @@ const GeneralSettings = () => {
/>
<SelectMainCurrency />
<SelectCurrency />
<SelectTimezone />
<Button disabled={isLoading} type="submit" className="w-1/4 ml-auto">
{isLoading ? (
<Spinner className="stroke-white/90 w-4 h-4" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Form, TimezoneSelect } from 'erxes-ui';
import { useFormContext } from 'react-hook-form';
import { TGeneralSettingsProps } from '../types';

export function SelectTimezone() {
const form = useFormContext<TGeneralSettingsProps>();

return (
<Form.Field
control={form.control}
name="TIMEZONE"
render={({ field }) => (
<Form.Item>
<Form.Label>Timezone</Form.Label>
<Form.Control>
<TimezoneSelect
value={field.value}
onValueChange={field.onChange}
/>
</Form.Control>
</Form.Item>
)}
/>
);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useForm } from 'react-hook-form';
import { TGeneralSettingsProps } from '../types';
import { zodResolver } from '@hookform/resolvers/zod';
import { generalSettingsSchema } from '../schema';
import { detectTimeZone } from 'erxes-ui/utils/localization/detectTimeZone';
import { useForm } from 'react-hook-form';
import { AvailableLanguage, useSwitchLanguage } from '~/i18n';
import { generalSettingsSchema } from '../schema';
import { TGeneralSettingsProps } from '../types';

const useGeneralSettingsForms = () => {
const { currentLanguage, switchLanguage } = useSwitchLanguage();
Expand All @@ -14,6 +15,7 @@ const useGeneralSettingsForms = () => {
CHECK_TEAM_MEMBER_SHOWN: false,
BRANCHES_MASTER_TEAM_MEMBERS_IDS: [],
DEPARTMENTS_MASTER_TEAM_MEMBERS_IDS: [],
TIMEZONE: detectTimeZone(),
},
resolver: zodResolver(generalSettingsSchema),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const generalSettingsSchema = z.object({
CHECK_TEAM_MEMBER_SHOWN: z.boolean(),
BRANCHES_MASTER_TEAM_MEMBERS_IDS: z.string().array(),
DEPARTMENTS_MASTER_TEAM_MEMBERS_IDS: z.string().array(),
TIMEZONE: z.string(),
});

export { generalSettingsSchema };
2 changes: 1 addition & 1 deletion frontend/core-ui/src/modules/types/paths/SettingsPath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export enum SettingsPath {
}

export enum SettingsWorkspacePath {
// General = 'general',
General = 'general',
FileUpload = 'file-upload',
MailConfig = 'mail-config',
Apps = 'apps',
Expand Down
Loading
Loading