diff --git a/.vscode/settings.json b/.vscode/settings.json index 0967ef424b..307e5686dc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1 +1,40 @@ -{} +{ + "workbench.colorTheme": "Dracula Soft", + "editor.fontSize": 12, + "editor.fontFamily": "Menlo, Monaco, 'Courier New', monospace", + "editor.wordWrap": "on", + "editor.lineHeight": 30, + "editor.cursorBlinking": "smooth", + "explorer.openEditors.visible": 0, + // "editor.minimap.enabled": false, + "explorer.sortOrder": "type", + "files.trimFinalNewlines": true, + "workbench.editor.highlightModifiedTabs": true, + "files.autoSave": "onFocusChange", + "editor.suggestSelection": "first", + "vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue", + "git.fetchOnPull": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "git.autofetch": true, + "git.confirmSync": false, + "tabnine.experimentalAutoImports": true, + "prettier.trailingComma": "none", + "typescript.preferences.quoteStyle": "single", + "javascript.preferences.quoteStyle": "single", + "javascript.format.insertSpaceAfterCommaDelimiter": false, + "editor.formatOnSave": true, + "workbench.editor.enablePreviewFromQuickOpen": true, + "prettier.jsxSingleQuote": true, + "html.completion.attributeDefaultValue": "singlequotes", + "typescript.disableAutomaticTypeAcquisition": true, + "prettier.singleAttributePerLine": true, + // "prettier.bracketSameLine": false, + "prettier.singleQuote": true, + "editor.formatOnPaste": true, + "[javascript]": { + "editor.formatOnSave": true + }, + "prettier.bracketSameLine": true, + "typescript.updateImportsOnFileMove.enabled": "always", + "editor.guides.indentation": true +} diff --git a/client-portal/modules/common/form/styles.tsx b/client-portal/modules/common/form/styles.tsx deleted file mode 100644 index 7a74fe440a..0000000000 --- a/client-portal/modules/common/form/styles.tsx +++ /dev/null @@ -1,410 +0,0 @@ -import { colors, dimensions, typography } from '../../styles'; -import styled, { css } from 'styled-components'; - -import { rgba } from '../../styles/ecolor'; -import styledTS from 'styled-components-ts'; - -const inputPadding = '0px'; -const inputHeight = '15px'; -const inputScale = '12px'; -const inputBorderWidth = '2px'; -const textInputHeight = '34px'; - -const Label = styledTS<{ uppercase?: boolean }>(styled.label)` - text-transform: ${props => (props.uppercase ? 'uppercase' : 'none')}; - display: inline-block; - font-weight: ${typography.fontWeightMedium}; - color: ${colors.textPrimary}; - font-size: ${typography.fontSizeUppercase}px; - - > span { - color: ${colors.colorCoreRed}; - } -`; - -const Formgroup = styledTS<{ horizontal?: boolean }>(styled.div)` - margin-bottom: 20px; - position: relative; - - ${props => - props.horizontal && - css` - display: flex; - gap: ${dimensions.coreSpacing}px; - - > div { - flex: 1; - } - `}; - - > label { - margin-right: ${dimensions.unitSpacing}px; - } - - p { - font-size: 12px; - color: ${colors.colorCoreGray}; - margin-bottom: 5px; - } -`; - -const ModalFooter = styled.div` - text-align: right; - margin-top: 30px; -`; - -const Input = styledTS<{ round?: boolean; hasError?: boolean }>(styled.input)` - display: block; - border: none; - width: 100%; - padding: ${dimensions.unitSpacing + 5}px; - color: ${colors.textPrimary}; - border: 1px solid; - border-radius: 12px; - border-color:${props => (props.hasError ? colors.colorCoreRed : '#DFDFE6')}; - background: none; - transition: all 0.3s ease; - - ${props => { - if (props.round) { - return ` - font-size: 13px; - border: 1px solid ${colors.borderDarker}; - border-radius: 20px; - padding: 5px 20px; - `; - } - - return ''; - }}; - - &:hover { - border-color: ${colors.colorLightGray}; - } - - &:focus { - outline: none; - border-color: ${colors.colorSecondary}; - } - - ::placeholder { - color: #aaa; - } -`; - -const SelectWrapper = styledTS<{ hasError?: boolean }>(styled.div)` - overflow: hidden; - border-bottom: 1px solid ${props => - props.hasError ? colors.colorCoreRed : colors.colorShadowGray}; - width: 100%; - height: ${textInputHeight}; - position: relative; - - &:after { - position: absolute; - right: 5px; - top: 12px; - content: '\\e9a6'; - font-size: 14px; - display: inline-block; - font-family: 'erxes'; - speak: none; - color: ${colors.colorCoreGray}; - font-style: normal; - font-weight: normal; - font-variant: normal; - text-transform: none; - text-rendering: auto; - line-height: 1; - -webkit-font-smoothing: antialiased; - } -`; - -const Select = styled(Input.withComponent('select'))` - border: none; - height: ${textInputHeight}; - padding: 0; - width: calc(100% + ${dimensions.coreSpacing}px); - -webkit-appearance: none; -`; - -const TextArea = styledTS<{ - maxHeight?: number; -}>(styled(Input.withComponent('textarea')))` - transition: none; - max-height: ${props => props.maxHeight && `${props.maxHeight}px`}; - min-height: 80px; - resize: none; -`; - -const FormLabel = styled.label` - position: relative; - display: inline-block; - font-weight: normal; - - span { - cursor: pointer; - display: inline-block; - } -`; - -const inputStyle = styledTS<{ disabled?: boolean; color?: string }>( - styled.input -)` - border: 0 !important; - clip: rect(1px, 1px, 1px, 1px) !important; - clip-path: inset(50%) !important; - height: 1px !important; - overflow: hidden !important; - padding: 0 !important; - position: absolute !important; - width: 1px !important; - white-space: nowrap !important; - cursor: ${props => props.disabled && 'not-allowed'} - - &:focus { - + span { - &::before { - box-shadow: 0 0 0 2px rgba(#333, 0.4) !important; - } - } - } - - &:hover { - + span { - &::before { - border-color: ${props => - props.color ? props.color : colors.colorLightGray}; - } - } - } - - &:active { - + span { - &::before { - transition-duration: 0; - } - } - } - - + span { - position: relative; - padding: ${inputPadding}; - user-select: none; - - &:before { - background-color: ${colors.colorWhite}; - border: ${inputBorderWidth} solid ${props => - props.color ? rgba(props.color, 0.7) : colors.colorShadowGray}; - box-sizing: content-box; - content: ''; - color: ${colors.colorWhite}; - margin-right: calc(${inputHeight} * 0.25); - top: 53%; - left: 0; - width: ${inputHeight}; - height: ${inputHeight}; - display: inline-block; - vertical-align: text-top; - border-radius: 2px; - cursor: ${props => props.disabled && 'not-allowed'} - } - - &:after { - box-sizing: content-box; - content: ''; - background-color: ${colors.colorWhite}; - position: absolute; - top: 56%; - left: calc(${inputPadding} + ${inputBorderWidth} + ${inputScale} / 2); - width: calc(${inputHeight} - ${inputScale}); - height: calc(${inputHeight} - ${inputScale}); - margin-top: calc((${inputHeight} - ${inputScale}) / -2); - transform: scale(0); - transform-origin: 51%; - transition: transform 200ms ease-out; - } - } - - + span:last-child:before { - margin-right: 0px; - } -`; - -const Radio = styled(inputStyle)` - + span { - &::before, - &::after { - border-radius: 50%; - } - } - - &:checked { - &:active, - &:focus { - + span { - &::before { - animation: none; - filter: none; - transition: none; - } - } - } - - + span { - &:before { - animation: none; - background-color: ${colors.colorSecondary}; - border-color: transparent; - } - - &:after { - transform: scale(1); - } - } - } -`; - -const Checkbox = styledTS<{ color?: string }>(styled(inputStyle))` - + span { - &:after { - background-color: transparent; - top: 53%; - left: calc(1px + ${inputHeight} / 5); - width: calc(${inputHeight} / 2); - height: calc(${inputHeight} / 5); - margin-top: calc(${inputHeight} / -2 / 2 * 0.8); - border-style: solid; - border-color: ${colors.colorWhite}; - border-width: 0 0 2px 2px; - border-radius: 0; - border-image: none; - transform: rotate(-45deg) scale(0); - transition: none; - } - } - - &:checked + span { - &:before { - animation: none; - background-color: ${props => - props.color ? props.color : colors.colorSecondary}; - border-color: transparent; - } - - &:after { - content: ''; - transform: rotate(-45deg) scale(1); - transition: transform 200ms ease-out; - } - } -`; - -const Error = styled.label` - color: ${colors.colorCoreRed}; - margin-top: ${dimensions.unitSpacing - 3}px; - display: block; - font-size: 12px; -`; - -const FlexWrapper = styled.span` - flex: 1; -`; - -const SortItem = styledTS<{ - isDragging: boolean; - isModal: boolean; - column?: number; -}>(styled.div)` - background: ${colors.colorWhite}; - display: block; - padding: 5px; - margin-bottom: 10px; - position: relative; - display: flex; - justify-content: space-between; - border-left: 2px solid transparent; - border-top: ${props => - !props.isDragging ? `1px solid ${colors.borderPrimary}` : 'none'}; - border-radius: 4px; - box-shadow: ${props => - props.isDragging ? `0 2px 8px ${colors.shadowPrimary}` : 'none'}; - left: ${props => - props.isDragging && props.isModal ? '40px!important' : 'auto'}; - &:last-child { - margin-bottom: 0; - } - - &:hover { - box-shadow: 0 2px 8px ${colors.shadowPrimary}; - border-color: ${colors.colorSecondary}; - border-top: none; - } - ${props => - props.column && - css` - width: ${100 / props.column}%; - display: inline-block; - `} -`; - -const SortableWrapper = styled.div` - width: 100%; - flex: 1; - label { - margin: 0; - } -`; - -const DragHandler = styled.div` - display: flex; - width: 20px; - margin-right: 5px; - align-items: center; - justify-content: center; - margin-top: 2px; - i { - color: ${colors.colorLightGray}; - } -`; - -const CustomRangeContainer = styled.div` - align-items: flex-end; - flex: 1; - margin-right: 8px; - .rdt { - display: block; - } - input[type='text'] { - border: none; - width: 100%; - height: 34px; - padding: 5px 0; - color: #444; - border-bottom: 1px solid; - border-color: #ddd; - background: none; - border-radius: 0; - box-shadow: none; - font-size: 13px; - } -`; - -export { - Input, - SelectWrapper, - Select, - TextArea, - Radio, - Checkbox, - FormLabel, - Label, - Formgroup, - FlexWrapper, - Error, - SortItem, - SortableWrapper, - DragHandler, - ModalFooter, - CustomRangeContainer -}; diff --git a/client-portal/pages/deals/index.tsx b/client-portal/pages/deals/index.tsx deleted file mode 100644 index 78435a2631..0000000000 --- a/client-portal/pages/deals/index.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import CardList from "../../modules/card/containers/List"; -import Layout from "../../modules/main/containers/Layout"; - -function Deal() { - return ( - - {(props) => { - return ; - }} - - ); -} - -export default Deal; diff --git a/packages/plugin-goals-api/.env.sample b/packages/plugin-goals-api/.env.sample new file mode 100644 index 0000000000..aa9e5b1498 --- /dev/null +++ b/packages/plugin-goals-api/.env.sample @@ -0,0 +1,14 @@ +# general +NODE_ENV=development +PORT=4011 + +# MongoDB +MONGO_URL=mongodb://localhost/erxes + +# RabbitMQ +RABBITMQ_HOST=amqp://localhost + +# Redis +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_PASSWORD= \ No newline at end of file diff --git a/packages/plugin-goals-api/package.json b/packages/plugin-goals-api/package.json new file mode 100644 index 0000000000..f2352741fd --- /dev/null +++ b/packages/plugin-goals-api/package.json @@ -0,0 +1,10 @@ +{ + "name": "@erxes/plugin-test-api", + "version": "1.0.0", + "scripts": { + "install-deps": "cd .erxes && yarn install", + "dev": "cd .erxes && yarn dev", + "build": "cd .erxes && yarn build", + "start": "cd .erxes/dist/plugin-test-api/.erxes && node src" + } +} diff --git a/packages/plugin-goals-api/src/configs.ts b/packages/plugin-goals-api/src/configs.ts new file mode 100644 index 0000000000..2a07517521 --- /dev/null +++ b/packages/plugin-goals-api/src/configs.ts @@ -0,0 +1,51 @@ +import * as serverTiming from 'server-timing'; +import typeDefs from './graphql/typeDefs'; +import resolvers from './graphql/resolvers'; +import { generateModels } from './connectionResolver'; + +import { initBroker } from './messageBroker'; +import { getSubdomain } from '@erxes/api-utils/src/core'; +import dashboards from './dashboards'; + +export let debug; +export let graphqlPubsub; +export let mainDb; +export let serviceDiscovery; + +export default { + name: 'goals', + hasDashboard: true, + graphql: async sd => { + serviceDiscovery = sd; + return { + typeDefs: await typeDefs(sd), + resolvers: await resolvers(sd) + }; + }, + apolloServerContext: async (context, req, res) => { + const subdomain = getSubdomain(req); + + context.subdomain = subdomain; + context.models = await generateModels(subdomain); + + context.serverTiming = { + startTime: res.startTime, + endTime: res.endTime, + setMetric: res.setMetric + }; + + return context; + }, + middlewares: [(serverTiming as any)()], + + onServerInit: async options => { + mainDb = options.db; + + initBroker(options.messageBrokerClient); + + debug = options.debug; + graphqlPubsub = options.pubsubClient; + }, + + meta: { dashboards } +}; diff --git a/packages/plugin-goals-api/src/connectionResolver.ts b/packages/plugin-goals-api/src/connectionResolver.ts new file mode 100644 index 0000000000..f3eb5dfe9a --- /dev/null +++ b/packages/plugin-goals-api/src/connectionResolver.ts @@ -0,0 +1,35 @@ +import * as mongoose from 'mongoose'; +import { IGoalDocument } from './models/definitions/goals'; +import { IGoalModel, loadGoalClass } from './models/Goals'; +import { IContext as IMainContext } from '@erxes/api-utils/src'; +import { createGenerateModels } from '@erxes/api-utils/src/core'; + +export interface IModels { + Goals: IGoalModel; +} +export interface IContext extends IMainContext { + subdomain: string; + models: IModels; + serverTiming: any; +} + +export let models: IModels | null = null; + +export const loadClasses = ( + db: mongoose.Connection, + subdomain: string +): IModels => { + models = {} as IModels; + + models.Goals = db.model( + 'goals', + loadGoalClass(models, subdomain) + ); + + return models; +}; + +export const generateModels = createGenerateModels( + models, + loadClasses +); diff --git a/packages/plugin-goals-api/src/dashboardSchemas/Goals.js b/packages/plugin-goals-api/src/dashboardSchemas/Goals.js new file mode 100644 index 0000000000..db08778f23 --- /dev/null +++ b/packages/plugin-goals-api/src/dashboardSchemas/Goals.js @@ -0,0 +1,69 @@ +const { tableSchema } = require('../tablePrefix'); + +cube(`Goals`, { + sql: `SELECT * FROM ${tableSchema()}.goals`, + + preAggregations: { + // Pre-Aggregations definitions go here + // Learn more here: https://cube.dev/docs/caching/pre-aggregations/getting-started + }, + + joins: {}, + + measures: { + count: { + type: `count`, + drillMembers: [name, parentid, createdat] + }, + + objectcount: { + sql: `${CUBE}.\`objectCount\``, + type: `sum` + } + }, + + dimensions: { + _id: { + sql: `${CUBE}.\`_id\``, + type: `string`, + primaryKey: true + }, + + colorcode: { + sql: `${CUBE}.\`colorCode\``, + type: `string` + }, + + contenttype: { + sql: `${CUBE}.\`contentType\``, + type: `string` + }, + + name: { + sql: `name`, + type: `string` + }, + + IGoal: { + sql: `IGoal`, + type: `string` + }, + + parentid: { + sql: `${CUBE}.\`parentId\``, + type: `string` + }, + + type: { + sql: `type`, + type: `string` + }, + + createdat: { + sql: `${CUBE}.\`createdAt\``, + type: `time` + } + }, + + dataSource: `default` +}); diff --git a/packages/plugin-goals-api/src/dashboards.ts b/packages/plugin-goals-api/src/dashboards.ts new file mode 100644 index 0000000000..321e13ae0c --- /dev/null +++ b/packages/plugin-goals-api/src/dashboards.ts @@ -0,0 +1,4 @@ +export default { + schemaNames: ['Goals'], + types: ['Goals'] +}; diff --git a/packages/plugin-goals-api/src/essyncer.js b/packages/plugin-goals-api/src/essyncer.js new file mode 100644 index 0000000000..6598a738f1 --- /dev/null +++ b/packages/plugin-goals-api/src/essyncer.js @@ -0,0 +1,7 @@ +module.exports = [ + { + name: 'goals', + schema: '{}', + script: '' + } +]; diff --git a/packages/plugin-goals-api/src/graphql/index.ts b/packages/plugin-goals-api/src/graphql/index.ts new file mode 100644 index 0000000000..1e974c15bd --- /dev/null +++ b/packages/plugin-goals-api/src/graphql/index.ts @@ -0,0 +1,9 @@ +import resolvers from './resolvers'; +import typeDefs from './typeDefs'; + +const mod = { + resolvers, + typeDefs +}; + +export default mod; diff --git a/packages/plugin-goals-api/src/graphql/resolvers/index.ts b/packages/plugin-goals-api/src/graphql/resolvers/index.ts new file mode 100644 index 0000000000..91ff11cf2f --- /dev/null +++ b/packages/plugin-goals-api/src/graphql/resolvers/index.ts @@ -0,0 +1,17 @@ +import customScalars from '@erxes/api-utils/src/customScalars'; + +import { Goals as GoalMutations } from './mutations'; + +import { Goals as GoalQueries } from './queries'; + +const resolvers: any = async serviceDiscovery => ({ + ...customScalars, + Mutation: { + ...GoalMutations + }, + Query: { + ...GoalQueries + } +}); + +export default resolvers; diff --git a/packages/plugin-goals-api/src/graphql/resolvers/mutations/goals.ts b/packages/plugin-goals-api/src/graphql/resolvers/mutations/goals.ts new file mode 100644 index 0000000000..582e3c9b46 --- /dev/null +++ b/packages/plugin-goals-api/src/graphql/resolvers/mutations/goals.ts @@ -0,0 +1,86 @@ +import { + checkPermission, + requireLogin +} from '@erxes/api-utils/src/permissions'; +import { IContext } from '../../../connectionResolver'; + +import { IGoal } from '../../../models/definitions/goals'; +import { fixRelatedItems, goalObject } from '../../../utils'; +// import { +// putCreateLog, +// putDeleteLog, +// putUpdateLog, +// putActivityLog +// } from '../../../logUtils'; + +import { sendCommonMessage } from '../../../messageBroker'; +import { serviceDiscovery } from '../../../configs'; + +interface IGoalsEdit extends IGoal { + _id: string; +} + +const goalMutations = { + /** + * Creates a new goal + */ + + async goalsAdd( + _root, + doc: IGoal, + { docModifier, models, subdomain, user }: IContext + ) { + const goal = await models.Goals.createGoal(docModifier(doc)); + + return goal; + }, + + /** + * Edits a goal + */ + async goalsEdit( + _root, + { _id, ...doc }: IGoalsEdit, + { models, subdomain, user }: IContext + ) { + const updated = await models.Goals.updateGoal(_id, doc); + + return updated; + }, + + /** + * Removes a goal + */ + goalTypesRemove: async ( + _root, + { goalTypeIds }: { goalTypeIds: string[] }, + { models, user, subdomain }: IContext + ) => { + // TODO: contracts check + // const goalTypes = await models.Goals.find({ + // _id: { $in: goalTypeIds } + // }).lean(); + + await models.Goals.removeGoal(goalTypeIds); + + return goalTypeIds; + }, + async goalsRemove( + _root, + { goalTypeIds }: { goalTypeIds: string[] }, + { models, user, subdomain }: IContext + ) { + await models.Goals.removeGoal(goalTypeIds); + + return goalTypeIds; + } +}; + +// requireLogin(goalMutations, 'goalsGoal'); + +// checkPermission(goalMutations, 'goalsAdd', 'manageGoals'); +// checkPermission(goalMutations, 'goalsEdit', 'manageGoals'); +// checkPermission(goalMutations, 'goalsRemove', 'manageGoals'); +// checkPermission(goalMutations, 'goalsMerge', 'manageGoals'); + +export default goalMutations; diff --git a/packages/plugin-goals-api/src/graphql/resolvers/mutations/index.ts b/packages/plugin-goals-api/src/graphql/resolvers/mutations/index.ts new file mode 100644 index 0000000000..e6edbe9c34 --- /dev/null +++ b/packages/plugin-goals-api/src/graphql/resolvers/mutations/index.ts @@ -0,0 +1,3 @@ +import Goals from './goals'; + +export { Goals }; diff --git a/packages/plugin-goals-api/src/graphql/resolvers/queries/goals.ts b/packages/plugin-goals-api/src/graphql/resolvers/queries/goals.ts new file mode 100644 index 0000000000..2cf82fbd48 --- /dev/null +++ b/packages/plugin-goals-api/src/graphql/resolvers/queries/goals.ts @@ -0,0 +1,159 @@ +import { + checkPermission, + requireLogin +} from '@erxes/api-utils/src/permissions'; +import { IContext } from '../../../connectionResolver'; +import { paginate } from '@erxes/api-utils/src'; +import * as dayjs from 'dayjs'; + +const generateFilter = async (params, commonQuerySelector) => { + const { branch, department, unit, contribution, date } = params; + let filter: any = {}; + // if (params.branch) { + // filter.branch = params.branch; + // } + // if (params.department) { + // filter.department = params.department; + // } + // if (params.unit) { + // filter.unit = params.unit; + // } + // return filter; + // const filter: any = { status: 'active' }; + // const filter: any = {}; + // if (branch) { + // filter.branchIds = { $in: [branch] }; + // } + if (branch) { + filter.branch = branch; + } + if (department) { + filter.department = { $in: [department] }; + } + if (unit) { + filter.unit = { $in: [unit] }; + } + if (contribution) { + filter.contribution = { $in: [contribution] }; + } + if (date) { + const now = dayjs(date); + const nowISO = now.toISOString(); + filter.$or = [ + { + isStartDateEnabled: false, + isEndDateEnabled: false + }, + { + isStartDateEnabled: true, + isEndDateEnabled: false, + startDate: { + $lt: nowISO + } + }, + { + isStartDateEnabled: false, + isEndDateEnabled: true, + endDate: { + $gt: nowISO + } + }, + { + isStartDateEnabled: true, + isEndDateEnabled: true, + startDate: { + $lt: nowISO + }, + endDate: { + $gt: nowISO + } + } + ]; + } + return filter; +}; + +export const sortBuilder = params => { + const sortField = params.sortField; + const sortDirection = params.sortDirection || 0; + + if (sortField) { + return { [sortField]: sortDirection }; + } + + return {}; +}; +const goalQueries = { + /** + * Goals list + */ + + // tslint:disable-next-line:no-empty + async goals(_root, _args, { models }: IContext) { + return await models.Goals.find({}).lean(); + // return paginate( + // models.Goals.find( + // await generateFilter(models, params, commonQuerySelector) + // ), + // { + // page: params.page, + // perPage: params.perPage + // } + // ); + }, + goalTypes: async ( + _root, + params, + { commonQuerySelector, models }: IContext + ) => { + return paginate( + models.Goals.find(await generateFilter(params, commonQuerySelector)), + { + page: params.page, + perPage: params.perPage + } + ); + }, + + /** + * goalTypes for only main list + */ + + goalTypesMain: async ( + _root, + params, + { commonQuerySelector, models }: IContext + ) => { + await models.Goals.progressIdsGoals(); + const filter = await generateFilter(params, commonQuerySelector); + return { + list: paginate(models.Goals.find(filter).sort(sortBuilder(params)), { + page: params.page, + perPage: params.perPage + }), + totalCount: models.Goals.find(filter).count() + }; + }, + + goalTypeMainProgress: async ( + _root, + params, + { commonQuerySelector, models }: IContext + ) => { + const goals = await models.Goals.progressIdsGoals(); + + return goals; + }, + /** + * Get one goal + */ + async goalDetail(_root, { _id }: { _id: string }, { models }: IContext) { + const goal = await models.Goals.progressGoal(_id); + return goal; + } +}; + +// requireLogin(goalQueries, 'goalDetail'); +// checkPermission(goalQueries, 'goals', 'showGoals', []); + +export default goalQueries; diff --git a/packages/plugin-goals-api/src/graphql/resolvers/queries/index.ts b/packages/plugin-goals-api/src/graphql/resolvers/queries/index.ts new file mode 100644 index 0000000000..e6edbe9c34 --- /dev/null +++ b/packages/plugin-goals-api/src/graphql/resolvers/queries/index.ts @@ -0,0 +1,3 @@ +import Goals from './goals'; + +export { Goals }; diff --git a/packages/plugin-goals-api/src/graphql/schema/goal.ts b/packages/plugin-goals-api/src/graphql/schema/goal.ts new file mode 100644 index 0000000000..85c0f7b67c --- /dev/null +++ b/packages/plugin-goals-api/src/graphql/schema/goal.ts @@ -0,0 +1,98 @@ +export const types = ` + type Goal @key(fields: "_id") @cacheControl(maxAge: 3) { +_id: String! + entity: String + stageId:String + pipelineId:String + boardId:String + contributionType: String + frequency:String + metric:String + goalType: String + contribution: [String] + department:String + unit:String + branch:String + progress:JSON + specificPeriodGoals:JSON + startDate: String + endDate: String + target:String + }, + type GoalType { + _id: String! + entity: String + stageId:String + pipelineId:String + boardId:String + contributionType: String + frequency:String + metric:String + goalType: String + contribution: [String] + department:String + unit:String + branch:String + specificPeriodGoals:JSON + progress:JSON + + startDate: String + endDate: String + target:String + }, + type GoalTypesListResponse { + list: [GoalType], + totalCount: Float, + } +`; + +const queryParams = ` + page: Int + perPage: Int + branch:String + department:String + unit:String + date:String + contribution: [String] + ids: [String] + searchValue: String + sortField: String + sortDirection: Int +`; + +export const queries = ` + goals(entity:String, contributionType:String,frequency:String,metric:String,goalType:String, contribution: [String],specificPeriodGoals:JSON stageId:String,pipelineId:String,boardId:String, + department:String,unit:String,branch:String, + startDate: String, progress:JSON + endDate: String,target:String): [Goal] + goalDetail(_id: String!): JSON + goalTypesMain(${queryParams}): GoalTypesListResponse + goalTypeMainProgress: JSON + goalTypes(${queryParams}): [GoalType] +`; + +const params = ` + entity: String + stageId: String + pipelineId: String + boardId: String + contributionType: String + frequency: String + metric: String + goalType: String + contribution: [String] + department:String + unit:String + branch:String + specificPeriodGoals:JSON + progress:JSON + startDate:String + endDate:String + target: String +`; + +export const mutations = ` + goalsAdd(${params}): Goal + goalsEdit(_id: String!, ${params}): Goal + goalsRemove(goalTypeIds: [String]): [String] +`; diff --git a/packages/plugin-goals-api/src/graphql/typeDefs.ts b/packages/plugin-goals-api/src/graphql/typeDefs.ts new file mode 100644 index 0000000000..70417dff4d --- /dev/null +++ b/packages/plugin-goals-api/src/graphql/typeDefs.ts @@ -0,0 +1,37 @@ +import gql from 'graphql-tag'; + +import { + types as goalTypes, + queries as goalQueries, + mutations as goalMutations +} from './schema/goal'; + +const typeDefs = async _serviceDiscovery => { + return gql` + scalar JSON + scalar Date + + enum CacheControlScope { + PUBLIC + PRIVATE + } + + directive @cacheControl( + maxAge: Int + scope: CacheControlScope + inheritMaxAge: Boolean + ) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION + + ${goalTypes} + + extend type Query { + ${goalQueries} + } + + extend type Mutation { + ${goalMutations} + } + `; +}; + +export default typeDefs; diff --git a/packages/plugin-goals-api/src/messageBroker.ts b/packages/plugin-goals-api/src/messageBroker.ts new file mode 100644 index 0000000000..a7badfa331 --- /dev/null +++ b/packages/plugin-goals-api/src/messageBroker.ts @@ -0,0 +1,89 @@ +import { generateModels } from './connectionResolver'; +import { + escapeRegExp, + ISendMessageArgs, + sendMessage +} from '@erxes/api-utils/src/core'; +import { serviceDiscovery } from './configs'; + +let client; + +export const initBroker = async cl => { + client = cl; + + const { consumeRPCQueue } = client; + + consumeRPCQueue('goals:find', async ({ subdomain, data }) => { + const models = await generateModels(subdomain); + + return { + data: await models.Goals.find(data).lean(), + status: 'success' + }; + }); + + consumeRPCQueue('goals:findOne', async ({ subdomain, data }) => { + const models = await generateModels(subdomain); + + return { + data: await models.Goals.findOne(data).lean(), + status: 'success' + }; + }); + + consumeRPCQueue('goals:createGoal', async ({ subdomain, data }) => { + const models = await generateModels(subdomain); + + return { + status: 'success', + data: await models.Goals.createGoal(data) + }; + }); +}; + +export const sendCardsMessage = (args: ISendMessageArgs): Promise => { + return sendMessage({ + client, + serviceDiscovery, + serviceName: 'cards', + ...args + }); +}; +export const sendBoardMessage = (args: ISendMessageArgs): Promise => { + return sendMessage({ + client, + serviceDiscovery, + serviceName: 'boards', + ...args + }); +}; +export const sendStageMessage = async ( + args: ISendMessageArgs +): Promise => { + try { + // Assuming 'client' and 'serviceDiscovery' are defined somewhere in your code + const result = await sendMessage({ + client, + serviceDiscovery, + serviceName: 'cards', + ...args + }); + return result; + } catch (error) { + console.error('Error sending stage message:', error); + throw error; + } +}; +export const sendCommonMessage = async ( + args: ISendMessageArgs & { serviceName: string } +): Promise => { + return sendMessage({ + serviceDiscovery, + client, + ...args + }); +}; + +export default function() { + return client; +} diff --git a/packages/plugin-goals-api/src/models/Goals.ts b/packages/plugin-goals-api/src/models/Goals.ts new file mode 100644 index 0000000000..9225db11ac --- /dev/null +++ b/packages/plugin-goals-api/src/models/Goals.ts @@ -0,0 +1,395 @@ +import { Model } from 'mongoose'; +import { IModels } from '../connectionResolver'; +import { IGoal, IGoalDocument, goalSchema } from './definitions/goals'; +import { + sendCardsMessage, + sendStageMessage, + sendBoardMessage +} from '../messageBroker'; +import { Goals } from '../graphql/resolvers/mutations'; +import _ = require('underscore'); +import { resolve } from 'path'; +export interface IGoalModel extends Model { + getGoal(_id: string): Promise; + createGoal(doc: IGoal): Promise; + updateGoal(_id: string, doc: IGoal): Promise; + removeGoal(_ids: string[]); + progressGoal(_id: string); + progressIdsGoals(): Promise; +} + +export const loadGoalClass = (models: IModels, subdomain: string) => { + class Goal { + public static async createGoal(doc: IGoal, createdUserId: string) { + return models.Goals.create({ + ...doc, + createdDate: new Date(), + createdUserId + }); + } + + public static async getGoal(_id: string) { + const goal = await models.Goals.findOne({ + _id + }); + + if (!goal) { + throw new Error('goal not found'); + } + return goal; + } + + public static async updateGoal(_id: string, doc: IGoal) { + await models.Goals.updateOne( + { + _id + }, + { + $set: doc + }, + { + runValidators: true + } + ); + + return models.Goals.findOne({ + _id + }); + } + + public static async removeGoal(_ids: string[]) { + return models.Goals.deleteMany({ + _id: { + $in: _ids + } + }); + } + + public static async progressGoal(_id: string) { + const goal = await models.Goals.findOne({ + _id + }); + const target = goal?.target; + + const action_name = goal?.entity + 's.find'; + const progressed = await progressFunction( + action_name, + goal, + goal?.metric, + target + ); + + // goal.progress.push(progressed); + + // const data = { ...goal, progressed }; + // const data = { goal, progressed }; + return goal; + } + + public static async progressIdsGoals() { + try { + const doc = await models.Goals.find({}).lean(); + const data = await progressFunctionIds(doc); + return data; + } catch (error) { + // Handle the error appropriately + console.error('Error fetching progress IDs goals:', error); + return []; // Return an empty array or handle the error accordingly + } + } + } + + async function progressFunction(action_name, goal, metric, target) { + const amount = await sendCardsMessage({ + subdomain, + action: action_name, + data: { + stageId: goal?.stageId + }, + isRPC: true + }); + let current; + let progress; + let amountData; + if (metric === 'Value') { + let mobileAmountsData; + // tslint:disable-next-line:no-shadowed-variable + let data; + let totalAmount = 0; + for (const items of amount) { + if (items.productsData && items.status === 'active') { + const productsData = items.productsData; + productsData.forEach(item => { + totalAmount += item.amount; + }); + } + if (items.mobileAmounts && items.mobileAmounts.length > 0) { + mobileAmountsData = items.mobileAmounts[0].amount; + } + if (items.paymentsData) { + const paymentsData = items.paymentsData; + if (paymentsData.prepay) { + data = paymentsData.prepay; + } else if (paymentsData.cash) { + data = paymentsData.cash; + } else if (paymentsData.bankTransaction) { + data = paymentsData.bankTransaction; + } else if (paymentsData.posTerminal) { + data = paymentsData.posTerminal; + } else if (paymentsData.wallet) { + data = paymentsData.wallet; + } else if (paymentsData.barter) { + data = paymentsData.barter; + } else if (paymentsData.receivable) { + data = paymentsData.receivable; + } else if (paymentsData.other) { + data = paymentsData.other; + } + } + } + current = totalAmount; + amountData = { + mobileAmountsData, + paymentsData: data + }; + progress = await differenceFunction( + current, + // tslint:disable-next-line:radix + parseInt(target || '') + ); + } else if (metric === 'Count') { + const activeElements = amount.filter(item => item.status === 'active'); + // Getting the count of elements with status 'active' + current = activeElements.length; + + progress = await differenceFunction( + current, + // tslint:disable-next-line:radix + parseInt(target || '') + ); + } + + const result = await models.Goals.updateOne( + { _id: goal._id }, + { $set: { progress: { current, progress, amountData, target } } }, + { runValuators: true } + ); + return result; + } + + async function progressFunctionIds(doc) { + // tslint:disable-next-line:interface-name + interface DataItem { + stageId: string; + _id: string; + current: string; + progress: string; + amountData: any; + target: string; + } + // tslint:disable-next-line:interface-name + + const data: DataItem[] = []; + + for (const item of doc) { + const amount = await sendCardsMessage({ + subdomain, + action: item.entity + 's.find', + data: { + stageId: item.stageId + }, + isRPC: true + }); + let current; + let progress; + let amountData; + + if (item.metric === 'Value') { + let mobileAmountsData; + // tslint:disable-next-line:no-shadowed-variable + let data; + let totalAmount = 0; + for (const items of amount) { + if (items.productsData && items.status === 'active') { + const productsData = items.productsData; + // tslint:disable-next-line:no-shadowed-variable + productsData.forEach(item => { + totalAmount += item.amount; + }); + } + if (items.mobileAmounts && items.mobileAmounts.length > 0) { + mobileAmountsData = items.mobileAmounts[0].amount; + } + if (items.paymentsData) { + const paymentsData = items.paymentsData; + if (paymentsData.prepay) { + data = paymentsData.prepay; + } else if (paymentsData.cash) { + data = paymentsData.cash; + } else if (paymentsData.bankTransaction) { + data = paymentsData.bankTransaction; + } else if (paymentsData.posTerminal) { + data = paymentsData.posTerminal; + } else if (paymentsData.wallet) { + data = paymentsData.wallet; + } else if (paymentsData.barter) { + data = paymentsData.barter; + } else if (paymentsData.receivable) { + data = paymentsData.receivable; + } else if (paymentsData.other) { + data = paymentsData.other; + } + } + } + current = totalAmount; + amountData = { + mobileAmountsData, + paymentsData: data + }; + progress = await differenceFunction( + current, + // tslint:disable-next-line:radix + parseInt(item.target || '') + ); + } else if (item.metric === 'Count') { + const activeElements = amount.filter( + // tslint:disable-next-line:no-shadowed-variable + item => item.status === 'active' + ); + current = activeElements.length; + + progress = await differenceFunction( + current, + // tslint:disable-next-line:radix + parseInt(item.target || '') + ); + } + + data.push({ + stageId: item.stageId, + _id: item._id, + current, + progress, + amountData, + target: item.target + }); + } + + for (const result of data) { + await models.Goals.updateOne( + { _id: result._id }, + { + $set: { + progress: { + current: result.current, + progress: result.progress, + amountData: result.amountData, + target: result.target, + _id: result._id + } + } + }, + { runValidators: true } + ); + } + + const updates = await models.Goals.find({}).lean(); + for (const item of updates) { + const action_name = item?.entity + 's.find'; + const stage = item?.stageId; + const amount = await sendCardsMessage({ + subdomain, + action: action_name, + data: { + stageId: stage + }, + isRPC: true + }); + let totalAmount = 0; + let current; + if (item.metric === 'Value') { + // let progress; + for (const items of amount) { + if (items.productsData && items.status === 'active') { + const productsData = items.productsData; + // tslint:disable-next-line:no-shadowed-variable + productsData.forEach(item => { + totalAmount += item.amount; + }); + } + current = totalAmount; + + if ( + item.specificPeriodGoals && + Array.isArray(item.specificPeriodGoals) + ) { + const updatedSpecificPeriodGoals = item.specificPeriodGoals.map( + result => { + const progress = (current / result.addTarget) * 100; + const convertedNumber = progress.toFixed(3); + + return { + ...result, + addMonthly: result.addMonthly, // update other properties as needed + progress: convertedNumber // updating the progress property + }; + } + ); + await models.Goals.updateOne( + { _id: item._id }, + { + $set: { + specificPeriodGoals: updatedSpecificPeriodGoals + } + } + ); + } + } + } else if (item.metric === 'Count') { + const activeElements = amount.filter( + // tslint:disable-next-line:no-shadowed-variable + item => item.status === 'active' + ); + current = activeElements.length; + if ( + item.specificPeriodGoals && + Array.isArray(item.specificPeriodGoals) + ) { + const updatedSpecificPeriodGoals = item.specificPeriodGoals.map( + result => { + const progress = (current / result.addTarget) * 100; + const convertedNumber = progress.toFixed(3); + + return { + ...result, + addMonthly: result.addMonthly, // update other properties as needed + current, + progress: convertedNumber // updating the progress property + }; + } + ); + await models.Goals.updateOne( + { _id: item._id }, + { + $set: { + specificPeriodGoals: updatedSpecificPeriodGoals + } + } + ); + } + } + } + return updates; + } + + async function differenceFunction(amount: number, target: number) { + const diff = (amount / target) * 100; + const convertedNumber = diff.toFixed(3); + + return convertedNumber; + } + + goalSchema.loadClass(Goal); + + return goalSchema; +}; diff --git a/packages/plugin-goals-api/src/models/GoalsTest.ts b/packages/plugin-goals-api/src/models/GoalsTest.ts new file mode 100644 index 0000000000..e925d8435f --- /dev/null +++ b/packages/plugin-goals-api/src/models/GoalsTest.ts @@ -0,0 +1,431 @@ +import { Model } from 'mongoose'; +import { IModels } from '../connectionResolver'; +import { IGoal, IGoalDocument, goalSchema } from './definitions/goals'; +import { + sendCardsMessage, + sendStageMessage, + sendBoardMessage +} from '../messageBroker'; +import { Goals } from '../graphql/resolvers/mutations'; +import _ = require('underscore'); +export interface IGoalModel extends Model { + getGoal(_id: string): Promise; + createGoal(doc: IGoal): Promise; + updateGoal(_id: string, doc: IGoal): Promise; + removeGoal(_ids: string[]); + progressGoal(_id: string); + progressIdsGoals(): Promise; +} + +export const loadGoalClass = (models: IModels, subdomain: string) => { + class Goal { + public static async createGoal(doc: IGoal, createdUserId: string) { + return models.Goals.create({ + ...doc, + createdDate: new Date(), + createdUserId + }); + } + + public static async getGoal(_id: string) { + const goal = await models.Goals.findOne({ + _id + }); + + if (!goal) { + throw new Error('goal not found'); + } + return goal; + } + + public static async updateGoal(_id: string, doc: IGoal) { + await models.Goals.updateOne( + { + _id + }, + { + $set: doc + }, + { + runValidators: true + } + ); + + return models.Goals.findOne({ + _id + }); + } + + public static async removeGoal(_ids: string[]) { + return models.Goals.deleteMany({ + _id: { + $in: _ids + } + }); + // const data = await models.Goals.getGoal(_id); + // if (!data) { + // throw new Error(`not found with id ${_id}`); + // } + // return models.Goals.remove({ _id }); + } + + public static async progressGoal(_id: string) { + const goal = await models.Goals.findOne({ + _id + }); + const target = goal?.target; + + const action_name = goal?.entity + 's.find'; + const progressed = await progressFunction( + action_name, + goal, + goal?.metric, + target + ); + + // goal.progress.push(progressed); + + // const data = { ...goal, progressed }; + // const data = { goal, progressed }; + return goal; + } + + public static async progressIdsGoals() { + try { + const doc = await models.Goals.find({}).lean(); + const data = await progressFunctionIds(doc); + return data; + } catch (error) { + // Handle the error appropriately + console.error('Error fetching progress IDs goals:', error); + return []; // Return an empty array or handle the error accordingly + } + + // const doc = await models.Goals.find({}).lean(); + // const data = await progressFunctionIds(doc); + // return data; + } + } + + async function progressFunction(action_name, goal, metric, target) { + const amount = await sendCardsMessage({ + subdomain, + action: action_name, + data: { + stageId: goal?.stageId + }, + isRPC: true + }); + let current; + let progress; + let amountData; + if (metric === 'Value') { + let mobileAmountsData; + // tslint:disable-next-line:no-shadowed-variable + let data; + let totalAmount = 0; + for (const items of amount) { + if (items.productsData && items.status === 'active') { + const productsData = items.productsData; + productsData.forEach(item => { + totalAmount += item.amount; + }); + } + if (items.mobileAmounts && items.mobileAmounts.length > 0) { + mobileAmountsData = items.mobileAmounts[0].amount; + } + if (items.paymentsData) { + const paymentsData = items.paymentsData; + if (paymentsData.prepay) { + data = paymentsData.prepay; + } else if (paymentsData.cash) { + data = paymentsData.cash; + } else if (paymentsData.bankTransaction) { + data = paymentsData.bankTransaction; + } else if (paymentsData.posTerminal) { + data = paymentsData.posTerminal; + } else if (paymentsData.wallet) { + data = paymentsData.wallet; + } else if (paymentsData.barter) { + data = paymentsData.barter; + } else if (paymentsData.receivable) { + data = paymentsData.receivable; + } else if (paymentsData.other) { + data = paymentsData.other; + } + } + } + current = totalAmount; + amountData = { + mobileAmountsData, + paymentsData: data + }; + progress = await differenceFunction( + current, + // tslint:disable-next-line:radix + parseInt(target || '') + ); + } else if (metric === 'Count') { + const activeElements = amount.filter(item => item.status === 'active'); + // Getting the count of elements with status 'active' + current = activeElements.length; + + progress = await differenceFunction( + current, + // tslint:disable-next-line:radix + parseInt(target || '') + ); + } + + const result = await models.Goals.updateOne( + { _id: goal._id }, + { $set: { progress: { current, progress, amountData, target } } }, + { runValdatiors: true } + ); + return result; + } + + async function progressFunctionIds(doc) { + // tslint:disable-next-line:interface-name + interface DataItem { + stageId: any; + _id: any; + current: any; + progress: any; + amountData: any; + target: any; + } + + const data: DataItem[] = []; + + for (const item of doc) { + const fetchedBoard = await sendCardsMessage({ + subdomain, + action: 'boards.find', + data: { + _id: item.boardId + }, + isRPC: true + }); + const fetchedStages = await sendCardsMessage({ + subdomain, + action: 'stages.find', + data: { + _id: item.stageId + }, + isRPC: true + }); + const fetchedPipelines = await sendCardsMessage({ + subdomain, + action: 'pipelines.find', + data: { + _id: item.pipelineId + }, + isRPC: true + }); + + for (const stagesItem of fetchedStages) { + await models.Goals.updateOne( + { stageId: stagesItem._id }, // Replace '_id' with the appropriate identifier for your Goals model + { $set: { stageName: stagesItem.name } }, + { runValidators: true } // This option ensures that the validators are run + ); + } + for (const boardItem of fetchedBoard) { + await models.Goals.updateOne( + { boardId: boardItem._id }, // Replace '_id' with the appropriate identifier for your Goals model + { $set: { boardName: boardItem.name } }, + { runValidators: true } // This option ensures that the validators are run + ); + } + for (const pipelineItem of fetchedPipelines) { + await models.Goals.updateOne( + { pipelineId: pipelineItem._id }, // Replace '_id' with the appropriate identifier for your Goals model + { $set: { pipelineName: pipelineItem.name } }, + { runValidators: true } // This option ensures that the validators are run + ); + } + + const amount = await sendCardsMessage({ + subdomain, + action: item.entity + 's.find', + data: { + stageId: item.stageId + }, + isRPC: true + }); + let current; + let progress; + let amountData; + + if (item.metric === 'Value') { + let mobileAmountsData; + // tslint:disable-next-line:no-shadowed-variable + let data; + let totalAmount = 0; + for (const items of amount) { + if (items.productsData && items.status === 'active') { + const productsData = items.productsData; + // tslint:disable-next-line:no-shadowed-variable + productsData.forEach(item => { + totalAmount += item.amount; + }); + } + if (items.mobileAmounts && items.mobileAmounts.length > 0) { + mobileAmountsData = items.mobileAmounts[0].amount; + } + if (items.paymentsData) { + const paymentsData = items.paymentsData; + if (paymentsData.prepay) { + data = paymentsData.prepay; + } else if (paymentsData.cash) { + data = paymentsData.cash; + } else if (paymentsData.bankTransaction) { + data = paymentsData.bankTransaction; + } else if (paymentsData.posTerminal) { + data = paymentsData.posTerminal; + } else if (paymentsData.wallet) { + data = paymentsData.wallet; + } else if (paymentsData.barter) { + data = paymentsData.barter; + } else if (paymentsData.receivable) { + data = paymentsData.receivable; + } else if (paymentsData.other) { + data = paymentsData.other; + } + } + } + current = totalAmount; + amountData = { + mobileAmountsData, + paymentsData: data + }; + progress = await differenceFunction( + current, + // tslint:disable-next-line:radix + parseInt(item.target || '') + ); + } else if (item.metric === 'Count') { + const activeElements = amount.filter( + // tslint:disable-next-line:no-shadowed-variable + item => item.status === 'active' + ); + current = activeElements.length; + + progress = await differenceFunction( + current, + // tslint:disable-next-line:radix + parseInt(item.target || '') + ); + } + data.push({ + stageId: item.stageId, + _id: item._id, + current, + progress, + amountData, + target: item.target + }); + } + + for (const result of data) { + await models.Goals.updateOne( + { _id: result._id }, + { + $set: { + progress: { + current: result.current, + progress: result.progress, + amountData: result.amountData, + target: result.target, + _id: result._id + } + } + }, + { runValidators: true } + ); + } + + const updates = await models.Goals.find({}).lean(); + + return updates; + } + + async function differenceFunction(amount: number, target: number) { + const diff = (amount / target) * 100; + return diff; + } + + goalSchema.loadClass(Goal); + + return goalSchema; +}; + +// async function amountFunction(action_name, goal) { +// const amount = await sendCardsMessage({ +// subdomain, +// action: action_name, +// data: { +// stageId: goal?.stageId +// }, +// isRPC: true +// }); + +// let mobileAmountsData; +// let data; +// let totalAmount = 0; +// for (const items of amount) { +// if (items.productsData && items.status === 'active') { +// const productsData = items.productsData; +// productsData.forEach((item) => { +// totalAmount += item.amount; +// }); +// } +// if (items.mobileAmounts && items.mobileAmounts.length > 0) { +// mobileAmountsData = items.mobileAmounts[0].amount; +// } +// if (items.paymentsData) { +// const paymentsData = items.paymentsData; +// if (paymentsData.prepay) { +// data = paymentsData.prepay; +// } else if (paymentsData.cash) { +// data = paymentsData.cash; +// } else if (paymentsData.bankTransaction) { +// data = paymentsData.bankTransaction; +// } else if (paymentsData.posTerminal) { +// data = paymentsData.posTerminal; +// } else if (paymentsData.wallet) { +// data = paymentsData.wallet; +// } else if (paymentsData.barter) { +// data = paymentsData.barter; +// } else if (paymentsData.receivable) { +// data = paymentsData.receivable; +// } else if (paymentsData.other) { +// data = paymentsData.other; +// } +// } +// } + +// const result = { +// mobileAmountsData, +// data, +// totalAmount +// }; + +// return result; +// } +// async function countFunction(action_name, goal) { +// const count = await sendCardsMessage({ +// subdomain, +// action: action_name, +// data: { +// stageId: goal?.stageId +// }, +// isRPC: true +// }); +// const activeElements = count.filter((item) => item.status === 'active'); + +// // Getting the count of elements with status 'active' +// const activeCount = activeElements.length; +// return activeCount; +// } diff --git a/packages/plugin-goals-api/src/models/definitions/goals.ts b/packages/plugin-goals-api/src/models/definitions/goals.ts new file mode 100644 index 0000000000..48daf66832 --- /dev/null +++ b/packages/plugin-goals-api/src/models/definitions/goals.ts @@ -0,0 +1,81 @@ +import { Document, Schema } from 'mongoose'; +import { field, schemaHooksWrapper } from './utils'; + +export interface ISpecificPeriodGoals { + monthly: Date; + target: string; + progress: any; +} + +export interface IGoal { + entity: string; + stageId: string; + pipelineId: string; + boardId: string; + contributionType: string; + frequency: string; + metric: string; + goalType: string; + contribution?: string[]; + department: string; + unit: string; + branch: string; + chooseStage: string; + specificPeriodGoals?: ISpecificPeriodGoals[]; + startDate: string; + endDate: string; + target: string; + progress: any; +} + +export interface IGoalDocument extends IGoal, Document { + _id: string; + createdAt: Date; +} + +export const goalSchema = schemaHooksWrapper( + new Schema({ + _id: field({ pkey: true }), + entity: field({ type: String, label: 'Choose Entity' }), + contributionType: field({ + type: String, + label: 'Contribution Type' + }), + specificPeriodGoals: field({ + type: Object, + optional: true, + label: 'Specific Period Goals' + }), + + stageId: field({ type: String, label: 'stageId' }), + pipelineId: field({ type: String, label: 'pipelineId' }), + boardId: field({ type: String, label: 'boardId' }), + frequency: field({ type: String, label: 'Frequency' }), + metric: field({ type: String, label: 'Metric' }), + goalType: field({ type: String, label: 'Choose Goal Type' }), + contribution: field({ type: [String], label: 'contribution' }), + startDate: field({ type: String, lable: 'StartDate Durable' }), + endDate: field({ type: String, label: 'EndDate Durable' }), + target: field({ type: String, label: 'Target' }), + progress: { + type: Object, + label: 'Progress' + }, + department: { + type: String, + label: 'Department' + }, + unit: { + type: String, + label: 'Unit' + }, + branch: { + type: String, + label: 'Branch' + } + }), + 'erxes_goals' +); + +// for goals query. increases search speed, avoids in-memory sorting +goalSchema.index({ type: 1, IGoal: 1, name: 1 }); diff --git a/packages/plugin-goals-api/src/models/definitions/utils.ts b/packages/plugin-goals-api/src/models/definitions/utils.ts new file mode 100644 index 0000000000..814cb8dc35 --- /dev/null +++ b/packages/plugin-goals-api/src/models/definitions/utils.ts @@ -0,0 +1,30 @@ +import { nanoid } from 'nanoid'; + +/* + * Mongoose field options wrapper + */ +export const field = options => { + const { pkey, type, optional } = options; + + if (type === String && !pkey && !optional) { + options.validate = /\S+/; + } + + // TODO: remove + if (pkey) { + options.type = String; + options.default = () => nanoid(); + } + + return options; +}; + +export const schemaWrapper = schema => { + schema.add({ scopeBrandIds: [String] }); + + return schema; +}; + +export const schemaHooksWrapper = (schema, _cacheKey: string) => { + return schemaWrapper(schema); +}; diff --git a/packages/plugin-goals-api/src/utils.ts b/packages/plugin-goals-api/src/utils.ts new file mode 100644 index 0000000000..03f78dc5db --- /dev/null +++ b/packages/plugin-goals-api/src/utils.ts @@ -0,0 +1,71 @@ +import { sendCommonMessage } from './messageBroker'; + +export const countDocuments = async ( + subdomain: string, + type: string, + _ids: string[] +) => { + const [serviceName, contentType] = type.split(':'); + + return sendCommonMessage({ + subdomain, + action: 'goal', + serviceName, + data: { + type: contentType, + _ids, + action: 'count' + }, + isRPC: true + }); +}; + +export const goalObject = async ( + subdomain: string, + type: string, + goalIds: string[], + targetIds: string[] +) => { + const [serviceName, contentType] = type.split(':'); + + return sendCommonMessage({ + subdomain, + serviceName, + action: 'goal', + data: { + goalIds, + targetIds, + type: contentType, + action: 'goalObject' + }, + isRPC: true + }); +}; + +export const fixRelatedItems = async ({ + subdomain, + type, + sourceId, + destId, + action +}: { + subdomain: string; + type: string; + sourceId: string; + destId?: string; + action: string; +}) => { + const [serviceName, contentType] = type.split(':'); + + sendCommonMessage({ + subdomain, + serviceName, + action: 'fixRelatedItems', + data: { + sourceId, + destId, + type: contentType, + action + } + }); +}; diff --git a/packages/plugin-goals-api/tsconfig.json b/packages/plugin-goals-api/tsconfig.json new file mode 100644 index 0000000000..51adf3baab --- /dev/null +++ b/packages/plugin-goals-api/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./.erxes/tsconfig.json" +} diff --git a/packages/plugin-goals-ui/package.json b/packages/plugin-goals-ui/package.json new file mode 100644 index 0000000000..1c9fc1b814 --- /dev/null +++ b/packages/plugin-goals-ui/package.json @@ -0,0 +1,9 @@ +{ + "name": "plugin-goals-ui", + "version": "1.0.0", + "scripts": { + "install-deps": "cd .erxes && yarn install", + "start": "cd .erxes && yarn start", + "build": "cd .erxes && yarn build" + } +} diff --git a/packages/plugin-goals-ui/src/App.tsx b/packages/plugin-goals-ui/src/App.tsx new file mode 100644 index 0000000000..1ae5a161a2 --- /dev/null +++ b/packages/plugin-goals-ui/src/App.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import GeneralRoutes from './generalRoutes'; +import { PluginLayout } from '@erxes/ui/src/styles/main'; +import { AppProvider } from '@erxes/ui/src/appContext'; + +const App = () => { + return ( + + + + + + ); +}; + +export default App; diff --git a/packages/plugin-goals-ui/src/components/Sidebar.tsx b/packages/plugin-goals-ui/src/components/Sidebar.tsx new file mode 100644 index 0000000000..620954e03e --- /dev/null +++ b/packages/plugin-goals-ui/src/components/Sidebar.tsx @@ -0,0 +1,143 @@ +import SelectProducts from '@erxes/ui-products/src/containers/SelectProducts'; +import Button from '@erxes/ui/src/components/Button'; +import FormControl from '@erxes/ui/src/components/form/Control'; +import DateControl from '@erxes/ui/src/components/form/DateControl'; +import FormGroup from '@erxes/ui/src/components/form/Group'; +import ControlLabel from '@erxes/ui/src/components/form/Label'; +import Icon from '@erxes/ui/src/components/Icon'; +import Tip from '@erxes/ui/src/components/Tip'; +import Wrapper from '@erxes/ui/src/layout/components/Wrapper'; +import SelectBranches from '@erxes/ui/src/team/containers/SelectBranches'; +import SelectDepartments from '@erxes/ui/src/team/containers/SelectDepartments'; +import SelectUnits from '@erxes/ui/src/team/containers/SelectUnits'; +import SelectTeamMembers from '@erxes/ui/src/team/containers/SelectTeamMembers'; + +import { router, __ } from '@erxes/ui/src/utils'; +import dayjs from 'dayjs'; +import React, { useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { SidebarFilters } from '../styles'; + +type Props = { + params: any; +}; + +const { Section } = Wrapper.Sidebar; + +export default function Sidebar(props: Props) { + const history = useHistory(); + + const [filters, setFilters] = useState(props.params); + const { branch, department, unit, date, contribution } = filters; + + const clearFilter = () => { + router.removeParams( + history, + 'branch', + 'department', + 'unit', + 'contribution', + 'date', + 'page' + ); + }; + + const runFilter = () => { + router.setParams(history, { ...filters }); + }; + + const setFilter = (key, value) => { + setFilters({ ...filters, [key]: value, page: 1 }); + }; + + return ( + + + {__('Filters')} + + {(branch || department || unit || contribution || date) && ( + + + + + + )} + + + + + Date + + setFilter('date', dayjs(date).format('YYYY-MM-DD HH:mm')) + } + /> + + + Branch + setFilter('branch', branch)} + multi={false} + /> + + + Department + setFilter('department', department)} + multi={false} + /> + + + Units + setFilter('unit', unit)} + multi={false} + /> + + + TeamMember + setFilter('contribution', contribution)} + multi={false} + /> + + + + + + + ); +} diff --git a/packages/plugin-goals-ui/src/components/goalForm.tsx b/packages/plugin-goals-ui/src/components/goalForm.tsx new file mode 100755 index 0000000000..f8c56c96f0 --- /dev/null +++ b/packages/plugin-goals-ui/src/components/goalForm.tsx @@ -0,0 +1,725 @@ +import { + Button, + ControlLabel, + Form, + DateControl, + MainStyleFormColumn as FormColumn, + FormControl, + FormGroup, + MainStyleFormWrapper as FormWrapper, + MainStyleModalFooter as ModalFooter, + MainStyleScrollWrapper as ScrollWrapper +} from '@erxes/ui/src'; +import { IButtonMutateProps, IFormProps } from '@erxes/ui/src/types'; +import BoardSelect from '@erxes/ui-cards/src/boards/containers/BoardSelect'; +import { IGoalType, IGoalTypeDoc, IAssignmentCampaign } from '../types'; +import { + ENTITY, + CONTRIBUTION, + GOAL_TYPE, + SPECIFIC_PERIOD_GOAL, + GOAL_STRUCTURE +} from '../constants'; + +import { __ } from 'coreui/utils'; +import { DateContainer } from '@erxes/ui/src/styles/main'; +import dayjs from 'dayjs'; +import client from '@erxes/ui/src/apolloClient'; +import { Alert } from '@erxes/ui/src/utils'; +import { IPipelineLabel } from '@erxes/ui-cards/src/boards/types'; +import { queries as pipelineQuery } from '@erxes/ui-cards/src/boards/graphql'; +import { isEnabled } from '@erxes/ui/src/utils/core'; +import SelectTeamMembers from '@erxes/ui/src/team/containers/SelectTeamMembers'; +import SelectSegments from '@erxes/ui-segments/src/containers/SelectSegments'; +import React, { useEffect, useState } from 'react'; +import { gql } from '@apollo/client'; + +import { + BranchesMainQueryResponse, + DepartmentsMainQueryResponse, + UnitsMainQueryResponse +} from '@erxes/ui/src/team/types'; +type Props = { + renderButton: (props: IButtonMutateProps) => JSX.Element; + goalType: IGoalType; + closeModal: () => void; + pipelineLabels?: IPipelineLabel[]; + assignmentCampaign?: IAssignmentCampaign; + branchListQuery: BranchesMainQueryResponse; + unitListQuery: UnitsMainQueryResponse; + departmentListQuery: DepartmentsMainQueryResponse; +}; + +type State = { + specificPeriodGoals: Array<{ + _id: string; + addMonthly: string; + addTarget: number; + }>; + periodGoal: string; + entity: string; + teamGoalType: string; + contributionType: string; + frequency: string; + goalType: string; + metric: string; + startDate: Date; + endDate: Date; + period: boolean; + contribution: string; + branch: string; + department: string; + unit: string; + pipelineLabels: IPipelineLabel[]; + stageId?: any; + pipelineId?: any; + boardId: any; + assignmentCampaign: IAssignmentCampaign; + stageRadio: boolean; + segmentRadio: boolean; +}; + +class GoalTypeForm extends React.Component { + constructor(props) { + super(props); + const { goalType = {} } = props; + this.state = { + branch: goalType.branch || '', + department: goalType.department || '', + unit: goalType.unit || '', + specificPeriodGoals: goalType.specificPeriodGoals || [], + stageRadio: goalType.stageRadio, + periodGoal: goalType.periodGoal, + segmentRadio: goalType.segmentRadio, + contribution: goalType.contribution, + pipelineLabels: goalType.pipelineLabels, + entity: goalType.entity || '', + teamGoalType: goalType.teamGoalType || '', + contributionType: goalType.contributionType || '', + frequency: goalType.frequency || '', + goalType: goalType.goalType || '', + metric: goalType.metric || '', + period: goalType.period, + startDate: goalType.startDate || new Date(), + endDate: goalType.endDate || new Date(), + stageId: goalType.stageId, + pipelineId: goalType.pipelineId, + boardId: goalType.boardId, + assignmentCampaign: this.props.assignmentCampaign || {} + }; + } + + onChangeStartDate = value => { + this.setState({ startDate: value }); + }; + onChangeStartDateAdd = (index, value) => { + const specificPeriodGoals = [...this.state.specificPeriodGoals]; + specificPeriodGoals[index] = { + ...specificPeriodGoals[index], + addMonthly: value + }; + this.setState({ specificPeriodGoals }); + }; + + onChangeTarget = (index, event) => { + const { specificPeriodGoals, periodGoal } = this.state; + const { value } = event.target; + + const updatedSpecificPeriodGoals = specificPeriodGoals.map((goal, i) => { + if (i === index) { + return { ...goal, addTarget: value }; + } + return goal; + }); + if (periodGoal === 'Monthly') { + const months = this.mapMonths(); + + months.forEach(month => { + const exists = specificPeriodGoals.some( + goal => goal.addMonthly === month + ); + if (!exists) { + const newElement = { + _id: Math.random().toString(), + addMonthly: month, + addTarget: 0 + }; + updatedSpecificPeriodGoals.push(newElement); + } + }); + + this.setState({ specificPeriodGoals: updatedSpecificPeriodGoals }); + } else { + const weeks = this.mapWeeks(); + + weeks.forEach(week => { + const exists = specificPeriodGoals.some( + goal => goal.addMonthly === week + ); + if (!exists) { + const newElement = { + _id: Math.random().toString(), + addMonthly: week, + addTarget: 0 + }; + updatedSpecificPeriodGoals.push(newElement); + } + }); + + this.setState({ specificPeriodGoals: updatedSpecificPeriodGoals }); + } + }; + + onDeleteElement = index => { + const specificPeriodGoals = [...this.state.specificPeriodGoals]; + specificPeriodGoals.splice(index, 1); + this.setState({ specificPeriodGoals }); + }; + + onChangeStage = stgId => { + this.setState({ stageId: stgId }); + }; + + onChangePipeline = plId => { + client + .query({ + query: gql(pipelineQuery.pipelineLabels), + fetchPolicy: 'network-only', + variables: { pipelineId: plId } + }) + .then(data => { + this.setState({ pipelineLabels: data.data.pipelineLabels }); + }) + .catch(e => { + Alert.error(e.message); + }); + + this.setState({ pipelineId: plId }); + }; + + onChangeBoard = brId => { + this.setState({ boardId: brId }); + }; + + /** + * Generates a document object based on the provided values and state. + * @param values An object containing the values to be included in the document. + * @returns An object representing the generated document. + */ + generateDoc = (values: { _id: string } & IGoalTypeDoc) => { + const { goalType } = this.props; + const { + startDate, + endDate, + stageId, + pipelineId, + boardId, + contribution, + stageRadio, + segmentRadio, + period, + specificPeriodGoals + } = this.state; + const finalValues = values; + //// assignmentCampaign segment + const { assignmentCampaign } = this.state; + if (goalType) { + finalValues._id = goalType._id; + } + const durationStart = dayjs(startDate).format('MMM D, h:mm A'); + const durationEnd = dayjs(endDate).format('MMM D, h:mm A'); + return { + _id: finalValues._id, + ...this.state, + entity: finalValues.entity, + department: finalValues.department, + unit: finalValues.unit, + branch: finalValues.branch, + specificPeriodGoals, // Renamed the property + stageRadio, + segmentRadio, + stageId, + pipelineId, + boardId, + contribution, + period, + contributionType: finalValues.contributionType, + frequency: finalValues.frequency, + metric: finalValues.metric, + goalType: finalValues.goalType, + startDate: durationStart, + endDate: durationEnd, + target: finalValues.target + }; + }; + + renderFormGroup = (label, props) => { + return ( + + {__(label)} + + + ); + }; + onChangeField = e => { + const name = (e.target as HTMLInputElement).name; + const value = + e.target.type === 'checkbox' + ? (e.target as HTMLInputElement).checked + : (e.target as HTMLInputElement).value; + + this.setState({ [name]: value } as any); + }; + + onChangeEndDate = value => { + this.setState({ endDate: value }); + }; + onUserChange = userId => { + this.setState({ contribution: userId }); + }; + + onChangeSegments = values => { + this.setState({ + assignmentCampaign: { + ...this.state.assignmentCampaign, + segmentIds: values.map(v => v.value) + } + }); + }; + + renderButton = () => { + return
; + }; + + mapMonths = (): string[] => { + const { startDate, endDate } = this.state; + const startDateObject = new Date(startDate); // Ensure startDate is a Date object + const endDateObject = new Date(endDate); // Ensure endDate is a Date object + const startMonth = startDateObject.getMonth(); + const endMonth = endDateObject.getMonth(); + const year = startDateObject.getFullYear(); // + const monthNames = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ]; + const months: string[] = []; + + for (let i = startMonth; i <= endMonth; i++) { + months.push(`${monthNames[i]} ${year}`); + } + return months; + }; + + mapWeeks = (): string[] => { + const { startDate, endDate } = this.state; + const weeks: string[] = []; + const currentDate = new Date(startDate); + while (currentDate <= endDate) { + const weekStart = new Date(currentDate); + const weekEnd = new Date(currentDate); + weekEnd.setDate(weekEnd.getDate() + 6); + weeks.push( + `Week of ${weekStart.toDateString()} - ${weekEnd.toDateString()}` + ); + currentDate.setDate(currentDate.getDate() + 7); + } + return weeks; + }; + renderContent = (formProps: IFormProps) => { + const goalType = this.props.goalType || ({} as IGoalType); + const { closeModal, renderButton } = this.props; + const { values, isSubmitted } = formProps; + const months: string[] = this.mapMonths(); + + const weeks = this.mapWeeks(); + const { departmentListQuery, branchListQuery, unitListQuery } = this.props; + const departments = departmentListQuery.departmentsMain?.list || []; + const branches = branchListQuery.branchesMain?.list || []; + const units = unitListQuery.unitsMain?.list || []; + return ( + <> + + + + + + {__('choose Entity')} + + + {ENTITY.map((item, index) => ( + + ))} + + + + + {__('Stage')} + + + {__('Segment')} + + + {this.state.segmentRadio === true && ( + + {isEnabled('segments') && ( + <> + {__('Segment')} + + this.onChangeSegments(segmentIds) + } + /> + + )} + + )} + {this.state.stageRadio === true && ( + + {isEnabled('cards') && ( + + )} + + )} + + {/* next development + {__('frequency')} + + {FREQUENCY.map((typeName, index) => ( + + ))} + + */} + + {__('start duration')}: + + + + + + {__('end duration')}: + + + + + + + + + {__('choose goalType type')} + + + {GOAL_TYPE.map((typeName, index) => ( + + ))} + + + + + {__('contribution type')} + + + {CONTRIBUTION.map((item, index) => ( + + ))} + + + {this.state.contributionType === 'person' && ( + + contribution + + + )} + {this.state.contributionType === 'team' && ( + + {__('Choose Structure')} + + {GOAL_STRUCTURE.map((item, index) => ( + + ))} + + + )} + {this.state.teamGoalType === 'Departments' && + this.state.contributionType === 'team' && ( + + {__('Departments')} + + {departments.map((item, index) => ( + + ))} + + + )} + {this.state.teamGoalType === 'Units' && + this.state.contributionType === 'team' && ( + + {__('Units')} + + {units.map((item, index) => ( + + ))} + + + )} + {this.state.teamGoalType === 'Branches' && + this.state.contributionType === 'team' && ( + + {__('Branches')} + + {branches.map((item, index) => ( + + ))} + + + )} + + + {__('metric')}: + + {['Value', 'Count'].map((typeName, index) => ( + + ))} + + + + {this.renderFormGroup('target', { + ...formProps, + name: 'target', + type: 'number', + defaultValue: goalType.target || 0 + })} + + + + {__('choose specific period goals')} + + + {SPECIFIC_PERIOD_GOAL.map((item, index) => ( + + ))} + + + + + {this.state.periodGoal === 'Monthly' && ( +
+ {months.map((month, index) => ( + + + {__('Period (Monthly)')} + + {month} + + + + {__('Target')} + + this.onChangeTarget(index, event)} + /> + + + + ))} +
+ )} + {this.state.periodGoal === 'Weekly' && ( +
+ {weeks.map((week, index) => ( + + + {__('Period (Weekly)')} + + {week} + + + + {__('Target')} + + this.onChangeTarget(index, event)} + /> + + + + ))} +
+ )} +
+ + + {renderButton({ + name: 'goalType', + values: this.generateDoc(values), + isSubmitted, + object: this.props.goalType + })} + + + ); + }; + + render() { + return ; + } +} + +export default GoalTypeForm; diff --git a/packages/plugin-goals-ui/src/components/goalRow.tsx b/packages/plugin-goals-ui/src/components/goalRow.tsx new file mode 100755 index 0000000000..4fdad24e64 --- /dev/null +++ b/packages/plugin-goals-ui/src/components/goalRow.tsx @@ -0,0 +1,203 @@ +import { Button, formatValue, FormControl, ModalTrigger } from '@erxes/ui/src'; +import _ from 'lodash'; +import React, { useEffect, useState } from 'react'; + +import GoalTypeForm from '../containers/goalForm'; +import { IGoalType } from '../types'; +import { mutations, queries } from '../graphql'; +import { gql, useQuery, useMutation } from '@apollo/client'; +import GoalView from './goalView'; +type Props = { + goalType: IGoalType; + history: any; + isChecked: boolean; + toggleBulk: (goalType: IGoalType, isChecked?: boolean) => void; +}; + +type State = { + showModal: boolean; + checkbox: boolean; + pipName: string; + boardName: string; + stageName: string; +}; + +function displayValue(goalType, name) { + const value = _.get(goalType, name); + + return formatValue(value); +} + +function renderFormTrigger(trigger: React.ReactNode, goalType: IGoalType) { + const content = props => ; + + return ( + + ); +} +function renderFormTViewrigger( + trigger: React.ReactNode, + goalType: IGoalType, + boardName: string, + pipelineName: string, + stageName: string, + emailName: string +) { + const content = props => ( + + ); + + return ( + + ); +} + +function renderEditAction(goalType: IGoalType) { + const trigger = ; + + return ( + + ); + } + + render() { + const { + goalTypes, + history, + loading, + toggleBulk, + bulk, + isAllSelected, + totalCount, + queryParams + } = this.props; + const query = queryString.parse(location.search); + + const params = { + ...query, + perPage: query.perPage && Number(query.perPage), + page: query.page && Number(query.page) + }; + + const mainContent = ( + + + + + + + + + + + + + + + + + + + + + + + + {goalTypes.map(goalType => ( + + ))} + +
+ + {__('entity ')}{__('boardName ')}{__('pipelineName ')}{__('stageName ')}{__('contributionType')}{__('frequency')}{__('metric')}{__('goalType')}{__('startDate')}{__('endDate')}{__('current')}{__('target')}{__('progress(%)')}{__('View')}{__('Edit')}
+
+ ); + + const addTrigger = ( + + ); + + let actionBarLeft: React.ReactNode; + + if (bulk.length > 0) { + const onClick = () => + confirm() + .then(() => { + this.removeGoalTypes(bulk); + }) + .catch(error => { + Alert.error(error.message); + }); + + actionBarLeft = ( + + + + ); + } + + const goalTypeForm = props => { + return ; + }; + + const actionBarRight = ( + + + + ); + + const actionBar = ( + + ); + return ( + + } + actionBar={actionBar} + leftSidebar={} + footer={} + content={ + + } + /> + ); + } +} + +export default withRouter(GoalTypesList); diff --git a/packages/plugin-goals-ui/src/components/goalView.tsx b/packages/plugin-goals-ui/src/components/goalView.tsx new file mode 100644 index 0000000000..2b4fcbb723 --- /dev/null +++ b/packages/plugin-goals-ui/src/components/goalView.tsx @@ -0,0 +1,153 @@ +import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { IGoalType } from '../types'; +import { + ControlLabel, + Form, + DateControl, + MainStyleFormColumn as FormColumn, + FormGroup, + MainStyleFormWrapper as FormWrapper, + MainStyleModalFooter as ModalFooter, + MainStyleScrollWrapper as ScrollWrapper +} from '@erxes/ui/src'; +import { Button, formatValue, FormControl, ModalTrigger } from '@erxes/ui/src'; +import { __ } from 'coreui/utils'; +import Table from '@erxes/ui/src/components/table'; +import { FlexContent, FlexItem } from '@erxes/ui/src/layout/styles'; +import { BoardHeader } from '@erxes/ui-cards/src/settings/boards/styles'; +import { UsersQueryResponse } from '@erxes/ui/src/auth/types'; +import { IUser } from '@erxes/ui/src/auth/types'; +import dayjs from 'dayjs'; +import React, { useEffect, useState } from 'react'; + +// Define the type for the props +interface IProps extends RouteComponentProps { + goalType: IGoalType; // Adjust the type of goalTypes as per your + boardName: string; + pipelineName: string; + stageName: string; + usersQuery: UsersQueryResponse; + emailName: string; + _id: string; + users: IUser[]; +} + +class GoalView extends React.Component { + constructor(props: IProps) { + super(props); + } + + render() { + const data = this.props.goalType; // Assuming this.props contains the 'data' object + const nestedProgressValue = data.progress.progress; // "100.000" + const current = data.progress.current; + const boardName = this.props.boardName; + const pipelineName = this.props.pipelineName; + const stageName = this.props.stageName; + const email = this.props.emailName; + return ( +
+
+ + {__(' Monthly: ' + data.entity + ', ' + email)} + + + + + + + + {__('Contributor: ') + data.contribution} + + + {__('Goal Type: ') + data.goalType} + + + + {__('Board: ')} + {boardName} + + + {__('Pipeline: ')} + {pipelineName} + + + {__('Stage: ')} + {stageName} + + + + + + + + + {__('Frequency: ') + data.frequency} + + + {__('Duration: ')} {data.startDate} - {data.endDate} + + {__('Current: ') + current} + {__('Target: ') + data.target} + + {__('Progress: ') + nestedProgressValue} + + + + +
+
+ {__('Month ' + data.entity)} + + + + + + {__( + data.entity + + ' progressed: ' + + pipelineName + + ', ' + + stageName + )} + + + + + +
+ + + + + + + + + + + + + + + {data.specificPeriodGoals.map((element, index) => ( + + + + + + + ))} + +
{__('Target')}{__('Current')} {__('progress(%)')}{__('Month')}
{element.addTarget}{current}{element.progress + '%'} + {dayjs(element.addMonthly).format('MMM D, h:mm A')} +
+
+
+
+
+ ); + } +} + +export default withRouter(GoalView); diff --git a/packages/plugin-goals-ui/src/configs.js b/packages/plugin-goals-ui/src/configs.js new file mode 100644 index 0000000000..b8421e9e97 --- /dev/null +++ b/packages/plugin-goals-ui/src/configs.js @@ -0,0 +1,45 @@ +module.exports = { + name: 'goalType', + port: 3017, + scope: 'goalType', + exposes: { + './routes': './src/routes.tsx' + }, + routes: { + url: 'http://localhost:3017/remoteEntry.js', + scope: 'goalType', + module: './routes' + }, + menus: [ + { + text: 'Goals', + to: '/erxes-plugin-goalType/goalType', + image: '/images/icons/erxes-18.svg', + location: 'settings', + scope: 'goalType' + } + ] +}; + +// module.exports = { +// name: 'goalType', +// port: 3017, +// scope: 'goalType', +// exposes: { +// './routes': './src/routes.tsx' +// }, +// routes: { +// url: 'http://localhost:3017/remoteEntry.js', +// scope: 'goalType', +// module: './routes' +// }, +// menus: [ +// { +// text: 'Goals', +// to: '/goals', +// image: '/images/icons/erxes-18.svg', +// location: 'settings', +// scope: 'goalType' +// } +// ] +// }; diff --git a/packages/plugin-goals-ui/src/constants.ts b/packages/plugin-goals-ui/src/constants.ts new file mode 100644 index 0000000000..cc68e9df39 --- /dev/null +++ b/packages/plugin-goals-ui/src/constants.ts @@ -0,0 +1,53 @@ +export const PRODUCT_CATEGORIES_STATUS = ['active', 'disabled', 'archived']; +export const PRODUCT_CATEGORIES_STATUS_FILTER = { + disabled: 'Disabled', + archived: 'Archived', + deleted: 'Deleted' +}; +export const PRODUCT_TYPE_CHOISES = { + product: 'Product', + service: 'Service', + unique: 'Unique' +}; + +export const ENTITY = [ + { name: 'Deal Based Goal', value: 'deal' }, + { name: 'Task Based Goal', value: 'task' }, + { name: 'Ticket Based Goal', value: 'ticket' }, + { name: 'Purchase Based Goal', value: 'purchase' } +]; +export const GOAL_STRUCTURE = [ + { name: 'Branches', value: 'Branches' }, + { name: 'Departments', value: 'Departments' }, + { name: 'Units', value: 'Units' } +]; +export const SPECIFIC_PERIOD_GOAL = [ + { + name: 'Weekly', + value: 'Weekly' + }, + { name: 'Monthly', value: 'Monthly' } +]; + +export const viewModes = [ + { label: 'List View', type: 'list', icon: 'list-ui-alt' }, + { label: 'Board View', type: 'board', icon: 'postcard' }, + { label: 'Calendar view', type: 'calendar', icon: 'postcard' } +]; + +// export const ENTITY = ['deal', 'task', 'ticket', 'purchase']; +// export const CONTRIBUTION = ['Team Goal', 'Personal Goal']; + +export const CONTRIBUTION = [ + { name: 'Team Goal', value: 'team' }, + { name: 'Personal Goal', value: 'person' } +]; + +export const FREQUENCY = ['Weekly', 'Monthly', ' Quarterly', 'Yearly']; + +export const GOAL_TYPE = [ + 'Added', + 'Processed', + 'Won (Deal based only)', + 'Meetings held (meeting based only)' +]; diff --git a/packages/plugin-goals-ui/src/containers/SelectgoalType.tsx b/packages/plugin-goals-ui/src/containers/SelectgoalType.tsx new file mode 100644 index 0000000000..762d88f3e7 --- /dev/null +++ b/packages/plugin-goals-ui/src/containers/SelectgoalType.tsx @@ -0,0 +1,50 @@ +import { SelectWithSearch } from '@erxes/ui/src'; +import { IOption, IQueryParams } from '@erxes/ui/src/types'; +import React from 'react'; + +import { queries } from '../graphql'; +import { IGoalType } from '../types'; + +// get goalType options for react-select-plus +export function generateGoalTypeOptions(array: IGoalType[] = []): IOption[] { + return array.map(item => { + const goalType = item || ({} as IGoalType); + + return { + value: goalType._id, + label: `${goalType.entity || ''}` + }; + }); +} + +export default ({ + queryParams, + onSelect, + value, + multi = true, + label, + name +}: { + queryParams?: IQueryParams; + label: string; + onSelect: (value: string[] | string, name: string) => void; + multi?: boolean; + customOption?: IOption; + value?: string | string[]; + name: string; +}) => { + const defaultValue = queryParams ? queryParams[name] : value; + + return ( + + ); +}; diff --git a/packages/plugin-goals-ui/src/containers/goalChooser.tsx b/packages/plugin-goals-ui/src/containers/goalChooser.tsx new file mode 100644 index 0000000000..9820b9d4e3 --- /dev/null +++ b/packages/plugin-goals-ui/src/containers/goalChooser.tsx @@ -0,0 +1,165 @@ +import { Chooser, withProps } from '@erxes/ui/src'; +import { gql } from '@apollo/client'; +import * as compose from 'lodash.flowright'; +import React from 'react'; +import { graphql } from '@apollo/client/react/hoc'; + +import { mutations, queries } from '../graphql'; +import { + AddMutationResponse, + GoalTypesQueryResponse, + IGoalType, + IGoalTypeDoc +} from '../types'; +import GoalTypeForm from './goalForm'; + +type Props = { + search: (value: string, loadMore?: boolean) => void; + perPage: number; + searchValue: string; +}; + +type FinalProps = { + goalTypesQuery: GoalTypesQueryResponse; +} & Props & + AddMutationResponse; + +class GoalTypeChooser extends React.Component< + WrapperProps & FinalProps, + { newGoalType?: IGoalType } +> { + constructor(props) { + super(props); + + this.state = { + newGoalType: undefined + }; + } + + resetAssociatedItem = () => { + return this.setState({ newGoalType: undefined }); + }; + + render() { + const { data, goalTypesQuery, search } = this.props; + + const renderName = goalType => { + return `${goalType.entity} - ${goalType.contributionType} `; + }; + + const getAssociatedGoalType = (newGoalType: IGoalType) => { + this.setState({ newGoalType }); + }; + + const updatedProps = { + ...this.props, + data: { + _id: data._id, + name: renderName(data), + datas: data.goalTypes, + mainTypeId: data.mainTypeId, + mainType: data.mainType, + relType: 'goalType' + }, + search, + clearState: () => search(''), + title: 'GoalType', + renderForm: formProps => ( + + ), + renderName, + newItem: this.state.newGoalType, + resetAssociatedItem: this.resetAssociatedItem, + datas: goalTypesQuery.goalTypes || [], + refetchQuery: queries.goalTypes + }; + + return ; + } +} + +const WithQuery = withProps( + compose( + graphql< + Props & WrapperProps, + GoalTypesQueryResponse, + { searchValue: string; perPage: number } + >(gql(queries.goalTypes), { + name: 'goalTypesQuery', + options: ({ searchValue, perPage, data }) => { + return { + variables: { + searchValue, + perPage, + mainType: data.mainType, + mainTypeId: data.mainTypeId, + isRelated: data.isRelated, + sortField: 'createdAt', + sortDirection: -1 + }, + fetchPolicy: data.isRelated ? 'network-only' : 'cache-first' + }; + } + }), + // mutations + graphql<{}, AddMutationResponse, IGoalTypeDoc>( + gql(mutations.goalTypesAdd), + { + name: 'goalTypesAdd' + } + ) + )(GoalTypeChooser) +); + +type WrapperProps = { + data: { + _id?: string; + name: string; + goalTypes: IGoalType[]; + mainTypeId?: string; + mainType?: string; + isRelated?: boolean; + }; + onSelect: (datas: IGoalType[]) => void; + closeModal: () => void; +}; + +export default class Wrapper extends React.Component< + WrapperProps, + { + perPage: number; + searchValue: string; + } +> { + constructor(props) { + super(props); + + this.state = { perPage: 20, searchValue: '' }; + } + + search = (value, loadmore) => { + let perPage = 20; + + if (loadmore) { + perPage = this.state.perPage + 20; + } + + return this.setState({ perPage, searchValue: value }); + }; + + render() { + const { searchValue, perPage } = this.state; + + return ( + + ); + } +} diff --git a/packages/plugin-goals-ui/src/containers/goalForm.tsx b/packages/plugin-goals-ui/src/containers/goalForm.tsx new file mode 100644 index 0000000000..f0fbc155c4 --- /dev/null +++ b/packages/plugin-goals-ui/src/containers/goalForm.tsx @@ -0,0 +1,118 @@ +import { Button, ButtonMutate, withProps } from '@erxes/ui/src'; +import { IUser } from '@erxes/ui/src/auth/types'; +import { UsersQueryResponse } from '@erxes/ui/src/auth/types'; +import { IButtonMutateProps } from '@erxes/ui/src/types'; +import * as compose from 'lodash.flowright'; +import React from 'react'; +import GoalTypeForm from '../components/goalForm'; +import { mutations, queries } from '../graphql'; +import { IGoalType } from '../types'; +import { __ } from 'coreui/utils'; +import { graphql } from '@apollo/client/react/hoc'; +import { gql } from '@apollo/client'; +import { + BranchesMainQueryResponse, + DepartmentsMainQueryResponse, + UnitsMainQueryResponse +} from '@erxes/ui/src/team/types'; +import { EmptyState, Spinner } from '@erxes/ui/src'; + +type Props = { + goalType: IGoalType; + getAssociatedGoalType?: (insuranceTypeId: string) => void; + closeModal: () => void; +}; + +type FinalProps = { + usersQuery: UsersQueryResponse; + currentUser: IUser; + branchListQuery: BranchesMainQueryResponse; + unitListQuery: UnitsMainQueryResponse; + departmentListQuery: DepartmentsMainQueryResponse; +} & Props; + +class GoalTypeFromContainer extends React.Component { + render() { + const { branchListQuery, unitListQuery, departmentListQuery } = this.props; + + if ( + branchListQuery.loading || + unitListQuery.loading || + departmentListQuery.loading + ) { + return ; + } + + const renderButton = ({ + name, + values, + isSubmitted, + object + }: IButtonMutateProps) => { + const { closeModal, getAssociatedGoalType } = this.props; + + const afterSave = data => { + closeModal(); + + if (getAssociatedGoalType) { + getAssociatedGoalType(data.goalTypesAdd); + } + }; + return ( + + {__('Save')} + + ); + }; + + const updatedProps = { + ...this.props, + renderButton + }; + return ; + } +} + +const getRefetchQueries = () => { + return ['goalTypesMain', 'goalTypeDetail', 'goalTypes']; +}; +export default withProps<{}>( + compose( + graphql<{}>(gql(queries.branchesMain), { + name: 'branchListQuery', + options: () => ({ + variables: { + withoutUserFilter: true + } + }) + }), + graphql<{}>(gql(queries.unitsMain), { + name: 'unitListQuery', + options: () => ({ + variables: { + withoutUserFilter: true + } + }) + }), + graphql<{}>(gql(queries.departmentsMain), { + name: 'departmentListQuery', + options: () => ({ + variables: { + withoutUserFilter: true + } + }) + }) + )(GoalTypeFromContainer) +); + +// export default withProps(compose()(GoalTypeFromContainer)); diff --git a/packages/plugin-goals-ui/src/containers/goalTypesList.tsx b/packages/plugin-goals-ui/src/containers/goalTypesList.tsx new file mode 100644 index 0000000000..63d9fde439 --- /dev/null +++ b/packages/plugin-goals-ui/src/containers/goalTypesList.tsx @@ -0,0 +1,120 @@ +import { Alert, Bulk, router, withProps } from '@erxes/ui/src'; +import { IRouterProps } from '@erxes/ui/src/types'; +import { gql } from '@apollo/client'; +import * as compose from 'lodash.flowright'; +import React from 'react'; +import { graphql } from '@apollo/client/react/hoc'; +import { withRouter } from 'react-router-dom'; +import { useQuery } from '@apollo/client'; +import GoalTypesList from '../components/goalTypesList'; +import { mutations, queries } from '../graphql'; +import { + ListQueryVariables, + MainQueryResponse, + RemoveMutationResponse, + RemoveMutationVariables +} from '../types'; + +type Props = { + queryParams: any; + history: any; +}; + +type FinalProps = { + goalTypesMainQuery: MainQueryResponse; +} & Props & + IRouterProps & + RemoveMutationResponse; + +type State = { + loading: boolean; +}; + +class GoalTypeListContainer extends React.Component { + constructor(props) { + super(props); + + this.state = { + loading: false + }; + } + + render() { + const { goalTypesMainQuery, goalTypesRemove } = this.props; + const removeGoalTypes = ({ goalTypeIds }, emptyBulk) => { + goalTypesRemove({ + variables: { goalTypeIds } + }) + .then(() => { + emptyBulk(); + Alert.success('You successfully deleted a goalType'); + }) + .catch(e => { + Alert.error(e.message); + }); + }; + + const query = this.props.queryParams.queryParams || ''; + + const { list = [], totalCount = 0 } = + goalTypesMainQuery.goalTypesMain || {}; + const updatedProps = { + ...this.props, + totalCount, + // searchValue, + goalTypes: list, + loading: goalTypesMainQuery.loading || this.state.loading, + removeGoalTypes + }; + + const goalTypesList = props => { + return ; + }; + + const refetch = () => { + this.props.goalTypesMainQuery.refetch(); + }; + + return ; + } +} + +const generateOptions = () => ({ + refetchQueries: ['goalTypesMain'] +}); + +export default withProps( + compose( + graphql<{ queryParams: any }, MainQueryResponse, ListQueryVariables>( + gql(queries.goalTypesMain), + { + name: 'goalTypesMainQuery', + options: ({ queryParams }) => { + return { + variables: { + ...router.generatePaginationParams(queryParams || {}), + date: queryParams.date, + branch: queryParams.branch, + department: queryParams.department, + unit: queryParams.unit, + contribution: queryParams.contribution, + sortField: queryParams.sortField, + sortDirection: queryParams.sortDirection + ? parseInt(queryParams.sortDirection, 10) + : undefined + }, + fetchPolicy: 'network-only' + }; + } + } + ), + // mutations + graphql<{}, RemoveMutationResponse, RemoveMutationVariables>( + gql(mutations.goalTypesRemove), + { + name: 'goalTypesRemove', + options: generateOptions + } + ) + )(withRouter(GoalTypeListContainer)) +); diff --git a/packages/plugin-goals-ui/src/generalRoutes.tsx b/packages/plugin-goals-ui/src/generalRoutes.tsx new file mode 100644 index 0000000000..47ec84b4f0 --- /dev/null +++ b/packages/plugin-goals-ui/src/generalRoutes.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import LoanRoutes from './routes'; + +const Routes = () => ( + + + +); + +export default Routes; diff --git a/packages/plugin-goals-ui/src/graphql/index.ts b/packages/plugin-goals-ui/src/graphql/index.ts new file mode 100755 index 0000000000..8cff0604ab --- /dev/null +++ b/packages/plugin-goals-ui/src/graphql/index.ts @@ -0,0 +1,4 @@ +import mutations from './mutations'; +import queries from './queries'; + +export { queries, mutations }; diff --git a/packages/plugin-goals-ui/src/graphql/mutations.ts b/packages/plugin-goals-ui/src/graphql/mutations.ts new file mode 100755 index 0000000000..3cbdf78a70 --- /dev/null +++ b/packages/plugin-goals-ui/src/graphql/mutations.ts @@ -0,0 +1,98 @@ +const commonFields = ` + $entity: String + $stageId: String + $pipelineId: String + $boardId: String + $contributionType: String + $frequency:String + $metric:String + $goalType: String + $contribution: [String] + $department:String + $unit:String + $branch:String + $specificPeriodGoals:JSON + $startDate:String + $endDate:String + $target:String +`; + +const commonVariables = ` + entity:$entity + stageId:$stageId + pipelineId:$pipelineId + boardId:$boardId + contributionType:$contributionType + frequency:$frequency + metric:$metric + goalType:$goalType + contribution:$contribution + department:$department + unit:$unit + branch:$branch + specificPeriodGoals:$specificPeriodGoals + startDate:$startDate + endDate:$endDate + target:$target + +`; + +const goalTypesAdd = ` + mutation goalsAdd(${commonFields}) { + goalsAdd(${commonVariables}) { + _id + entity + boardId + pipelineId + stageId + contributionType + frequency + metric + goalType + contribution + department + unit + branch + specificPeriodGoals + startDate + endDate + target + } + } +`; + +const goalTypesEdit = ` + mutation goalsEdit($_id: String!, ${commonFields}) { + goalsEdit(_id: $_id, ${commonVariables}) { + _id + entity + stageId + pipelineId + boardId + contributionType + frequency + metric + goalType + contribution + department + unit + branch + specificPeriodGoals + startDate + endDate + target + } + } +`; + +const goalTypesRemove = ` + mutation goalsRemove($ goalTypeIds: [String]) { + goalsRemove( goalTypeIds: $ goalTypeIds) + } +`; + +export default { + goalTypesAdd, + goalTypesEdit, + goalTypesRemove +}; diff --git a/packages/plugin-goals-ui/src/graphql/queries.ts b/packages/plugin-goals-ui/src/graphql/queries.ts new file mode 100755 index 0000000000..f460cb6379 --- /dev/null +++ b/packages/plugin-goals-ui/src/graphql/queries.ts @@ -0,0 +1,306 @@ +import { pipeline } from 'stream'; + +const insuranceTypeFields = ` + _id + entity + stageId + pipelineId + boardId + contributionType + frequency + metric + goalType + contribution + specificPeriodGoals + progress + startDate + endDate + target +`; + +const listParamsDef = ` + $page: Int + $perPage: Int + $ids: [String] + $date: String + $branch: String + $department: String + $unit: String + $contribution: [String] + $searchValue: String + $sortField: String + $sortDirection: Int +`; + +const listParamsValue = ` + page: $page + perPage: $perPage + ids: $ids + date: $date + branch: $branch + department: $department + contribution:$contribution + unit: $unit + searchValue: $searchValue + sortField: $sortField + sortDirection: $sortDirection +`; + +export const goalTypes = ` + query goalTypes(${listParamsDef}) { + goalTypes(${listParamsValue}) { + ${insuranceTypeFields} + } + } +`; + +export const goalTypesMain = ` + query goalTypesMain(${listParamsDef}) { + goalTypesMain(${listParamsValue}) { + list { + ${insuranceTypeFields} + } + + totalCount + } + } +`; + +export const goalTypeCounts = ` + query goalTypeCounts(${listParamsDef}, $only: String) { + goalTypeCounts(${listParamsValue}, only: $only) + } +`; + +export const goalTypeDetail = ` + query goalTypeDetail($_id: String!) { + goalTypeDetail(_id: $_id) { + ${insuranceTypeFields} + } + } +`; + +const pipelineDetail = ` + query pipelineDetail($_id: String!) { + pipelineDetail(_id: $_id) { + _id + name + } + } +`; +const boardDetail = ` + query boardDetail($_id: String!) { + boardDetail(_id: $_id) { + _id + name + pipelines { + _id + name + } + } + } +`; +const stageDetail = ` + query stageDetail($_id:String!){ + stageDetail(_id: $_id) { + _id + name + } + } +`; +export const commonStructureParamsDef = ` + $ids: [String] + $excludeIds: Boolean, + $perPage: Int, + $page: Int + $searchValue: String, + $status:String, +`; + +export const commonStructureParamsValue = ` + ids: $ids + excludeIds: $excludeIds, + perPage: $perPage, + page: $page + searchValue: $searchValue + status: $status +`; + +export const branchField = ` + _id + title + address + parentId + supervisorId + code + order + userIds + userCount + users { + _id + details { + avatar + fullName + } + } + radius +`; + +const nameFields = ` + firstName + middleName + lastName +`; +const detailFields = ` + avatar + fullName + shortName + birthDate + position + workStartedDate + location + description + operatorPhone + ${nameFields} +`; + +export const departmentField = ` + _id + title + description + parentId + code + order + supervisorId + supervisor { + _id + username + email + status + isActive + groupIds + brandIds + score + + details { + ${detailFields} + } + + links + } + userIds + userCount + users { + _id + details { + ${detailFields} + } + } +`; +export const unitField = ` + _id + title + description + department + department { + ${departmentField} + } + supervisorId + supervisor { + _id + username + email + status + isActive + groupIds + brandIds + score + + details { + ${detailFields} + } + + links + } + code + userIds + users { + _id + details { + avatar + fullName + } + } +`; +const branchesMain = ` + query branchesMain(${commonStructureParamsDef}, $withoutUserFilter: Boolean) { + branchesMain (${commonStructureParamsValue}, withoutUserFilter: $withoutUserFilter){ + list { + ${branchField} + parent {${branchField}} + } + totalCount + totalUsersCount + } + } +`; +const unitsMain = ` + query unitsMain(${commonStructureParamsDef}) { + unitsMain(${commonStructureParamsValue}) { + list { + ${unitField} + } + totalCount + totalUsersCount + } + } +`; +const departmentsMain = ` + query departmentsMain(${commonStructureParamsDef},$withoutUserFilter:Boolean) { + departmentsMain(${commonStructureParamsValue},withoutUserFilter:$withoutUserFilter) { + list { + ${departmentField} + } + totalCount + totalUsersCount + } + } +`; +const userDetail = ` + query userDetail($_id: String) { + userDetail(_id: $_id) { + _id + username + email + isActive + status + groupIds + branchIds + departmentIds + + details { + ${detailFields} + } + links + emailSignatures + getNotificationByEmail + customFieldsData + score + employeeId + brandIds + } + } +`; +export default { + goalTypes, + goalTypesMain, + goalTypeCounts, + goalTypeDetail, + pipelineDetail, + boardDetail, + stageDetail, + branchesMain, + unitsMain, + departmentsMain, + userDetail +}; diff --git a/packages/plugin-goals-ui/src/index.js b/packages/plugin-goals-ui/src/index.js new file mode 100644 index 0000000000..6961fd4974 --- /dev/null +++ b/packages/plugin-goals-ui/src/index.js @@ -0,0 +1,6 @@ +import App from './App'; +import '@erxes/ui/src/styles/global-styles'; +import 'erxes-icon/css/erxes.min.css'; +import '@erxes/ui/src/styles/style.min.css'; + +export default App; diff --git a/packages/plugin-goals-ui/src/routes.tsx b/packages/plugin-goals-ui/src/routes.tsx new file mode 100644 index 0000000000..8c3e2b1b1e --- /dev/null +++ b/packages/plugin-goals-ui/src/routes.tsx @@ -0,0 +1,30 @@ +import queryString from 'query-string'; +import React from 'react'; +import { Route } from 'react-router-dom'; +import asyncComponent from '@erxes/ui/src/components/AsyncComponent'; + +const GoalTypesList = asyncComponent(() => + import(/* webpackChunkName: "GoalTypesList" */ './containers/goalTypesList') +); + +const goalTypesLists = ({ location, history }) => { + return ( + + ); +}; + +const GoalRoutes = () => { + return ( + + + + ); +}; + +export default GoalRoutes; diff --git a/packages/plugin-goals-ui/src/styles.ts b/packages/plugin-goals-ui/src/styles.ts new file mode 100644 index 0000000000..cba51a0e09 --- /dev/null +++ b/packages/plugin-goals-ui/src/styles.ts @@ -0,0 +1,260 @@ +import styled from 'styled-components'; +import { DrawerDetail } from '@erxes/ui-automations/src/styles'; +import styledTS from 'styled-components-ts'; +import { colors, dimensions } from '@erxes/ui/src/styles'; +import { highlight } from '@erxes/ui/src/utils/animations'; +import { rgba } from '@erxes/ui/src/styles/ecolor'; +import { Column as CommonColumn } from '@erxes/ui/src/styles/main'; +export const SectionContent = styledTS<{}>(styled.div)` + display:flex; + padding: 10px 10px; + justify-content:space-around; + align-items: center; +`; + +export const Padding = styledTS<{ + horizontal?: boolean; + vertical?: boolean; + padding?: string; +}>(styled.div)` + padding: ${({ horizontal, vertical, padding }) => + !horizontal && !vertical + ? '10px' + : `${vertical ? (padding ? `${padding}px` : '10px') : '0px'} ${ + horizontal ? (padding ? `${padding}px` : '10px') : '0px' + }`} +`; +export const ClearableBtn = styled.a` + cursor: pointer; +`; + +export const SidebarHeader = styled.h5` + margin-bottom: ${dimensions.coreSpacing}px; + color: ${colors.colorPrimary}; + padding-left: 10px; +`; + +export const SelectBox = styled.div` + display: flex; + width: 125px; + height: 40px; + text-align: center; + margin-bottom: 5px; + border-radius: 6px; + box-shadow: 0 0 5px 0 rgba(221, 221, 221, 0.7); + justify-content: center; + place-items: center; + cursor: pointer; + gap: 5px; + + &.active { + animation: ${highlight} 0.9s ease; + box-shadow: 0 0 5px 0 #63d2d6; + } +`; + +export const SelectBoxContainer = styledTS<{ + row?: boolean; +}>(styled.div)` + display: flex; + flex-wrap: wrap; + flex-direction: ${({ row }) => row && 'row'}; + padding-top:10px; + place-content:center; + gap: 5px; +`; + +export const CustomRangeContainer = styled.div` + margin-top: 10px; + margin-bottom: 10px; + display: flex; + align-items: flex-end; + > div { + flex: 1; + margin-right: 8px; + input[type='text'] { + border: none; + width: 100%; + height: 34px; + padding: 5px 0; + color: #444; + border-bottom: 1px solid; + border-color: #ddd; + background: none; + border-radius: 0; + box-shadow: none; + font-size: 13px; + } + } +`; + +export const EndDateContainer = styled.div` + .rdtPicker { + left: -98px !important; + } +`; + +export const Row = styledTS<{ gap?: number; spaceBetween?: boolean }>( + styled.div +)` + display: flex; + flex-wrap: wrap; + flex-direction: row; + align-items: center; + gap: ${({ gap }) => (gap ? `${gap}px` : '')}; + justify-content: ${({ spaceBetween }) => + spaceBetween ? `space-between` : ''}; + margin-right: ${dimensions.coreSpacing}px; +`; + +export const ResponseCard = styled.div` + display: flex; + flex-direction: column; + gap: 15px; + width: 100%; + padding: 10px; + margin-bottom: 10px; + color: ${colors.colorCoreGray}; + box-shadow: 0 0 5px 0 rgba(221, 221, 221, 0.7); + border-radius: 5px; +`; +export const ResponseCardContent = styled.div` + display: flex; + justify-content: space-between; +`; +export const ResponseCardDescription = styled.div` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + width: 150px; +`; + +export const AssignedMemberCard = styled.div` + color: ${colors.textSecondary}; + padding:10px + display: flex; + justify-content: space-between; + align-items:center +`; + +export const Column = styledTS<{ border?: boolean }>(styled(CommonColumn))` + border-right: ${({ border }) => (border ? '1px solid #ddd' : '')} + margin-right: ${dimensions.coreSpacing}px; +`; + +export const DividerBox = styled.span` + margin-bottom: ${dimensions.coreSpacing}px; + color: ${colors.colorCoreRed}; + border: 1px solid ${colors.colorCoreRed}; + border-radius: 2px; + padding: 3px 5px; + font-size: 8px; + display: inline-block; + font-weight: bold; + text-transform: uppercase; +`; +const Options = styled.div` + display: inline-block; + width: 100%; + margin-top: 10px; +`; +const GoalTypesTableWrapper = styled.div` + td { + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +`; + +const FieldWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + width: 48%; + float: left; + min-height: 110px; + border: 1px solid ${colors.borderPrimary}; + border-radius: 4px; + margin-bottom: 4%; + padding: 20px; + transition: all ease 0.3s; + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); + + &:nth-of-type(even) { + margin-left: 4%; + } + + > i { + margin-bottom: 10px; + } + + &:hover { + cursor: pointer; + box-shadow: 0 10px 20px ${rgba(colors.colorCoreDarkGray, 0.12)}; + } +`; + +const SidebarFilters = styledTS(styled.div)` + overflow: hidden; + padding: 5px 15px 30px 15px; +`; +export { SidebarFilters }; +export { GoalTypesTableWrapper, FlexRow }; + +export const BoardItemWrapper = styled(DrawerDetail)` + > div > div { + padding: 0; + } +`; + +export const Card = styled.div` + display: flex; + width: 150px; + height: 40px; + text-align: center; + margin-bottom: 5px; + border-radius: 6px; + box-shadow: 0 0 5px 0 rgba(221, 221, 221, 0.7); + justify-content: center; + place-items: center; + cursor: pointer; + gap: 5px; + + &.active { + animation: ${highlight} 0.9s ease; + box-shadow: 0 0 5px 0 #63d2d6; + } +`; +const FlexRow = styled.div` + display: flex; + flex-direction: row; + align-items: center; + + .flex-item { + flex: 1; + margin-left: ${dimensions.coreSpacing}px; + + &:first-child { + margin: 0; + } + + input[type='checkbox'] { + display: inline-block; + height: auto; + width: auto; + margin-right: 5px; + } + } + + button { + margin-left: ${dimensions.coreSpacing / 2}px; + } + + & + div { + margin-top: ${dimensions.coreSpacing / 2}px; + } +`; + +export { FieldWrapper, Options }; diff --git a/packages/plugin-goals-ui/src/types.ts b/packages/plugin-goals-ui/src/types.ts new file mode 100644 index 0000000000..dbdada0214 --- /dev/null +++ b/packages/plugin-goals-ui/src/types.ts @@ -0,0 +1,227 @@ +import { + IAttachment, + MutationVariables, + QueryResponse +} from '@erxes/ui/src/types'; +// tslint:disable-next-line:interface-name +interface SpecificPeriodGoal { + progress: string; + _id: string; + current: string; + addMonthly: string; + addTarget: string; +} +// tslint:disable-next-line:interface-name +interface ProgressGoal { + current: string; + progress: string; + amountData: string; + target: string; + _id: string; +} + +export interface IGoalTypeDoc { + createdAt?: Date; + entity: string; + stageId: any; + stageName: string; + pipelineId: any; + boardId: any; + contributionType: string; + frequency: string; + metric: string; + goalType: string; + contribution: [string]; + department: string; + unit: string; + branch: string; + specificPeriodGoals: SpecificPeriodGoal[]; + progress: { + current: string; + progress: string; + amountData: string; + target: string; + _id: string; + }; + chooseStage: string; + startDate: string; + endDate: string; + target: string; +} + +export interface ICommonTypes { + _id?: string; + createdAt?: Date; + createdBy?: string; + modifiedAt?: Date; + modifiedBy?: string; + + title?: string; + description?: string; + startDate?: Date; + endDate?: Date; + finishDateOfUse?: Date; + attachment?: IAttachment; + + status?: string; +} +export interface IAssignmentCampaign extends ICommonTypes { + segmentIds?: string[]; + voucherCampaignId?: string; +} + +export interface IPipeline { + _id: string; + name: string; + boardId: string; + tagId?: string; + visibility: string; + status: string; + createdAt: Date; + members?: any[]; + departmentIds?: string[]; + memberIds?: string[]; + condition?: string; + label?: string; + bgColor?: string; + isWatched: boolean; + startDate?: Date; + endDate?: Date; + metric?: string; + hackScoringType?: string; + templateId?: string; + state?: string; + itemsTotalCount?: number; + isCheckUser?: boolean; + isCheckDepartment?: boolean; + excludeCheckUserIds?: string[]; + numberConfig?: string; + numberSize?: string; +} +export interface IBoard { + _id: string; + name: string; + pipelines?: IPipeline[]; +} +export interface IGoalType extends IGoalTypeDoc { + _id: string; + map(arg0: (item: any, index: any) => void): import('react').ReactNode; + forEach(arg0: (goal: any) => void): unknown; +} + +// mutation types + +export type EditMutationResponse = { + goalTypesEdit: (params: { variables: IGoalType }) => Promise; +}; + +export type BoardsQueryResponse = { + boards: IBoard[]; +} & QueryResponse; + +export type RemoveMutationVariables = { + goalTypeIds: string[]; +}; + +export type RemoveMutationResponse = { + goalTypesRemove: (params: { + variables: RemoveMutationVariables; + }) => Promise; +}; + +export type MergeMutationVariables = { + goalTypeIds: string[]; + goalTypeFields: any; +}; + +export type MergeMutationResponse = { + goalTypesMerge: (params: { + variables: MergeMutationVariables; + }) => Promise; +}; + +export type AddMutationResponse = { + goalTypesAdd: (params: { variables: IGoalTypeDoc }) => Promise; +}; + +// query types + +export type ListQueryVariables = { + page?: number; + perPage?: number; + ids?: string[]; + searchValue?: string; + sortField?: string; + sortDirection?: number; +}; + +type ListConfig = { + name: string; + label: string; + order: number; +}; +export interface IStage { + _id: string; + name: string; + type: string; + probability: string; + index?: number; + itemId?: string; + unUsedAmount?: any; + amount?: any; + itemsTotalCount: number; + formId: string; + pipelineId: string; + visibility: string; + memberIds: string[]; + canMoveMemberIds?: string[]; + canEditMemberIds?: string[]; + departmentIds: string[]; + status: string; + order: number; + code?: string; + age?: number; + defaultTick?: boolean; +} +export type StagesQueryResponse = { + stages: IStage[]; + loading: boolean; + refetch: ({ pipelineId }: { pipelineId?: string }) => Promise; +}; +export type PipelinesQueryResponse = { + pipelines: IPipeline[]; + loading: boolean; + refetch: ({ + boardId, + type + }: { + boardId?: string; + type?: string; + }) => Promise; +}; +export type MainQueryResponse = { + goalTypesMain: { list: IGoalType[]; totalCount: number }; + loading: boolean; + refetch: () => void; +}; + +export type GoalTypesQueryResponse = { + goalTypes: IGoalType[]; + loading: boolean; + refetch: () => void; +}; + +export type ListConfigQueryResponse = { + fieldsDefaultColumnsConfig: ListConfig[]; + loading: boolean; +}; + +export type DetailQueryResponse = { + goalTypeDetail: IGoalType; + loading: boolean; +}; + +export type CountQueryResponse = { + loading: boolean; + refetch: () => void; +}; diff --git a/packages/plugin-goals-ui/src/utils.tsx b/packages/plugin-goals-ui/src/utils.tsx new file mode 100644 index 0000000000..6acf2cf680 --- /dev/null +++ b/packages/plugin-goals-ui/src/utils.tsx @@ -0,0 +1,248 @@ +import { gql } from '@apollo/client'; +import { STORAGE_BOARD_KEY, STORAGE_PIPELINE_KEY } from './constants'; + +import { IDateColumn } from '@erxes/ui/src/types'; +import React from 'react'; +import { graphql } from '@apollo/client/react/hoc'; +import { + PRODUCT_TYPE_CHOISES, + PRODUCT_CATEGORIES_STATUS_FILTER +} from './constants'; +type Options = { + _id: string; + name?: string; + type?: string; + index?: number; + itemId?: string; +}; +export const categoryStatusChoises = __ => { + const options: Array<{ value: string; label: string }> = []; + + for (const key of Object.keys(PRODUCT_CATEGORIES_STATUS_FILTER)) { + options.push({ + value: key, + label: __(PRODUCT_CATEGORIES_STATUS_FILTER[key]) + }); + } + + return options; +}; +// get options for react-select-plus +export function selectOptions(array: Options[] = []) { + return array.map(item => ({ value: item._id, label: item.name })); +} + +export function collectOrders(array: Options[] = []) { + return array.map((item: Options, index: number) => ({ + _id: item._id, + order: index + })); +} + +// a little function to help us with reordering the result +export const reorder = ( + list: any[], + startIndex: number, + endIndex: number +): any[] => { + const result = Array.from(list); + const [removed] = result.splice(startIndex, 1); + + result.splice(endIndex, 0, removed); + + return result; +}; + +type ReorderItemMap = {}; + +export const updateItemInfo = (state, item) => { + const { itemMap } = state; + const items = [...itemMap[item.stageId]]; + const index = items.findIndex(d => d._id === item._id); + + items[index] = item; + + return { ...itemMap, [item.stageId]: items }; +}; + +export const getDefaultBoardAndPipelines = () => { + const defaultBoards = localStorage.getItem(STORAGE_BOARD_KEY) || '{}'; + const defaultPipelines = localStorage.getItem(STORAGE_PIPELINE_KEY) || '{}'; + + return { + defaultBoards: JSON.parse(defaultBoards), + defaultPipelines: JSON.parse(defaultPipelines) + }; +}; + +export const invalidateCache = () => { + localStorage.setItem('cacheInvalidated', 'true'); +}; + +export const toArray = (item: string | string[] = []) => { + if (item instanceof Array) { + return item; + } + + return [item]; +}; + +export const renderPriority = (priority?: string) => { + if (!priority) { + return null; + } +}; + +export const generateButtonClass = (closeDate: Date, isComplete?: boolean) => { + let colorName = ''; + + if (isComplete) { + colorName = 'green'; + } else if (closeDate) { + const now = new Date(); + const oneDay = 24 * 60 * 60 * 1000; + + if (new Date(closeDate).getTime() - now.getTime() < oneDay) { + colorName = 'yellow'; + } + + if (now > closeDate) { + colorName = 'red'; + } + } + + return colorName; +}; + +export const generateButtonStart = (startDate: Date) => { + let colorName = 'teal'; + + if (startDate) { + const now = new Date(); + const oneDay = 24 * 60 * 60 * 1000; + + if (new Date(startDate).getTime() - now.getTime() < oneDay) { + colorName = 'blue'; + } + + if (now > startDate) { + colorName = 'red'; + } + } + + return colorName; +}; + +export const onCalendarLoadMore = (fetchMore, queryName, skip: number) => { + fetchMore({ + variables: { skip }, + updateQuery: (prevResult, { fetchMoreResult }) => { + if (!fetchMoreResult || fetchMoreResult[queryName].length === 0) { + return prevResult; + } + + return { + [queryName]: prevResult[queryName].concat(fetchMoreResult[queryName]) + }; + } + }); +}; + +export const getColors = (index: number) => { + const COLORS = [ + '#EA475D', + '#3CCC38', + '#FDA50D', + '#63D2D6', + '#3B85F4', + '#0A1E41', + '#5629B6', + '#6569DF', + '#888888' + ]; + + if (index > 9) { + return COLORS[2]; + } + + return COLORS[index]; +}; + +export const isRefresh = (queryParams: any, routerUtils: any, history: any) => { + const keys = Object.keys(queryParams || {}); + + if (!(keys.length === 2 || (keys.includes('key') && keys.length === 3))) { + routerUtils.setParams(history, { key: Math.random() }); + } +}; + +export const getBoardViewType = () => { + let viewType = 'board'; + + if (window.location.href.includes('calendar')) { + viewType = 'calendar'; + } + + if (window.location.href.includes('activity')) { + viewType = 'activity'; + } + + if (window.location.href.includes('conversion')) { + viewType = 'conversion'; + } + + if (window.location.href.includes('list')) { + viewType = 'list'; + } + + if (window.location.href.includes('chart')) { + viewType = 'chart'; + } + + if (window.location.href.includes('gantt')) { + viewType = 'gantt'; + } + + if (window.location.href.includes('time')) { + viewType = 'time'; + } + + return viewType; +}; + +export const getWarningMessage = (type: string): string => { + return `This will permanently delete the current ${type}. Are you absolutely sure?`; +}; + +export const getFilterParams = ( + queryParams: any, + getExtraParams: (queryParams) => any +) => { + if (!queryParams) { + return {}; + } + + const selectType = { + search: queryParams.search, + customerIds: queryParams.customerIds, + companyIds: queryParams.companyIds, + date: queryParams.date, + branch: queryParams.branch, + department: queryParams.department, + unit: queryParams.unit, + assignedUserIds: queryParams.assignedUserIds, + labelIds: queryParams.labelIds, + userIds: queryParams.userIds, + segment: queryParams.segment, + assignedToMe: queryParams.assignedToMe, + startDate: queryParams.startDate, + endDate: queryParams.endDate, + pipelineId: queryParams.pipelineId, + hasStartAndCloseDate: true, + tagIds: queryParams.tagIds, + limit: 100, + ...getExtraParams(queryParams) + }; + + return selectType; +}; diff --git a/packages/plugin-goals-ui/src/withConsumer.tsx b/packages/plugin-goals-ui/src/withConsumer.tsx new file mode 100644 index 0000000000..dd5e4c2e48 --- /dev/null +++ b/packages/plugin-goals-ui/src/withConsumer.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { AppConsumer } from 'coreui/appContext'; + +const withConsumer = (WrappedComponent: any) => { + return props => ( + + {(context: any) => } + + ); +}; + +export default withConsumer; diff --git a/packages/plugin-goals-ui/yarn.lock b/packages/plugin-goals-ui/yarn.lock new file mode 100644 index 0000000000..fb57ccd13a --- /dev/null +++ b/packages/plugin-goals-ui/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +