From 9a243ee60c5fbe3e1b824b7c2884a6e629a07206 Mon Sep 17 00:00:00 2001 From: batmnkh2344 Date: Thu, 19 Jun 2025 16:38:14 +0800 Subject: [PATCH 1/3] add operation plugin --- backend/plugins/operation_api/.gitignore | 4 ++ backend/plugins/operation_api/package.json | 18 +++++ backend/plugins/operation_api/project.json | 57 ++++++++++++++++ .../src/apollo/resolvers/index.ts | 16 +++++ .../src/apollo/resolvers/mutations.ts | 5 ++ .../src/apollo/resolvers/queries.ts | 5 ++ .../src/apollo/resolvers/resolvers.ts | 5 ++ .../operation_api/src/apollo/schema/schema.ts | 19 ++++++ .../operation_api/src/apollo/typeDefs.ts | 17 +++++ .../operation_api/src/connectionResolvers.ts | 28 ++++++++ backend/plugins/operation_api/src/main.ts | 32 +++++++++ .../src/modules/tasks/@types/tasks.ts | 11 ++++ .../src/modules/tasks/db/definitions/tasks.ts | 15 +++++ .../src/modules/tasks/db/models/Tasks.ts | 64 ++++++++++++++++++ .../resolvers/customResolvers/tasks.ts | 6 ++ .../graphql/resolvers/mutations/tasks.ts | 17 +++++ .../tasks/graphql/resolvers/queries/tasks.ts | 12 ++++ .../modules/tasks/graphql/schemas/tasks.ts | 18 +++++ .../operation_api/src/trpc/init-trpc.ts | 15 +++++ .../operation_api/src/trpc/trpcClients.ts | 20 ++++++ .../plugins/operation_api/tsconfig.build.json | 22 +++++++ backend/plugins/operation_api/tsconfig.json | 56 ++++++++++++++++ .../plugins/operation_ui/eslint.config.js | 11 ++++ frontend/plugins/operation_ui/jest.config.ts | 11 ++++ .../operation_ui/module-federation.config.ts | 34 ++++++++++ frontend/plugins/operation_ui/project.json | 65 +++++++++++++++++++ .../operation_ui/rspack.config.prod.ts | 1 + .../plugins/operation_ui/rspack.config.ts | 14 ++++ .../plugins/operation_ui/src/assets/README.md | 24 +++++++ .../operation_ui/src/assets/example-icon.svg | 5 ++ .../operation_ui/src/assets/example-image.svg | 5 ++ .../plugins/operation_ui/src/bootstrap.tsx | 11 ++++ frontend/plugins/operation_ui/src/config.ts | 19 ++++++ frontend/plugins/operation_ui/src/index.html | 14 ++++ frontend/plugins/operation_ui/src/main.ts | 1 + .../operation_ui/src/modules/tasks/Main.tsx | 20 ++++++ .../src/modules/tasks/Settings.tsx | 9 +++ .../src/pages/tasks/IndexPage.tsx | 49 ++++++++++++++ .../operation_ui/src/widgets/Widgets.tsx | 13 ++++ .../plugins/operation_ui/tsconfig.app.json | 28 ++++++++ frontend/plugins/operation_ui/tsconfig.json | 46 +++++++++++++ .../plugins/operation_ui/tsconfig.spec.json | 27 ++++++++ 42 files changed, 869 insertions(+) create mode 100644 backend/plugins/operation_api/.gitignore create mode 100644 backend/plugins/operation_api/package.json create mode 100644 backend/plugins/operation_api/project.json create mode 100644 backend/plugins/operation_api/src/apollo/resolvers/index.ts create mode 100644 backend/plugins/operation_api/src/apollo/resolvers/mutations.ts create mode 100644 backend/plugins/operation_api/src/apollo/resolvers/queries.ts create mode 100644 backend/plugins/operation_api/src/apollo/resolvers/resolvers.ts create mode 100644 backend/plugins/operation_api/src/apollo/schema/schema.ts create mode 100644 backend/plugins/operation_api/src/apollo/typeDefs.ts create mode 100644 backend/plugins/operation_api/src/connectionResolvers.ts create mode 100644 backend/plugins/operation_api/src/main.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/@types/tasks.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/db/definitions/tasks.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/db/models/Tasks.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/tasks.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/tasks.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/tasks.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/schemas/tasks.ts create mode 100644 backend/plugins/operation_api/src/trpc/init-trpc.ts create mode 100644 backend/plugins/operation_api/src/trpc/trpcClients.ts create mode 100644 backend/plugins/operation_api/tsconfig.build.json create mode 100644 backend/plugins/operation_api/tsconfig.json create mode 100644 frontend/plugins/operation_ui/eslint.config.js create mode 100644 frontend/plugins/operation_ui/jest.config.ts create mode 100644 frontend/plugins/operation_ui/module-federation.config.ts create mode 100644 frontend/plugins/operation_ui/project.json create mode 100644 frontend/plugins/operation_ui/rspack.config.prod.ts create mode 100644 frontend/plugins/operation_ui/rspack.config.ts create mode 100644 frontend/plugins/operation_ui/src/assets/README.md create mode 100644 frontend/plugins/operation_ui/src/assets/example-icon.svg create mode 100644 frontend/plugins/operation_ui/src/assets/example-image.svg create mode 100644 frontend/plugins/operation_ui/src/bootstrap.tsx create mode 100644 frontend/plugins/operation_ui/src/config.ts create mode 100644 frontend/plugins/operation_ui/src/index.html create mode 100644 frontend/plugins/operation_ui/src/main.ts create mode 100644 frontend/plugins/operation_ui/src/modules/tasks/Main.tsx create mode 100644 frontend/plugins/operation_ui/src/modules/tasks/Settings.tsx create mode 100644 frontend/plugins/operation_ui/src/pages/tasks/IndexPage.tsx create mode 100644 frontend/plugins/operation_ui/src/widgets/Widgets.tsx create mode 100644 frontend/plugins/operation_ui/tsconfig.app.json create mode 100644 frontend/plugins/operation_ui/tsconfig.json create mode 100644 frontend/plugins/operation_ui/tsconfig.spec.json diff --git a/backend/plugins/operation_api/.gitignore b/backend/plugins/operation_api/.gitignore new file mode 100644 index 0000000000..3b24e4a138 --- /dev/null +++ b/backend/plugins/operation_api/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +.env +*.log diff --git a/backend/plugins/operation_api/package.json b/backend/plugins/operation_api/package.json new file mode 100644 index 0000000000..8a95a3438c --- /dev/null +++ b/backend/plugins/operation_api/package.json @@ -0,0 +1,18 @@ +{ + "name": "operation_api", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "dev": "tsx watch src/main.ts", + "build": "tsc --project tsconfig.build.json && tsc-alias -p tsconfig.build.json", + "start": "node -r tsconfig-paths/register dist/src/main.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "erxes-api-shared": "workspace:^" + }, + "devDependencies": {} +} \ No newline at end of file diff --git a/backend/plugins/operation_api/project.json b/backend/plugins/operation_api/project.json new file mode 100644 index 0000000000..2c04221374 --- /dev/null +++ b/backend/plugins/operation_api/project.json @@ -0,0 +1,57 @@ +{ + "name": "operation_api", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "backend/plugins/operation_api/src", + "projectType": "application", + "tags": [], + "targets": { + "build": { + "executor": "nx:run-commands", + "cache": true, + "options": { + "cwd": "backend/plugins/operation_api", + "commands": [ + "pnpm build" + ] + }, + "dependsOn": [ + "^build", + "build:packageJson" + ] + }, + "build:packageJson": { + "executor": "@nx/js:tsc", + "options": { + "main": "backend/plugins/operation_api/dist/src/main.js", + "tsConfig": "backend/plugins/operation_api/tsconfig.build.json", + "outputPath": "backend/plugins/operation_api/dist", + "updateBuildableProjectDepsInPackageJson": true, + "buildableProjectDepsInPackageJsonType": "dependencies" + } + }, + "start": { + "executor": "nx:run-commands", + "dependsOn": [ + "typecheck", + "build" + ], + "options": { + "cwd": "backend/plugins/operation_api", + "command": "NODE_ENV=development node dist/src/main.js" + } + }, + "serve": { + "executor": "nx:run-commands", + "options": { + "cwd": "backend/plugins/operation_api", + "command": "NODE_ENV=development pnpm dev" + } + }, + "docker-build": { + "dependsOn": [ + "build" + ], + "command": "docker build -f backend/plugins/operation_api/Dockerfile . -t erxes/erxes-next-operation_api" + } + } +} \ No newline at end of file diff --git a/backend/plugins/operation_api/src/apollo/resolvers/index.ts b/backend/plugins/operation_api/src/apollo/resolvers/index.ts new file mode 100644 index 0000000000..d4b64cf3b6 --- /dev/null +++ b/backend/plugins/operation_api/src/apollo/resolvers/index.ts @@ -0,0 +1,16 @@ +import { apolloCustomScalars } from 'erxes-api-shared/utils'; +import { customResolvers } from './resolvers'; +import { mutations } from './mutations'; +import { queries } from './queries'; +const resolvers: any = { + Mutation: { + ...mutations, + }, + Query: { + ...queries, + }, + ...apolloCustomScalars, + ...customResolvers, +}; + +export default resolvers; diff --git a/backend/plugins/operation_api/src/apollo/resolvers/mutations.ts b/backend/plugins/operation_api/src/apollo/resolvers/mutations.ts new file mode 100644 index 0000000000..e4dfcbdbf7 --- /dev/null +++ b/backend/plugins/operation_api/src/apollo/resolvers/mutations.ts @@ -0,0 +1,5 @@ +import { tasksMutations } from '@/tasks/graphql/resolvers/mutations/tasks'; + +export const mutations = { + ...tasksMutations, +}; diff --git a/backend/plugins/operation_api/src/apollo/resolvers/queries.ts b/backend/plugins/operation_api/src/apollo/resolvers/queries.ts new file mode 100644 index 0000000000..ab0b21f8b3 --- /dev/null +++ b/backend/plugins/operation_api/src/apollo/resolvers/queries.ts @@ -0,0 +1,5 @@ +import { tasksQueries } from '@/tasks/graphql/resolvers/queries/tasks'; + +export const queries = { + ...tasksQueries, +}; diff --git a/backend/plugins/operation_api/src/apollo/resolvers/resolvers.ts b/backend/plugins/operation_api/src/apollo/resolvers/resolvers.ts new file mode 100644 index 0000000000..bc4e154235 --- /dev/null +++ b/backend/plugins/operation_api/src/apollo/resolvers/resolvers.ts @@ -0,0 +1,5 @@ +import { Tasks } from '@/tasks/graphql/resolvers/customResolvers/tasks'; + +export const customResolvers = { + Tasks, +}; diff --git a/backend/plugins/operation_api/src/apollo/schema/schema.ts b/backend/plugins/operation_api/src/apollo/schema/schema.ts new file mode 100644 index 0000000000..ef8055a52d --- /dev/null +++ b/backend/plugins/operation_api/src/apollo/schema/schema.ts @@ -0,0 +1,19 @@ +import { + mutations as TasksMutations, + queries as TasksQueries, + types as TasksTypes, +} from '@/tasks/graphql/schemas/tasks'; + +export const types = ` + ${TasksTypes} +`; + +export const queries = ` + ${TasksQueries} +`; + +export const mutations = ` + ${TasksMutations} +`; + +export default { types, queries, mutations }; diff --git a/backend/plugins/operation_api/src/apollo/typeDefs.ts b/backend/plugins/operation_api/src/apollo/typeDefs.ts new file mode 100644 index 0000000000..0b1ea5e35c --- /dev/null +++ b/backend/plugins/operation_api/src/apollo/typeDefs.ts @@ -0,0 +1,17 @@ +import { apolloCommonTypes } from 'erxes-api-shared/utils'; +import { DocumentNode } from 'graphql'; +import { gql } from 'graphql-tag'; +import { mutations, queries, types } from '~/apollo/schema/schema'; + +export const typeDefs = async (): Promise => { + return gql` + ${apolloCommonTypes} + ${types} + extend type Query { + ${queries} + } + extend type Mutation { + ${mutations} + } + `; +}; diff --git a/backend/plugins/operation_api/src/connectionResolvers.ts b/backend/plugins/operation_api/src/connectionResolvers.ts new file mode 100644 index 0000000000..3f10ea2bdd --- /dev/null +++ b/backend/plugins/operation_api/src/connectionResolvers.ts @@ -0,0 +1,28 @@ +import { createGenerateModels } from 'erxes-api-shared/utils'; +import { IMainContext } from 'erxes-api-shared/core-types'; +import { ITasksDocument } from '@/tasks/@types/tasks'; + +import mongoose from 'mongoose'; + +import { loadTasksClass, ITasksModel } from '@/tasks/db/models/Tasks'; + +export interface IModels { + Tasks: ITasksModel; +} + +export interface IContext extends IMainContext { + models: IModels; +} + +export const loadClasses = (db: mongoose.Connection): IModels => { + const models = {} as IModels; + + models.Tasks = db.model( + 'tasks', + loadTasksClass(models), + ); + + return models; +}; + +export const generateModels = createGenerateModels(loadClasses); diff --git a/backend/plugins/operation_api/src/main.ts b/backend/plugins/operation_api/src/main.ts new file mode 100644 index 0000000000..5480462a5d --- /dev/null +++ b/backend/plugins/operation_api/src/main.ts @@ -0,0 +1,32 @@ +import { startPlugin } from 'erxes-api-shared/utils'; +import { typeDefs } from '~/apollo/typeDefs'; +import { appRouter } from '~/trpc/init-trpc'; +import resolvers from './apollo/resolvers'; +import { generateModels } from './connectionResolvers'; + +startPlugin({ + name: 'operation', + port: 33010, + graphql: async () => ({ + typeDefs: await typeDefs(), + resolvers, + }), + apolloServerContext: async (subdomain, context) => { + const models = await generateModels(subdomain); + + context.models = models; + + return context; + }, + trpcAppRouter: { + router: appRouter, + createContext: async (subdomain, context) => { + const models = await generateModels(subdomain); + + context.models = models; + + return context; + }, + }, +}); + diff --git a/backend/plugins/operation_api/src/modules/tasks/@types/tasks.ts b/backend/plugins/operation_api/src/modules/tasks/@types/tasks.ts new file mode 100644 index 0000000000..d5b2ce741b --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/@types/tasks.ts @@ -0,0 +1,11 @@ +import { Document } from 'mongoose'; + +export interface ITasks { + name?: string; +} + +export interface ITasksDocument extends ITasks, Document { + _id: string; + createdAt: Date; + updatedAt: Date; +} diff --git a/backend/plugins/operation_api/src/modules/tasks/db/definitions/tasks.ts b/backend/plugins/operation_api/src/modules/tasks/db/definitions/tasks.ts new file mode 100644 index 0000000000..1b99fe44b4 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/db/definitions/tasks.ts @@ -0,0 +1,15 @@ +import { Schema } from 'mongoose'; + +import { mongooseStringRandomId, schemaWrapper } from 'erxes-api-shared/utils'; + +export const tasksSchema = schemaWrapper( + new Schema( + { + _id: mongooseStringRandomId, + name: { type: String, label: 'Name' }, + }, + { + timestamps: true, + }, + ), +); diff --git a/backend/plugins/operation_api/src/modules/tasks/db/models/Tasks.ts b/backend/plugins/operation_api/src/modules/tasks/db/models/Tasks.ts new file mode 100644 index 0000000000..dc144a9194 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/db/models/Tasks.ts @@ -0,0 +1,64 @@ +import { Model } from 'mongoose'; +import { IModels } from '~/connectionResolvers'; +import { tasksSchema } from '@/tasks/db/definitions/tasks'; +import { ITasks, ITasksDocument } from '@/tasks/@types/tasks'; + +export interface ITasksModel extends Model { + getTasks(_id: string): Promise; + getTaskss(): Promise; + createTasks(doc: ITasks): Promise; + updateTasks(_id: string, doc: ITasks): Promise; + removeTasks(TasksId: string): Promise<{ ok: number }>; +} + +export const loadTasksClass = (models: IModels) => { + class Tasks { + /** + * Retrieves operation + */ + public static async getTasks(_id: string) { + const Tasks = await models.Tasks.findOne({ _id }).lean(); + + if (!Tasks) { + throw new Error('Tasks not found'); + } + + return Tasks; + } + + /** + * Retrieves all operations + */ + public static async getTaskss(): Promise { + return models.Tasks.find().lean(); + } + + /** + * Create a operation + */ + public static async createTasks(doc: ITasks): Promise { + return models.Tasks.create(doc); + } + + /* + * Update operation + */ + public static async updateTasks(_id: string, doc: ITasks) { + return await models.Tasks.findOneAndUpdate( + { _id }, + { $set: { ...doc } }, + ); + } + + /** + * Remove operation + */ + public static async removeTasks(TasksId: string[]) { + return models.Tasks.deleteOne({ _id: { $in: TasksId } }); + } + } + + tasksSchema.loadClass(Tasks); + + return tasksSchema; +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/tasks.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/tasks.ts new file mode 100644 index 0000000000..baafe1ddde --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/tasks.ts @@ -0,0 +1,6 @@ +export const Tasks = { + async description() { + return 'Tasks description'; + }, + }; + \ No newline at end of file diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/tasks.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/tasks.ts new file mode 100644 index 0000000000..08d3791588 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/tasks.ts @@ -0,0 +1,17 @@ + + import { IContext } from '~/connectionResolvers'; + + export const tasksMutations = { + createTasks: async (_parent: undefined, { name }, { models }: IContext) => { + return models.Tasks.createTasks({name}); + }, + + updateTasks: async (_parent: undefined, { _id, name }, { models }: IContext) => { + return models.Tasks.updateTasks(_id, {name}); + }, + + removeTasks: async (_parent: undefined, { _id }, { models }: IContext) => { + return models.Tasks.removeTasks(_id); + }, + }; + diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/tasks.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/tasks.ts new file mode 100644 index 0000000000..6eb6b24958 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/tasks.ts @@ -0,0 +1,12 @@ + + import { IContext } from '~/connectionResolvers'; + + export const tasksQueries = { + getTasks: async (_parent: undefined, { _id }, { models }: IContext) => { + return models.Tasks.getTasks(_id); + }, + + getTaskss: async (_parent: undefined, { models }: IContext) => { + return models.Tasks.getTaskss(); + }, + }; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/tasks.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/tasks.ts new file mode 100644 index 0000000000..1a2489e7a6 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/tasks.ts @@ -0,0 +1,18 @@ +export const types = ` + type Tasks { + _id: String + name: String + description: String + } +`; + +export const queries = ` + getTasks(_id: String!): Tasks + getTaskss: [Tasks] +`; + +export const mutations = ` + createTasks(name: String!): Tasks + updateTasks(_id: String!, name: String!): Tasks + removeTasks(_id: String!): Tasks +`; diff --git a/backend/plugins/operation_api/src/trpc/init-trpc.ts b/backend/plugins/operation_api/src/trpc/init-trpc.ts new file mode 100644 index 0000000000..b6915deee6 --- /dev/null +++ b/backend/plugins/operation_api/src/trpc/init-trpc.ts @@ -0,0 +1,15 @@ +import { initTRPC } from '@trpc/server'; + +import { ITRPCContext } from 'erxes-api-shared/utils'; + +const t = initTRPC.context().create(); + +export const appRouter = t.router({ + operation: { + hello: t.procedure.query(() => { + return 'Hello operation'; + }), + }, +}); + +export type AppRouter = typeof appRouter; diff --git a/backend/plugins/operation_api/src/trpc/trpcClients.ts b/backend/plugins/operation_api/src/trpc/trpcClients.ts new file mode 100644 index 0000000000..6ebd627b51 --- /dev/null +++ b/backend/plugins/operation_api/src/trpc/trpcClients.ts @@ -0,0 +1,20 @@ +import { httpBatchLink, createTRPCUntypedClient } from '@trpc/client'; +import { getPlugin, isEnabled } from 'erxes-api-shared/utils'; + +export const coreTRPCClient = async (): Promise< + ReturnType +> => { + const isCoreEnabled = await isEnabled('core'); + + if (!isCoreEnabled) { + throw new Error('Core plugin is not enabled'); + } + + const core = await getPlugin('core'); + + const client = createTRPCUntypedClient({ + links: [httpBatchLink({ url: core.address + '/trpc' })], + }); + + return client; +}; diff --git a/backend/plugins/operation_api/tsconfig.build.json b/backend/plugins/operation_api/tsconfig.build.json new file mode 100644 index 0000000000..52a47e2daa --- /dev/null +++ b/backend/plugins/operation_api/tsconfig.build.json @@ -0,0 +1,22 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "paths": { + "~/*": [ + "./src/*" + ], + "@/*": [ + "./src/modules/*" + ] + }, + "types": [ + "node" + ] + }, + "exclude": [ + "node_modules", + "dist", + "**/*spec.ts" + ] +} \ No newline at end of file diff --git a/backend/plugins/operation_api/tsconfig.json b/backend/plugins/operation_api/tsconfig.json new file mode 100644 index 0000000000..7076b74596 --- /dev/null +++ b/backend/plugins/operation_api/tsconfig.json @@ -0,0 +1,56 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "allowUnreachableCode": false, + "esModuleInterop": true, + "target": "es2017", + "sourceMap": true, + "inlineSources": true, + "outDir": "./dist", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "alwaysStrict": true, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "resolveJsonModule": true, + "types": [ + "jest", + "node" + ], + "paths": { + "~/*": [ + "./src/*" + ], + "@/*": [ + "./src/modules/*" + ], + "erxes-api-shared/*": [ + "../../erxes-api-shared/src/*" + ] + } + }, + "ts-node": { + "files": true, + "require": [ + "tsconfig-paths/register" + ] + }, + "exclude": [ + "dist", + "frontend/**/*" + ], + "include": [ + "src/**/*.ts", + "src/**/*.tsx" + ] +} \ No newline at end of file diff --git a/frontend/plugins/operation_ui/eslint.config.js b/frontend/plugins/operation_ui/eslint.config.js new file mode 100644 index 0000000000..b3387659a3 --- /dev/null +++ b/frontend/plugins/operation_ui/eslint.config.js @@ -0,0 +1,11 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.js'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/react'], + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'], + rules: {}, + }, +]; diff --git a/frontend/plugins/operation_ui/jest.config.ts b/frontend/plugins/operation_ui/jest.config.ts new file mode 100644 index 0000000000..fc7ce658e4 --- /dev/null +++ b/frontend/plugins/operation_ui/jest.config.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +export default { + displayName: 'operation-ui', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/plugins/operation_ui', +}; diff --git a/frontend/plugins/operation_ui/module-federation.config.ts b/frontend/plugins/operation_ui/module-federation.config.ts new file mode 100644 index 0000000000..7f2fd1f0c1 --- /dev/null +++ b/frontend/plugins/operation_ui/module-federation.config.ts @@ -0,0 +1,34 @@ +import { ModuleFederationConfig } from '@nx/rspack/module-federation'; + +const coreLibraries = new Set([ + 'react', + 'react-dom', + 'react-router', + 'react-router-dom', + 'erxes-ui', + '@apollo/client', + 'jotai', + 'ui-modules', + 'react-i18next', +]); + +const config: ModuleFederationConfig = { + name: 'operation_ui', + exposes: { + './config': './src/config.ts', + './tasks': './src/modules/tasks/Main.tsx', + './tasksSettings': './src/modules/tasks/Settings.tsx', + './widgets': './src/widgets/Widgets.tsx', + }, + + shared: (libraryName, defaultConfig) => { + if (coreLibraries.has(libraryName)) { + return defaultConfig; + } + + // Returning false means the library is not shared. + return false; + }, +}; + +export default config; diff --git a/frontend/plugins/operation_ui/project.json b/frontend/plugins/operation_ui/project.json new file mode 100644 index 0000000000..966ebf7618 --- /dev/null +++ b/frontend/plugins/operation_ui/project.json @@ -0,0 +1,65 @@ +{ + "name": "operation_ui", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "frontend/plugins/operation_ui/src", + "projectType": "application", + "tags": [], + "targets": { + "build": { + "executor": "@nx/rspack:rspack", + "outputs": [ + "{options.outputPath}" + ], + "defaultConfiguration": "production", + "options": { + "target": "web", + "outputPath": "dist/frontend/plugins/operation_ui", + "main": "frontend/plugins/operation_ui/src/main.ts", + "tsConfig": "frontend/plugins/operation_ui/tsconfig.app.json", + "rspackConfig": "frontend/plugins/operation_ui/rspack.config.ts", + "assets": [ + "frontend/plugins/operation_ui/src/assets" + ] + }, + "configurations": { + "development": { + "mode": "development" + }, + "production": { + "mode": "production", + "optimization": true, + "sourceMap": false, + "rspackConfig": "frontend/plugins/operation_ui/rspack.config.prod.ts" + } + } + }, + "serve": { + "executor": "@nx/rspack:module-federation-dev-server", + "options": { + "buildTarget": "operation_ui:build:development", + "port": 3005 + }, + "configurations": { + "development": {}, + "production": { + "buildTarget": "operation_ui:build:production" + } + } + }, + "serve-static": { + "executor": "@nx/rspack:module-federation-static-server", + "defaultConfiguration": "production", + "options": { + "serveTarget": "operation_ui:serve" + }, + "configurations": { + "development": { + "serveTarget": "operation_ui:serve:development" + }, + "production": { + "serveTarget": "operation_ui:serve:production" + } + } + } + } +} \ No newline at end of file diff --git a/frontend/plugins/operation_ui/rspack.config.prod.ts b/frontend/plugins/operation_ui/rspack.config.prod.ts new file mode 100644 index 0000000000..59435a495f --- /dev/null +++ b/frontend/plugins/operation_ui/rspack.config.prod.ts @@ -0,0 +1 @@ +export default require('./rspack.config'); \ No newline at end of file diff --git a/frontend/plugins/operation_ui/rspack.config.ts b/frontend/plugins/operation_ui/rspack.config.ts new file mode 100644 index 0000000000..d718022abe --- /dev/null +++ b/frontend/plugins/operation_ui/rspack.config.ts @@ -0,0 +1,14 @@ +import { composePlugins, withNx, withReact } from '@nx/rspack'; +import { withModuleFederation } from '@nx/rspack/module-federation'; + +import baseConfig from './module-federation.config'; + +const config = { + ...baseConfig, +}; + +export default composePlugins( + withNx(), + withReact(), + withModuleFederation(config, { dts: false }), +); diff --git a/frontend/plugins/operation_ui/src/assets/README.md b/frontend/plugins/operation_ui/src/assets/README.md new file mode 100644 index 0000000000..a5e88d0360 --- /dev/null +++ b/frontend/plugins/operation_ui/src/assets/README.md @@ -0,0 +1,24 @@ +# Assets Directory + +This directory contains static assets used by the plugin. + +## Contents + +- `example-icon.svg`: Example icon in SVG format +- `example-image.svg`: Example placeholder image in SVG format + +## Usage + +Import assets in your components like this: + +```tsx +import exampleIcon from '~/assets/example-icon.svg'; +import exampleImage from '~/assets/example-image.svg'; +``` + +## Best Practices + +1. Use SVG format for icons and simple graphics +2. Optimize images before adding them to the assets folder +3. Keep file names descriptive and in kebab-case +4. Document any new assets added to this directory diff --git a/frontend/plugins/operation_ui/src/assets/example-icon.svg b/frontend/plugins/operation_ui/src/assets/example-icon.svg new file mode 100644 index 0000000000..bca4713f79 --- /dev/null +++ b/frontend/plugins/operation_ui/src/assets/example-icon.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/plugins/operation_ui/src/assets/example-image.svg b/frontend/plugins/operation_ui/src/assets/example-image.svg new file mode 100644 index 0000000000..a62ab22c39 --- /dev/null +++ b/frontend/plugins/operation_ui/src/assets/example-image.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/plugins/operation_ui/src/bootstrap.tsx b/frontend/plugins/operation_ui/src/bootstrap.tsx new file mode 100644 index 0000000000..32482de416 --- /dev/null +++ b/frontend/plugins/operation_ui/src/bootstrap.tsx @@ -0,0 +1,11 @@ +import { StrictMode } from 'react'; +import * as ReactDOM from 'react-dom/client'; + +const root = ReactDOM.createRoot( + document.getElementById('root') as HTMLElement, +); +root.render( + +
App
+
, +); diff --git a/frontend/plugins/operation_ui/src/config.ts b/frontend/plugins/operation_ui/src/config.ts new file mode 100644 index 0000000000..5cab6ff032 --- /dev/null +++ b/frontend/plugins/operation_ui/src/config.ts @@ -0,0 +1,19 @@ +import { IconSandbox } from '@tabler/icons-react'; + + +import { IUIConfig } from 'erxes-ui/types'; + +export const CONFIG: IUIConfig = { + name: 'operation', + icon: IconSandbox, + modules: [ + { + name: 'tasks', + icon: IconSandbox, + path: 'tasks', + hasSettings: true, + hasRelationWidget: false, + hasFloatingWidget: false, + }, + ], +}; diff --git a/frontend/plugins/operation_ui/src/index.html b/frontend/plugins/operation_ui/src/index.html new file mode 100644 index 0000000000..7587918287 --- /dev/null +++ b/frontend/plugins/operation_ui/src/index.html @@ -0,0 +1,14 @@ + + + + + operation + + + + + + +
+ + diff --git a/frontend/plugins/operation_ui/src/main.ts b/frontend/plugins/operation_ui/src/main.ts new file mode 100644 index 0000000000..137c64f9f4 --- /dev/null +++ b/frontend/plugins/operation_ui/src/main.ts @@ -0,0 +1 @@ +import('./bootstrap'); \ No newline at end of file diff --git a/frontend/plugins/operation_ui/src/modules/tasks/Main.tsx b/frontend/plugins/operation_ui/src/modules/tasks/Main.tsx new file mode 100644 index 0000000000..a3513ed86d --- /dev/null +++ b/frontend/plugins/operation_ui/src/modules/tasks/Main.tsx @@ -0,0 +1,20 @@ +import { lazy, Suspense } from 'react'; +import { Route, Routes } from 'react-router'; + +const IndexPage = lazy(() => + import('~/pages/tasks/IndexPage').then((module) => ({ + default: module.IndexPage, + })), +); + +const tasksMain = () => { + return ( + }> + + } /> + + + ); +}; + +export default tasksMain; diff --git a/frontend/plugins/operation_ui/src/modules/tasks/Settings.tsx b/frontend/plugins/operation_ui/src/modules/tasks/Settings.tsx new file mode 100644 index 0000000000..33c667eaa5 --- /dev/null +++ b/frontend/plugins/operation_ui/src/modules/tasks/Settings.tsx @@ -0,0 +1,9 @@ +const tasksSettings = () => { + return ( +
+

tasks Settings

+
+ ); +}; + +export default tasksSettings; diff --git a/frontend/plugins/operation_ui/src/pages/tasks/IndexPage.tsx b/frontend/plugins/operation_ui/src/pages/tasks/IndexPage.tsx new file mode 100644 index 0000000000..40901daeaa --- /dev/null +++ b/frontend/plugins/operation_ui/src/pages/tasks/IndexPage.tsx @@ -0,0 +1,49 @@ +import { + IconCaretDownFilled, + IconSandbox, + IconSettings, +} from '@tabler/icons-react'; +import { Breadcrumb, Button, Separator } from 'erxes-ui'; +import { PageHeader } from 'ui-modules'; +import { Link } from 'react-router-dom'; + +export const IndexPage = () => { + return ( +
+ + + + + + + + + + + + + + + + + +
+
+

tasks

+
+
+
+ ); +}; diff --git a/frontend/plugins/operation_ui/src/widgets/Widgets.tsx b/frontend/plugins/operation_ui/src/widgets/Widgets.tsx new file mode 100644 index 0000000000..86a4a379e4 --- /dev/null +++ b/frontend/plugins/operation_ui/src/widgets/Widgets.tsx @@ -0,0 +1,13 @@ +export const Widgets = ({ + module, + contentId, + contentType, +}: { + module: any; + contentId: string; + contentType: string; +}) => { + return
tasks Widget
; +}; + +export default Widgets; diff --git a/frontend/plugins/operation_ui/tsconfig.app.json b/frontend/plugins/operation_ui/tsconfig.app.json new file mode 100644 index 0000000000..e327240abd --- /dev/null +++ b/frontend/plugins/operation_ui/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "node", + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.jsx", + "src/**/*.test.jsx" + ], + "include": [ + "src/**/*.js", + "src/**/*.jsx", + "src/**/*.ts", + "src/**/*.tsx" + ] +} \ No newline at end of file diff --git a/frontend/plugins/operation_ui/tsconfig.json b/frontend/plugins/operation_ui/tsconfig.json new file mode 100644 index 0000000000..c87b2e96eb --- /dev/null +++ b/frontend/plugins/operation_ui/tsconfig.json @@ -0,0 +1,46 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": false, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "paths": { + "erxes-ui/*": [ + "frontend/libs/erxes-ui/src/*" + ], + "erxes-ui": [ + "frontend/libs/erxes-ui/src" + ], + "~/*": [ + "frontend/plugins/operation_ui/src/*" + ], + "~": [ + "frontend/plugins/operation_ui/src" + ], + "@/*": [ + "frontend/plugins/operation_ui/src/modules/*" + ], + "@": [ + "frontend/plugins/operation_ui/src/modules" + ], + "ui-modules/*": [ + "frontend/libs/ui-modules/src/*" + ], + "ui-modules": [ + "frontend/libs/ui-modules/src" + ] + } + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json" +} \ No newline at end of file diff --git a/frontend/plugins/operation_ui/tsconfig.spec.json b/frontend/plugins/operation_ui/tsconfig.spec.json new file mode 100644 index 0000000000..5073ef03e6 --- /dev/null +++ b/frontend/plugins/operation_ui/tsconfig.spec.json @@ -0,0 +1,27 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "moduleResolution": "node10", + "jsx": "react-jsx", + "types": [ + "jest", + "node", + "@nx/react/typings/cssmodule.d.ts", + "@nx/react/typings/image.d.ts" + ] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} \ No newline at end of file From c57ba793b2f217f8f6f99f40a1107a0a513a07f9 Mon Sep 17 00:00:00 2001 From: batmnkh2344 Date: Fri, 20 Jun 2025 16:14:00 +0800 Subject: [PATCH 2/3] add task to operation --- .../src/apollo/resolvers/mutations.ts | 4 +- .../src/apollo/resolvers/queries.ts | 4 +- .../src/apollo/resolvers/resolvers.ts | 4 +- .../src/apollo/schema/extension.ts | 25 + .../operation_api/src/apollo/schema/schema.ts | 4 +- .../operation_api/src/connectionResolvers.ts | 28 +- .../src/modules/tasks/@types/boards.ts | 15 + .../src/modules/tasks/@types/checklists.ts | 26 + .../src/modules/tasks/@types/labels.ts | 13 + .../src/modules/tasks/@types/pipelines.ts | 36 + .../src/modules/tasks/@types/stages.ts | 26 + .../src/modules/tasks/@types/tasks.ts | 87 +- .../src/modules/tasks/constants.ts | 87 ++ .../modules/tasks/db/definitions/boards.ts | 20 + .../tasks/db/definitions/checklists.ts | 39 + .../modules/tasks/db/definitions/labels.ts | 20 + .../modules/tasks/db/definitions/pipelines.ts | 94 ++ .../modules/tasks/db/definitions/stages.ts | 58 + .../src/modules/tasks/db/definitions/tasks.ts | 126 +- .../src/modules/tasks/db/models/Boards.ts | 62 + .../src/modules/tasks/db/models/Checklists.ts | 197 +++ .../src/modules/tasks/db/models/Labels.ts | 156 +++ .../src/modules/tasks/db/models/Pipelines.ts | 159 +++ .../src/modules/tasks/db/models/Stages.ts | 104 ++ .../src/modules/tasks/db/models/Tasks.ts | 169 ++- .../src/modules/tasks/db/models/utils.ts | 206 +++ .../resolvers/customResolvers/board.ts | 88 ++ .../resolvers/customResolvers/checklist.ts | 26 + .../resolvers/customResolvers/index.ts | 8 + .../resolvers/customResolvers/pipeline.ts | 75 ++ .../resolvers/customResolvers/stage.ts | 243 ++++ .../resolvers/customResolvers/tasks.ts | 164 ++- .../graphql/resolvers/mutations/board.ts | 67 + .../graphql/resolvers/mutations/checklist.ts | 129 ++ .../graphql/resolvers/mutations/index.ts | 15 + .../graphql/resolvers/mutations/label.ts | 51 + .../graphql/resolvers/mutations/pipeline.ts | 155 +++ .../graphql/resolvers/mutations/stage.ts | 109 ++ .../tasks/graphql/resolvers/mutations/task.ts | 568 +++++++++ .../graphql/resolvers/mutations/tasks.ts | 17 - .../tasks/graphql/resolvers/queries/board.ts | 65 + .../graphql/resolvers/queries/checklist.ts | 31 + .../tasks/graphql/resolvers/queries/index.ts | 15 + .../tasks/graphql/resolvers/queries/label.ts | 33 + .../graphql/resolvers/queries/pipeline.ts | 160 +++ .../tasks/graphql/resolvers/queries/stage.ts | 112 ++ .../tasks/graphql/resolvers/queries/task.ts | 299 +++++ .../tasks/graphql/resolvers/queries/tasks.ts | 12 - .../modules/tasks/graphql/resolvers/utils.ts | 1132 +++++++++++++++++ .../modules/tasks/graphql/schemas/board.ts | 34 + .../tasks/graphql/schemas/checklist.ts | 37 + .../modules/tasks/graphql/schemas/index.ts | 62 + .../modules/tasks/graphql/schemas/label.ts | 28 + .../modules/tasks/graphql/schemas/pipeline.ts | 77 ++ .../modules/tasks/graphql/schemas/stage.ts | 83 ++ .../src/modules/tasks/graphql/schemas/task.ts | 173 +++ .../modules/tasks/graphql/schemas/tasks.ts | 18 - .../src/modules/tasks/resolver.ts | 67 + .../operation_api/src/modules/tasks/utils.ts | 9 + frontend/plugins/operation_ui/project.json | 12 +- 60 files changed, 5825 insertions(+), 118 deletions(-) create mode 100644 backend/plugins/operation_api/src/apollo/schema/extension.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/@types/boards.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/@types/checklists.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/@types/labels.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/@types/pipelines.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/@types/stages.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/constants.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/db/definitions/boards.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/db/definitions/checklists.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/db/definitions/labels.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/db/definitions/pipelines.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/db/definitions/stages.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/db/models/Boards.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/db/models/Checklists.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/db/models/Labels.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/db/models/Pipelines.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/db/models/Stages.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/db/models/utils.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/board.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/checklist.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/index.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/pipeline.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/stage.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/board.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/checklist.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/index.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/label.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/pipeline.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/stage.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/task.ts delete mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/tasks.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/board.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/checklist.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/index.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/label.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/pipeline.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/stage.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/task.ts delete mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/tasks.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/utils.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/schemas/board.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/schemas/checklist.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/schemas/index.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/schemas/label.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/schemas/pipeline.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/schemas/stage.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/schemas/task.ts delete mode 100644 backend/plugins/operation_api/src/modules/tasks/graphql/schemas/tasks.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/resolver.ts create mode 100644 backend/plugins/operation_api/src/modules/tasks/utils.ts diff --git a/backend/plugins/operation_api/src/apollo/resolvers/mutations.ts b/backend/plugins/operation_api/src/apollo/resolvers/mutations.ts index e4dfcbdbf7..7a6f279e83 100644 --- a/backend/plugins/operation_api/src/apollo/resolvers/mutations.ts +++ b/backend/plugins/operation_api/src/apollo/resolvers/mutations.ts @@ -1,5 +1,5 @@ -import { tasksMutations } from '@/tasks/graphql/resolvers/mutations/tasks'; +import { mutations as taskMutations } from '~/modules/tasks/graphql/resolvers/mutations'; export const mutations = { - ...tasksMutations, + ...taskMutations, }; diff --git a/backend/plugins/operation_api/src/apollo/resolvers/queries.ts b/backend/plugins/operation_api/src/apollo/resolvers/queries.ts index ab0b21f8b3..65f9e4e610 100644 --- a/backend/plugins/operation_api/src/apollo/resolvers/queries.ts +++ b/backend/plugins/operation_api/src/apollo/resolvers/queries.ts @@ -1,5 +1,5 @@ -import { tasksQueries } from '@/tasks/graphql/resolvers/queries/tasks'; +import { queries as taskQueries } from '~/modules/tasks/graphql/resolvers/queries'; export const queries = { - ...tasksQueries, + ...taskQueries, }; diff --git a/backend/plugins/operation_api/src/apollo/resolvers/resolvers.ts b/backend/plugins/operation_api/src/apollo/resolvers/resolvers.ts index bc4e154235..ea1f32d3ae 100644 --- a/backend/plugins/operation_api/src/apollo/resolvers/resolvers.ts +++ b/backend/plugins/operation_api/src/apollo/resolvers/resolvers.ts @@ -1,5 +1,5 @@ -import { Tasks } from '@/tasks/graphql/resolvers/customResolvers/tasks'; +import taskResolvers from '~/modules/tasks/graphql/resolvers/customResolvers'; export const customResolvers = { - Tasks, + ...taskResolvers, }; diff --git a/backend/plugins/operation_api/src/apollo/schema/extension.ts b/backend/plugins/operation_api/src/apollo/schema/extension.ts new file mode 100644 index 0000000000..4a7b235992 --- /dev/null +++ b/backend/plugins/operation_api/src/apollo/schema/extension.ts @@ -0,0 +1,25 @@ +export const TypeExtensions = ` + extend type User @key(fields: "_id") { + _id: String @external + } + + extend type Branch @key(fields: "_id") { + _id: String @external + } + + extend type Department @key(fields: "_id") { + _id: String @external + } + + extend type Company @key(fields: "_id") { + _id: String @external + } + + extend type Customer @key(fields: "_id") { + _id: String @external + } + + extend type Tag @key(fields: "_id") { + _id: String @external + } +`; diff --git a/backend/plugins/operation_api/src/apollo/schema/schema.ts b/backend/plugins/operation_api/src/apollo/schema/schema.ts index ef8055a52d..86b2d57737 100644 --- a/backend/plugins/operation_api/src/apollo/schema/schema.ts +++ b/backend/plugins/operation_api/src/apollo/schema/schema.ts @@ -1,10 +1,12 @@ +import { TypeExtensions } from '~/apollo/schema/extension'; import { mutations as TasksMutations, queries as TasksQueries, types as TasksTypes, -} from '@/tasks/graphql/schemas/tasks'; +} from '~/modules/tasks/graphql/schemas'; export const types = ` + ${TypeExtensions} ${TasksTypes} `; diff --git a/backend/plugins/operation_api/src/connectionResolvers.ts b/backend/plugins/operation_api/src/connectionResolvers.ts index 3f10ea2bdd..3674f28355 100644 --- a/backend/plugins/operation_api/src/connectionResolvers.ts +++ b/backend/plugins/operation_api/src/connectionResolvers.ts @@ -1,13 +1,28 @@ -import { createGenerateModels } from 'erxes-api-shared/utils'; import { IMainContext } from 'erxes-api-shared/core-types'; -import { ITasksDocument } from '@/tasks/@types/tasks'; +import { createGenerateModels } from 'erxes-api-shared/utils'; import mongoose from 'mongoose'; -import { loadTasksClass, ITasksModel } from '@/tasks/db/models/Tasks'; +import { ITaskModel } from '@/tasks/db/models/Tasks'; +import { IBoardModel } from '~/modules/tasks/db/models/Boards'; +import { + IChecklistItemModel, + IChecklistModel, +} from '~/modules/tasks/db/models/Checklists'; +import { IPipelineLabelModel } from '~/modules/tasks/db/models/Labels'; +import { IPipelineModel } from '~/modules/tasks/db/models/Pipelines'; +import { IStageModel } from '~/modules/tasks/db/models/Stages'; +import { loadTaskClasses } from '~/modules/tasks/resolver'; export interface IModels { - Tasks: ITasksModel; + // TASK MODULE + Boards: IBoardModel; + Pipelines: IPipelineModel; + Stages: IStageModel; + Tasks: ITaskModel; + Checklists: IChecklistModel; + ChecklistItems: IChecklistItemModel; + PipelineLabels: IPipelineLabelModel; } export interface IContext extends IMainContext { @@ -17,10 +32,7 @@ export interface IContext extends IMainContext { export const loadClasses = (db: mongoose.Connection): IModels => { const models = {} as IModels; - models.Tasks = db.model( - 'tasks', - loadTasksClass(models), - ); + loadTaskClasses(models, db); return models; }; diff --git a/backend/plugins/operation_api/src/modules/tasks/@types/boards.ts b/backend/plugins/operation_api/src/modules/tasks/@types/boards.ts new file mode 100644 index 0000000000..864b9f2f33 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/@types/boards.ts @@ -0,0 +1,15 @@ +import { Document } from 'mongoose'; +import { IPipeline } from '~/modules/tasks/@types/pipelines'; + +export interface IBoard { + name?: string; + userId?: string; +} + +export interface IBoardDocument extends IBoard, Document { + _id: string; + + type: string; + pipelines?: IPipeline[]; + order?: number; +} diff --git a/backend/plugins/operation_api/src/modules/tasks/@types/checklists.ts b/backend/plugins/operation_api/src/modules/tasks/@types/checklists.ts new file mode 100644 index 0000000000..d22e3fa98f --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/@types/checklists.ts @@ -0,0 +1,26 @@ +import { Document } from 'mongoose'; + +export interface IChecklist { + contentType: string; + contentTypeId: string; + title: string; +} + +export interface IChecklistDocument extends IChecklist, Document { + _id: string; + createdUserId: string; + createdDate: Date; +} + +export interface IChecklistItem { + checklistId: string; + content: string; + isChecked: boolean; +} + +export interface IChecklistItemDocument extends IChecklistItem, Document { + _id: string; + order: number; + createdUserId: string; + createdDate: Date; +} diff --git a/backend/plugins/operation_api/src/modules/tasks/@types/labels.ts b/backend/plugins/operation_api/src/modules/tasks/@types/labels.ts new file mode 100644 index 0000000000..15f0f71206 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/@types/labels.ts @@ -0,0 +1,13 @@ +import { Document } from 'mongoose'; + +export interface IPipelineLabel { + name: string; + colorCode: string; + pipelineId: string; + createdBy?: string; + createdAt?: Date; +} + +export interface IPipelineLabelDocument extends IPipelineLabel, Document { + _id: string; +} diff --git a/backend/plugins/operation_api/src/modules/tasks/@types/pipelines.ts b/backend/plugins/operation_api/src/modules/tasks/@types/pipelines.ts new file mode 100644 index 0000000000..839b50bd06 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/@types/pipelines.ts @@ -0,0 +1,36 @@ +import { Document } from 'mongoose'; + +export interface IPipeline { + name?: string; + boardId: string; + status?: string; + visibility?: string; + memberIds?: string[]; + bgColor?: string; + watchedUserIds?: string[]; + startDate?: Date; + endDate?: Date; + metric?: string; + hackScoringType?: string; + templateId?: string; + isCheckDate?: boolean; + isCheckUser?: boolean; + isCheckDepartment?: boolean; + excludeCheckUserIds?: string[]; + numberConfig?: string; + numberSize?: string; + nameConfig?: string; + lastNum?: string; + departmentIds?: string[]; + branchIds?: string[]; + tagId?: string; + userId?: string; +} + +export interface IPipelineDocument extends IPipeline, Document { + _id: string; + + createdAt?: Date; + order?: number; + type: string; +} diff --git a/backend/plugins/operation_api/src/modules/tasks/@types/stages.ts b/backend/plugins/operation_api/src/modules/tasks/@types/stages.ts new file mode 100644 index 0000000000..4fd1e750f5 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/@types/stages.ts @@ -0,0 +1,26 @@ +import { Document } from 'mongoose'; + +export interface IStage { + name?: string; + probability?: string; + pipelineId: string; + visibility?: string; + memberIds?: string[]; + canMoveMemberIds?: string[]; + canEditMemberIds?: string[]; + departmentIds?: string[]; + formId?: string; + status?: string; + code?: string; + age?: number; + defaultTick?: boolean; + userId?: string; +} + +export interface IStageDocument extends IStage, Document { + _id: string; + + createdAt?: Date; + order?: number; + type: string; +} diff --git a/backend/plugins/operation_api/src/modules/tasks/@types/tasks.ts b/backend/plugins/operation_api/src/modules/tasks/@types/tasks.ts index d5b2ce741b..1cda271b6c 100644 --- a/backend/plugins/operation_api/src/modules/tasks/@types/tasks.ts +++ b/backend/plugins/operation_api/src/modules/tasks/@types/tasks.ts @@ -1,11 +1,94 @@ +import { + ICursorPaginateParams, + ICustomField, + IListParams, +} from 'erxes-api-shared/core-types'; import { Document } from 'mongoose'; -export interface ITasks { +export interface ITask { name?: string; + // TODO migrate after remove 2row + companyIds?: string[]; + customerIds?: string[]; + startDate?: Date; + closeDate?: Date; + stageChangedDate?: Date; + description?: string; + assignedUserIds?: string[]; + watchedUserIds?: string[]; + notifiedUserIds?: string[]; + labelIds?: string[]; + attachments?: any[]; + stageId: string; + initialStageId?: string; + modifiedAt?: Date; + modifiedBy?: string; + userId?: string; + createdAt?: Date; + order?: number; + searchText?: string; + priority?: string; + sourceConversationIds?: string[]; + status?: string; + timeTrack?: { + status: string; + timeSpent: number; + startDate?: string; + }; + customFieldsData?: ICustomField[]; + score?: number; + number?: string; + data?: any; + tagIds?: string[]; + branchIds?: string[]; + departmentIds?: string[]; + parentId?: string; } -export interface ITasksDocument extends ITasks, Document { +export interface ITaskDocument extends ITask, Document { _id: string; createdAt: Date; updatedAt: Date; } + +export interface ITaskQueryParams extends IListParams, ICursorPaginateParams { + pipelineId: string; + pipelineIds: string[]; + stageId: string; + _ids?: string; + date?: { + month: number; + year: number; + }; + search?: string; + customerIds?: string[]; + companyIds?: string[]; + assignedUserIds?: string[]; + labelIds?: string[]; + userIds?: string[]; + segment?: string; + segmentData?: string; + stageChangedStartDate?: Date; + stageChangedEndDate?: Date; + noSkipArchive?: boolean; + tagIds?: string[]; + number?: string; +} + +export interface IArchivedTaskQueryParams + extends IListParams, + ICursorPaginateParams { + pipelineId: string; + search: string; + userIds?: string[]; + priorities?: string[]; + assignedUserIds?: string[]; + labelIds?: string[]; + productIds?: string[]; + companyIds?: string[]; + customerIds?: string[]; + startDate?: string; + endDate?: string; + sources?: string[]; + hackStages?: string[]; +} diff --git a/backend/plugins/operation_api/src/modules/tasks/constants.ts b/backend/plugins/operation_api/src/modules/tasks/constants.ts new file mode 100644 index 0000000000..e8b1465a00 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/constants.ts @@ -0,0 +1,87 @@ +export const VISIBILITIES = { + PUBLIC: 'public', + PRIVATE: 'private', + ALL: ['public', 'private'], +}; + +export const HACK_SCORING_TYPES = { + RICE: 'rice', + ICE: 'ice', + PIE: 'pie', + ALL: ['rice', 'ice', 'pie'], +}; + +export const PROBABILITY = { + TEN: '10%', + TWENTY: '20%', + THIRTY: '30%', + FOURTY: '40%', + FIFTY: '50%', + SIXTY: '60%', + SEVENTY: '70%', + EIGHTY: '80%', + NINETY: '90%', + WON: 'Won', + LOST: 'Lost', + DONE: 'Done', + RESOLVED: 'Resolved', + ALL: [ + '10%', + '20%', + '30%', + '40%', + '50%', + '60%', + '70%', + '80%', + '90%', + 'Won', + 'Lost', + 'Done', + 'Resolved', + ], +}; + +export const TASK_STATUSES = { + ACTIVE: 'active', + ARCHIVED: 'archived', + ALL: ['active', 'archived'], +}; + +export const TIME_TRACK_TYPES = { + STARTED: 'started', + STOPPED: 'stopped', + PAUSED: 'paused', + COMPLETED: 'completed', + ALL: ['started', 'stopped', 'paused', 'completed'], +}; + +export const CLOSE_DATE_TYPES = { + NEXT_DAY: 'nextDay', + NEXT_WEEK: 'nextWeek', + NEXT_MONTH: 'nextMonth', + NO_CLOSE_DATE: 'noCloseDate', + OVERDUE: 'overdue', + ALL: [ + { + name: 'Next day', + value: 'nextDay', + }, + { + name: 'Next week', + value: 'nextWeek', + }, + { + name: 'Next month', + value: 'nextMonth', + }, + { + name: 'No close date', + value: 'noCloseDate', + }, + { + name: 'Over due', + value: 'overdue', + }, + ], +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/db/definitions/boards.ts b/backend/plugins/operation_api/src/modules/tasks/db/definitions/boards.ts new file mode 100644 index 0000000000..c65a0ffe08 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/db/definitions/boards.ts @@ -0,0 +1,20 @@ +import { mongooseStringRandomId } from 'erxes-api-shared/utils'; +import { Schema } from 'mongoose'; + +export const boardSchema = new Schema( + { + _id: mongooseStringRandomId, + name: { type: String, label: 'Name' }, + userId: { type: String, label: 'Created by' }, + order: { type: Number, label: 'Order' }, + type: { + type: String, + required: true, + label: 'Type', + default: 'task', + }, + }, + { + timestamps: true, + }, +); diff --git a/backend/plugins/operation_api/src/modules/tasks/db/definitions/checklists.ts b/backend/plugins/operation_api/src/modules/tasks/db/definitions/checklists.ts new file mode 100644 index 0000000000..3ee4b68e25 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/db/definitions/checklists.ts @@ -0,0 +1,39 @@ +import { mongooseStringRandomId } from 'erxes-api-shared/utils'; +import { Schema } from 'mongoose'; + +export const checklistSchema = new Schema( + { + _id: mongooseStringRandomId, + contentType: { + type: String, + label: 'Content type', + index: true, + default: 'task', + }, + order: { type: Number }, + contentTypeId: { + type: String, + label: 'Content type item', + index: true, + }, + title: { type: String, label: 'Title' }, + createdUserId: { type: String, label: 'Created by' }, + }, + { + timestamps: true, + }, +); + +export const checklistItemSchema = new Schema( + { + _id: mongooseStringRandomId, + checklistId: { type: String, label: 'Check list', index: true }, + content: { type: String, label: 'Content' }, + isChecked: { type: Boolean, label: 'Is checked' }, + createdDate: { type: Date, label: 'Created at' }, + order: { type: Number }, + }, + { + timestamps: true, + }, +); diff --git a/backend/plugins/operation_api/src/modules/tasks/db/definitions/labels.ts b/backend/plugins/operation_api/src/modules/tasks/db/definitions/labels.ts new file mode 100644 index 0000000000..c7e9bec75e --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/db/definitions/labels.ts @@ -0,0 +1,20 @@ +import { mongooseStringRandomId } from 'erxes-api-shared/utils'; +import { Schema } from 'mongoose'; + +export const pipelineLabelSchema = new Schema( + { + _id: mongooseStringRandomId, + name: { type: String, label: 'Name' }, + colorCode: { type: String, label: 'Color code' }, + pipelineId: { type: String, label: 'Pipeline' }, + createdBy: { type: String, label: 'Created by' }, + }, + { + timestamps: true, + }, +); + +pipelineLabelSchema.index( + { pipelineId: 1, name: 1, colorCode: 1 }, + { unique: true }, +); diff --git a/backend/plugins/operation_api/src/modules/tasks/db/definitions/pipelines.ts b/backend/plugins/operation_api/src/modules/tasks/db/definitions/pipelines.ts new file mode 100644 index 0000000000..fbe51c43ae --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/db/definitions/pipelines.ts @@ -0,0 +1,94 @@ +import { mongooseStringRandomId } from 'erxes-api-shared/utils'; +import { Schema } from 'mongoose'; +import { + HACK_SCORING_TYPES, + TASK_STATUSES, + VISIBILITIES, +} from '~/modules/tasks/constants'; + +export const pipelineSchema = new Schema( + { + _id: mongooseStringRandomId, + name: { type: String, label: 'Name' }, + boardId: { type: String, label: 'Board' }, + tagId: { + type: String, + optional: true, + label: 'Tags', + }, + status: { + type: String, + enum: TASK_STATUSES.ALL, + default: TASK_STATUSES.ACTIVE, + label: 'Status', + }, + visibility: { + type: String, + enum: VISIBILITIES.ALL, + default: VISIBILITIES.PUBLIC, + label: 'Visibility', + }, + watchedUserIds: { type: [String], label: 'Watched users' }, + memberIds: { type: [String], label: 'Members' }, + bgColor: { type: String, label: 'Background color' }, + // Growth hack + startDate: { type: Date, optional: true, label: 'Start date' }, + endDate: { type: Date, optional: true, label: 'End date' }, + metric: { type: String, optional: true, label: 'Metric' }, + hackScoringType: { + type: String, + enum: HACK_SCORING_TYPES.ALL, + label: 'Hacking scoring type', + }, + templateId: { type: String, optional: true, label: 'Template' }, + isCheckDate: { + type: Boolean, + optional: true, + label: 'Select the day after the card created date', + }, + isCheckUser: { + type: Boolean, + optional: true, + label: 'Show only the users created or assigned cards', + }, + isCheckDepartment: { + type: Boolean, + optional: true, + label: 'Show only the departments created or assigned cards', + }, + excludeCheckUserIds: { + type: [String], + optional: true, + label: 'Users elligible to see all cards', + }, + numberConfig: { type: String, optional: true, label: 'Number config' }, + numberSize: { type: String, optional: true, label: 'Number count' }, + nameConfig: { type: String, optional: true, label: 'Name config' }, + lastNum: { + type: String, + optional: true, + label: 'Last generated number', + }, + departmentIds: { + type: [String], + optional: true, + label: 'Related departments', + }, + branchIds: { + type: [String], + optional: true, + label: 'Related branches', + }, + userId: { type: String, label: 'Created by' }, + order: { type: Number, label: 'Order' }, + type: { + type: String, + required: true, + label: 'Type', + default: 'task', + }, + }, + { + timestamps: true, + }, +); diff --git a/backend/plugins/operation_api/src/modules/tasks/db/definitions/stages.ts b/backend/plugins/operation_api/src/modules/tasks/db/definitions/stages.ts new file mode 100644 index 0000000000..e2f7295619 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/db/definitions/stages.ts @@ -0,0 +1,58 @@ +import { mongooseStringRandomId } from 'erxes-api-shared/utils'; +import { Schema } from 'mongoose'; +import { + PROBABILITY, + TASK_STATUSES, + VISIBILITIES, +} from '~/modules/tasks/constants'; + +export const stageSchema = new Schema( + { + _id: mongooseStringRandomId, + name: { type: String, label: 'Name' }, + probability: { + type: String, + enum: PROBABILITY.ALL, + label: 'Probability', + }, // Win probability + pipelineId: { type: String, label: 'Pipeline' }, + formId: { type: String, label: 'Form' }, + status: { + type: String, + enum: TASK_STATUSES.ALL, + default: TASK_STATUSES.ACTIVE, + }, + visibility: { + type: String, + enum: VISIBILITIES.ALL, + default: VISIBILITIES.PUBLIC, + label: 'Visibility', + }, + code: { + type: String, + label: 'Code', + optional: true, + }, + age: { type: Number, optional: true, label: 'Age' }, + memberIds: { type: [String], label: 'Members' }, + canMoveMemberIds: { type: [String], label: 'Can move members' }, + canEditMemberIds: { type: [String], label: 'Can edit members' }, + departmentIds: { type: [String], label: 'Departments' }, + defaultTick: { + type: Boolean, + label: 'Default tick used', + optional: true, + }, + userId: { type: String, label: 'Created by' }, + order: { type: Number, label: 'Order' }, + type: { + type: String, + required: true, + label: 'Type', + default: 'task', + }, + }, + { + timestamps: true, + }, +); diff --git a/backend/plugins/operation_api/src/modules/tasks/db/definitions/tasks.ts b/backend/plugins/operation_api/src/modules/tasks/db/definitions/tasks.ts index 1b99fe44b4..0e8db8cabf 100644 --- a/backend/plugins/operation_api/src/modules/tasks/db/definitions/tasks.ts +++ b/backend/plugins/operation_api/src/modules/tasks/db/definitions/tasks.ts @@ -1,15 +1,123 @@ import { Schema } from 'mongoose'; -import { mongooseStringRandomId, schemaWrapper } from 'erxes-api-shared/utils'; +import { + attachmentSchema, + customFieldSchema, +} from 'erxes-api-shared/core-modules'; +import { mongooseStringRandomId } from 'erxes-api-shared/utils'; +import { TASK_STATUSES, TIME_TRACK_TYPES } from '~/modules/tasks/constants'; -export const tasksSchema = schemaWrapper( - new Schema( - { - _id: mongooseStringRandomId, - name: { type: String, label: 'Name' }, +const timeTrackSchema = new Schema( + { + startDate: { type: String }, + timeSpent: { type: Number }, + status: { + type: String, + enum: TIME_TRACK_TYPES.ALL, + default: TIME_TRACK_TYPES.STOPPED, }, - { - timestamps: true, + }, + { _id: false }, +); + +const relationSchema = new Schema( + { + id: { type: String }, + start: { type: String }, + end: { type: String }, + }, + { _id: false }, +); + +export const tasksSchema = new Schema( + { + _id: mongooseStringRandomId, + parentId: { type: String, optional: true, label: 'Parent Id' }, + userId: { type: String, optional: true, esType: 'keyword' }, + order: { type: Number, index: true }, + name: { type: String, label: 'Name' }, + startDate: { type: Date, label: 'Start date', esType: 'date' }, + closeDate: { type: Date, label: 'Close date', esType: 'date' }, + stageChangedDate: { + type: Date, + label: 'Stage changed date', + esType: 'date', + }, + reminderMinute: { type: Number, label: 'Reminder minute' }, + isComplete: { + type: Boolean, + default: false, + label: 'Is complete', + esType: 'boolean', + }, + description: { type: String, optional: true, label: 'Description' }, + assignedUserIds: { type: [String], esType: 'keyword' }, + watchedUserIds: { type: [String], esType: 'keyword' }, + labelIds: { type: [String], esType: 'keyword' }, + attachments: { type: [attachmentSchema], label: 'Attachments' }, + stageId: { type: String, index: true }, + initialStageId: { + type: String, + optional: true, + }, + modifiedBy: { type: String, esType: 'keyword' }, + searchText: { type: String, optional: true, index: true }, + priority: { type: String, optional: true, label: 'Priority' }, + // TODO remove after migration + sourceConversationId: { type: String, optional: true }, + sourceConversationIds: { type: [String], optional: true }, + timeTrack: { + type: timeTrackSchema, + }, + status: { + type: String, + enum: TASK_STATUSES.ALL, + default: TASK_STATUSES.ACTIVE, + label: 'Status', + index: true, + }, + customFieldsData: { + type: [customFieldSchema], + optional: true, + label: 'Custom fields data', + }, + score: { + type: Number, + optional: true, + label: 'Score', + esType: 'number', + }, + number: { + type: String, + unique: true, + sparse: true, + label: 'Item number', + }, + relations: { + type: [relationSchema], + optional: true, + label: 'Related items used for gantt chart', + }, + tagIds: { + type: [String], + optional: true, + index: true, + label: 'Tags', + }, + branchIds: { + type: [String], + optional: true, + index: true, + label: 'Tags', + }, + departmentIds: { + type: [String], + optional: true, + index: true, + label: 'Tags', }, - ), + }, + { + timestamps: true, + }, ); diff --git a/backend/plugins/operation_api/src/modules/tasks/db/models/Boards.ts b/backend/plugins/operation_api/src/modules/tasks/db/models/Boards.ts new file mode 100644 index 0000000000..4f8a6adf57 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/db/models/Boards.ts @@ -0,0 +1,62 @@ +import { Model } from 'mongoose'; +import { IModels } from '~/connectionResolvers'; +import { IBoard, IBoardDocument } from '~/modules/tasks/@types/boards'; +import { boardSchema } from '~/modules/tasks/db/definitions/boards'; +import { removePipelines } from '~/modules/tasks/db/models/utils'; + +export interface IBoardModel extends Model { + getBoard(_id: string): Promise; + createBoard(doc: IBoard): Promise; + updateBoard(_id: string, doc: IBoard): Promise; + removeBoard(_id: string): object; +} + +export const loadBoardClass = (models: IModels) => { + class Board { + /* + * Get a Board + */ + public static async getBoard(_id: string) { + const board = await models.Boards.findOne({ _id }); + + if (!board) { + throw new Error('Board not found'); + } + + return board; + } + + /** + * Create a board + */ + public static async createBoard(doc: IBoard) { + return models.Boards.create(doc); + } + + /** + * Update Board + */ + public static async updateBoard(_id: string, doc: IBoard) { + return await models.Boards.findOneAndUpdate( + { _id }, + { $set: doc }, + { new: true }, + ); + } + + /** + * Remove Board + */ + public static async removeBoard(_id: string) { + const board = await models.Boards.getBoard(_id); + + await removePipelines(models, [board._id]); + + return models.Boards.findOneAndDelete({ _id }); + } + } + + boardSchema.loadClass(Board); + + return boardSchema; +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/db/models/Checklists.ts b/backend/plugins/operation_api/src/modules/tasks/db/models/Checklists.ts new file mode 100644 index 0000000000..26a737c080 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/db/models/Checklists.ts @@ -0,0 +1,197 @@ +import { IUserDocument } from 'erxes-api-shared/core-types'; +import { Model } from 'mongoose'; +import { IModels } from '~/connectionResolvers'; +import { + IChecklist, + IChecklistDocument, + IChecklistItem, + IChecklistItemDocument, +} from '~/modules/tasks/@types/checklists'; +import { + checklistItemSchema, + checklistSchema, +} from '~/modules/tasks/db/definitions/checklists'; + +export interface IChecklistModel extends Model { + getChecklist(_id: string): Promise; + createChecklist( + { contentTypeId, ...fields }: IChecklist, + user: IUserDocument, + ): Promise; + updateChecklist(_id: string, doc: IChecklist): Promise; + removeChecklists(contentTypeIds: string[]): Promise; +} + +export const loadChecklistClass = (models: IModels) => { + class Checklist { + /* + * Get a checklist + */ + public static async getChecklist(_id: string) { + const checklist = await models.Checklists.findOne({ _id }); + + if (!checklist) { + throw new Error('Checklist not found'); + } + + return checklist; + } + + /* + * Create new checklist + */ + public static async createChecklist( + { contentTypeId, ...fields }: IChecklist, + user: IUserDocument, + ) { + await models.Checklists.create({ + contentTypeId, + createdUserId: user._id, + ...fields, + }); + } + /* + * Update checklist + */ + public static async updateChecklist(_id: string, doc: IChecklist) { + return await models.Checklists.findOneAndUpdate( + { _id }, + { $set: doc }, + { new: true }, + ); + } + + /* + * Remove checklist + */ + public static async removeChecklists(contentTypeIds: string[]) { + const checklists = await models.Checklists.find({ + contentTypeId: { $in: contentTypeIds }, + }); + + if (checklists && checklists.length === 0) { + return; + } + + const checklistIds = checklists.map((list) => list._id); + + await models.ChecklistItems.deleteMany({ + checklistId: { $in: checklistIds }, + }); + + await models.Checklists.deleteMany({ _id: { $in: checklistIds } }); + } + } + + checklistSchema.loadClass(Checklist); + + return checklistSchema; +}; + +export interface IChecklistItemModel extends Model { + getChecklistItem(_id: string): Promise; + createChecklistItem( + { checklistId, ...fields }: IChecklistItem, + user: IUserDocument, + ): Promise; + updateChecklistItem( + _id: string, + doc: IChecklistItem, + ): Promise; + removeChecklistItem(_id: string): Promise; + updateItemOrder( + _id: string, + destinationOrder: number, + ): Promise; +} + +export const loadChecklistItemClass = (models: IModels) => { + class ChecklistItem { + /* + * Get a checklistItem + */ + public static async getChecklistItem(_id: string) { + const checklistItem = await models.ChecklistItems.findOne({ _id }); + + if (!checklistItem) { + throw new Error('Checklist item not found'); + } + + return checklistItem; + } + + /* + * Create new checklistItem + */ + public static async createChecklistItem( + { checklistId, ...fields }: IChecklistItem, + user: IUserDocument, + ) { + const itemsCount = await models.ChecklistItems.find({ + checklistId, + }).countDocuments(); + + const checklistItem = await models.ChecklistItems.create({ + checklistId, + createdUserId: user._id, + order: itemsCount + 1, + ...fields, + }); + + return checklistItem; + } + + /* + * Update checklistItem + */ + public static async updateChecklistItem(_id: string, doc: IChecklistItem) { + return await models.ChecklistItems.findOneAndUpdate( + { _id }, + { $set: doc }, + { new: true }, + ); + } + + /* + * Remove checklist + */ + public static async removeChecklistItem(_id: string) { + const checklistItem = await models.ChecklistItems.findOneAndDelete({ + _id, + }); + + if (!checklistItem) { + throw new Error(`Checklist's item not found with id ${_id}`); + } + + return checklistItem; + } + + /* + * Update checklistItem order + */ + public static async updateItemOrder(_id: string, destinationOrder: number) { + const currentItem = await models.ChecklistItems.findOne({ _id }).lean(); + + if (!currentItem) { + throw new Error(`ChecklistItems _id = ${_id} not found`); + } + + await models.ChecklistItems.updateOne( + { checklistId: currentItem.checklistId, order: destinationOrder }, + { $set: { order: currentItem.order } }, + ); + + await models.ChecklistItems.updateOne( + { _id }, + { $set: { order: destinationOrder } }, + ); + + return models.ChecklistItems.findOne({ _id }).lean(); + } + } + + checklistItemSchema.loadClass(ChecklistItem); + + return checklistItemSchema; +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/db/models/Labels.ts b/backend/plugins/operation_api/src/modules/tasks/db/models/Labels.ts new file mode 100644 index 0000000000..41f5cb86d9 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/db/models/Labels.ts @@ -0,0 +1,156 @@ +import { Model } from 'mongoose'; +import { IModels } from '~/connectionResolvers'; +import { + IPipelineLabel, + IPipelineLabelDocument, +} from '~/modules/tasks/@types/labels'; +import { pipelineLabelSchema } from '~/modules/tasks/db/definitions/labels'; + +interface IFilter extends IPipelineLabel { + _id?: any; +} + +export interface IPipelineLabelModel extends Model { + getPipelineLabel(_id: string): Promise; + createPipelineLabel(doc: IPipelineLabel): Promise; + updatePipelineLabel( + _id: string, + doc: IPipelineLabel, + ): Promise; + removePipelineLabel(_id: string): void; + validateUniqueness(filter: IFilter, _id?: string): Promise; + labelObject(params: { labelIds: string[]; targetId: string }): void; + labelsLabel(targetId: string, labelIds: string[]): void; +} + +export const loadPipelineLabelClass = (models: IModels) => { + class PipelineLabel { + /* + * Get a pipeline label + */ + public static async getPipelineLabel(_id: string) { + const pipelineLabel = await models.PipelineLabels.findOne({ _id }); + + if (!pipelineLabel) { + throw new Error('Label not found'); + } + + return pipelineLabel; + } + + /** + * Create a pipeline label + */ + public static async createPipelineLabel(doc: IPipelineLabel) { + const filter: IFilter = { + name: doc.name, + pipelineId: doc.pipelineId, + colorCode: doc.colorCode, + }; + + const isUnique = await models.PipelineLabels.validateUniqueness(filter); + + if (!isUnique) { + throw new Error('Label duplicated'); + } + + return models.PipelineLabels.create(doc); + } + + /** + * Update pipeline label + */ + public static async updatePipelineLabel(_id: string, doc: IPipelineLabel) { + const isUnique = await models.PipelineLabels.validateUniqueness( + { ...doc }, + _id, + ); + + if (!isUnique) { + throw new Error('Label duplicated'); + } + + return await models.PipelineLabels.findOneAndUpdate( + { _id }, + { $set: doc }, + { new: true }, + ); + } + + /** + * Remove pipeline label + */ + public static async removePipelineLabel(_id: string) { + const pipelineLabel = await models.PipelineLabels.findOne({ _id }); + + if (!pipelineLabel) { + throw new Error('Label not found'); + } + + // delete labelId from collection that used labelId + await models.Tasks.updateMany( + { labelIds: { $in: [pipelineLabel._id] } }, + { $pull: { labelIds: pipelineLabel._id } }, + ); + + return models.PipelineLabels.findOneAndDelete({ _id }); + } + + /* + * Validates label uniquness + */ + public static async validateUniqueness( + filter: IFilter, + _id?: string, + ): Promise { + if (_id) { + filter._id = { $ne: _id }; + } + + if (await models.PipelineLabels.findOne(filter)) { + return false; + } + + return true; + } + + /* + * Common helper for objects like deal, task and growth hack etc ... + */ + public static async labelObject({ + labelIds, + targetId, + }: { + labelIds: string[]; + targetId: string; + }) { + const prevLabelsCount = await models.PipelineLabels.find({ + _id: { $in: labelIds }, + }).countDocuments(); + + if (prevLabelsCount !== labelIds.length) { + throw new Error('Label not found'); + } + + await models.Tasks.findOneAndUpdate( + { _id: targetId }, + { $set: { labelIds } }, + { new: true }, + ); + } + + /** + * Attach a label + */ + public static async labelsLabel(targetId: string, labelIds: string[]) { + await models.PipelineLabels.labelObject({ + labelIds, + targetId, + }); + } + } + + pipelineLabelSchema.loadClass(PipelineLabel); + + return pipelineLabelSchema; +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/db/models/Pipelines.ts b/backend/plugins/operation_api/src/modules/tasks/db/models/Pipelines.ts new file mode 100644 index 0000000000..c736240315 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/db/models/Pipelines.ts @@ -0,0 +1,159 @@ +import { IOrderInput } from 'erxes-api-shared/core-types'; +import { updateOrder } from 'erxes-api-shared/utils'; +import { Model } from 'mongoose'; +import { IModels } from '~/connectionResolvers'; +import { IPipeline, IPipelineDocument } from '~/modules/tasks/@types/pipelines'; +import { IStageDocument } from '~/modules/tasks/@types/stages'; +import { TASK_STATUSES } from '~/modules/tasks/constants'; +import { pipelineSchema } from '~/modules/tasks/db/definitions/pipelines'; +import { + createOrUpdatePipelineStages, + generateLastNum, + removeStages, +} from '~/modules/tasks/db/models/utils'; + +export interface IPipelineModel extends Model { + getPipeline(_id: string): Promise; + createPipeline( + doc: IPipeline, + stages?: IStageDocument[], + ): Promise; + updatePipeline( + _id: string, + doc: IPipeline, + stages?: IStageDocument[], + ): Promise; + updateOrder(orders: IOrderInput[]): Promise; + watchPipeline(_id: string, isAdd: boolean, userId: string): void; + removePipeline(_id: string, checked?: boolean): object; + archivePipeline(_id: string, status?: string): object; +} + +export const loadPipelineClass = (models: IModels) => { + class Pipeline { + /* + * Get a pipeline + */ + public static async getPipeline(_id: string) { + const pipeline = await models.Pipelines.findOne({ _id }).lean(); + + if (!pipeline) { + throw new Error('Pipeline not found'); + } + + return pipeline; + } + + /** + * Create a pipeline + */ + public static async createPipeline( + doc: IPipeline, + stages?: IStageDocument[], + ) { + if (doc.numberSize) { + doc.lastNum = await generateLastNum(models, doc); + } + + const pipeline = await models.Pipelines.create(doc); + + await createOrUpdatePipelineStages(models, pipeline._id, stages); + + return pipeline; + } + + /** + * Update a pipeline + */ + public static async updatePipeline( + _id: string, + doc: IPipeline, + stages?: IStageDocument[], + ) { + await createOrUpdatePipelineStages(models, _id, stages); + + if (doc.numberSize) { + const pipeline = await models.Pipelines.getPipeline(_id); + + if (pipeline.numberConfig !== doc.numberConfig) { + doc.lastNum = await generateLastNum(models, doc); + } + } + + return await models.Pipelines.findOneAndUpdate( + { _id }, + { $set: doc }, + { new: true }, + ); + } + + /** + * Remove a pipeline + */ + public static async removePipeline(_id: string, checked?: boolean) { + const pipeline = await models.Pipelines.getPipeline(_id); + + if (!checked) { + await removeStages(models, [pipeline._id]); + } + + return await models.Pipelines.findOneAndDelete({ _id }); + } + + /* + * Update given pipelines orders + */ + public static async updateOrder(orders: IOrderInput[]) { + return updateOrder(models.Pipelines, orders); + } + + /** + * Archive a pipeline + */ + public static async archivePipeline(_id: string) { + const pipeline = await models.Pipelines.getPipeline(_id); + + const status = + pipeline.status === TASK_STATUSES.ACTIVE + ? TASK_STATUSES.ARCHIVED + : TASK_STATUSES.ACTIVE; + + return await models.Pipelines.findOneAndUpdate( + { _id }, + { $set: { status } }, + { new: true }, + ); + } + + /** + * Watch a pipeline + */ + public static async watchPipeline( + _id: string, + isAdd: boolean, + userId: string, + ) { + const pipeline = await models.Pipelines.getPipeline(_id); + + const watchedUserIds = pipeline.watchedUserIds || []; + + if (isAdd) { + watchedUserIds.push(userId); + } else { + const index = watchedUserIds.indexOf(userId); + + watchedUserIds.splice(index, 1); + } + + return await models.Pipelines.findOneAndUpdate( + { _id }, + { $set: { watchedUserIds } }, + { new: true }, + ); + } + } + + pipelineSchema.loadClass(Pipeline); + + return pipelineSchema; +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/db/models/Stages.ts b/backend/plugins/operation_api/src/modules/tasks/db/models/Stages.ts new file mode 100644 index 0000000000..430307ff26 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/db/models/Stages.ts @@ -0,0 +1,104 @@ +import { IOrderInput } from 'erxes-api-shared/core-types'; +import { sendTRPCMessage, updateOrder } from 'erxes-api-shared/utils'; +import { Model } from 'mongoose'; +import { IModels } from '~/connectionResolvers'; +import { IStage, IStageDocument } from '~/modules/tasks/@types/stages'; +import { stageSchema } from '~/modules/tasks/db/definitions/stages'; +import { removeTasks } from '~/modules/tasks/db/models/utils'; + +export interface IStageModel extends Model { + getStage(_id: string): Promise; + createStage(doc: IStage): Promise; + removeStage(_id: string): object; + updateStage(_id: string, doc: IStage): Promise; + updateOrder(orders: IOrderInput[]): Promise; + checkCodeDuplication(code: string): string; +} + +export const loadStageClass = (models: IModels) => { + class Stage { + /* + * Get a stage + */ + public static async getStage(_id: string) { + const stage = await models.Stages.findOne({ _id }); + + if (!stage) { + throw new Error('Stage not found'); + } + + return stage; + } + /** + * Create a stage + */ + public static async createStage(doc: IStage) { + if (doc.code) { + await this.checkCodeDuplication(doc.code); + } + return models.Stages.create(doc); + } + + /** + * Update Stage + */ + public static async updateStage(_id: string, doc: IStage) { + if (doc.code) { + await this.checkCodeDuplication(doc.code); + } + + return await models.Stages.findOneAndUpdate( + { _id }, + { $set: doc }, + { new: true }, + ); + } + + /** + * Remove a stage + */ + public static async removeStage(_id: string) { + const stage = await models.Stages.getStage(_id); + + await removeTasks(models, [_id]); + + if (stage.formId) { + await sendTRPCMessage({ + pluginName: 'core', + method: 'mutation', + module: 'forms', + action: 'removeForm', + input: { + formId: stage.formId, + }, + }); + } + + return await models.Stages.findOneAndDelete({ _id }); + } + + /* + * Update given stages orders + */ + public static async updateOrder(orders: IOrderInput[]) { + return updateOrder(models.Stages, orders); + } + + /** + * Check code duplication + */ + static async checkCodeDuplication(code: string) { + const stage = await models.Stages.findOne({ + code, + }); + + if (stage) { + throw new Error('Code must be unique'); + } + } + } + + stageSchema.loadClass(Stage); + + return stageSchema; +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/db/models/Tasks.ts b/backend/plugins/operation_api/src/modules/tasks/db/models/Tasks.ts index dc144a9194..8791bebe05 100644 --- a/backend/plugins/operation_api/src/modules/tasks/db/models/Tasks.ts +++ b/backend/plugins/operation_api/src/modules/tasks/db/models/Tasks.ts @@ -1,64 +1,171 @@ +import { ITask, ITaskDocument } from '@/tasks/@types/tasks'; +import { tasksSchema } from '@/tasks/db/definitions/tasks'; import { Model } from 'mongoose'; import { IModels } from '~/connectionResolvers'; -import { tasksSchema } from '@/tasks/db/definitions/tasks'; -import { ITasks, ITasksDocument } from '@/tasks/@types/tasks'; - -export interface ITasksModel extends Model { - getTasks(_id: string): Promise; - getTaskss(): Promise; - createTasks(doc: ITasks): Promise; - updateTasks(_id: string, doc: ITasks): Promise; - removeTasks(TasksId: string): Promise<{ ok: number }>; +import { + boardNumberGenerator, + fillSearchTextItem, +} from '~/modules/tasks/db/models/utils'; + +export interface ITaskModel extends Model { + getTask(_id: string): Promise; + createTask(doc: ITask): Promise; + updateTask(_id: string, doc: ITask): Promise; + removeTask(_id: string): Promise<{ ok: number }>; + updateTimeTracking( + _id: string, + status: string, + timeSpent: number, + startDate?: string, + ): Promise; + watchTask(_id: string, isAdd: boolean, userId: string): void; + removeTasks(_ids: string[]): Promise<{ n: number; ok: number }>; } -export const loadTasksClass = (models: IModels) => { - class Tasks { +export const loadTaskClass = (models: IModels) => { + class Task { + /** + * Retreives Task + */ + public static async getTask(_id: string) { + const task = await models.Tasks.findOne({ _id }); + + if (!task) { + throw new Error('Task not found'); + } + + return task; + } + /** - * Retrieves operation + * Create a Task */ - public static async getTasks(_id: string) { - const Tasks = await models.Tasks.findOne({ _id }).lean(); + public static async createTask(doc: ITask) { + if (doc.sourceConversationIds) { + const convertedTask = await models.Tasks.findOne({ + sourceConversationIds: { $in: doc.sourceConversationIds }, + }); + + if (convertedTask) { + throw new Error('Already converted a task'); + } + } + + const stage = await models.Stages.getStage(doc.stageId); + const pipeline = await models.Pipelines.getPipeline(stage.pipelineId); + + if (pipeline.numberSize) { + const { numberSize, numberConfig = '' } = pipeline; + + const number = await boardNumberGenerator( + models, + numberConfig, + numberSize, + false, + pipeline.type, + ); + + doc.number = number; + } + + const task = await models.Tasks.create({ + ...doc, + createdAt: new Date(), + modifiedAt: new Date(), + stageChangedDate: new Date(), + searchText: fillSearchTextItem(doc), + }); - if (!Tasks) { - throw new Error('Tasks not found'); + // update numberConfig of the same configed pipelines + if (doc.number) { + await models.Pipelines.updateMany( + { + numberConfig: pipeline.numberConfig, + type: pipeline.type, + }, + { $set: { lastNum: doc.number } }, + ); } - return Tasks; + return task; } /** - * Retrieves all operations + * Update Task */ - public static async getTaskss(): Promise { - return models.Tasks.find().lean(); + public static async updateTask(_id: string, doc: ITask) { + const searchText = fillSearchTextItem( + doc, + await models.Tasks.getTask(_id), + ); + + return await models.Tasks.findOneAndUpdate( + { _id }, + { $set: { ...doc, searchText } }, + { new: true }, + ); } /** - * Create a operation + * Remove Tasks */ - public static async createTasks(doc: ITasks): Promise { - return models.Tasks.create(doc); + public static async removeTasks(_ids: string[]) { + // completely remove all related things + await models.Checklists.removeChecklists(_ids); + + return await models.Tasks.deleteMany({ _id: { $in: _ids } }); } - /* - * Update operation + /** + * Watch task */ - public static async updateTasks(_id: string, doc: ITasks) { + public static async watchTask(_id: string, isAdd: boolean, userId: string) { + const task = await models.Tasks.getTask(_id); + + const watchedUserIds = task.watchedUserIds || []; + + if (isAdd) { + watchedUserIds.push(userId); + } else { + const index = watchedUserIds.indexOf(userId); + + watchedUserIds.splice(index, 1); + } + return await models.Tasks.findOneAndUpdate( { _id }, - { $set: { ...doc } }, + { $set: { watchedUserIds } }, + { new: true }, ); } /** - * Remove operation + * Update time tracking */ - public static async removeTasks(TasksId: string[]) { - return models.Tasks.deleteOne({ _id: { $in: TasksId } }); + public static async updateTimeTracking( + _id: string, + status: string, + timeSpent: number, + startDate?: string, + ) { + const doc: { status: string; timeSpent: number; startDate?: string } = { + status, + timeSpent, + }; + + if (startDate) { + doc.startDate = startDate; + } + + return await models.Tasks.findOneAndUpdate( + { _id }, + { $set: { timeTrack: doc } }, + { new: true }, + ); } } - tasksSchema.loadClass(Tasks); + tasksSchema.loadClass(Task); return tasksSchema; }; diff --git a/backend/plugins/operation_api/src/modules/tasks/db/models/utils.ts b/backend/plugins/operation_api/src/modules/tasks/db/models/utils.ts new file mode 100644 index 0000000000..71372e1dcb --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/db/models/utils.ts @@ -0,0 +1,206 @@ +import { validSearchText } from 'erxes-api-shared/utils'; +import { DeleteResult } from 'mongoose'; +import { IModels } from '~/connectionResolvers'; +import { IPipeline } from '~/modules/tasks/@types/pipelines'; +import { IStage, IStageDocument } from '~/modules/tasks/@types/stages'; +import { ITask } from '~/modules/tasks/@types/tasks'; +import { configReplacer } from '~/modules/tasks/utils'; + +const numberCalculator = (size: number, num?: any, skip?: boolean) => { + if (num && !skip) { + num = parseInt(num, 10) + 1; + } + + if (skip) { + num = 0; + } + + num = num.toString(); + + while (num.length < size) { + num = '0' + num; + } + + return num; +}; + +export const boardNumberGenerator = async ( + models: IModels, + config: string, + size: string, + skip: boolean, + type?: string, +) => { + const replacedConfig = await configReplacer(config); + const re = replacedConfig + '[0-9]+$'; + + let number; + + if (!skip) { + const pipeline = await models.Pipelines.findOne({ + lastNum: new RegExp(re), + type, + }); + + if (pipeline?.lastNum) { + const lastNum = pipeline.lastNum; + + const lastGeneratedNumber = lastNum.slice(replacedConfig.length); + + number = + replacedConfig + + (await numberCalculator(parseInt(size, 10), lastGeneratedNumber)); + + return number; + } + } + + number = + replacedConfig + (await numberCalculator(parseInt(size, 10), '', skip)); + + return number; +}; + +export const generateLastNum = async (models: IModels, doc: IPipeline) => { + const replacedConfig = await configReplacer(doc.numberConfig); + const re = replacedConfig + '[0-9]+$'; + + const pipeline = await models.Pipelines.findOne({ + lastNum: new RegExp(re), + }); + + if (pipeline) { + return pipeline.lastNum; + } + + const task = await models.Tasks.findOne({ + number: new RegExp(re), + }).sort({ createdAt: -1 }); + + if (task) { + return task.number; + } + + // generate new number by new numberConfig + const generatedNum = await boardNumberGenerator( + models, + doc.numberConfig || '', + doc.numberSize || '', + true, + ); + + return generatedNum; +}; + +export const fillSearchTextItem = (doc: ITask, item?: ITask) => { + const document = item || { name: '', description: '' }; + Object.assign(document, doc); + + return validSearchText([document.name || '', document.description || '']); +}; + +export const removeTasks = async (models: IModels, stageIds: string[]) => { + const taskIds = await models.Tasks.find({ + stageId: { $in: stageIds }, + }).distinct('_id'); + + await models.Checklists.removeChecklists(taskIds); + + await models.Tasks.deleteMany({ _id: { $in: taskIds } }); +}; + +export const removeStages = async (models: IModels, pipelineIds: string[]) => { + const stageIds = await models.Stages.find({ + pipelineId: { $in: pipelineIds }, + }).distinct('_id'); + + await removeTasks(models, stageIds); + + await models.Stages.deleteMany({ _id: { $in: stageIds } }); +}; + +export const removePipelines = async (models: IModels, boardIds: string[]) => { + const pipelineIds = await models.Pipelines.find({ + boardId: { $in: boardIds }, + }).distinct('_id'); + + await removeStages(models, pipelineIds); + + await models.Pipelines.deleteMany({ _id: { $in: pipelineIds } }); +}; + +export const createOrUpdatePipelineStages = async ( + models: IModels, + pipelineId: string, + stages?: IStageDocument[], +): Promise => { + if (!stages) { + return models.Stages.deleteMany({ pipelineId }); + } + + const validStageIds: string[] = []; + + const bulkOpsPrevEntry: Array<{ + updateOne: { + filter: { _id: string }; + update: { $set: IStage }; + }; + }> = []; + + const stageIds = stages.map((stage) => stage._id); + + // fetch stage from database + const inStageIds = await models.Stages.find({ + _id: { $in: stageIds }, + }).distinct('_id'); + + const outStageIds = await models.Stages.find({ + pipelineId, + _id: { $nin: stageIds }, + }).distinct('_id'); + + await models.Tasks.deleteMany({ stageId: { $in: outStageIds } }); + + await models.Stages.deleteMany({ pipelineId, _id: { $nin: stageIds } }); + + let order = 0; + + for (const stage of stages) { + order++; + + const doc: any = { ...stage, order, pipelineId }; + + const _id = doc._id; + + const validStage = inStageIds.includes(_id); + + // edit + if (validStage) { + validStageIds.push(_id); + + bulkOpsPrevEntry.push({ + updateOne: { + filter: { + _id, + }, + update: { + $set: doc, + }, + }, + }); + // create + } else { + doc._id = undefined; + + const stage = await models.Stages.createStage(doc); + + validStageIds.push(stage._id); + } + } + + if (bulkOpsPrevEntry.length > 0) { + await models.Stages.bulkWrite(bulkOpsPrevEntry); + } + + return models.Stages.deleteMany({ pipelineId, _id: { $nin: validStageIds } }); +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/board.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/board.ts new file mode 100644 index 0000000000..0fa3234a8f --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/board.ts @@ -0,0 +1,88 @@ +import { sendTRPCMessage } from 'erxes-api-shared/utils'; +import { IContext } from '~/connectionResolvers'; +import { IBoardDocument } from '~/modules/tasks/@types/boards'; + +export default { + pipelines: async ( + board: IBoardDocument, + _args: undefined, + { user, models }: IContext, + ) => { + if (board.pipelines) { + return board.pipelines; + } + + if (user.isOwner) { + return models.Pipelines.find({ + boardId: board._id, + status: { $ne: 'archived' }, + }).lean(); + } + + const userDetail = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'users', + action: 'findOne', + input: { + _id: user._id, + }, + defaultValue: {}, + }); + + const userDepartmentIds = userDetail?.departmentIds || []; + const branchIds = userDetail?.branchIds || []; + + const supervisorDepartmentIds = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'departments', + action: 'findWithChild', + input: { + query: { + supervisorId: user._id, + }, + fields: { + _id: 1, + }, + }, + defaultValue: [], + }); + + const departmentIds = [ + ...userDepartmentIds, + ...(supervisorDepartmentIds.map((x: any) => x._id) || []), + ]; + + const query: any = { + $and: [ + { status: { $ne: 'archived' } }, + { boardId: board._id }, + { + $or: [ + { visibility: 'public' }, + { + visibility: 'private', + $or: [{ memberIds: { $in: [user._id] } }, { userId: user._id }], + }, + ], + }, + ], + }; + + if (departmentIds.length > 0) { + query.$and[2].$or.push({ + $and: [ + { visibility: 'private' }, + { departmentIds: { $in: departmentIds } }, + ], + }); + } + if (branchIds.length > 0) { + query.$and[2].$or.push({ + $and: [{ visibility: 'private' }, { branchIds: { $in: branchIds } }], + }); + } + return models.Pipelines.find(query).lean(); + }, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/checklist.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/checklist.ts new file mode 100644 index 0000000000..da0e6bcaa1 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/checklist.ts @@ -0,0 +1,26 @@ +import { IContext } from '~/connectionResolvers'; +import { IChecklistDocument } from '~/modules/tasks/@types/checklists'; + +export default { + async items(checklist: IChecklistDocument, _args, { models }: IContext) { + return models.ChecklistItems.find({ checklistId: checklist._id }).sort({ + order: 1, + }); + }, + + async percent(checklist: IChecklistDocument, _args, { models }: IContext) { + const items = await models.ChecklistItems.find({ + checklistId: checklist._id, + }); + + if (items.length === 0) { + return 0; + } + + const checkedItems = items.filter((item) => { + return item.isChecked; + }); + + return (checkedItems.length / items.length) * 100; + }, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/index.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/index.ts new file mode 100644 index 0000000000..f6e03104ed --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/index.ts @@ -0,0 +1,8 @@ +import TasksBoard from './board'; + +import TasksChecklist from './checklist'; +import TasksPipeline from './pipeline'; +import TasksStage from './stage'; +import Task from './tasks'; + +export default { Task, TasksBoard, TasksChecklist, TasksPipeline, TasksStage }; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/pipeline.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/pipeline.ts new file mode 100644 index 0000000000..d44be24b7e --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/pipeline.ts @@ -0,0 +1,75 @@ +import { IContext } from '~/connectionResolvers'; +import { IPipelineDocument } from '~/modules/tasks/@types/pipelines'; +import { VISIBILITIES } from '~/modules/tasks/constants'; +import { generateFilter } from '~/modules/tasks/graphql/resolvers/utils'; + +export default { + createdUser(pipeline: IPipelineDocument) { + if (!pipeline.userId) { + return; + } + + return { __typename: 'User', _id: pipeline.userId }; + }, + + members(pipeline: IPipelineDocument) { + if (pipeline.visibility === VISIBILITIES.PRIVATE && pipeline.memberIds) { + return pipeline.memberIds.map((memberId) => ({ + __typename: 'User', + _id: memberId, + })); + } + + return []; + }, + + isWatched(pipeline: IPipelineDocument, _args, { user }: IContext) { + const watchedUserIds = pipeline.watchedUserIds || []; + + if (watchedUserIds.includes(user._id)) { + return true; + } + + return false; + }, + + state(pipeline: IPipelineDocument) { + if (pipeline.startDate && pipeline.endDate) { + const now = new Date().getTime(); + + const startDate = new Date(pipeline.startDate).getTime(); + const endDate = new Date(pipeline.endDate).getTime(); + + if (now > endDate) { + return 'Completed'; + } else if (now < endDate && now > startDate) { + return 'In progress'; + } else { + return 'Not started'; + } + } + + return ''; + }, + + async itemsTotalCount( + pipeline: IPipelineDocument, + _args, + { user, models }: IContext, + ) { + const filter = await generateFilter(models, user._id, { + pipelineId: pipeline._id, + }); + + return models.Tasks.find(filter).countDocuments(); + }, + + async tag(pipeline: IPipelineDocument) { + if (pipeline.tagId) { + return { + __typename: 'Tag', + _id: pipeline.tagId, + }; + } + }, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/stage.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/stage.ts new file mode 100644 index 0000000000..03d2c57d9e --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/stage.ts @@ -0,0 +1,243 @@ +import { IContext } from '~/connectionResolvers'; +import { IStageDocument } from '~/modules/tasks/@types/stages'; +import { TASK_STATUSES, VISIBILITIES } from '~/modules/tasks/constants'; +import { + generateFilter, + getAmountsMap, +} from '~/modules/tasks/graphql/resolvers/utils'; + +export default { + async __resolveReference({ _id }, { models }: IContext) { + return models.Stages.findOne({ _id }); + }, + + members(stage: IStageDocument) { + if (stage.visibility === VISIBILITIES.PRIVATE && stage.memberIds) { + return stage.memberIds.map((memberId) => ({ + __typename: 'User', + _id: memberId, + })); + } + + return []; + }, + + async unUsedAmount( + stage: IStageDocument, + _args, + { user, models }: IContext, + { variableValues: args }, + ) { + const amountsMap = getAmountsMap( + models, + models.Tasks, + user, + args, + stage, + false, + ); + + return amountsMap; + }, + + async amount( + stage: IStageDocument, + _args, + { user, models }: IContext, + { variableValues: args }, + ) { + const amountsMap = getAmountsMap(models, models.Tasks, user, args, stage); + + return amountsMap; + }, + + async itemsTotalCount( + stage: IStageDocument, + _args, + { user, models }: IContext, + { variableValues: args }, + ) { + const filter = await generateFilter( + models, + user._id, + { ...args, stageId: stage._id, pipelineId: stage.pipelineId }, + args.extraParams, + ); + + return models.Tasks.find(filter).countDocuments(); + }, + + /* + * Total count of task that are created on this stage initially + */ + async initialTaskTotalCount( + stage: IStageDocument, + _args, + { user, models }: IContext, + { variableValues: args }, + ) { + const filter = await generateFilter( + models, + user._id, + { ...args, initialStageId: stage._id }, + args.extraParams, + ); + + return models.Tasks.find(filter).countDocuments(); + }, + + /* + * Total count of tasks that are + * 1. created on this stage initially + * 2. moved to other stage which has probability other than Lost + */ + async inProcessTasksTotalCount( + stage: IStageDocument, + _args, + { models: { Stages } }: IContext, + ) { + const filter = { + pipelineId: stage.pipelineId, + probability: { $ne: 'Lost' }, + _id: { $ne: stage._id }, + }; + + const tasks = await Stages.aggregate([ + { + $match: filter, + }, + { + $lookup: { + from: 'tasks', + let: { stageId: '$_id' }, + pipeline: [ + { + $match: { + $expr: { + $and: [ + { $eq: ['$stageId', '$$stageId'] }, + { $ne: ['$status', TASK_STATUSES.ARCHIVED] }, + ], + }, + }, + }, + ], + as: 'tasks', + }, + }, + { + $project: { + name: 1, + tasks: 1, + }, + }, + { + $unwind: '$tasks', + }, + { + $match: { + 'tasks.initialStageId': stage._id, + }, + }, + ]); + + return tasks.length; + }, + + async stayedTasksTotalCount( + stage: IStageDocument, + _args, + { user, models }: IContext, + { variableValues: args }, + ) { + const filter = await generateFilter( + models, + user._id, + { + ...args, + initialStageId: stage._id, + stageId: stage._id, + pipelineId: stage.pipelineId, + }, + args.extraParams, + ); + + return models.Tasks.find(filter).countDocuments(); + }, + + async compareNextStageTask( + stage: IStageDocument, + _args, + { models: { Stages } }: IContext, + ) { + const result: { count?: number; percent?: number } = {}; + + const { order = 1 } = stage; + + const filter = { + order: { $in: [order, order + 1] }, + probability: { $ne: 'Lost' }, + pipelineId: stage.pipelineId, + }; + + const stages = await Stages.aggregate([ + { + $match: filter, + }, + { + $lookup: { + from: 'tasks', + let: { stageId: '$_id' }, + pipeline: [ + { + $match: { + $expr: { + $and: [ + { $eq: ['$stageId', '$$stageId'] }, + { $ne: ['$status', TASK_STATUSES.ARCHIVED] }, + ], + }, + }, + }, + ], + as: 'currentTasks', + }, + }, + { + $lookup: { + from: 'tasks', + let: { stageId: '$_id' }, + pipeline: [ + { + $match: { + $expr: { + $and: [ + { $eq: ['$initialStageId', '$$stageId'] }, + { $ne: ['$status', TASK_STATUSES.ARCHIVED] }, + ], + }, + }, + }, + ], + as: 'initialTasks', + }, + }, + { + $project: { + order: 1, + currentTaskCount: { $size: '$currentTasks' }, + initialTaskCount: { $size: '$initialTasks' }, + }, + }, + { $sort: { order: 1 } }, + ]); + + if (stages.length === 2) { + const [first, second] = stages; + result.count = first.currentTaskCount - second.currentTaskCount; + result.percent = (second.initialTaskCount * 100) / first.initialTaskCount; + } + + return result; + }, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/tasks.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/tasks.ts index baafe1ddde..95c409706e 100644 --- a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/tasks.ts +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/tasks.ts @@ -1,6 +1,158 @@ -export const Tasks = { - async description() { - return 'Tasks description'; - }, - }; - \ No newline at end of file +import { sendTRPCMessage } from 'erxes-api-shared/utils'; +import { IContext } from '~/connectionResolvers'; +import { ITaskDocument } from '~/modules/tasks/@types/tasks'; + +export default { + async __resolveReference({ _id }, { models }: IContext) { + return models.Tasks.findOne({ _id }); + }, + + async companies( + task: ITaskDocument, + _args, + _context: IContext, + { isSubscription }, + ) { + if (!task.companyIds?.length) { + return []; + } + + const activeCompanies = await sendTRPCMessage({ + pluginName: 'core', + module: 'companies', + action: 'findActiveCompanies', + input: { + selector: { _id: { $in: task.companyIds } }, + }, + defaultValue: [], + }); + + if (isSubscription) { + return activeCompanies; + } + + return (activeCompanies || []).map(({ _id }) => ({ + __typename: 'Company', + _id, + })); + }, + + createdUser(task: ITaskDocument) { + if (!task.userId) { + return; + } + + return { __typename: 'User', _id: task.userId }; + }, + + async customers( + task: ITaskDocument, + _args, + _context: IContext, + { isSubscription }, + ) { + if (!task.customerIds?.length) { + return []; + } + + const customers = await sendTRPCMessage({ + pluginName: 'core', + module: 'customers', + action: 'findActiveCustomers', + input: { + selector: { + _id: { $in: task.customerIds }, + }, + }, + defaultValue: [], + }); + + if (isSubscription) { + return customers; + } + + return (customers || []).map(({ _id }) => ({ + __typename: 'Customer', + _id, + })); + }, + + async assignedUsers( + task: ITaskDocument, + _args, + _context: IContext, + { isSubscription }, + ) { + if (isSubscription && task.assignedUserIds?.length) { + return sendTRPCMessage({ + pluginName: 'core', + module: 'users', + action: 'find', + input: { + query: { + _id: { $in: task.assignedUserIds }, + }, + }, + defaultValue: [], + }); + } + + return (task.assignedUserIds || []) + .filter((e) => e) + .map((_id) => ({ + __typename: 'User', + _id, + })); + }, + + async pipeline(task: ITaskDocument, _args, { models }: IContext) { + const stage = await models.Stages.getStage(task.stageId); + + return models.Pipelines.findOne({ _id: stage.pipelineId }); + }, + + async boardId(task: ITaskDocument, _args, { models }: IContext) { + const stage = await models.Stages.getStage(task.stageId); + const pipeline = await models.Pipelines.getPipeline(stage.pipelineId); + const board = await models.Boards.getBoard(pipeline.boardId); + + return board._id; + }, + + async stage(task: ITaskDocument, _args, { models }: IContext) { + return models.Stages.getStage(task.stageId); + }, + + async isWatched(task: ITaskDocument, _args, { user }: IContext) { + const watchedUserIds = task.watchedUserIds || []; + + if (watchedUserIds.includes(user._id)) { + return true; + } + + return false; + }, + + // async hasNotified(task: ITaskDocument, _args, { user }: IContext) { + // return sendNotificationsMessage({ + // subdomain, + // action: 'checkIfRead', + // data: { + // userId: user._id, + // itemId: task._id, + // }, + // isRPC: true, + // defaultValue: true, + // }); + // }, + + async tags(task: ITaskDocument) { + return (task.tagIds || []) + .filter((_id) => !!_id) + .map((_id) => ({ __typename: 'Tag', _id })); + }, + + async labels(task: ITaskDocument, _args, { models }: IContext) { + return models.PipelineLabels.find({ _id: { $in: task.labelIds || [] } }); + }, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/board.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/board.ts new file mode 100644 index 0000000000..90063b6336 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/board.ts @@ -0,0 +1,67 @@ +import { sendTRPCMessage } from 'erxes-api-shared/utils'; +import { IContext } from '~/connectionResolvers'; +import { IBoard } from '~/modules/tasks/@types/boards'; + +export const boardMutations = { + /** + * Create new board + */ + async tasksBoardsAdd(_root, doc: IBoard, { models }: IContext) { + return await models.Boards.createBoard(doc); + }, + + /** + * Edit board + */ + async tasksBoardsEdit( + _root, + { _id, ...doc }: IBoard & { _id: string }, + { models }: IContext, + ) { + return await models.Boards.updateBoard(_id, doc); + }, + + /** + * Remove board + */ + async tasksBoardsRemove( + _root, + { _id }: { _id: string }, + { models }: IContext, + ) { + const board = await models.Boards.getBoard(_id); + + const removed = await models.Boards.removeBoard(_id); + + const relatedFieldsGroups = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'fieldsGroups', + action: 'find', + input: { + query: { + boardIds: board._id, + }, + }, + defaultValue: [], + }); + + for (const fieldGroup of relatedFieldsGroups) { + const boardIds = fieldGroup.boardIds || []; + fieldGroup.boardIds = boardIds.filter((e) => e !== board._id); + + await sendTRPCMessage({ + pluginName: 'core', + method: 'mutation', + module: 'fieldsGroups', + action: 'updateGroup', + input: { + groupId: fieldGroup._id, + fieldGroup, + }, + }); + } + + return removed; + }, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/checklist.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/checklist.ts new file mode 100644 index 0000000000..cf26b67853 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/checklist.ts @@ -0,0 +1,129 @@ +import { graphqlPubsub } from 'erxes-api-shared/utils'; +import { IContext } from '~/connectionResolvers'; +import { IChecklist, IChecklistItem } from '~/modules/tasks/@types/checklists'; + +const checklistsChanged = (checklist: IChecklist & { _id: string }) => { + graphqlPubsub.publish( + `tasksChecklistsChanged:${checklist.contentType}:${checklist.contentTypeId}`, + { + tasksChecklistsChanged: { + _id: checklist._id, + contentType: checklist.contentType, + contentTypeId: checklist.contentTypeId, + }, + }, + ); +}; + +const tasksChecklistDetailChanged = (_id: string) => { + graphqlPubsub.publish(`tasksChecklistDetailChanged:${_id}`, { + tasksChecklistDetailChanged: { + _id, + }, + }); +}; + +export const checklistMutations = { + /** + * Adds checklist object and also adds an activity log + */ + async tasksChecklistsAdd( + _root, + args: IChecklist, + { models, user }: IContext, + ) { + const checklist = await models.Checklists.createChecklist(args, user); + + checklistsChanged(checklist); + + return checklist; + }, + + /** + * Updates checklist object + */ + async tasksChecklistsEdit( + _root, + { _id, ...doc }: IChecklist & { _id: string }, + { models }: IContext, + ) { + const updated = await models.Checklists.updateChecklist(_id, doc); + + tasksChecklistDetailChanged(_id); + + return updated; + }, + + /** + * Removes a checklist + */ + async tasksChecklistsRemove( + _root, + { _id }: { _id: string }, + { models }: IContext, + ) { + const checklist = await models.Checklists.getChecklist(_id); + const removed = await models.Checklists.removeChecklists([_id]); + + checklistsChanged(checklist); + + return removed; + }, + + /** + * Adds a checklist item and also adds an activity log + */ + async tasksChecklistItemsAdd( + _root, + args: IChecklistItem, + { user, models }: IContext, + ) { + const checklistItem = await models.ChecklistItems.createChecklistItem( + args, + user, + ); + + tasksChecklistDetailChanged(checklistItem.checklistId); + + return checklistItem; + }, + + /** + * Updates a checklist item + */ + async tasksChecklistItemsEdit( + _root, + { _id, ...doc }: IChecklistItem & { _id: string }, + { models }: IContext, + ) { + const updated = await models.ChecklistItems.updateChecklistItem(_id, doc); + + tasksChecklistDetailChanged(updated.checklistId); + + return updated; + }, + + /** + * Removes a checklist item + */ + async tasksChecklistItemsRemove( + _root, + { _id }: { _id: string }, + { models }: IContext, + ) { + const checklistItem = await models.ChecklistItems.getChecklistItem(_id); + const removed = await models.ChecklistItems.removeChecklistItem(_id); + + tasksChecklistDetailChanged(checklistItem.checklistId); + + return removed; + }, + + async tasksChecklistItemsOrder( + _root, + { _id, destinationIndex }: { _id: string; destinationIndex: number }, + { models }: IContext, + ) { + return models.ChecklistItems.updateItemOrder(_id, destinationIndex); + }, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/index.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/index.ts new file mode 100644 index 0000000000..3026d2ab3f --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/index.ts @@ -0,0 +1,15 @@ +import { boardMutations } from './board'; +import { checklistMutations } from './checklist'; +import { pipelineLabelMutations } from './label'; +import { pipelineMutations } from './pipeline'; +import { stageMutations } from './stage'; +import { taskMutations } from './task'; + +export const mutations = { + ...boardMutations, + ...pipelineMutations, + ...stageMutations, + ...taskMutations, + ...checklistMutations, + ...pipelineLabelMutations, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/label.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/label.ts new file mode 100644 index 0000000000..8d3eeab939 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/label.ts @@ -0,0 +1,51 @@ +import { IContext } from '~/connectionResolvers'; +import { IPipelineLabel } from '~/modules/tasks/@types/labels'; + +export const pipelineLabelMutations = { + /** + * Creates a new pipeline label + */ + async tasksPipelineLabelsAdd( + _root, + { ...doc }: IPipelineLabel, + { user, models }: IContext, + ) { + return await models.PipelineLabels.createPipelineLabel({ + createdBy: user._id, + ...doc, + }); + }, + + /** + * Edit pipeline label + */ + async tasksPipelineLabelsEdit( + _root, + { _id, ...doc }: IPipelineLabel & { _id: string }, + { models }: IContext, + ) { + return await models.PipelineLabels.updatePipelineLabel(_id, doc); + }, + + /** + * Remove pipeline label + */ + async tasksPipelineLabelsRemove( + _root, + { _id }: { _id: string }, + { models }: IContext, + ) { + return await models.PipelineLabels.removePipelineLabel(_id); + }, + + /** + * Attach a label + */ + async tasksPipelineLabelsLabel( + _root, + { targetId, labelIds }: { targetId: string; labelIds: string[] }, + { models }: IContext, + ) { + return models.PipelineLabels.labelsLabel(targetId, labelIds); + }, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/pipeline.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/pipeline.ts new file mode 100644 index 0000000000..de547393b7 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/pipeline.ts @@ -0,0 +1,155 @@ +import { IOrderInput } from 'erxes-api-shared/core-types'; +import { sendTRPCMessage } from 'erxes-api-shared/utils'; +import { IContext } from '~/connectionResolvers'; +import { IPipeline } from '~/modules/tasks/@types/pipelines'; +import { IStageDocument } from '~/modules/tasks/@types/stages'; +import { checkNumberConfig } from '~/modules/tasks/graphql/resolvers/utils'; + +export const pipelineMutations = { + /** + * Create new pipeline + */ + async tasksPipelinesAdd( + _root, + { stages, ...doc }: IPipeline & { stages: IStageDocument[] }, + { user, models }: IContext, + ) { + if (doc.numberConfig || doc.numberSize) { + await checkNumberConfig(doc.numberConfig || '', doc.numberSize || ''); + } + + return await models.Pipelines.createPipeline( + { userId: user._id, ...doc }, + stages, + ); + }, + + /** + * Edit pipeline + */ + async tasksPipelinesEdit( + _root, + { + _id, + stages, + ...doc + }: IPipeline & { stages: IStageDocument[]; _id: string }, + { models }: IContext, + ) { + if (doc.numberConfig || doc.numberSize) { + await checkNumberConfig(doc.numberConfig || '', doc.numberSize || ''); + } + + return await models.Pipelines.updatePipeline(_id, doc, stages); + }, + + /** + * Update pipeline orders + */ + async tasksPipelinesUpdateOrder( + _root, + { orders }: { orders: IOrderInput[] }, + { models }: IContext, + ) { + return models.Pipelines.updateOrder(orders); + }, + + /** + * Watch pipeline + */ + async tasksPipelinesWatch( + _root, + { _id, isAdd }: { _id: string; isAdd: boolean }, + { user, models }: IContext, + ) { + return models.Pipelines.watchPipeline(_id, isAdd, user._id); + }, + + /** + * Remove pipeline + */ + async tasksPipelinesRemove( + _root, + { _id }: { _id: string }, + { models }: IContext, + ) { + const pipeline = await models.Pipelines.getPipeline(_id); + + const removed = await models.Pipelines.removePipeline(_id); + + const relatedFieldsGroups = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'fieldsGroups', + action: 'find', + input: { + query: { + pipelineIds: pipeline._id, + }, + }, + defaultValue: [], + }); + + for (const fieldGroup of relatedFieldsGroups) { + const pipelineIds = fieldGroup.pipelineIds || []; + fieldGroup.pipelineIds = pipelineIds.filter((e) => e !== pipeline._id); + + await sendTRPCMessage({ + pluginName: 'core', + method: 'mutation', + module: 'fieldsGroups', + action: 'updateGroup', + input: { + groupId: fieldGroup._id, + fieldGroup, + }, + }); + } + + return removed; + }, + + /** + * Archive pipeline + */ + async tasksPipelinesArchive( + _root, + { _id, status }: { _id; status: string }, + { models }: IContext, + ) { + return await models.Pipelines.archivePipeline(_id, status); + }, + + /** + * Duplicate pipeline + */ + async tasksPipelinesCopied( + _root, + { _id }: { _id: string }, + { models }: IContext, + ) { + const sourcePipeline = await models.Pipelines.getPipeline(_id); + const sourceStages = await models.Stages.find({ pipelineId: _id }).lean(); + + const pipelineDoc = { + ...sourcePipeline, + _id: undefined, + status: sourcePipeline.status || 'active', + name: `${sourcePipeline.name}-copied`, + }; + + const pipeline = await models.Pipelines.createPipeline(pipelineDoc); + + for (const stage of sourceStages) { + const { _id, ...rest } = stage; + + await models.Stages.createStage({ + ...rest, + probability: stage.probability || '10%', + pipelineId: pipeline._id, + }); + } + + return pipeline; + }, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/stage.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/stage.ts new file mode 100644 index 0000000000..e4da04c493 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/stage.ts @@ -0,0 +1,109 @@ +import { IOrderInput } from 'erxes-api-shared/core-types'; +import { graphqlPubsub } from 'erxes-api-shared/utils'; +import { IContext } from '~/connectionResolvers'; +import { IStage } from '~/modules/tasks/@types/stages'; +import { bulkUpdateOrders } from '~/modules/tasks/graphql/resolvers/utils'; + +export const stageMutations = { + /** + * Update stage orders + */ + async tasksStagesUpdateOrder( + _root, + { orders }: { orders: IOrderInput[] }, + { models }: IContext, + ) { + return models.Stages.updateOrder(orders); + }, + + /** + * Edit stage + */ + async tasksStagesEdit( + _root, + { _id, ...doc }: IStage & { _id: string }, + { models }: IContext, + ) { + return await models.Stages.updateStage(_id, doc); + }, + + /** + * Remove stage + */ + async tasksStagesRemove( + _root, + { _id }: { _id: string }, + { models }: IContext, + ) { + return await models.Stages.removeStage(_id); + }, + + /** + * Sort items + */ + async tasksStagesSortItems( + _root, + { + stageId, + proccessId, + sortType, + }: { + stageId: string; + proccessId: string; + sortType: string; + }, + { models }: IContext, + ) { + const sortTypes = { + 'created-asc': { createdAt: 1 }, + 'created-desc': { createdAt: -1 }, + 'modified-asc': { modifiedAt: 1 }, + 'modified-desc': { modifiedAt: -1 }, + 'close-asc': { closeDate: 1, order: 1 }, + 'close-desc': { closeDate: -1, order: 1 }, + 'alphabetically-asc': { name: 1 }, + }; + const sort: { [key: string]: any } = sortTypes[sortType]; + + if (sortType === 'close-asc') { + await bulkUpdateOrders({ + collection: models.Tasks, + stageId, + sort, + additionFilter: { closeDate: { $ne: null } }, + }); + await bulkUpdateOrders({ + collection: models.Tasks, + stageId, + sort: { order: 1 }, + additionFilter: { closeDate: null }, + startOrder: 100001, + }); + } else { + const response = await bulkUpdateOrders({ + collection: models.Tasks, + stageId, + sort, + }); + + if (!response) { + return; + } + } + + const stage = await models.Stages.getStage(stageId); + + graphqlPubsub.publish(`tasksPipelinesChanged:${stage.pipelineId}`, { + tasksPipelinesChanged: { + _id: stage.pipelineId, + proccessId, + action: 'reOrdered', + data: { + destinationStageId: stageId, + }, + }, + }); + + return 'ok'; + }, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/task.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/task.ts new file mode 100644 index 0000000000..3c6bdc8859 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/task.ts @@ -0,0 +1,568 @@ +import { graphqlPubsub, sendTRPCMessage } from 'erxes-api-shared/utils'; +import { IContext } from '~/connectionResolvers'; +import { ITask } from '~/modules/tasks/@types/tasks'; +import { TASK_STATUSES } from '~/modules/tasks/constants'; +import { + changeItemStatus, + checkMovePermission, + copyPipelineLabels, + getNewOrder, + itemResolver, +} from '~/modules/tasks/graphql/resolvers/utils'; + +export const taskMutations = { + /** + * Creates a new task + */ + async tasksAdd( + _root, + doc: ITask & { proccessId: string; aboveItemId: string }, + { user, models }: IContext, + ) { + doc.initialStageId = doc.stageId; + doc.watchedUserIds = user && [user._id]; + + const extendedDoc = { + ...doc, + modifiedBy: user && user._id, + userId: user ? user._id : doc.userId, + order: await getNewOrder({ + collection: models.Tasks, + stageId: doc.stageId, + aboveItemId: doc.aboveItemId, + }), + }; + + if (extendedDoc.customFieldsData) { + // clean custom field values + extendedDoc.customFieldsData = await sendTRPCMessage({ + pluginName: 'core', + method: 'mutation', + module: 'fields', + action: 'prepareCustomFieldsData', + input: { + data: extendedDoc.customFieldsData, + }, + defaultValue: [], + }); + } + + const task = await models.Tasks.createTask(extendedDoc); + const stage = await models.Stages.getStage(task.stageId); + + // if (user) { + // const pipeline = await models.Pipelines.getPipeline(stage.pipelineId); + + // sendNotifications(models, subdomain, { + // item, + // user, + // type: `${type}Add`, + // action: `invited you to the ${pipeline.name}`, + // content: `'${item.name}'.`, + // contentType: type + // }); + // } + + graphqlPubsub.publish(`tasksPipelinesChanged:${stage.pipelineId}`, { + tasksPipelinesChanged: { + _id: stage.pipelineId, + proccessId: doc.proccessId, + action: 'itemAdd', + data: { + item: task, + aboveItemId: doc.aboveItemId, + destinationStageId: stage._id, + }, + }, + }); + + return task; + }, + + /** + * Edit task + */ + async tasksEdit( + _root, + { _id, proccessId, ...doc }: ITask & { _id: string; proccessId: string }, + { user, models }: IContext, + ) { + const oldTask = await models.Tasks.getTask(_id); + + const extendedDoc = { + ...doc, + modifiedAt: new Date(), + modifiedBy: user._id, + }; + + const stage = await models.Stages.getStage(oldTask.stageId); + + const { canEditMemberIds } = stage; + + if ( + canEditMemberIds && + canEditMemberIds.length > 0 && + !canEditMemberIds.includes(user._id) + ) { + throw new Error('Permission denied'); + } + + if (extendedDoc.customFieldsData) { + // clean custom field values + extendedDoc.customFieldsData = await sendTRPCMessage({ + pluginName: 'core', + method: 'mutation', + module: 'fields', + action: 'prepareCustomFieldsData', + input: { + data: extendedDoc.customFieldsData, + }, + defaultValue: [], + }); + } + + const updatedItem = await models.Tasks.updateTask(_id, extendedDoc); + // labels should be copied to newly moved pipeline + if (doc.stageId) { + await copyPipelineLabels(models, { item: oldTask, doc, user }); + } + + // const notificationDoc: IBoardNotificationParams = { + // item: updatedItem, + // user, + // type: `${type}Edit`, + // contentType: type + // }; + + if (doc.status && oldTask.status && oldTask.status !== doc.status) { + const activityAction = doc.status === 'active' ? 'activated' : 'archived'; + + // order notification + await changeItemStatus(models, user, { + item: updatedItem, + status: activityAction, + proccessId, + stage, + }); + } + + // await sendNotifications(models, subdomain, notificationDoc); + + // if (!notificationDoc.invitedUsers && !notificationDoc.removedUsers) { + // sendCoreMessage({ + // subdomain: "os", + // action: "sendMobileNotification", + // data: { + // title: notificationDoc?.item?.name, + // body: `${ + // user?.details?.fullName || user?.details?.shortName + // } has updated`, + // receivers: notificationDoc?.item?.assignedUserIds, + // data: { + // type, + // id: _id + // } + // } + // }); + // } + + // exclude [null] + if (doc.tagIds && doc.tagIds.length) { + doc.tagIds = doc.tagIds.filter((ti) => ti); + } + + const updatedStage = await models.Stages.getStage(updatedItem.stageId); + + if (doc.tagIds || doc.startDate || doc.closeDate || doc.name) { + graphqlPubsub.publish(`tasksPipelinesChanged:${stage.pipelineId}`, { + tasksPipelinesChanged: { + _id: stage.pipelineId, + }, + }); + } + + if (updatedStage.pipelineId !== stage.pipelineId) { + graphqlPubsub.publish(`tasksPipelinesChanged:${stage.pipelineId}`, { + tasksPipelinesChanged: { + _id: stage.pipelineId, + proccessId, + action: 'itemRemove', + data: { + item: oldTask, + oldStageId: stage._id, + }, + }, + }); + graphqlPubsub.publish(`tasksPipelinesChanged:${stage.pipelineId}`, { + tasksPipelinesChanged: { + _id: updatedStage.pipelineId, + proccessId, + action: 'itemAdd', + data: { + item: { + ...updatedItem, + ...(await itemResolver(models, user, updatedItem)), + }, + aboveItemId: '', + destinationStageId: updatedStage._id, + }, + }, + }); + } else { + graphqlPubsub.publish(`tasksPipelinesChanged:${stage.pipelineId}`, { + tasksPipelinesChanged: { + _id: stage.pipelineId, + proccessId, + action: 'itemUpdate', + data: { + item: { + ...updatedItem, + ...(await itemResolver(models, user, updatedItem)), + }, + }, + }, + }); + } + + if (oldTask.stageId === updatedItem.stageId) { + return updatedItem; + } + + // // if task moves between stages + // const { content, action } = await itemMover( + // models, + // user._id, + // oldTask, + // updatedItem.stageId + // ); + + // await sendNotifications(models, subdomain, { + // item: updatedItem, + // user, + // type: `${type}Change`, + // content, + // action, + // contentType: type + // }); + + if (updatedItem.assignedUserIds) { + // sendCoreMessage({ + // subdomain, + // action: 'registerOnboardHistory', + // data: { + // type: `taskAssignUser`, + // user, + // }, + // }); + } + + return updatedItem; + }, + + /** + * Change task + */ + async tasksChange( + _root, + doc: { + proccessId: string; + itemId: string; + aboveItemId?: string; + destinationStageId: string; + sourceStageId: string; + }, + { user, models }: IContext, + ) { + const { + proccessId, + itemId, + aboveItemId, + destinationStageId, + sourceStageId, + } = doc; + + const item = await models.Tasks.getTask(itemId); + const stage = await models.Stages.getStage(item.stageId); + + const extendedDoc: ITask = { + modifiedAt: new Date(), + modifiedBy: user._id, + stageId: destinationStageId, + order: await getNewOrder({ + collection: models.Tasks, + stageId: destinationStageId, + aboveItemId, + }), + }; + + if (item.stageId !== destinationStageId) { + checkMovePermission(stage, user); + + const destinationStage = await models.Stages.getStage(destinationStageId); + + checkMovePermission(destinationStage, user); + + extendedDoc.stageChangedDate = new Date(); + } + + const updatedItem = await models.Tasks.updateTask(itemId, extendedDoc); + + // const { content, action } = await itemMover( + // models, + // user._id, + // item, + // destinationStageId, + // ); + + // await sendNotifications(models, subdomain, { + // item, + // user, + // type: `${type}Change`, + // content, + // action, + // contentType: type, + // }); + + // if (item?.assignedUserIds && item?.assignedUserIds?.length > 0) { + // sendCoreMessage({ + // subdomain: 'os', + // action: 'sendMobileNotification', + // data: { + // title: `${item.name}`, + // body: `${user?.details?.fullName || user?.details?.shortName} ${ + // action + content + // }`, + // receivers: item?.assignedUserIds, + // data: { + // type, + // id: item._id, + // }, + // }, + // }); + // } + + // order notification + const labels = await models.PipelineLabels.find({ + _id: { + $in: item.labelIds, + }, + }); + + graphqlPubsub.publish(`tasksPipelinesChanged:${stage.pipelineId}`, { + tasksPipelinesChanged: { + _id: stage.pipelineId, + proccessId, + action: 'orderUpdated', + data: { + item: { + ...item, + ...(await itemResolver(models, user, item)), + labels, + }, + aboveItemId, + destinationStageId, + oldStageId: sourceStageId, + }, + }, + }); + + return updatedItem; + }, + + /** + * Remove task + */ + async tasksRemove(_root, { _id }: { _id: string }, { models }: IContext) { + const task = await models.Tasks.getTask(_id); + + // await sendNotifications(models, subdomain, { + // item, + // user, + // type: `${type}Delete`, + // action: `deleted ${type}:`, + // content: `'${item.name}'`, + // contentType: type, + // }); + + // if (item?.assignedUserIds && item?.assignedUserIds?.length > 0) { + // sendCoreMessage({ + // subdomain: 'os', + // action: 'sendMobileNotification', + // data: { + // title: `${item.name}`, + // body: `${ + // user?.details?.fullName || user?.details?.shortName + // } deleted the ${type}`, + // receivers: item?.assignedUserIds, + // data: { + // type, + // id: item._id, + // }, + // }, + // }); + // } + + await models.Checklists.removeChecklists([task._id]); + + const removed = await models.Tasks.findOneAndDelete({ _id: task._id }); + + return removed; + }, + + /** + * Watch task + */ + async tasksWatch( + _root, + { _id, isAdd }: { _id: string; isAdd: boolean }, + { user, models }: IContext, + ) { + return models.Tasks.watchTask(_id, isAdd, user._id); + }, + + async tasksCopy( + _root, + { _id, proccessId }: { _id: string; proccessId: string }, + { user, models }: IContext, + ) { + const item = await models.Tasks.getTask(_id); + + const doc = { + ...item, + _id: undefined, + userId: user._id, + modifiedBy: user._id, + watchedUserIds: [user._id], + assignedUserIds: item.assignedUserIds, + name: `${item.name}-copied`, + initialStageId: item.initialStageId, + stageId: item.stageId, + description: item.description, + priority: item.priority, + labelIds: item.labelIds, + order: await getNewOrder({ + collection: models.Tasks, + stageId: item.stageId, + aboveItemId: item._id, + }), + + attachments: (item.attachments || []).map((a) => ({ + url: a.url, + name: a.name, + type: a.type, + size: a.size, + })), + }; + + delete doc.sourceConversationIds; + + const clone = await models.Tasks.createTask(doc); + + const originalChecklists = await models.Checklists.find({ + contentTypeId: _id, + }).lean(); + + const clonedChecklists = await models.Checklists.insertMany( + originalChecklists.map((originalChecklist) => ({ + contentTypeId: clone._id, + title: originalChecklist.title, + createdUserId: user._id, + createdDate: new Date(), + })), + { ordered: true }, + ); + + const originalChecklistIdToClonedId = new Map(); + + for (let i = 0; i < originalChecklists.length; i++) { + originalChecklistIdToClonedId.set( + originalChecklists[i]._id, + clonedChecklists[i]._id, + ); + } + + const originalChecklistItems = await models.ChecklistItems.find({ + checklistId: { $in: originalChecklists.map((x) => x._id) }, + }).lean(); + + await models.ChecklistItems.insertMany( + originalChecklistItems.map(({ content, order, checklistId }) => ({ + checklistId: originalChecklistIdToClonedId.get(checklistId), + isChecked: false, + createdUserId: user._id, + createdDate: new Date(), + content, + order, + })), + { ordered: false }, + ); + + // order notification + const stage = await models.Stages.getStage(clone.stageId); + + graphqlPubsub.publish(`tasksPipelinesChanged:${stage.pipelineId}`, { + tasksPipelinesChanged: { + _id: stage.pipelineId, + proccessId, + action: 'itemAdd', + data: { + item: { + ...clone, + ...(await itemResolver(models, user, clone)), + }, + aboveItemId: _id, + destinationStageId: stage._id, + }, + }, + }); + graphqlPubsub.publish(`tasksPipelinesChanged:${stage.pipelineId}`, { + tasksPipelinesChanged: { + _id: stage.pipelineId, + proccessId: Math.random().toString(), + action: 'itemOfConformitiesUpdate', + data: { + item: { + ...item, + }, + }, + }, + }); + + return clone; + }, + + async tasksArchive( + _root, + { stageId, proccessId }: { stageId: string; proccessId: string }, + { models }: IContext, + ) { + const items = await models.Tasks.find({ + stageId, + status: { $ne: TASK_STATUSES.ARCHIVED }, + }).lean(); + + await models.Tasks.updateMany( + { stageId }, + { $set: { status: TASK_STATUSES.ARCHIVED } }, + ); + + // order notification + const stage = await models.Stages.getStage(stageId); + + for (const item of items) { + graphqlPubsub.publish(`tasksPipelinesChanged:${stage.pipelineId}`, { + tasksPipelinesChanged: { + _id: stage.pipelineId, + proccessId, + action: 'itemsRemove', + data: { + item, + destinationStageId: stage._id, + }, + }, + }); + } + + return 'ok'; + }, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/tasks.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/tasks.ts deleted file mode 100644 index 08d3791588..0000000000 --- a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/tasks.ts +++ /dev/null @@ -1,17 +0,0 @@ - - import { IContext } from '~/connectionResolvers'; - - export const tasksMutations = { - createTasks: async (_parent: undefined, { name }, { models }: IContext) => { - return models.Tasks.createTasks({name}); - }, - - updateTasks: async (_parent: undefined, { _id, name }, { models }: IContext) => { - return models.Tasks.updateTasks(_id, {name}); - }, - - removeTasks: async (_parent: undefined, { _id }, { models }: IContext) => { - return models.Tasks.removeTasks(_id); - }, - }; - diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/board.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/board.ts new file mode 100644 index 0000000000..fabd3847e7 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/board.ts @@ -0,0 +1,65 @@ +import { IContext } from '~/connectionResolvers'; + +export const boardQueries = { + /** + * Boards list + */ + async tasksBoards(_root, _args, { models }: IContext) { + return await models.Boards.find({}).lean(); + }, + + /** + * Boards count + */ + async tasksBoardCounts(_root, _args, { models }: IContext) { + const boards = await models.Boards.find({}) + .sort({ + name: 1, + }) + .lean(); + + const counts: Array<{ _id: string; name: string; count: number }> = []; + + let allCount = 0; + + for (const board of boards) { + const count = await models.Pipelines.find({ + boardId: board._id, + }).countDocuments(); + + counts.push({ + _id: board._id, + name: board.name || '', + count, + }); + + allCount += count; + } + + counts.unshift({ _id: '', name: 'All', count: allCount }); + + return counts; + }, + + /** + * Get last board + */ + async tasksBoardGetLast(_root, _args, { models }: IContext) { + return models.Boards.findOne({}) + .sort({ + createdAt: -1, + }) + .lean(); + }, + + /** + * Board detail + */ + async tasksBoardDetail( + _root, + { _id }: { _id: string }, + { models }: IContext, + ) { + return models.Boards.findOne({ _id }).lean(); + }, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/checklist.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/checklist.ts new file mode 100644 index 0000000000..0b5fab220e --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/checklist.ts @@ -0,0 +1,31 @@ +import { IContext } from '~/connectionResolvers'; + +export const checklistQueries = { + /** + * Checklists list + */ + async tasksChecklists( + _root, + { + contentType, + contentTypeId, + }: { contentType: string; contentTypeId: string }, + { models }: IContext, + ) { + return models.Checklists.find({ contentType, contentTypeId }).sort({ + createdDate: 1, + order: 1, + }); + }, + + /** + * Checklist + */ + async tasksChecklistDetail( + _root, + { _id }: { _id: string }, + { models }: IContext, + ) { + return models.Checklists.findOne({ _id }).sort({ order: 1 }); + }, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/index.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/index.ts new file mode 100644 index 0000000000..3351d568f9 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/index.ts @@ -0,0 +1,15 @@ +import { boardQueries } from './board'; +import { checklistQueries } from './checklist'; +import { pipelineLabelQueries } from './label'; +import { pipelineQueries } from './pipeline'; +import { stageQueries } from './stage'; +import { taskQueries } from './task'; + +export const queries = { + ...boardQueries, + ...pipelineQueries, + ...stageQueries, + ...taskQueries, + ...checklistQueries, + ...pipelineLabelQueries, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/label.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/label.ts new file mode 100644 index 0000000000..2e85f0cedd --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/label.ts @@ -0,0 +1,33 @@ +import { IContext } from '~/connectionResolvers'; + +export const pipelineLabelQueries = { + /** + * Pipeline label list + */ + async tasksPipelineLabels( + _root, + { pipelineId, pipelineIds }: { pipelineId: string; pipelineIds: string[] }, + { models }: IContext, + ) { + const filter: any = {}; + + filter.pipelineId = pipelineId; + + if (pipelineIds) { + filter.pipelineId = { $in: pipelineIds }; + } + + return models.PipelineLabels.find(filter); + }, + + /** + * Pipeline label detail + */ + async tasksPipelineLabelDetail( + _root, + { _id }: { _id: string }, + { models }: IContext, + ) { + return models.PipelineLabels.findOne({ _id }); + }, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/pipeline.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/pipeline.ts new file mode 100644 index 0000000000..b58c32daa5 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/pipeline.ts @@ -0,0 +1,160 @@ +import { sendTRPCMessage } from 'erxes-api-shared/utils'; +import { IContext } from '~/connectionResolvers'; + +export const pipelineQueries = { + /** + * Pipelines list + */ + async tasksPipelines( + _root, + { + boardId, + isAll, + }: { + boardId: string; + isAll: boolean; + }, + { user, models }: IContext, + ) { + const query: any = + user.isOwner || isAll + ? {} + : { + status: { $ne: 'archived' }, + $or: [ + { visibility: 'public' }, + { + $and: [ + { visibility: 'private' }, + { + $or: [ + { memberIds: { $in: [user._id] } }, + { userId: user._id }, + ], + }, + ], + }, + ], + }; + + if (!user.isOwner && !isAll) { + const userDetail = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'users', + action: 'findOne', + input: { + _id: user._id, + }, + defaultValue: {}, + }); + + const departmentIds = userDetail?.departmentIds || []; + + if (Object.keys(query) && departmentIds.length > 0) { + query.$or.push({ + $and: [ + { visibility: 'private' }, + { departmentIds: { $in: departmentIds } }, + ], + }); + } + } + + if (boardId) { + query.boardId = boardId; + } + + return models.Pipelines.find(query) + .sort({ order: 1, createdAt: -1 }) + .lean(); + }, + + async tasksPipelineStateCount( + _root, + { boardId }: { boardId: string }, + { models }: IContext, + ) { + const query: any = {}; + + if (boardId) { + query.boardId = boardId; + } + + const counts: any = {}; + const now = new Date(); + + const notStartedQuery = { + ...query, + startDate: { $gt: now }, + }; + + const notStartedCount = await models.Pipelines.find( + notStartedQuery, + ).countDocuments(); + + counts['Not started'] = notStartedCount; + + const inProgressQuery = { + ...query, + startDate: { $lt: now }, + endDate: { $gt: now }, + }; + + const inProgressCount = await models.Pipelines.find( + inProgressQuery, + ).countDocuments(); + + counts['In progress'] = inProgressCount; + + const completedQuery = { + ...query, + endDate: { $lt: now }, + }; + + const completedCounted = await models.Pipelines.find( + completedQuery, + ).countDocuments(); + + counts.Completed = completedCounted; + + counts.All = notStartedCount + inProgressCount + completedCounted; + + return counts; + }, + + /** + * Pipeline detail + */ + async tasksPipelineDetail( + _root, + { _id }: { _id: string }, + { models }: IContext, + ) { + return models.Pipelines.findOne({ _id }).lean(); + }, + + /** + * Pipeline related assigned users + */ + async tasksPipelineAssignedUsers( + _root: undefined, + { _id }: { _id: string }, + { models }: IContext, + ) { + const pipeline = await models.Pipelines.getPipeline(_id); + + const stageIds = await models.Stages.find({ + pipelineId: pipeline._id, + }).distinct('_id'); + + const assignedUserIds = await models.Tasks.find({ + stageId: { $in: stageIds }, + }).distinct('assignedUserIds'); + + return assignedUserIds.map((userId) => ({ + __typename: 'User', + _id: userId || '', + })); + }, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/stage.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/stage.ts new file mode 100644 index 0000000000..0e2317f110 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/stage.ts @@ -0,0 +1,112 @@ +import { regexSearchText, sendTRPCMessage } from 'erxes-api-shared/utils'; +import { IContext } from '~/connectionResolvers'; +import { TASK_STATUSES } from '~/modules/tasks/constants'; + +export const stageQueries = { + /** + * Stages list + */ + async tasksStages( + _root, + { + pipelineId, + pipelineIds, + isNotLost, + isAll, + }: { + pipelineId: string; + pipelineIds: string[]; + isNotLost: boolean; + isAll: boolean; + }, + { user, models }: IContext, + ) { + const filter: any = {}; + + filter.pipelineId = pipelineId; + + if (pipelineIds) { + filter.pipelineId = { $in: pipelineIds }; + } + + if (isNotLost) { + filter.probability = { $ne: 'Lost' }; + } + + if (!isAll) { + filter.status = { $ne: TASK_STATUSES.ARCHIVED }; + + filter.$or = [ + { visibility: { $in: ['public', null] } }, + { + $and: [{ visibility: 'private' }, { memberIds: { $in: [user._id] } }], + }, + ]; + + const userDetail = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'users', + action: 'findOne', + input: { + _id: user._id, + }, + defaultValue: {}, + }); + + const departmentIds = userDetail?.departmentIds || []; + if (departmentIds.length > 0) { + filter.$or.push({ + $and: [ + { visibility: 'private' }, + { departmentIds: { $in: departmentIds } }, + ], + }); + } + } + + return models.Stages.find(filter).sort({ order: 1, createdAt: -1 }).lean(); + }, + /** + * Stage detail + */ + async tasksStageDetail( + _root, + { _id }: { _id: string }, + { models }: IContext, + ) { + return models.Stages.findOne({ _id }).lean(); + }, + + /** + * Archived stages + */ + + async tasksArchivedStages( + _root, + { pipelineId, search }: { pipelineId: string; search?: string }, + { models }: IContext, + ) { + const filter: any = { pipelineId, status: TASK_STATUSES.ARCHIVED }; + + if (search) { + Object.assign(filter, regexSearchText(search, 'name')); + } + + return await models.Stages.find(filter).sort({ createdAt: -1 }).lean(); + }, + + async tasksArchivedStagesCount( + _root, + { pipelineId, search }: { pipelineId: string; search?: string }, + { models }: IContext, + ) { + const filter: any = { pipelineId, status: TASK_STATUSES.ARCHIVED }; + + if (search) { + Object.assign(filter, regexSearchText(search, 'name')); + } + + return models.Stages.countDocuments(filter); + }, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/task.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/task.ts new file mode 100644 index 0000000000..d4dbf409f5 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/task.ts @@ -0,0 +1,299 @@ +import { cursorPaginate, sendTRPCMessage } from 'erxes-api-shared/utils'; +import { IContext } from '~/connectionResolvers'; +import { + IArchivedTaskQueryParams, + ITaskQueryParams, +} from '~/modules/tasks/@types/tasks'; +import { + compareDepartmentIds, + generateArhivedTasksFilter, + generateFilter, +} from '~/modules/tasks/graphql/resolvers/utils'; + +export const taskQueries = { + /** + * Tasks list + */ + async tasks(_root, args: ITaskQueryParams, { user, models }: IContext) { + const filter = await generateFilter(models, user._id, args); + + const { list, pageInfo, totalCount } = await cursorPaginate({ + model: models.Tasks, + params: args, + query: filter, + }); + + const companyIds: string[] = []; + const customerIds: string[] = []; + const companyIdsByItemId = {}; + const customerIdsByItemId = {}; + + const companies = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'companies', + action: 'findActiveCompanies', + input: { + selector: { + _id: { $in: [...new Set(companyIds)] }, + }, + fields: { + primaryName: 1, + primaryEmail: 1, + primaryPhone: 1, + emails: 1, + phones: 1, + }, + }, + }); + + const customers = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'customers', + action: 'findActiveCustomers', + input: { + selector: { + _id: { $in: [...new Set(customerIds)] }, + }, + fields: { + firstName: 1, + lastName: 1, + middleName: 1, + visitorContactInfo: 1, + primaryEmail: 1, + primaryPhone: 1, + emails: 1, + phones: 1, + }, + }, + defaultValue: [], + }); + + const getCocsByItemId = ( + itemId: string, + cocIdsByItemId: any, + cocs: any[], + ) => { + const cocIds = cocIdsByItemId[itemId] || []; + + return cocIds.flatMap((cocId: string) => { + const found = cocs.find((coc) => cocId === coc._id); + + return found || []; + }); + }; + + const updatedList: any[] = []; + + // const notifications = await sendNotificationsMessage({ + // subdomain, + // action: 'find', + // data: { + // selector: { + // contentTypeId: { $in: ids }, + // isRead: false, + // receiver: user._id + // }, + // fields: { contentTypeId: 1 } + // }, + // isRPC: true, + // defaultValue: [] + // }); + + const fields = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'fields', + action: 'find', + input: { + query: { + showInCard: true, + contentType: 'tasks:task', + }, + }, + defaultValue: [], + }); + + // add just incremented order to each item in list, not from db + let order = 0; + for (const item of list as any) { + if ( + item.customFieldsData && + item.customFieldsData.length > 0 && + fields.length > 0 + ) { + item.customProperties = []; + + fields.forEach((field) => { + const fieldData = item.customFieldsData.find( + (f) => f.field === field._id, + ); + + if (fieldData) { + item.customProperties.push({ + name: `${field.text} - ${fieldData.value}`, + }); + } + }); + } + + // const notification = notifications.find(n => n.contentTypeId === item._id); + + updatedList.push({ + ...item, + order: order++, + isWatched: (item.watchedUserIds || []).includes(user._id), + // hasNotified: notification ? false : true, + customers: getCocsByItemId(item._id, customerIdsByItemId, customers), + companies: getCocsByItemId(item._id, companyIdsByItemId, companies), + }); + } + + return { list: updatedList, pageInfo, totalCount }; + }, + + async tasksTotalCount( + _root, + args: ITaskQueryParams, + { user, models }: IContext, + ) { + const filter = await generateFilter(models, user._id, args); + + return models.Tasks.find(filter).countDocuments(); + }, + + /** + * Archived list + */ + async archivedTasks( + _root, + args: IArchivedTaskQueryParams, + { models }: IContext, + ) { + const { pipelineId } = args; + + const stages = await models.Stages.find({ pipelineId }).lean(); + + if (stages.length > 0) { + const filter = generateArhivedTasksFilter(args, stages); + + const { list, pageInfo, totalCount } = await cursorPaginate({ + model: models.Tasks, + params: args, + query: filter, + }); + + return { list, pageInfo, totalCount }; + } + + return []; + }, + + async archivedTasksCount( + _root, + args: IArchivedTaskQueryParams, + { models }: IContext, + ) { + const { pipelineId } = args; + + const stages = await models.Stages.find({ pipelineId }); + + if (stages.length > 0) { + const filter = generateArhivedTasksFilter(args, stages); + + return models.Tasks.find(filter).countDocuments(); + } + + return 0; + }, + + /** + * Tasks detail + */ + async taskDetail( + _root, + { _id, clientPortalCard }: { _id: string; clientPortalCard: boolean }, + { user, models }: IContext, + ) { + const task = await models.Tasks.getTask(_id); + + // no need to check permission on cp task + if (clientPortalCard) { + return task; + } + + const stage = await models.Stages.getStage(task.stageId); + + const { + visibility, + memberIds, + departmentIds = [], + branchIds = [], + isCheckUser, + excludeCheckUserIds, + } = await models.Pipelines.getPipeline(stage.pipelineId); + + const supervisorDepartments = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'departments', + action: 'findWithChild', + input: { + query: { + supervisorId: user?._id, + }, + fields: { + _id: 1, + }, + }, + defaultValue: [], + }); + + const supervisorDepartmentIds = + supervisorDepartments?.map((x) => x._id) || []; + // const userDepartmentIds = user.departmentIds || []; + // const userBranchIds = user?.branchIds || []; + + // // check permission on department + // const hasUserInDepartment = compareDepartmentIds(departmentIds, [ + // ...userDepartmentIds, + // ...supervisorDepartmentIds, + // ]); + // const isUserInBranch = compareDepartmentIds(branchIds, userBranchIds); + + // if ( + // visibility === 'private' && + // !(memberIds || []).includes(user._id) && + // !hasUserInDepartment && + // !isUserInBranch && + // user?.role !== USER_ROLES.SYSTEM + // ) { + // throw new Error('You do not have permission to view.'); + // } + + const isSuperVisorInDepartment = compareDepartmentIds( + departmentIds, + supervisorDepartmentIds, + ); + if (isSuperVisorInDepartment) { + return task; + } + + // pipeline is Show only the users assigned(created) cards checked + // and current user nothing dominant users + // current user hans't this carts assigned and created + // if ( + // isCheckUser && + // !(excludeCheckUserIds || []).includes(user._id) && + // !( + // (task.assignedUserIds || []).includes(user._id) || + // task.userId === user._id + // ) + // ) { + // throw new Error('You do not have permission to view.'); + // } + + return task; + }, +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/tasks.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/tasks.ts deleted file mode 100644 index 6eb6b24958..0000000000 --- a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/tasks.ts +++ /dev/null @@ -1,12 +0,0 @@ - - import { IContext } from '~/connectionResolvers'; - - export const tasksQueries = { - getTasks: async (_parent: undefined, { _id }, { models }: IContext) => { - return models.Tasks.getTasks(_id); - }, - - getTaskss: async (_parent: undefined, { models }: IContext) => { - return models.Tasks.getTaskss(); - }, - }; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/utils.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/utils.ts new file mode 100644 index 0000000000..f43b5e3bf8 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/utils.ts @@ -0,0 +1,1132 @@ +import { IUserDocument } from 'erxes-api-shared/core-types'; +import { + getNextMonth, + getToday, + graphqlPubsub, + regexSearchText, + sendTRPCMessage, +} from 'erxes-api-shared/utils'; +import moment from 'moment'; +import * as _ from 'underscore'; +import resolvers from '~/apollo/resolvers'; +import { IModels } from '~/connectionResolvers'; +import { IStageDocument } from '~/modules/tasks/@types/stages'; +import { + IArchivedTaskQueryParams, + ITask, + ITaskDocument, +} from '~/modules/tasks/@types/tasks'; +import { CLOSE_DATE_TYPES, TASK_STATUSES } from '~/modules/tasks/constants'; +import { configReplacer } from '~/modules/tasks/utils'; + +export const itemResolver = async (models: IModels, user: any, item: ITask) => { + const additionInfo = {}; + const resolver = resolvers['Task'] || {}; + + for (const subResolver of Object.keys(resolver)) { + try { + additionInfo[subResolver] = await resolver[subResolver]( + item, + {}, + { models, user }, + { isSubscription: true }, + ); + } catch (unused) { + continue; + } + } + + return additionInfo; +}; + +export const checkNumberConfig = async ( + numberConfig: string, + numberSize: string, +) => { + if (!numberConfig) { + throw new Error('Please input number configuration.'); + } + + if (!numberSize) { + throw new Error('Please input fractional part.'); + } + + const replaced = await configReplacer(numberConfig); + const re = /[0-9]$/; + + if (re.test(replaced)) { + throw new Error( + `Please make sure that the number configuration itself doesn't end with any number.`, + ); + } + + return; +}; + +export const bulkUpdateOrders = async ({ + collection, + stageId, + sort = { order: 1 }, + additionFilter = {}, + startOrder = 100, +}: { + collection: any; + stageId: string; + sort?: { [key: string]: any }; + additionFilter?: any; + startOrder?: number; +}) => { + const bulkOps: Array<{ + updateOne: { + filter: { _id: string }; + update: { order: number }; + }; + }> = []; + + let ord = startOrder; + + const allItems = await collection + .find( + { + stageId, + status: { $ne: TASK_STATUSES.ARCHIVED }, + ...additionFilter, + }, + { _id: 1, order: 1 }, + ) + .sort(sort); + + for (const item of allItems) { + bulkOps.push({ + updateOne: { + filter: { _id: item._id }, + update: { order: ord }, + }, + }); + + ord = ord + 10; + } + + if (!bulkOps.length) { + return ''; + } + + await collection.bulkWrite(bulkOps); + return 'ok'; +}; + +export const getAmountsMap = async ( + models, + collection, + user, + args, + stage, + tickUsed = true, +) => { + const amountsMap = {}; + const filter = await generateFilter( + models, + user._id, + { ...args, stageId: stage._id, pipelineId: stage.pipelineId }, + args.extraParams, + ); + + const amountList = await collection.aggregate([ + { + $match: filter, + }, + { + $unwind: '$productsData', + }, + { + $project: { + amount: '$productsData.amount', + currency: '$productsData.currency', + tickUsed: '$productsData.tickUsed', + }, + }, + { + $match: { tickUsed }, + }, + { + $group: { + _id: '$currency', + amount: { $sum: '$amount' }, + }, + }, + ]); + + amountList.forEach((item) => { + if (item._id) { + amountsMap[item._id] = item.amount; + } + }); + return amountsMap; +}; + +export const getCloseDateByType = (closeDateType: string) => { + if (closeDateType === CLOSE_DATE_TYPES.NEXT_DAY) { + const tommorrow = moment().add(1, 'days'); + + return { + $gte: new Date(tommorrow.startOf('day').toISOString()), + $lte: new Date(tommorrow.endOf('day').toISOString()), + }; + } + + if (closeDateType === CLOSE_DATE_TYPES.NEXT_WEEK) { + const monday = moment() + .day(1 + 7) + .format('YYYY-MM-DD'); + const nextSunday = moment() + .day(7 + 7) + .format('YYYY-MM-DD'); + + return { + $gte: new Date(monday), + $lte: new Date(nextSunday), + }; + } + + if (closeDateType === CLOSE_DATE_TYPES.NEXT_MONTH) { + const now = new Date(); + const { start, end } = getNextMonth(now); + + return { + $gte: new Date(start), + $lte: new Date(end), + }; + } + + if (closeDateType === CLOSE_DATE_TYPES.NO_CLOSE_DATE) { + return { $exists: false }; + } + + if (closeDateType === CLOSE_DATE_TYPES.OVERDUE) { + const now = new Date(); + const today = getToday(now); + + return { $lt: today }; + } +}; + +const dateSelector = (date: { month: number; year: number }) => { + const { year, month } = date; + + const start = new Date(Date.UTC(year, month, 1, 0, 0, 0)); + const end = new Date(Date.UTC(year, month + 1, 1, 0, 0, 0)); + + return { + $gte: start, + $lte: end, + }; +}; + +export const generateExtraFilters = async (filter, extraParams) => { + const { + source, + userIds, + priority, + startDate, + endDate, + createdStartDate, + createdEndDate, + stateChangedStartDate, + stateChangedEndDate, + startDateStartDate, + startDateEndDate, + closeDateStartDate, + closeDateEndDate, + } = extraParams; + + const isListEmpty = (value) => { + return value.length === 1 && value[0].length === 0; + }; + + if (source) { + filter.source = { $in: source }; + } + + if (userIds) { + const isEmpty = isListEmpty(userIds); + + filter.userId = isEmpty ? { $in: [null, []] } : { $in: userIds }; + } + + if (priority) { + filter.priority = { $in: priority }; + } + + if (startDate) { + filter.closeDate = { + $gte: new Date(startDate), + }; + } + + if (endDate) { + if (filter.closeDate) { + filter.closeDate.$lte = new Date(endDate); + } else { + filter.closeDate = { + $lte: new Date(endDate), + }; + } + } + + if (createdStartDate || createdEndDate) { + filter.createdAt = { + $gte: new Date(createdStartDate), + $lte: new Date(createdEndDate), + }; + } + + if (stateChangedStartDate || stateChangedEndDate) { + filter.stageChangedDate = { + $gte: new Date(stateChangedStartDate), + $lte: new Date(stateChangedEndDate), + }; + } + + if (startDateStartDate || startDateEndDate) { + filter.startDate = { + $gte: new Date(startDateStartDate), + $lte: new Date(startDateEndDate), + }; + } + + if (closeDateStartDate || closeDateEndDate) { + filter.closeDate = { + $gte: new Date(closeDateStartDate), + $lte: new Date(closeDateEndDate), + }; + } + + return filter; +}; + +export const calendarFilters = async (models: IModels, filter, args) => { + const { + date, + pipelineId, + createdStartDate, + createdEndDate, + stateChangedStartDate, + stateChangedEndDate, + startDateStartDate, + startDateEndDate, + closeDateStartDate, + closeDateEndDate, + } = args; + + if (date) { + const stageIds = await models.Stages.find({ pipelineId }).distinct('_id'); + + filter.closeDate = dateSelector(date); + filter.stageId = { $in: stageIds }; + } + + if (createdStartDate || createdEndDate) { + filter.createdAt = { + $gte: new Date(createdStartDate), + $lte: new Date(createdEndDate), + }; + } + if (stateChangedStartDate || stateChangedEndDate) { + filter.stageChangedDate = { + $gte: new Date(stateChangedStartDate), + $lte: new Date(stateChangedEndDate), + }; + } + if (startDateStartDate || startDateEndDate) { + filter.startDate = { + $gte: new Date(startDateStartDate), + $lte: new Date(startDateEndDate), + }; + } + if (closeDateStartDate || closeDateEndDate) { + filter.closeDate = { + $gte: new Date(closeDateStartDate), + $lte: new Date(closeDateEndDate), + }; + } + + return filter; +}; + +export const generateFilter = async ( + models: IModels, + currentUserId: string, + args = {} as any, + extraParams?: any, +) => { + const { + _ids, + pipelineId, + pipelineIds, + stageId, + parentId, + boardIds, + stageCodes, + search, + closeDateType, + assignedUserIds, + initialStageId, + labelIds, + priority, + userIds, + tagIds, + assignedToMe, + startDate, + endDate, + hasStartAndCloseDate, + stageChangedStartDate, + stageChangedEndDate, + noSkipArchive, + number, + branchIds, + departmentIds, + dateRangeFilters, + customFieldsDataFilters, + resolvedDayBetween, + } = args; + + const { productIds } = extraParams || args; + + const isListEmpty = (value) => { + return value.length === 1 && value[0].length === 0; + }; + + const filter: any = noSkipArchive + ? {} + : { status: { $ne: TASK_STATUSES.ARCHIVED }, parentId: undefined }; + + if (parentId) { + filter.parentId = parentId; + } + + if (assignedUserIds) { + // Filter by assigned to no one + const notAssigned = isListEmpty(assignedUserIds); + + filter.assignedUserIds = notAssigned ? [] : { $in: assignedUserIds }; + } + + if (branchIds) { + const branches = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'branches', + action: 'findWithChild', + input: { + query: { _id: { $in: branchIds } }, + fields: { _id: 1 }, + }, + defaultValue: [], + }); + + filter.branchIds = { $in: branches.map((item) => item._id) }; + } + + if (departmentIds) { + const departments = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'departments', + action: 'findWithChild', + input: { + query: { _id: { $in: departmentIds } }, + fields: { _id: 1 }, + }, + defaultValue: [], + }); + + filter.departmentIds = { $in: departments.map((item) => item._id) }; + } + + if (_ids && _ids.length) { + filter._id = { $in: _ids }; + } + + if (initialStageId) { + filter.initialStageId = initialStageId; + } + + if (closeDateType) { + filter.closeDate = getCloseDateByType(closeDateType); + } + + if (startDate) { + filter.closeDate = { + $gte: new Date(startDate), + }; + } + + if (endDate) { + if (filter.closeDate) { + filter.closeDate.$lte = new Date(endDate); + } else { + filter.closeDate = { + $lte: new Date(endDate), + }; + } + } + + if (dateRangeFilters) { + for (const dateRangeFilter of dateRangeFilters) { + const { name, from, to } = dateRangeFilter; + + if (from) { + filter[name] = { $gte: new Date(from) }; + } + + if (to) { + filter[name] = { ...filter[name], $lte: new Date(to) }; + } + } + } + + if (customFieldsDataFilters) { + for (const { value, name } of customFieldsDataFilters) { + if (Array.isArray(value) && value?.length) { + filter[`customFieldsData.${name}`] = { $in: value }; + } else { + filter[`customFieldsData.${name}`] = value; + } + } + } + + const stageChangedDateFilter: any = {}; + if (stageChangedStartDate) { + stageChangedDateFilter.$gte = new Date(stageChangedStartDate); + } + if (stageChangedEndDate) { + stageChangedDateFilter.$lte = new Date(stageChangedEndDate); + } + if (Object.keys(stageChangedDateFilter).length) { + filter.stageChangedDate = stageChangedDateFilter; + } + + if (search) { + Object.assign(filter, regexSearchText(search)); + } + + if (stageId) { + filter.stageId = stageId; + } else if (pipelineId || pipelineIds) { + let filterPipeline = pipelineId; + + if (pipelineIds) { + filterPipeline = { $in: pipelineIds }; + } + + const stageIds = await models.Stages.find({ + pipelineId: filterPipeline, + status: { $ne: TASK_STATUSES.ARCHIVED }, + }).distinct('_id'); + + filter.stageId = { $in: stageIds }; + } + + if (boardIds) { + const pipelineIds = await models.Pipelines.find({ + boardId: { $in: boardIds }, + status: { $ne: TASK_STATUSES.ARCHIVED }, + }).distinct('_id'); + + const filterStages: any = { + pipelineId: { $in: pipelineIds }, + status: { $ne: TASK_STATUSES.ARCHIVED }, + }; + + if (filter?.stageId?.$in) { + filterStages._id = { $in: filter?.stageId?.$in }; + } + + const stageIds = await models.Stages.find(filterStages).distinct('_id'); + + filter.stageId = { $in: stageIds }; + } + + if (stageCodes) { + const filterStages: any = { code: { $in: stageCodes } }; + + if (filter?.stageId?.$in) { + filterStages._id = { $in: filter?.stageId?.$in }; + } + + const stageIds = await models.Stages.find(filterStages).distinct('_id'); + + filter.stageId = { $in: stageIds }; + } + + if (labelIds) { + const isEmpty = isListEmpty(labelIds); + + filter.labelIds = isEmpty ? { $in: [null, []] } : { $in: labelIds }; + } + + if (priority) { + filter.priority = { $eq: priority }; + } + + if (tagIds) { + filter.tagIds = { $in: tagIds }; + } + + if (pipelineId) { + const pipeline = await models.Pipelines.getPipeline(pipelineId); + + const user = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'users', + action: 'findOne', + input: { + query: { + _id: currentUserId, + }, + }, + defaultValue: {}, + }); + + const departments = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'departments', + action: 'findWithChild', + input: { + query: { + supervisorId: currentUserId, + }, + fields: { + _id: 1, + }, + }, + defaultValue: [], + }); + + const supervisorDepartmentIds = departments?.map((x) => x._id) || []; + const pipelineDepartmentIds = pipeline.departmentIds || []; + + const commonIds = + supervisorDepartmentIds.filter((id) => + pipelineDepartmentIds.includes(id), + ) || []; + const isEligibleSeeAllCards = (pipeline.excludeCheckUserIds || []).includes( + currentUserId, + ); + if ( + commonIds?.length > 0 && + (pipeline.isCheckUser || pipeline.isCheckDepartment) && + !isEligibleSeeAllCards + ) { + // current user is supervisor in departments and this pipeline has included that some of user's departments + // so user is eligible to see all cards of people who share same department. + const otherDepartmentUsers = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'users', + action: 'find', + input: { + query: { departmentIds: { $in: commonIds } }, + }, + defaultValue: [], + }); + + let includeCheckUserIds = otherDepartmentUsers.map((x) => x._id) || []; + includeCheckUserIds = includeCheckUserIds.concat(user._id || []); + + const uqinueCheckUserIds = [ + ...new Set(includeCheckUserIds.concat(currentUserId)), + ]; + + Object.assign(filter, { + $or: [ + { assignedUserIds: { $in: uqinueCheckUserIds } }, + { userId: { $in: uqinueCheckUserIds } }, + ], + }); + } else { + if ( + (pipeline.isCheckUser || pipeline.isCheckDepartment) && + !isEligibleSeeAllCards + ) { + let includeCheckUserIds: string[] = []; + + if (pipeline.isCheckDepartment) { + const userDepartmentIds = user?.departmentIds || []; + const commonIds = userDepartmentIds.filter((id) => + pipelineDepartmentIds.includes(id), + ); + + const otherDepartmentUsers = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'users', + action: 'find', + input: { + query: { departmentIds: { $in: commonIds } }, + }, + defaultValue: [], + }); + + for (const departmentUser of otherDepartmentUsers) { + includeCheckUserIds = [...includeCheckUserIds, departmentUser._id]; + } + + if ( + pipelineDepartmentIds.filter((departmentId) => + userDepartmentIds.includes(departmentId), + ).length + ) { + includeCheckUserIds = includeCheckUserIds.concat(user._id || []); + } + } + + const uqinueCheckUserIds = [ + ...new Set(includeCheckUserIds.concat(currentUserId)), + ]; + + Object.assign(filter, { + $or: [ + { assignedUserIds: { $in: uqinueCheckUserIds } }, + { userId: { $in: uqinueCheckUserIds } }, + ], + }); + } + } + } + + if (userIds) { + const isEmpty = isListEmpty(userIds); + + filter.userId = isEmpty ? { $in: [null, []] } : { $in: userIds }; + } + + if (assignedToMe) { + filter.assignedUserIds = { $in: [currentUserId] }; + } + + if (hasStartAndCloseDate) { + filter.startDate = { $exists: true }; + filter.closeDate = { $exists: true }; + } + + if (number) { + filter.number = { $regex: `${number}`, $options: 'mui' }; + } + + if ((stageId || stageCodes) && resolvedDayBetween) { + const [dayFrom, dayTo] = resolvedDayBetween; + filter.$expr = { + $and: [ + // Convert difference between stageChangedDate and createdAt to days + { + $gte: [ + { + $divide: [ + { $subtract: ['$stageChangedDate', '$createdAt'] }, + 1000 * 60 * 60 * 24, // Convert milliseconds to days + ], + }, + dayFrom, // Minimum day (0 days) + ], + }, + { + $lt: [ + { + $divide: [ + { $subtract: ['$stageChangedDate', '$createdAt'] }, + 1000 * 60 * 60 * 24, + ], + }, + dayTo, // Maximum day (3 days) + ], + }, + ], + }; + } + + if (extraParams) { + await generateExtraFilters(filter, extraParams); + } + + if (productIds) { + filter['productsData.productId'] = { $in: productIds }; + } + + // Calendar monthly date + await calendarFilters(models, filter, args); + + return filter; +}; + +export const generateArhivedTasksFilter = ( + params: IArchivedTaskQueryParams, + stages: IStageDocument[], +) => { + const { + search, + userIds, + priorities, + assignedUserIds, + labelIds, + productIds, + startDate, + endDate, + sources, + hackStages, + } = params; + + const filter: any = { status: TASK_STATUSES.ARCHIVED }; + + filter.stageId = { $in: stages.map((stage) => stage._id) }; + + if (search) { + Object.assign(filter, regexSearchText(search, 'name')); + } + + if (userIds && userIds.length) { + filter.userId = { $in: userIds }; + } + + if (priorities && priorities.length) { + filter.priority = { $in: priorities }; + } + + if (assignedUserIds && assignedUserIds.length) { + filter.assignedUserIds = { $in: assignedUserIds }; + } + + if (labelIds && labelIds.length) { + filter.labelIds = { $in: labelIds }; + } + + if (productIds && productIds.length) { + filter['productsData.productId'] = { $in: productIds }; + } + + if (startDate) { + filter.closeDate = { + $gte: new Date(startDate), + }; + } + + if (endDate) { + if (filter.closeDate) { + filter.closeDate.$lte = new Date(endDate); + } else { + filter.closeDate = { + $lte: new Date(endDate), + }; + } + } + + if (sources && sources.length) { + filter.source = { $in: sources }; + } + + if (hackStages && hackStages.length) { + filter.hackStages = { $in: hackStages }; + } + + return filter; +}; + +export const compareDepartmentIds = ( + pipelineDepartmentIds: string[], + userDepartmentIds: string[], +): boolean => { + if (!pipelineDepartmentIds?.length || !userDepartmentIds?.length) { + return false; + } + + for (const uDepartmentId of userDepartmentIds) { + if (pipelineDepartmentIds.includes(uDepartmentId)) { + return true; + } + } + + return false; +}; + +const randomBetween = (min: number, max: number) => { + return Math.random() * (max - min) + min; +}; + +const orderHeler = (aboveOrder, belowOrder) => { + // empty stage + if (!aboveOrder && !belowOrder) { + return 100; + } + + // end of stage + if (!belowOrder) { + return aboveOrder + 10; + } + + // begin of stage + if (!aboveOrder) { + return randomBetween(0, belowOrder); + } + + // between items on stage + return randomBetween(aboveOrder, belowOrder); +}; + +export const getNewOrder = async ({ + collection, + stageId, + aboveItemId, +}: { + collection: any; + stageId: string; + aboveItemId?: string; +}) => { + const aboveItem = await collection.findOne({ _id: aboveItemId }); + + const aboveOrder = aboveItem?.order || 0; + + const belowItems = await collection + .find({ + stageId, + order: { $gt: aboveOrder }, + status: { $ne: TASK_STATUSES.ARCHIVED }, + }) + .sort({ order: 1 }) + .limit(1); + + const belowOrder = belowItems[0]?.order; + + const order = orderHeler(aboveOrder, belowOrder); + + // if duplicated order, then in stages items bulkUpdate 100, 110, 120, 130 + if ([aboveOrder, belowOrder].includes(order)) { + await bulkUpdateOrders({ collection, stageId }); + + return getNewOrder({ collection, stageId, aboveItemId }); + } + + return order; +}; + +/** + * Copies pipeline labels alongside task when they are moved between different pipelines. + */ +export const copyPipelineLabels = async ( + models: IModels, + params: { + item: ITaskDocument; + doc: any; + user: IUserDocument; + }, +) => { + const { item, doc, user } = params; + + const oldStage = await models.Stages.findOne({ _id: item.stageId }).lean(); + const newStage = await models.Stages.findOne({ _id: doc.stageId }).lean(); + + if (!(oldStage && newStage)) { + throw new Error('Stage not found'); + } + + if (oldStage.pipelineId === newStage.pipelineId) { + return; + } + + const oldLabels = await models.PipelineLabels.find({ + _id: { $in: item.labelIds }, + }).lean(); + + const updatedLabelIds: string[] = []; + + const existingLabels = await models.PipelineLabels.find({ + name: { $in: oldLabels.map((o) => o.name) }, + colorCode: { $in: oldLabels.map((o) => o.colorCode) }, + pipelineId: newStage.pipelineId, + }).lean(); + + // index using only name and colorCode, since all pipelineIds are same + const existingLabelsByUnique = _.indexBy( + existingLabels, + ({ name, colorCode }) => JSON.stringify({ name, colorCode }), + ); + + // Collect labels that don't exist on the new stage's pipeline here + const notExistingLabels: any[] = []; + + for (const label of oldLabels) { + const exists = + existingLabelsByUnique[ + JSON.stringify({ name: label.name, colorCode: label.colorCode }) + ]; + if (!exists) { + notExistingLabels.push({ + name: label.name, + colorCode: label.colorCode, + pipelineId: newStage.pipelineId, + createdAt: new Date(), + createdBy: user._id, + }); + } else { + updatedLabelIds.push(exists._id); + } + } // end label loop + + // Insert labels that don't already exist on the new stage's pipeline + const newLabels = await models.PipelineLabels.insertMany(notExistingLabels, { + ordered: false, + }); + + for (const newLabel of newLabels) { + updatedLabelIds.push(newLabel._id); + } + + await models.PipelineLabels.labelsLabel(item._id, updatedLabelIds); +}; + +export const changeItemStatus = async ( + models: IModels, + user: any, + { + item, + status, + proccessId, + stage, + }: { + item: any; + status: string; + proccessId: string; + stage: IStageDocument; + }, +) => { + if (status === 'archived') { + graphqlPubsub.publish(`tasksPipelinesChanged:${stage.pipelineId}`, { + tasksPipelinesChanged: { + _id: stage.pipelineId, + proccessId, + action: 'itemRemove', + data: { + item, + oldStageId: item.stageId, + }, + }, + }); + + return; + } + + const aboveItems = await models.Tasks.find({ + stageId: item.stageId, + status: { $ne: TASK_STATUSES.ARCHIVED }, + order: { $lt: item.order }, + }) + .sort({ order: -1 }) + .limit(1); + + const aboveItemId = aboveItems[0]?._id || ''; + + // maybe, recovered order includes to oldOrders + await models.Tasks.updateOne( + { + _id: item._id, + }, + { + order: await getNewOrder({ + collection: models.Tasks, + stageId: item.stageId, + aboveItemId, + }), + }, + ); + + graphqlPubsub.publish(`tasksPipelinesChanged:${stage.pipelineId}`, { + tasksPipelinesChanged: { + _id: stage.pipelineId, + proccessId, + action: 'itemAdd', + data: { + item: { + ...item._doc, + ...(await itemResolver(models, user, item)), + }, + aboveItemId, + destinationStageId: item.stageId, + }, + }, + }); +}; + +export const itemMover = async ( + models: IModels, + userId: string, + item: ITaskDocument, + destinationStageId: string, +) => { + const oldStageId = item.stageId; + + let action = `changed order of your task:`; + let content = `'${item.name}'`; + + if (oldStageId !== destinationStageId) { + const stage = await models.Stages.getStage(destinationStageId); + const oldStage = await models.Stages.getStage(oldStageId); + + const pipeline = await models.Pipelines.getPipeline(stage.pipelineId); + const oldPipeline = await models.Pipelines.getPipeline(oldStage.pipelineId); + + const board = await models.Boards.getBoard(pipeline.boardId); + const oldBoard = await models.Boards.getBoard(oldPipeline.boardId); + + action = `moved '${item.name}' from ${oldBoard.name}-${oldPipeline.name}-${oldStage.name} to `; + + content = `${board.name}-${pipeline.name}-${stage.name}`; + + // const link = `/${contentType}/board?id=${board._id}&pipelineId=${pipeline._id}&itemId=${item._id}`; + + // const activityLogContent = { + // oldStageId, + // destinationStageId, + // text: `${oldStage.name} to ${stage.name}` + // }; + + // await putActivityLog(subdomain, { + // action: "createBoardItemMovementLog", + // data: { + // item, + // contentType, + // userId, + // activityLogContent, + // link, + // action: "moved", + // contentId: item._id, + // createdBy: userId, + // content: activityLogContent + // } + // }); + + // sendNotificationsMessage({ + // subdomain, + // action: "batchUpdate", + // data: { + // selector: { contentType, contentTypeId: item._id }, + // modifier: { $set: { link } } + // } + // }); + } + + return { content, action }; +}; + +export const checkMovePermission = ( + stage: IStageDocument, + user: IUserDocument, +) => { + if ( + stage.canMoveMemberIds && + stage.canMoveMemberIds.length > 0 && + !stage.canMoveMemberIds.includes(user._id) + ) { + throw new Error('Permission denied'); + } +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/board.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/board.ts new file mode 100644 index 0000000000..ededc15a04 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/board.ts @@ -0,0 +1,34 @@ +const typeDeps = ` + type TasksBoardCount { + _id: String + name: String + count: Int + } +`; + +export const types = ` + + ${typeDeps} + + type TasksBoard @key(fields: "_id") { + _id: String! + name: String! + order: Int + createdAt: Date + type: String + pipelines: [TasksPipeline] + } +`; + +export const queries = ` + tasksBoards: [TasksBoard] + tasksBoardCounts: [TasksBoardCount] + tasksBoardGetLast: TasksBoard + tasksBoardDetail(_id: String!): TasksBoard +`; + +export const mutations = ` + tasksBoardsAdd(name: String!): TasksBoard + tasksBoardsEdit(_id: String!, name: String!): TasksBoard + tasksBoardsRemove(_id: String!): JSON +`; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/checklist.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/checklist.ts new file mode 100644 index 0000000000..fc83ac56a2 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/checklist.ts @@ -0,0 +1,37 @@ +export const types = ` + type TasksChecklistItem { + _id: String! + checklistId: String + isChecked: Boolean + content: String + order: Int + } + + type TasksChecklist { + _id: String! + contentType: String + contentTypeId: String + title: String + createdUserId: String + createdDate: Date + items: [TasksChecklistItem] + percent: Float + } + +`; + +export const queries = ` + tasksChecklists(contentTypeId: String): [TasksChecklist] + tasksChecklistDetail(_id: String!): TasksChecklist +`; + +export const mutations = ` + tasksChecklistsAdd(contentTypeId: String, title: String): TasksChecklist + tasksChecklistsEdit(_id: String!, title: String, contentTypeId: String): TasksChecklist + tasksChecklistsRemove(_id: String!): TasksChecklist + tasksChecklistItemsOrder(_id: String!, destinationIndex: Int): TasksChecklistItem + + tasksChecklistItemsAdd(checklistId: String, content: String, isChecked: Boolean): TasksChecklistItem + tasksChecklistItemsEdit(_id: String!, checklistId: String, content: String, isChecked: Boolean): TasksChecklistItem + tasksChecklistItemsRemove(_id: String!): TasksChecklistItem +`; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/index.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/index.ts new file mode 100644 index 0000000000..6c2d020d48 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/index.ts @@ -0,0 +1,62 @@ +import { + mutations as boardMutations, + queries as boardQueries, + types as boardTypes, +} from './board'; + +import { + mutations as pipelineMutations, + queries as pipelineQueries, + types as pipelineTypes, +} from './pipeline'; + +import { + mutations as checkListMutations, + queries as checkListQueries, + types as checkListTypes, +} from './checklist'; + +import { + mutations as taskMutations, + queries as taskQueries, + types as taskTypes, +} from './task'; + +import { + mutations as pipelineLabelMutations, + queries as pipelineLabelQueries, + types as pipelineLabelTypes, +} from './label'; + +import { + mutations as stageMutations, + queries as stageQueries, + types as stageTypes, +} from './stage'; + +export const types = ` + ${checkListTypes} + ${boardTypes} + ${pipelineTypes} + ${taskTypes} + ${pipelineLabelTypes} + ${stageTypes} +`; + +export const queries = ` + ${checkListQueries} + ${boardQueries} + ${pipelineQueries} + ${taskQueries} + ${pipelineLabelQueries} + ${stageQueries} +`; + +export const mutations = ` + ${checkListMutations} + ${boardMutations} + ${pipelineMutations} + ${taskMutations} + ${pipelineLabelMutations} + ${stageMutations} +`; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/label.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/label.ts new file mode 100644 index 0000000000..1f7c7d663b --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/label.ts @@ -0,0 +1,28 @@ +export const types = ` + type TasksPipelineLabel @key(fields: "_id") { + _id: String! + name: String! + colorCode: String + pipelineId: String + createdBy: String + createdAt: Date + } +`; + +export const queries = ` + tasksPipelineLabels(pipelineId: String, pipelineIds: [String]): [TasksPipelineLabel] + tasksPipelineLabelDetail(_id: String!): TasksPipelineLabel +`; + +const mutationParams = ` + name: String! + colorCode: String! + pipelineId: String! +`; + +export const mutations = ` + tasksPipelineLabelsAdd(${mutationParams}): TasksPipelineLabel + tasksPipelineLabelsEdit(_id: String!, ${mutationParams}): TasksPipelineLabel + tasksPipelineLabelsRemove(_id: String!): JSON + tasksPipelineLabelsLabel(pipelineId: String!, targetId: String!, labelIds: [String!]!): String +`; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/pipeline.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/pipeline.ts new file mode 100644 index 0000000000..09520f6f0c --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/pipeline.ts @@ -0,0 +1,77 @@ +export const types = ` + type TasksPipeline @key(fields: "_id") { + _id: String! + name: String! + status: String + boardId: String! + tagId: String + tag: Tag + visibility: String! + memberIds: [String] + departmentIds: [String] + branchIds: [String] + members: [User] + bgColor: String + isWatched: Boolean + itemsTotalCount: Int + userId: String + createdUser: User + startDate: Date + endDate: Date + metric: String + hackScoringType: String + templateId: String + state: String + isCheckDate: Boolean + isCheckUser: Boolean + isCheckDepartment: Boolean + excludeCheckUserIds: [String] + numberConfig: String + numberSize: String + nameConfig: String + order: Int + createdAt: Date + type: String + } +`; + +export const queries = ` + tasksPipelines(boardId: String, isAll: Boolean): [TasksPipeline] + tasksPipelineDetail(_id: String!): TasksPipeline + tasksPipelineAssignedUsers(_id: String!): [User] + tasksPipelineStateCount(boardId: String): JSON +`; + +const mutationParams = ` + name: String!, + boardId: String!, + stages: JSON, + visibility: String!, + memberIds: [String], + tagId: String, + bgColor: String, + startDate: Date, + endDate: Date, + metric: String, + hackScoringType: String, + templateId: String, + isCheckDate: Boolean + isCheckUser: Boolean + isCheckDepartment: Boolean + excludeCheckUserIds: [String], + numberConfig: String, + numberSize: String, + nameConfig: String, + departmentIds: [String], + branchIds: [String], +`; + +export const mutations = ` + tasksPipelinesAdd(${mutationParams}): TasksPipeline + tasksPipelinesEdit(_id: String!, ${mutationParams}): TasksPipeline + tasksPipelinesUpdateOrder(orders: [TasksOrderItem]): [TasksPipeline] + tasksPipelinesWatch(_id: String!, isAdd: Boolean, type: String!): TasksPipeline + tasksPipelinesRemove(_id: String!): JSON + tasksPipelinesArchive(_id: String!): JSON + tasksPipelinesCopied(_id: String!): JSON +`; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/stage.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/stage.ts new file mode 100644 index 0000000000..80aa0a869d --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/stage.ts @@ -0,0 +1,83 @@ +const inputDeps = ` + input TasksOrderItem { + _id: String! + order: Int! + } +`; + +export const types = ` + + ${inputDeps} + + type TasksStage @key(fields: "_id") { + _id: String! + name: String! + pipelineId: String! + visibility: String + code: String + memberIds: [String] + canMoveMemberIds: [String] + canEditMemberIds: [String] + members: [User] + departmentIds: [String] + probability: String + status: String + unUsedAmount: JSON + amount: JSON + itemsTotalCount: Int + compareNextStageTasks: JSON + stayedTasksTotalCount: Int + initialTasksTotalCount: Int + inProcessTasksTotalCount: Int + formId: String + age: Int + defaultTick: Boolean + order: Int + createdAt: Date + type: String + } +`; + +const queryParams = ` + search: String, + companyIds: [String] + customerIds: [String] + assignedUserIds: [String] + labelIds: [String] + extraParams: JSON, + closeDateType: String, + assignedToMe: String, + age: Int, + branchIds: [String] + departmentIds: [String] + segment: String + segmentData:String + createdStartDate: Date + createdEndDate: Date + stateChangedStartDate: Date + stateChangedEndDate: Date + startDateStartDate: Date + startDateEndDate: Date + closeDateStartDate: Date + closeDateEndDate: Date +`; + +export const queries = ` + tasksStages( + isNotLost: Boolean, + isAll: Boolean, + pipelineId: String, + pipelineIds: [String], + ${queryParams} + ): [TasksStage] + tasksStageDetail(_id: String!, ${queryParams}): TasksStage + tasksArchivedStages(pipelineId: String!, search: String): [TasksStage] + tasksArchivedStagesCount(pipelineId: String!, search: String): Int +`; + +export const mutations = ` + tasksStagesUpdateOrder(orders: [TasksOrderItem]): [TasksStage] + tasksStagesRemove(_id: String!): JSON + tasksStagesEdit(_id: String!, name: String, status: String): TasksStage + tasksStagesSortItems(stageId: String!, proccessId: String, sortType: String): String +`; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/task.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/task.ts new file mode 100644 index 0000000000..0fad804560 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/task.ts @@ -0,0 +1,173 @@ +import { GQL_CURSOR_PARAM_DEFS } from 'erxes-api-shared/utils'; + +const typeDefs = ` + type TasksTimeTrack { + status: String, + timeSpent: Int, + startDate: String + } +`; + +const inputDeps = ` + input TasksItemDate { + month: Int + year: Int + } +`; + +export const types = ` + + ${typeDefs} + ${inputDeps} + + type Task @key(fields: "_id") { + _id: String! + name: String! + order: Float + createdAt: Date + hasNotified: Boolean + assignedUserIds: [String] + branchIds: [String] + departmentIds:[String] + labelIds: [String] + startDate: Date + closeDate: Date + description: String + modifiedAt: Date + modifiedBy: String + reminderMinute: Int, + isComplete: Boolean, + isWatched: Boolean, + stageId: String + boardId: String + priority: String + status: String + attachments: [Attachment] + userId: String + tagIds: [String] + + assignedUsers: [User] + stage: TasksStage + labels: [TasksPipelineLabel] + pipeline: TasksPipeline + createdUser: User + customFieldsData: JSON + score: Float + timeTrack: TasksTimeTrack + number: String + stageChangedDate: Date + + customProperties: JSON + companies: [Company] + customers: [Customer] + tags: [Tag] + + cursor: String + } + + type TasksListResponse { + list: [Task] + pageInfo: PageInfo + totalCount: Int + } +`; + +const queryParams = ` + _ids: [String] + pipelineId: String + pipelineIds: [String] + parentId:String + stageId: String + customerIds: [String] + companyIds: [String] + date: TasksItemDate + skip: Int + limit: Int + search: String + assignedUserIds: [String] + closeDateType: String + priority: [String] + labelIds: [String] + userIds: [String] + segment: String + segmentData: String + assignedToMe: String + startDate: String + endDate: String + hasStartAndCloseDate: Boolean + tagIds: [String] + noSkipArchive: Boolean + number: String + branchIds: [String] + departmentIds: [String] + boardIds: [String] + stageCodes: [String] + dateRangeFilters:JSON + customFieldsDataFilters:JSON + createdStartDate: Date, + createdEndDate: Date + stateChangedStartDate: Date + stateChangedEndDate: Date + startDateStartDate: Date + startDateEndDate: Date + closeDateStartDate: Date + closeDateEndDate: Date + resolvedDayBetween:[Int] + + ${GQL_CURSOR_PARAM_DEFS} +`; + +const archivedQueryParams = ` + pipelineId: String! + search: String + userIds: [String] + priorities: [String] + assignedUserIds: [String] + labelIds: [String] + companyIds: [String] + customerIds: [String] + startDate: String + endDate: String + + ${GQL_CURSOR_PARAM_DEFS} +`; + +export const queries = ` + tasks(${queryParams}): TasksListResponse + tasksTotalCount(${queryParams}): Int + taskDetail(_id: String!, clientPortalCard:Boolean): Task + archivedTasks(${archivedQueryParams}): TasksListResponse + archivedTasksCount(${archivedQueryParams}): Int +`; + +const mutationParams = ` + parentId:String, + proccessId: String, + aboveItemId: String, + stageId: String, + assignedUserIds: [String], + attachments: [AttachmentInput], + startDate: Date, + closeDate: Date, + description: String, + order: Int, + reminderMinute: Int, + isComplete: Boolean, + priority: String, + status: String, + sourceConversationIds: [String], + customFieldsData: JSON, + tagIds: [String], + branchIds: [String], + departmentIds: [String], +`; + +export const mutations = ` + tasksAdd(name: String!, companyIds: [String], customerIds: [String], labelIds: [String], ${mutationParams}): Task + tasksEdit(_id: String!, name: String, ${mutationParams}): Task + tasksChange(itemId: String!, aboveItemId: String, destinationStageId: String!, sourceStageId: String, proccessId: String): Task + tasksRemove(_id: String!): Task + tasksWatch(_id: String, isAdd: Boolean): Task + tasksCopy(_id: String!, proccessId: String): Task + tasksArchive(stageId: String!, proccessId: String): String +`; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/tasks.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/tasks.ts deleted file mode 100644 index 1a2489e7a6..0000000000 --- a/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/tasks.ts +++ /dev/null @@ -1,18 +0,0 @@ -export const types = ` - type Tasks { - _id: String - name: String - description: String - } -`; - -export const queries = ` - getTasks(_id: String!): Tasks - getTaskss: [Tasks] -`; - -export const mutations = ` - createTasks(name: String!): Tasks - updateTasks(_id: String!, name: String!): Tasks - removeTasks(_id: String!): Tasks -`; diff --git a/backend/plugins/operation_api/src/modules/tasks/resolver.ts b/backend/plugins/operation_api/src/modules/tasks/resolver.ts new file mode 100644 index 0000000000..b578f8cda5 --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/resolver.ts @@ -0,0 +1,67 @@ +import mongoose from 'mongoose'; +import { IModels } from '~/connectionResolvers'; +import { IBoardDocument } from '~/modules/tasks/@types/boards'; +import { + IChecklistDocument, + IChecklistItemDocument, +} from '~/modules/tasks/@types/checklists'; +import { IPipelineLabelDocument } from '~/modules/tasks/@types/labels'; +import { IPipelineDocument } from '~/modules/tasks/@types/pipelines'; +import { IStageDocument } from '~/modules/tasks/@types/stages'; +import { ITaskDocument } from '~/modules/tasks/@types/tasks'; +import { IBoardModel, loadBoardClass } from '~/modules/tasks/db/models/Boards'; +import { + IChecklistItemModel, + IChecklistModel, + loadChecklistClass, + loadChecklistItemClass, +} from '~/modules/tasks/db/models/Checklists'; +import { + IPipelineLabelModel, + loadPipelineLabelClass, +} from '~/modules/tasks/db/models/Labels'; +import { + IPipelineModel, + loadPipelineClass, +} from '~/modules/tasks/db/models/Pipelines'; +import { IStageModel, loadStageClass } from '~/modules/tasks/db/models/Stages'; +import { ITaskModel, loadTaskClass } from '~/modules/tasks/db/models/Tasks'; + +export const loadTaskClasses = (models: IModels, db: mongoose.Connection) => { + models.Boards = db.model( + 'tasks_boards', + loadBoardClass(models), + ); + + models.Pipelines = db.model( + 'tasks_pipelines', + loadPipelineClass(models), + ); + + models.Stages = db.model( + 'tasks_stages', + loadStageClass(models), + ); + + models.Tasks = db.model( + 'tasks', + loadTaskClass(models), + ); + + models.Checklists = db.model( + 'tasks_checklists', + loadChecklistClass(models), + ); + + models.ChecklistItems = db.model( + 'tasks_checklist_items', + loadChecklistItemClass(models), + ); + + models.PipelineLabels = db.model( + 'tasks_pipeline_labels', + loadPipelineLabelClass(models), + ); + + return models; +}; diff --git a/backend/plugins/operation_api/src/modules/tasks/utils.ts b/backend/plugins/operation_api/src/modules/tasks/utils.ts new file mode 100644 index 0000000000..426ad0c1be --- /dev/null +++ b/backend/plugins/operation_api/src/modules/tasks/utils.ts @@ -0,0 +1,9 @@ +export const configReplacer = (config) => { + const now = new Date(); + + // replace type of date + return config + .replace(/\{year}/g, now.getFullYear().toString()) + .replace(/\{month}/g, `0${(now.getMonth() + 1).toString()}`.slice(-2)) + .replace(/\{day}/g, `0${now.getDate().toString()}`.slice(-2)); +}; diff --git a/frontend/plugins/operation_ui/project.json b/frontend/plugins/operation_ui/project.json index 966ebf7618..49b1633c42 100644 --- a/frontend/plugins/operation_ui/project.json +++ b/frontend/plugins/operation_ui/project.json @@ -7,9 +7,7 @@ "targets": { "build": { "executor": "@nx/rspack:rspack", - "outputs": [ - "{options.outputPath}" - ], + "outputs": ["{options.outputPath}"], "defaultConfiguration": "production", "options": { "target": "web", @@ -17,9 +15,7 @@ "main": "frontend/plugins/operation_ui/src/main.ts", "tsConfig": "frontend/plugins/operation_ui/tsconfig.app.json", "rspackConfig": "frontend/plugins/operation_ui/rspack.config.ts", - "assets": [ - "frontend/plugins/operation_ui/src/assets" - ] + "assets": ["frontend/plugins/operation_ui/src/assets"] }, "configurations": { "development": { @@ -37,7 +33,7 @@ "executor": "@nx/rspack:module-federation-dev-server", "options": { "buildTarget": "operation_ui:build:development", - "port": 3005 + "port": 3006 }, "configurations": { "development": {}, @@ -62,4 +58,4 @@ } } } -} \ No newline at end of file +} From 53287f8ef00d70d9dccf1d41f7b9cbe50b303a80 Mon Sep 17 00:00:00 2001 From: batmnkh2344 Date: Fri, 20 Jun 2025 19:06:16 +0800 Subject: [PATCH 3/3] resolve review --- .../src/apollo/resolvers/index.ts | 3 +- .../src/modules/tasks/constants.ts | 4 +- .../src/modules/tasks/db/models/Checklists.ts | 10 +- .../src/modules/tasks/db/models/utils.ts | 6 +- .../resolvers/customResolvers/stage.ts | 24 ++--- .../graphql/resolvers/mutations/pipeline.ts | 2 +- .../tasks/graphql/resolvers/mutations/task.ts | 10 +- .../tasks/graphql/resolvers/queries/task.ts | 6 +- .../modules/tasks/graphql/resolvers/utils.ts | 102 +++++++++--------- .../modules/tasks/graphql/schemas/pipeline.ts | 6 +- .../modules/tasks/graphql/schemas/stage.ts | 2 +- 11 files changed, 80 insertions(+), 95 deletions(-) diff --git a/backend/plugins/operation_api/src/apollo/resolvers/index.ts b/backend/plugins/operation_api/src/apollo/resolvers/index.ts index d4b64cf3b6..a6a6ef91c0 100644 --- a/backend/plugins/operation_api/src/apollo/resolvers/index.ts +++ b/backend/plugins/operation_api/src/apollo/resolvers/index.ts @@ -1,7 +1,8 @@ import { apolloCustomScalars } from 'erxes-api-shared/utils'; -import { customResolvers } from './resolvers'; import { mutations } from './mutations'; import { queries } from './queries'; +import { customResolvers } from './resolvers'; + const resolvers: any = { Mutation: { ...mutations, diff --git a/backend/plugins/operation_api/src/modules/tasks/constants.ts b/backend/plugins/operation_api/src/modules/tasks/constants.ts index e8b1465a00..4f5812bc91 100644 --- a/backend/plugins/operation_api/src/modules/tasks/constants.ts +++ b/backend/plugins/operation_api/src/modules/tasks/constants.ts @@ -15,7 +15,7 @@ export const PROBABILITY = { TEN: '10%', TWENTY: '20%', THIRTY: '30%', - FOURTY: '40%', + FORTY: '40%', FIFTY: '50%', SIXTY: '60%', SEVENTY: '70%', @@ -80,7 +80,7 @@ export const CLOSE_DATE_TYPES = { value: 'noCloseDate', }, { - name: 'Over due', + name: 'Overdue', value: 'overdue', }, ], diff --git a/backend/plugins/operation_api/src/modules/tasks/db/models/Checklists.ts b/backend/plugins/operation_api/src/modules/tasks/db/models/Checklists.ts index 26a737c080..bb9963f4bc 100644 --- a/backend/plugins/operation_api/src/modules/tasks/db/models/Checklists.ts +++ b/backend/plugins/operation_api/src/modules/tasks/db/models/Checklists.ts @@ -44,7 +44,7 @@ export const loadChecklistClass = (models: IModels) => { { contentTypeId, ...fields }: IChecklist, user: IUserDocument, ) { - await models.Checklists.create({ + return await models.Checklists.create({ contentTypeId, createdUserId: user._id, ...fields, @@ -131,14 +131,12 @@ export const loadChecklistItemClass = (models: IModels) => { checklistId, }).countDocuments(); - const checklistItem = await models.ChecklistItems.create({ + return await models.ChecklistItems.create({ checklistId, createdUserId: user._id, order: itemsCount + 1, ...fields, }); - - return checklistItem; } /* @@ -153,7 +151,7 @@ export const loadChecklistItemClass = (models: IModels) => { } /* - * Remove checklist + * Remove checklist item */ public static async removeChecklistItem(_id: string) { const checklistItem = await models.ChecklistItems.findOneAndDelete({ @@ -161,7 +159,7 @@ export const loadChecklistItemClass = (models: IModels) => { }); if (!checklistItem) { - throw new Error(`Checklist's item not found with id ${_id}`); + throw new Error(`Checklist item not found with id ${_id}`); } return checklistItem; diff --git a/backend/plugins/operation_api/src/modules/tasks/db/models/utils.ts b/backend/plugins/operation_api/src/modules/tasks/db/models/utils.ts index 71372e1dcb..ac27fab3f9 100644 --- a/backend/plugins/operation_api/src/modules/tasks/db/models/utils.ts +++ b/backend/plugins/operation_api/src/modules/tasks/db/models/utils.ts @@ -43,7 +43,7 @@ export const boardNumberGenerator = async ( }); if (pipeline?.lastNum) { - const lastNum = pipeline.lastNum; + const { lastNum } = pipeline; const lastGeneratedNumber = lastNum.slice(replacedConfig.length); @@ -82,14 +82,12 @@ export const generateLastNum = async (models: IModels, doc: IPipeline) => { } // generate new number by new numberConfig - const generatedNum = await boardNumberGenerator( + return await boardNumberGenerator( models, doc.numberConfig || '', doc.numberSize || '', true, ); - - return generatedNum; }; export const fillSearchTextItem = (doc: ITask, item?: ITask) => { diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/stage.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/stage.ts index 03d2c57d9e..a70bb44bde 100644 --- a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/stage.ts +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/customResolvers/stage.ts @@ -22,33 +22,22 @@ export default { return []; }, - async unUsedAmount( + async unusedAmount( stage: IStageDocument, _args, { user, models }: IContext, { variableValues: args }, ) { - const amountsMap = getAmountsMap( - models, - models.Tasks, - user, - args, - stage, - false, - ); - - return amountsMap; + return getAmountsMap(models, models.Tasks, user, args, stage, false); }, async amount( - stage: IStageDocument, + stage: IStageDocument, _args, { user, models }: IContext, { variableValues: args }, ) { - const amountsMap = getAmountsMap(models, models.Tasks, user, args, stage); - - return amountsMap; + return getAmountsMap(models, models.Tasks, user, args, stage); }, async itemsTotalCount( @@ -235,7 +224,10 @@ export default { if (stages.length === 2) { const [first, second] = stages; result.count = first.currentTaskCount - second.currentTaskCount; - result.percent = (second.initialTaskCount * 100) / first.initialTaskCount; + result.percent = + first.initialTaskCount > 0 + ? (second.initialTaskCount * 100) / first.initialTaskCount + : 0; } return result; diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/pipeline.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/pipeline.ts index de547393b7..656df881a4 100644 --- a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/pipeline.ts +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/pipeline.ts @@ -114,7 +114,7 @@ export const pipelineMutations = { */ async tasksPipelinesArchive( _root, - { _id, status }: { _id; status: string }, + { _id, status }: { _id: string; status: string }, { models }: IContext, ) { return await models.Pipelines.archivePipeline(_id, status); diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/task.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/task.ts index 3c6bdc8859..776e99148d 100644 --- a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/task.ts +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/mutations/task.ts @@ -20,11 +20,11 @@ export const taskMutations = { { user, models }: IContext, ) { doc.initialStageId = doc.stageId; - doc.watchedUserIds = user && [user._id]; + doc.watchedUserIds = user ? [user._id] : undefined; const extendedDoc = { ...doc, - modifiedBy: user && user._id, + modifiedBy: user ? user._id : undefined, userId: user ? user._id : doc.userId, order: await getNewOrder({ collection: models.Tasks, @@ -404,9 +404,7 @@ export const taskMutations = { await models.Checklists.removeChecklists([task._id]); - const removed = await models.Tasks.findOneAndDelete({ _id: task._id }); - - return removed; + return await models.Tasks.findOneAndDelete({ _id: task._id }); }, /** @@ -454,7 +452,7 @@ export const taskMutations = { })), }; - delete doc.sourceConversationIds; + doc.sourceConversationIds = undefined; const clone = await models.Tasks.createTask(doc); diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/task.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/task.ts index d4dbf409f5..406be186ee 100644 --- a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/task.ts +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/queries/task.ts @@ -6,7 +6,7 @@ import { } from '~/modules/tasks/@types/tasks'; import { compareDepartmentIds, - generateArhivedTasksFilter, + generateArchivedTasksFilter, generateFilter, } from '~/modules/tasks/graphql/resolvers/utils'; @@ -176,7 +176,7 @@ export const taskQueries = { const stages = await models.Stages.find({ pipelineId }).lean(); if (stages.length > 0) { - const filter = generateArhivedTasksFilter(args, stages); + const filter = generateArchivedTasksFilter(args, stages); const { list, pageInfo, totalCount } = await cursorPaginate({ model: models.Tasks, @@ -200,7 +200,7 @@ export const taskQueries = { const stages = await models.Stages.find({ pipelineId }); if (stages.length > 0) { - const filter = generateArhivedTasksFilter(args, stages); + const filter = generateArchivedTasksFilter(args, stages); return models.Tasks.find(filter).countDocuments(); } diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/utils.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/utils.ts index f43b5e3bf8..556b2f99b6 100644 --- a/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/utils.ts +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/resolvers/utils.ts @@ -104,7 +104,7 @@ export const bulkUpdateOrders = async ({ }, }); - ord = ord + 10; + ord += 10; } if (!bulkOps.length) { @@ -166,11 +166,11 @@ export const getAmountsMap = async ( export const getCloseDateByType = (closeDateType: string) => { if (closeDateType === CLOSE_DATE_TYPES.NEXT_DAY) { - const tommorrow = moment().add(1, 'days'); + const tomorrow = moment().add(1, 'days'); return { - $gte: new Date(tommorrow.startOf('day').toISOString()), - $lte: new Date(tommorrow.endOf('day').toISOString()), + $gte: new Date(tomorrow.startOf('day').toISOString()), + $lte: new Date(tomorrow.endOf('day').toISOString()), }; } @@ -646,54 +646,52 @@ export const generateFilter = async ( { userId: { $in: uqinueCheckUserIds } }, ], }); - } else { - if ( - (pipeline.isCheckUser || pipeline.isCheckDepartment) && - !isEligibleSeeAllCards - ) { - let includeCheckUserIds: string[] = []; - - if (pipeline.isCheckDepartment) { - const userDepartmentIds = user?.departmentIds || []; - const commonIds = userDepartmentIds.filter((id) => - pipelineDepartmentIds.includes(id), - ); - - const otherDepartmentUsers = await sendTRPCMessage({ - pluginName: 'core', - method: 'query', - module: 'users', - action: 'find', - input: { - query: { departmentIds: { $in: commonIds } }, - }, - defaultValue: [], - }); - - for (const departmentUser of otherDepartmentUsers) { - includeCheckUserIds = [...includeCheckUserIds, departmentUser._id]; - } - - if ( - pipelineDepartmentIds.filter((departmentId) => - userDepartmentIds.includes(departmentId), - ).length - ) { - includeCheckUserIds = includeCheckUserIds.concat(user._id || []); - } - } + } else if ( + (pipeline.isCheckUser || pipeline.isCheckDepartment) && + !isEligibleSeeAllCards + ) { + let includeCheckUserIds: string[] = []; + + if (pipeline.isCheckDepartment) { + const userDepartmentIds = user?.departmentIds || []; + const commonIds = userDepartmentIds.filter((id) => + pipelineDepartmentIds.includes(id), + ); + + const otherDepartmentUsers = await sendTRPCMessage({ + pluginName: 'core', + method: 'query', + module: 'users', + action: 'find', + input: { + query: { departmentIds: { $in: commonIds } }, + }, + defaultValue: [], + }); - const uqinueCheckUserIds = [ - ...new Set(includeCheckUserIds.concat(currentUserId)), - ]; + for (const departmentUser of otherDepartmentUsers) { + includeCheckUserIds = [...includeCheckUserIds, departmentUser._id]; + } - Object.assign(filter, { - $or: [ - { assignedUserIds: { $in: uqinueCheckUserIds } }, - { userId: { $in: uqinueCheckUserIds } }, - ], - }); + if ( + pipelineDepartmentIds.filter((departmentId) => + userDepartmentIds.includes(departmentId), + ).length + ) { + includeCheckUserIds = includeCheckUserIds.concat(user._id || []); + } } + + const uqinueCheckUserIds = [ + ...new Set(includeCheckUserIds.concat(currentUserId)), + ]; + + Object.assign(filter, { + $or: [ + { assignedUserIds: { $in: uqinueCheckUserIds } }, + { userId: { $in: uqinueCheckUserIds } }, + ], + }); } } @@ -761,7 +759,7 @@ export const generateFilter = async ( return filter; }; -export const generateArhivedTasksFilter = ( +export const generateArchivedTasksFilter = ( params: IArchivedTaskQueryParams, stages: IStageDocument[], ) => { @@ -854,7 +852,7 @@ const randomBetween = (min: number, max: number) => { return Math.random() * (max - min) + min; }; -const orderHeler = (aboveOrder, belowOrder) => { +const orderHelper = (aboveOrder, belowOrder) => { // empty stage if (!aboveOrder && !belowOrder) { return 100; @@ -898,7 +896,7 @@ export const getNewOrder = async ({ const belowOrder = belowItems[0]?.order; - const order = orderHeler(aboveOrder, belowOrder); + const order = orderHelper(aboveOrder, belowOrder); // if duplicated order, then in stages items bulkUpdate 100, 110, 120, 130 if ([aboveOrder, belowOrder].includes(order)) { diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/pipeline.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/pipeline.ts index 09520f6f0c..93b6bbcce8 100644 --- a/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/pipeline.ts +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/pipeline.ts @@ -55,9 +55,9 @@ const mutationParams = ` metric: String, hackScoringType: String, templateId: String, - isCheckDate: Boolean - isCheckUser: Boolean - isCheckDepartment: Boolean + isCheckDate: Boolean, + isCheckUser: Boolean, + isCheckDepartment: Boolean, excludeCheckUserIds: [String], numberConfig: String, numberSize: String, diff --git a/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/stage.ts b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/stage.ts index 80aa0a869d..9bb269a558 100644 --- a/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/stage.ts +++ b/backend/plugins/operation_api/src/modules/tasks/graphql/schemas/stage.ts @@ -22,7 +22,7 @@ export const types = ` departmentIds: [String] probability: String status: String - unUsedAmount: JSON + unusedAmount: JSON amount: JSON itemsTotalCount: Int compareNextStageTasks: JSON