neo-cmp-cli 1.13.16 → 1.13.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/index2.js +1 -1
- package/dist/main2.js +1 -1
- package/dist/neo/env.js +1 -1
- package/dist/neo/neoLogin.js +1 -1
- package/dist/neo/pushCmp.js +1 -1
- package/dist/package.json.js +1 -1
- package/package.json +3 -2
- package/template/antd-custom-cmp-template/package.json +1 -1
- package/template/asset-manage-template/docs/README.md +1 -232
- package/template/asset-manage-template/package.json +2 -2
- package/template/echarts-custom-cmp-template/package.json +1 -1
- package/template/empty-custom-cmp-template/package.json +2 -2
- package/template/map-custom-cmp-template/package.json +1 -1
- package/template/neo-bi-cmps/neo.config.js +7 -1
- package/template/neo-bi-cmps/package.json +8 -7
- package/template/neo-bi-cmps/public/403.html +77 -0
- package/template/neo-bi-cmps/public/demo.html +2453 -0
- package/template/neo-bi-cmps/src/assets/icon/barChart.svg +1 -0
- package/template/neo-bi-cmps/src/assets/icon/card.svg +1 -0
- package/template/neo-bi-cmps/src/assets/icon/filter.svg +1 -0
- package/template/neo-bi-cmps/src/assets/icon/funnel.svg +1 -0
- package/template/neo-bi-cmps/src/assets/icon/tab.svg +1 -0
- package/template/neo-bi-cmps/src/components/filterBar__c/README.md +3 -14
- package/template/neo-bi-cmps/src/components/filterBar__c/common.scss +29 -0
- package/template/neo-bi-cmps/src/components/filterBar__c/index.tsx +668 -146
- package/template/neo-bi-cmps/src/components/filterBar__c/model.ts +26 -48
- package/template/neo-bi-cmps/src/components/filterBar__c/style.scss +46 -139
- package/template/neo-bi-cmps/src/components/targetNumber__c/customStyleConfig/index.tsx +11 -10
- package/template/neo-bi-cmps/src/components/targetNumber__c/index.tsx +9 -16
- package/template/neo-bi-cmps/src/components/targetNumber__c/model.ts +1 -1
- package/template/neo-bi-cmps/src/utils/common.ts +231 -0
- package/template/neo-bi-cmps/src/utils/filter2chartFilter.ts +268 -0
- package/template/neo-bi-cmps/src/utils/filterBar.ts +140 -0
- package/template/neo-bi-cmps/src/utils/pipelineFunnel.ts +341 -0
- package/template/{neo-h5-cmps/src/utils/queryObjectData.ts → neo-bi-cmps/src/utils/queryByCustomSQL.ts} +18 -13
- package/template/neo-bi-cmps/src/utils/requestDebounce.ts +22 -0
- package/template/neo-bi-cmps/src/utils/simpleTable.tsx +344 -0
- package/template/neo-bi-cmps/src/utils/stageSwitch.ts +15 -0
- package/template/neo-bi-cmps/src/utils/stageTimeChart.ts +90 -0
- package/template/neo-bi-cmps/src/utils/targetNumber.ts +12 -0
- package/template/neo-custom-cmp-template/docs/README.md +0 -231
- package/template/neo-custom-cmp-template/package.json +2 -2
- package/template/neo-h5-cmps/package.json +2 -2
- package/template/neo-h5-cmps/src/components/entityList__c/index.tsx +1 -2
- package/template/neo-h5-cmps/src/components/entityTabs__c/index.tsx +1 -1
- package/template/neo-h5-cmps/src/components/globalSearchInput__c/index.tsx +1 -1
- package/template/neo-h5-cmps/src/components/openChatPageBtn__c/index.tsx +1 -2
- package/template/neo-order-cmps/package.json +2 -2
- package/template/neo-pipeline-cmps/.prettierrc.js +12 -0
- package/template/neo-pipeline-cmps/@types/neo-ui-common.d.ts +36 -0
- package/template/neo-pipeline-cmps/README.md +99 -0
- package/template/neo-pipeline-cmps/commitlint.config.js +59 -0
- package/template/neo-pipeline-cmps/neo.config.js +135 -0
- package/template/neo-pipeline-cmps/package.json +66 -0
- package/template/neo-pipeline-cmps/public/403.html +77 -0
- package/template/neo-pipeline-cmps/public/css/base.css +283 -0
- package/template/neo-pipeline-cmps/public/demo.html +2453 -0
- package/template/neo-pipeline-cmps/public/scripts/app/bluebird.js +6679 -0
- package/template/neo-pipeline-cmps/public/template.html +13 -0
- package/template/neo-pipeline-cmps/src/assets/css/common.scss +127 -0
- package/template/neo-pipeline-cmps/src/assets/css/mixin.scss +47 -0
- package/template/neo-pipeline-cmps/src/assets/icon/barChart.svg +1 -0
- package/template/neo-pipeline-cmps/src/assets/icon/card.svg +1 -0
- package/template/neo-pipeline-cmps/src/assets/icon/filter.svg +1 -0
- package/template/neo-pipeline-cmps/src/assets/icon/funnel.svg +1 -0
- package/template/neo-pipeline-cmps/src/assets/icon/tab.svg +1 -0
- package/template/neo-pipeline-cmps/src/assets/img/AIBtn.gif +0 -0
- package/template/neo-pipeline-cmps/src/assets/img/NeoCRM.jpg +0 -0
- package/template/neo-pipeline-cmps/src/assets/img/aiLogo.png +0 -0
- package/template/neo-pipeline-cmps/src/assets/img/card-list.svg +1 -0
- package/template/neo-pipeline-cmps/src/assets/img/contact-form.svg +1 -0
- package/template/neo-pipeline-cmps/src/assets/img/custom-form.svg +1 -0
- package/template/neo-pipeline-cmps/src/assets/img/custom-widget.svg +1 -0
- package/template/neo-pipeline-cmps/src/assets/img/data-list.svg +1 -0
- package/template/neo-pipeline-cmps/src/assets/img/detail.svg +1 -0
- package/template/neo-pipeline-cmps/src/assets/img/favicon.png +0 -0
- package/template/neo-pipeline-cmps/src/assets/img/map.svg +1 -0
- package/template/neo-pipeline-cmps/src/assets/img/search.svg +1 -0
- package/template/neo-pipeline-cmps/src/assets/img/table.svg +1 -0
- package/template/neo-pipeline-cmps/src/components/filterBar__c/README.md +24 -0
- package/template/neo-pipeline-cmps/src/components/filterBar__c/common.scss +29 -0
- package/template/neo-pipeline-cmps/src/components/filterBar__c/index.tsx +731 -0
- package/template/neo-pipeline-cmps/src/components/filterBar__c/model.ts +52 -0
- package/template/neo-pipeline-cmps/src/components/filterBar__c/style.scss +119 -0
- package/template/neo-pipeline-cmps/src/components/pipelineFunnel__c/README.md +39 -0
- package/template/neo-pipeline-cmps/src/components/pipelineFunnel__c/index.tsx +416 -0
- package/template/neo-pipeline-cmps/src/components/pipelineFunnel__c/model.ts +80 -0
- package/template/neo-pipeline-cmps/src/components/pipelineFunnel__c/style.scss +83 -0
- package/template/neo-pipeline-cmps/src/components/showHealthResult__c/index.tsx +470 -0
- package/template/neo-pipeline-cmps/src/components/showHealthResult__c/model.ts +45 -0
- package/template/neo-pipeline-cmps/src/components/showHealthResult__c/style.scss +137 -0
- package/template/neo-pipeline-cmps/src/components/simpleTable__c/README.md +89 -0
- package/template/neo-pipeline-cmps/src/components/simpleTable__c/common.scss +195 -0
- package/template/neo-pipeline-cmps/src/components/simpleTable__c/index.tsx +667 -0
- package/template/neo-pipeline-cmps/src/components/simpleTable__c/model.ts +124 -0
- package/template/neo-pipeline-cmps/src/components/simpleTable__c/style.scss +192 -0
- package/template/neo-pipeline-cmps/src/components/stageSwitch__c/README.md +36 -0
- package/template/neo-pipeline-cmps/src/components/stageSwitch__c/index.tsx +513 -0
- package/template/neo-pipeline-cmps/src/components/stageSwitch__c/model.ts +71 -0
- package/template/{neo-bi-cmps → neo-pipeline-cmps}/src/components/stageSwitch__c/style.scss +4 -2
- package/template/neo-pipeline-cmps/src/components/stageTimeChart__c/README.md +37 -0
- package/template/neo-pipeline-cmps/src/components/stageTimeChart__c/index.tsx +455 -0
- package/template/neo-pipeline-cmps/src/components/stageTimeChart__c/model.ts +106 -0
- package/template/{neo-bi-cmps → neo-pipeline-cmps}/src/components/stageTimeChart__c/style.scss +3 -2
- package/template/neo-pipeline-cmps/src/utils/common.ts +229 -0
- package/template/neo-pipeline-cmps/src/utils/filter2chartFilter.ts +266 -0
- package/template/neo-pipeline-cmps/src/utils/filterBar.ts +140 -0
- package/template/neo-pipeline-cmps/src/utils/pipelineFunnel.ts +343 -0
- package/template/neo-pipeline-cmps/src/utils/queryByCustomSQL.ts +121 -0
- package/template/neo-pipeline-cmps/src/utils/requestDebounce.ts +22 -0
- package/template/neo-pipeline-cmps/src/utils/simpleTable.tsx +349 -0
- package/template/neo-pipeline-cmps/src/utils/stageSwitch.ts +15 -0
- package/template/neo-pipeline-cmps/src/utils/stageTimeChart.ts +90 -0
- package/template/neo-pipeline-cmps/src/utils/targetNumber.ts +12 -0
- package/template/neo-pipeline-cmps/tsconfig.json +40 -0
- package/template/neo-web-entity-grid/package.json +2 -2
- package/template/neo-web-form/package.json +2 -2
- package/template/neo-web-form/src/components/batchAddTable__c/index.tsx +161 -41
- package/template/neo-web-form/src/components/batchAddTable__c/model.ts +4 -2
- package/template/react-custom-cmp-template/package.json +1 -1
- package/template/react-ts-custom-cmp-template/package.json +1 -1
- package/template/vue2-custom-cmp-template/package.json +1 -1
- package/template/asset-manage-template/src/utils/axiosFetcher.ts +0 -37
- package/template/asset-manage-template/src/utils/queryObjectData.ts +0 -112
- package/template/asset-manage-template/src/utils/xobjects.ts +0 -162
- package/template/neo-bi-cmps/.npmrc copy +0 -1
- package/template/neo-bi-cmps/src/components/aiCommitDrawer__c/README.md +0 -52
- package/template/neo-bi-cmps/src/components/aiCommitDrawer__c/index.tsx +0 -183
- package/template/neo-bi-cmps/src/components/aiCommitDrawer__c/model.ts +0 -90
- package/template/neo-bi-cmps/src/components/aiCommitDrawer__c/style.scss +0 -218
- package/template/neo-bi-cmps/src/components/forecastChart__c/README.md +0 -31
- package/template/neo-bi-cmps/src/components/forecastChart__c/index.tsx +0 -158
- package/template/neo-bi-cmps/src/components/forecastChart__c/model.ts +0 -40
- package/template/neo-bi-cmps/src/components/forecastChart__c/style.scss +0 -154
- package/template/neo-bi-cmps/src/components/forecastGrid__c/README.md +0 -36
- package/template/neo-bi-cmps/src/components/forecastGrid__c/index.tsx +0 -86
- package/template/neo-bi-cmps/src/components/forecastGrid__c/model.ts +0 -62
- package/template/neo-bi-cmps/src/components/forecastGrid__c/style.scss +0 -48
- package/template/neo-bi-cmps/src/components/gapCloser__c/README.md +0 -24
- package/template/neo-bi-cmps/src/components/gapCloser__c/index.tsx +0 -100
- package/template/neo-bi-cmps/src/components/gapCloser__c/model.ts +0 -46
- package/template/neo-bi-cmps/src/components/gapCloser__c/style.scss +0 -60
- package/template/neo-bi-cmps/src/components/kpiCards__c/README.md +0 -35
- package/template/neo-bi-cmps/src/components/kpiCards__c/index.tsx +0 -70
- package/template/neo-bi-cmps/src/components/kpiCards__c/model.ts +0 -50
- package/template/neo-bi-cmps/src/components/kpiCards__c/style.scss +0 -33
- package/template/neo-bi-cmps/src/components/oppList__c/README.md +0 -52
- package/template/neo-bi-cmps/src/components/oppList__c/index.tsx +0 -285
- package/template/neo-bi-cmps/src/components/oppList__c/model.ts +0 -86
- package/template/neo-bi-cmps/src/components/oppList__c/style.scss +0 -133
- package/template/neo-bi-cmps/src/components/pipelineFunnel__c/README.md +0 -39
- package/template/neo-bi-cmps/src/components/pipelineFunnel__c/index.tsx +0 -130
- package/template/neo-bi-cmps/src/components/pipelineFunnel__c/model.ts +0 -66
- package/template/neo-bi-cmps/src/components/pipelineFunnel__c/style.scss +0 -133
- package/template/neo-bi-cmps/src/components/stageSwitch__c/README.md +0 -36
- package/template/neo-bi-cmps/src/components/stageSwitch__c/index.tsx +0 -118
- package/template/neo-bi-cmps/src/components/stageSwitch__c/model.ts +0 -92
- package/template/neo-bi-cmps/src/components/stageTimeChart__c/README.md +0 -37
- package/template/neo-bi-cmps/src/components/stageTimeChart__c/index.tsx +0 -126
- package/template/neo-bi-cmps/src/components/stageTimeChart__c/model.ts +0 -57
- package/template/neo-bi-cmps/src/components/tabSwitch__c/README.md +0 -37
- package/template/neo-bi-cmps/src/components/tabSwitch__c/index.tsx +0 -80
- package/template/neo-bi-cmps/src/components/tabSwitch__c/model.ts +0 -45
- package/template/neo-bi-cmps/src/components/tabSwitch__c/style.scss +0 -37
- package/template/neo-bi-cmps/src/utils/axiosFetcher.ts +0 -37
- package/template/neo-bi-cmps/src/utils/queryObjectData.ts +0 -76
- package/template/neo-bi-cmps/src/utils/xobjects.ts +0 -162
- package/template/neo-custom-cmp-template/src/utils/axiosFetcher.ts +0 -37
- package/template/neo-custom-cmp-template/src/utils/queryObjectData.ts +0 -112
- package/template/neo-custom-cmp-template/src/utils/xobjects.ts +0 -162
- package/template/neo-h5-cmps/src/utils/axiosFetcher.ts +0 -37
- package/template/neo-h5-cmps/src/utils/xobjects.ts +0 -167
- package/template/neo-order-cmps/src/utils/axiosFetcher.ts +0 -37
- package/template/neo-order-cmps/src/utils/queryObjectData.ts +0 -112
- package/template/neo-order-cmps/src/utils/xobjects.ts +0 -162
- package/template/neo-web-entity-grid/src/utils/axiosFetcher.ts +0 -37
- package/template/neo-web-entity-grid/src/utils/queryObjectData.ts +0 -112
- package/template/neo-web-entity-grid/src/utils/xobjects.ts +0 -167
- package/template/neo-web-form/src/utils/axiosFetcher.ts +0 -37
- package/template/neo-web-form/src/utils/queryObjectData.ts +0 -112
- package/template/neo-web-form/src/utils/xobjects.ts +0 -167
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Stage Switch Card Component
|
|
3
|
+
* @description Toggle between different sales stage data; opportunities from xObject.query, stages from stage API
|
|
4
|
+
*/
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { Spin } from 'antd';
|
|
7
|
+
// @ts-ignore
|
|
8
|
+
import { request, xObject } from 'neo-open-api';
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
import isEqual from 'lodash/isEqual';
|
|
11
|
+
// @ts-ignore
|
|
12
|
+
import { BaseCmp, NeoEvent } from 'neo-ui-common';
|
|
13
|
+
|
|
14
|
+
import queryByCustomSQL from '../../utils/queryByCustomSQL';
|
|
15
|
+
import {
|
|
16
|
+
closeRangeFromFilter,
|
|
17
|
+
entityTypeIdForWhere,
|
|
18
|
+
extractStageKeyFromStageName,
|
|
19
|
+
formatAmountDisplay,
|
|
20
|
+
normalizeOwnerIdsForWhere,
|
|
21
|
+
parseNumberOrZero,
|
|
22
|
+
getDefaultFilterByProps,
|
|
23
|
+
} from '../../utils/common';
|
|
24
|
+
import { rowMoney, rowOpportunityStage } from '../../utils/stageSwitch';
|
|
25
|
+
|
|
26
|
+
import './style.scss';
|
|
27
|
+
|
|
28
|
+
const STAGE_LIST_URL =
|
|
29
|
+
'/rest/data/v2.0/xobjects/stage/actions/getStageListByEntityTypeApiKey';
|
|
30
|
+
|
|
31
|
+
interface StageTab {
|
|
32
|
+
key: string;
|
|
33
|
+
name: string;
|
|
34
|
+
amount: string;
|
|
35
|
+
count: number;
|
|
36
|
+
/** Can be empty object when no period-over-period data, not rendered */
|
|
37
|
+
changes?: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface StageSwitchProps {
|
|
41
|
+
stages?: StageTab[];
|
|
42
|
+
activeStage?: string;
|
|
43
|
+
data?: {
|
|
44
|
+
__NeoCurrentUser?: { id?: string | number };
|
|
45
|
+
};
|
|
46
|
+
onStageChange?: (stage: StageTab) => void;
|
|
47
|
+
className?: string;
|
|
48
|
+
style?: React.CSSProperties;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface StageSwitchState {
|
|
52
|
+
activeStage: string;
|
|
53
|
+
opportunityList: Record<string, unknown>[];
|
|
54
|
+
/** Display stages generated from API + getOpportunityList */
|
|
55
|
+
stages: StageTab[];
|
|
56
|
+
loading: boolean;
|
|
57
|
+
error: string | null;
|
|
58
|
+
/** Consistent with FilterBar event payload (closeDateCustomRange / opportunityOwner / businessType, etc.); falls back to defaultFilter when empty object */
|
|
59
|
+
filter: any;
|
|
60
|
+
defaultEntityTypeApiKey: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
class StageSwitch extends BaseCmp<StageSwitchProps, StageSwitchState> {
|
|
64
|
+
constructor(props: StageSwitchProps) {
|
|
65
|
+
super(props);
|
|
66
|
+
|
|
67
|
+
const defaultFilter = getDefaultFilterByProps(props);
|
|
68
|
+
|
|
69
|
+
this.state = {
|
|
70
|
+
activeStage: props.activeStage || '',
|
|
71
|
+
opportunityList: [],
|
|
72
|
+
stages: props.stages?.length ? props.stages : [],
|
|
73
|
+
loading: false,
|
|
74
|
+
error: null,
|
|
75
|
+
filter: defaultFilter,
|
|
76
|
+
defaultEntityTypeApiKey:
|
|
77
|
+
defaultFilter.businessTypeApiKey ?? 'defaultBusiType',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
this.getOpportunityQueryFilter = this.getOpportunityQueryFilter.bind(this);
|
|
81
|
+
this.fetchOpportunityList = this.fetchOpportunityList.bind(this);
|
|
82
|
+
this.getOpportunityStats = this.getOpportunityStats.bind(this);
|
|
83
|
+
this.fetchStageRows = this.fetchStageRows.bind(this);
|
|
84
|
+
this.buildStagesFromApi = this.buildStagesFromApi.bind(this);
|
|
85
|
+
this.fetchAllData = this.fetchAllData.bind(this);
|
|
86
|
+
this.handleStageClick = this.handleStageClick.bind(this);
|
|
87
|
+
this.refreshData = this.refreshData.bind(this);
|
|
88
|
+
this.setFilter = this.setFilter.bind(this);
|
|
89
|
+
this.updateActiveStage = this.updateActiveStage.bind(this);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
componentDidMount() {
|
|
93
|
+
this.fetchAllData();
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
// Listen to a broadcast event
|
|
97
|
+
NeoEvent.listen('updateFilterData', (filterData: any) => {
|
|
98
|
+
console.log('StageSwitch received a broadcast event updateFilterData: ', filterData);
|
|
99
|
+
this.setFilter(filterData);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Broadcast event: update the currently active sales stage
|
|
103
|
+
NeoEvent.listen('updateActiveStage', (activeStage: string) => {
|
|
104
|
+
console.log('SimpleTable received a broadcast event updateActiveStage: ', activeStage);
|
|
105
|
+
this.updateActiveStage(activeStage);
|
|
106
|
+
});
|
|
107
|
+
*/
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
componentDidUpdate(prevProps: StageSwitchProps) {
|
|
111
|
+
if (
|
|
112
|
+
this.props.activeStage != null &&
|
|
113
|
+
this.props.activeStage !== '' &&
|
|
114
|
+
this.props.activeStage !== prevProps.activeStage
|
|
115
|
+
) {
|
|
116
|
+
this.setState({ activeStage: this.props.activeStage });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Convert FilterBar structured filter to xObject.query where string array (SDK concatenates with AND in order) */
|
|
121
|
+
getOpportunityQueryFilter(): string[] {
|
|
122
|
+
const defaultFilter = getDefaultFilterByProps(this.props);
|
|
123
|
+
const raw = this.state.filter;
|
|
124
|
+
const curFilter: Record<string, unknown> =
|
|
125
|
+
raw && typeof raw === 'object' && !Array.isArray(raw)
|
|
126
|
+
? Object.keys(raw as object).length > 0
|
|
127
|
+
? (raw as Record<string, unknown>)
|
|
128
|
+
: defaultFilter
|
|
129
|
+
: defaultFilter;
|
|
130
|
+
|
|
131
|
+
const where: string[] = [];
|
|
132
|
+
|
|
133
|
+
const range = closeRangeFromFilter(curFilter.closeDateCustomRange);
|
|
134
|
+
if (range) {
|
|
135
|
+
where.push(`closeDate >= ${range.start} and closeDate <= ${range.end}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const ownerIds = normalizeOwnerIdsForWhere(curFilter.opportunityOwner);
|
|
139
|
+
if (ownerIds.length === 1) {
|
|
140
|
+
where.push(`ownerId = ${ownerIds[0]}`);
|
|
141
|
+
} else if (ownerIds.length > 1) {
|
|
142
|
+
where.push(`ownerId in (${ownerIds.join(', ')})`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const entityTypeId = entityTypeIdForWhere(curFilter.businessType);
|
|
146
|
+
if (entityTypeId != null) {
|
|
147
|
+
where.push(`entityType = ${entityTypeId}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return where;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Generate query conditions for fetching historical opportunity list data */
|
|
154
|
+
getHistoryOpportunityQueryFilter(): string[] {
|
|
155
|
+
const defaultFilter = getDefaultFilterByProps(this.props);
|
|
156
|
+
const raw = this.state.filter;
|
|
157
|
+
const curFilter: Record<string, unknown> =
|
|
158
|
+
raw && typeof raw === 'object' && !Array.isArray(raw)
|
|
159
|
+
? Object.keys(raw as object).length > 0
|
|
160
|
+
? (raw as Record<string, unknown>)
|
|
161
|
+
: defaultFilter
|
|
162
|
+
: defaultFilter;
|
|
163
|
+
|
|
164
|
+
const where: string[] = [];
|
|
165
|
+
|
|
166
|
+
const range = closeRangeFromFilter(curFilter.closeDateCustomRange);
|
|
167
|
+
if (range) {
|
|
168
|
+
where.push(
|
|
169
|
+
`opportunity_1_closeDate >= ${range.start} and opportunity_1_closeDate <= ${range.end}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const ownerIds = normalizeOwnerIdsForWhere(curFilter.opportunityOwner);
|
|
174
|
+
if (ownerIds.length === 1) {
|
|
175
|
+
where.push(`opportunity_1_ownerId = ${ownerIds[0]}`);
|
|
176
|
+
} else if (ownerIds.length > 1) {
|
|
177
|
+
where.push(`opportunity_1_ownerId in (${ownerIds.join(', ')})`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const entityTypeId = entityTypeIdForWhere(curFilter.businessType);
|
|
181
|
+
if (entityTypeId != null) {
|
|
182
|
+
where.push(`opportunity_1_entityType = ${entityTypeId}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (curFilter.changesSinceCustomTime) {
|
|
186
|
+
where.push(`version = ${curFilter.changesSinceCustomTime}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return where;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Query up to 1000 opportunities per request */
|
|
193
|
+
async fetchOpportunityList(): Promise<Record<string, unknown>[]> {
|
|
194
|
+
const result = await xObject.query({
|
|
195
|
+
xObjectApiKey: 'opportunity',
|
|
196
|
+
fields: ['id', 'money', 'customItem248__c'],
|
|
197
|
+
page: 1,
|
|
198
|
+
pageSize: 1000,
|
|
199
|
+
where: this.getOpportunityQueryFilter(),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return result?.status
|
|
203
|
+
? ((result.data ?? []) as Record<string, unknown>[])
|
|
204
|
+
: [];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Query historical opportunity list data */
|
|
208
|
+
async fetchHistoryOpportunityList(): Promise<unknown[]> {
|
|
209
|
+
const result = await queryByCustomSQL({
|
|
210
|
+
xObjectApiKey: 'biCustomModel_397169_20260401104916618',
|
|
211
|
+
fields: [
|
|
212
|
+
'opportunity_1_opportunityName',
|
|
213
|
+
'opportunity_1_saleStageId',
|
|
214
|
+
'opportunity_1_money',
|
|
215
|
+
],
|
|
216
|
+
page: 1,
|
|
217
|
+
pageSize: 1000,
|
|
218
|
+
where: this.getHistoryOpportunityQueryFilter(),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return result?.status ? result.data ?? [] : [];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Filter opportunities by stageName: extract the segment after the first `.` in stageName as the stage key, and compare with the stage text on the opportunity row using the same rule.
|
|
226
|
+
* @returns count — number of records; amount — sum of money (or amount field if money is absent)
|
|
227
|
+
*/
|
|
228
|
+
getOpportunityStats(
|
|
229
|
+
stageName: string,
|
|
230
|
+
list: Record<string, unknown>[], // opportunity list
|
|
231
|
+
): { amount: number; count: number } {
|
|
232
|
+
const key = extractStageKeyFromStageName(stageName);
|
|
233
|
+
let amount = 0; // total opportunity amount
|
|
234
|
+
let count = 0; // total opportunity count
|
|
235
|
+
|
|
236
|
+
for (const row of list) {
|
|
237
|
+
if (!row || typeof row !== 'object') continue;
|
|
238
|
+
const rec = row as Record<string, unknown>;
|
|
239
|
+
const rowKey = extractStageKeyFromStageName(rowOpportunityStage(rec));
|
|
240
|
+
if (key !== rowKey) continue;
|
|
241
|
+
count += 1;
|
|
242
|
+
amount += rowMoney(rec);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { amount, count };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Historical opportunity list is a 2D array returned by custom SQL:
|
|
250
|
+
* `[opportunityName, saleStageId, money, ...]` — column 2 is the stage ID (corresponds to stage `key`), column 3 is the sales amount.
|
|
251
|
+
* @returns count — number of records; amount — sum of sales amount
|
|
252
|
+
*/
|
|
253
|
+
getHistoryOpportunityStats(
|
|
254
|
+
stageKey: string,
|
|
255
|
+
list: unknown[],
|
|
256
|
+
): { amount: number; count: number } {
|
|
257
|
+
let amount = 0;
|
|
258
|
+
let count = 0;
|
|
259
|
+
const targetId = String(stageKey);
|
|
260
|
+
|
|
261
|
+
for (const row of list) {
|
|
262
|
+
if (!Array.isArray(row) || row.length < 3) continue;
|
|
263
|
+
if (String(row[1]) !== targetId) continue;
|
|
264
|
+
count += 1;
|
|
265
|
+
amount += parseNumberOrZero(row[2]);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { amount, count };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async fetchStageRows(): Promise<Record<string, unknown>[]> {
|
|
272
|
+
const defaultEntityTypeApiKey =
|
|
273
|
+
this.state.defaultEntityTypeApiKey ?? 'defaultBusiType';
|
|
274
|
+
|
|
275
|
+
const res = await request({
|
|
276
|
+
url: STAGE_LIST_URL,
|
|
277
|
+
method: 'POST',
|
|
278
|
+
data: {
|
|
279
|
+
data: {
|
|
280
|
+
entityTypeApiKey: defaultEntityTypeApiKey,
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
return res.data || [];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
buildStagesFromApi(
|
|
289
|
+
rows: any[], // stage list
|
|
290
|
+
opportunityList: Record<string, unknown>[], // opportunity list
|
|
291
|
+
historyOpportunityList: unknown[], // historical opportunities: SQL 2D array rows
|
|
292
|
+
): StageTab[] {
|
|
293
|
+
console.log(
|
|
294
|
+
'[StageSwitch__c] buildStagesFromApi:',
|
|
295
|
+
rows,
|
|
296
|
+
opportunityList,
|
|
297
|
+
historyOpportunityList,
|
|
298
|
+
);
|
|
299
|
+
return rows.map((row, index) => {
|
|
300
|
+
const stageName = row.stageName;
|
|
301
|
+
const { amount, count } = this.getOpportunityStats(
|
|
302
|
+
stageName,
|
|
303
|
+
opportunityList,
|
|
304
|
+
);
|
|
305
|
+
const key = String(
|
|
306
|
+
row.id ?? row.apiKey ?? (stageName || `stage-${index}`),
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// Find the corresponding opportunity from the historical opportunity list by opportunity ID
|
|
310
|
+
const { amount: historyAmount, count: historyCount } =
|
|
311
|
+
this.getHistoryOpportunityStats(key, historyOpportunityList);
|
|
312
|
+
|
|
313
|
+
const tab: StageTab = {
|
|
314
|
+
key,
|
|
315
|
+
name: stageName,
|
|
316
|
+
amount: formatAmountDisplay(amount),
|
|
317
|
+
count: count,
|
|
318
|
+
changes: {
|
|
319
|
+
amount: formatAmountDisplay(historyAmount),
|
|
320
|
+
count: historyCount,
|
|
321
|
+
amountDirection:
|
|
322
|
+
historyAmount < amount
|
|
323
|
+
? 'up'
|
|
324
|
+
: historyAmount == amount
|
|
325
|
+
? 'same'
|
|
326
|
+
: 'down',
|
|
327
|
+
countDirection:
|
|
328
|
+
historyCount < count
|
|
329
|
+
? 'up'
|
|
330
|
+
: historyCount == count
|
|
331
|
+
? 'same'
|
|
332
|
+
: 'down',
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
return tab;
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async fetchAllData() {
|
|
340
|
+
this.setState({ loading: true, error: null });
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
const [opportunityList, stageRows, historyOpportunityList] =
|
|
344
|
+
await Promise.all([
|
|
345
|
+
this.fetchOpportunityList(),
|
|
346
|
+
this.fetchStageRows(),
|
|
347
|
+
this.fetchHistoryOpportunityList(),
|
|
348
|
+
]);
|
|
349
|
+
|
|
350
|
+
const stages = this.buildStagesFromApi(
|
|
351
|
+
stageRows,
|
|
352
|
+
opportunityList,
|
|
353
|
+
historyOpportunityList,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
this.setState({
|
|
357
|
+
opportunityList,
|
|
358
|
+
stages,
|
|
359
|
+
loading: false,
|
|
360
|
+
});
|
|
361
|
+
} catch (e: unknown) {
|
|
362
|
+
console.error('StageSwitch failed to load:', e);
|
|
363
|
+
const msg = e instanceof Error ? e.message : 'Failed to load';
|
|
364
|
+
this.setState({
|
|
365
|
+
loading: false,
|
|
366
|
+
error: msg,
|
|
367
|
+
opportunityList: [],
|
|
368
|
+
stages: [],
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// When a stage is clicked, update the currently active sales stage
|
|
374
|
+
handleStageClick(stage: StageTab) {
|
|
375
|
+
const { onStageChange } = this.props;
|
|
376
|
+
if (onStageChange) {
|
|
377
|
+
onStageChange(stage);
|
|
378
|
+
}
|
|
379
|
+
// Broadcast event: update the currently active sales stage
|
|
380
|
+
const activeStage = extractStageKeyFromStageName(stage.name);
|
|
381
|
+
this.setState({ activeStage: activeStage });
|
|
382
|
+
|
|
383
|
+
this.onActiveStageChange({
|
|
384
|
+
activeStage,
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// Trigger a broadcast event
|
|
388
|
+
// NeoEvent.broadcast('updateActiveStage', activeStage);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
@NeoEvent.function
|
|
392
|
+
async refreshData() {
|
|
393
|
+
await this.fetchAllData();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Consistent with pipelineFunnel: pass in { relation, filter }, will re-fetch opportunities and merge stage statistics.
|
|
398
|
+
*/
|
|
399
|
+
@NeoEvent.function
|
|
400
|
+
setFilter(filter?: any) {
|
|
401
|
+
if (isEqual(filter, this.state.filter)) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
let defaultEntityTypeApiKey = this.state.defaultEntityTypeApiKey;
|
|
405
|
+
if (filter.businessTypeApiKey) {
|
|
406
|
+
defaultEntityTypeApiKey = filter.businessTypeApiKey;
|
|
407
|
+
}
|
|
408
|
+
console.log('[StageSwitch__c] setFilter:', filter);
|
|
409
|
+
this.setState(
|
|
410
|
+
{ filter: filter, defaultEntityTypeApiKey: defaultEntityTypeApiKey },
|
|
411
|
+
() => {
|
|
412
|
+
this.fetchAllData();
|
|
413
|
+
},
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Update the currently active sales stage
|
|
419
|
+
*/
|
|
420
|
+
@NeoEvent.function
|
|
421
|
+
updateActiveStage(activeStage?: string) {
|
|
422
|
+
if (isEqual(activeStage, this.state.activeStage)) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
console.log('[StageSwitch__c] updateActiveStage:', activeStage);
|
|
426
|
+
this.setState({ activeStage: activeStage || '' });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
@NeoEvent.dispatch
|
|
430
|
+
onActiveStageChange(eventData?: any) {}
|
|
431
|
+
|
|
432
|
+
render() {
|
|
433
|
+
const { className, style } = this.props;
|
|
434
|
+
const { activeStage, stages, loading, error } = this.state;
|
|
435
|
+
|
|
436
|
+
return (
|
|
437
|
+
<div
|
|
438
|
+
className={`stageSwitch__c ${className || ''}`}
|
|
439
|
+
style={style}
|
|
440
|
+
data-time="2026.4.17 01"
|
|
441
|
+
>
|
|
442
|
+
<Spin spinning={loading}>
|
|
443
|
+
{error ? (
|
|
444
|
+
<div
|
|
445
|
+
className="stageSwitch__c-error"
|
|
446
|
+
style={{ color: '#cf1322', fontSize: 12, marginBottom: 8 }}
|
|
447
|
+
>
|
|
448
|
+
{error}
|
|
449
|
+
</div>
|
|
450
|
+
) : null}
|
|
451
|
+
<div className="stage-tabs">
|
|
452
|
+
{stages.map((stage) => (
|
|
453
|
+
<div
|
|
454
|
+
key={stage.key}
|
|
455
|
+
className={`stage-tab ${
|
|
456
|
+
activeStage === extractStageKeyFromStageName(stage.name)
|
|
457
|
+
? 'active'
|
|
458
|
+
: ''
|
|
459
|
+
}`}
|
|
460
|
+
onClick={() => this.handleStageClick(stage)}
|
|
461
|
+
>
|
|
462
|
+
<div className="stage-name">{stage.name}</div>
|
|
463
|
+
<div className="stage-amount-row">
|
|
464
|
+
<span className="stage-amount">{stage.amount}</span>
|
|
465
|
+
<span className="stage-count">({stage.count})</span>
|
|
466
|
+
</div>
|
|
467
|
+
{stage.changes && Object.keys(stage.changes).length > 0 && (
|
|
468
|
+
<div className="stage-sub">
|
|
469
|
+
<span
|
|
470
|
+
className={`change-amount ${
|
|
471
|
+
stage.changes.amountDirection === 'up'
|
|
472
|
+
? 'positive'
|
|
473
|
+
: stage.changes.amountDirection === 'down'
|
|
474
|
+
? 'negative'
|
|
475
|
+
: ''
|
|
476
|
+
}`}
|
|
477
|
+
>
|
|
478
|
+
{stage.changes.amountDirection === 'up'
|
|
479
|
+
? '↑'
|
|
480
|
+
: stage.changes.amountDirection === 'same'
|
|
481
|
+
? ''
|
|
482
|
+
: '↓'}{' '}
|
|
483
|
+
{stage.changes.amount}
|
|
484
|
+
</span>
|
|
485
|
+
<span> </span>
|
|
486
|
+
<span
|
|
487
|
+
className={`change-count ${
|
|
488
|
+
stage.changes.countDirection === 'up'
|
|
489
|
+
? 'positive'
|
|
490
|
+
: stage.changes.amountDirection === 'down'
|
|
491
|
+
? 'negative'
|
|
492
|
+
: ''
|
|
493
|
+
}`}
|
|
494
|
+
>
|
|
495
|
+
{stage.changes.countDirection === 'up'
|
|
496
|
+
? '↑'
|
|
497
|
+
: stage.changes.countDirection === 'same'
|
|
498
|
+
? ''
|
|
499
|
+
: '↓'}{' '}
|
|
500
|
+
{stage.changes.count}
|
|
501
|
+
</span>
|
|
502
|
+
</div>
|
|
503
|
+
)}
|
|
504
|
+
</div>
|
|
505
|
+
))}
|
|
506
|
+
</div>
|
|
507
|
+
</Spin>
|
|
508
|
+
</div>
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export default StageSwitch;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export class StageSwitchModel {
|
|
2
|
+
label: string = 'Stage Switch Card';
|
|
3
|
+
description: string =
|
|
4
|
+
'Toggle between different sales stage data, including amount, count and change trends';
|
|
5
|
+
iconUrl: string = 'https://custom-widgets.bj.bcebos.com/card.svg';
|
|
6
|
+
targetPage: string[] = ['all'];
|
|
7
|
+
targetDevice: string = 'all';
|
|
8
|
+
|
|
9
|
+
defaultComProps = {
|
|
10
|
+
/** defaultEntityTypeApiKey passed when requesting the stage list API */
|
|
11
|
+
// defaultEntityTypeApiKey: 'defaultBusiType',
|
|
12
|
+
stages: [] as unknown[],
|
|
13
|
+
activeStage: 'Prospecting',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
events = [
|
|
17
|
+
{
|
|
18
|
+
apiKey: 'onActiveStageChange',
|
|
19
|
+
label: 'After active stage changes',
|
|
20
|
+
helpText:
|
|
21
|
+
'Triggered after the active stage changes; event params include activeStage (current active stage)',
|
|
22
|
+
eventParams:
|
|
23
|
+
'[{"apiKey":"eventParam","children":[{"apiKey":"activeStage","label":"Current active stage","type":"String"}],"label":"Event parameters","type":"Object"}]',
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
functions = [
|
|
28
|
+
{
|
|
29
|
+
apiKey: 'updateActiveStage',
|
|
30
|
+
label: 'Update active stage',
|
|
31
|
+
helpTextKey: 'Set the current active sales stage',
|
|
32
|
+
funcInParams: [
|
|
33
|
+
{
|
|
34
|
+
apiKey: 'activeStage',
|
|
35
|
+
label: 'Current active stage',
|
|
36
|
+
type: 'String',
|
|
37
|
+
required: false,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
apiKey: 'setFilter',
|
|
43
|
+
label: 'Set filter conditions',
|
|
44
|
+
helpTextKey:
|
|
45
|
+
'Set the filter consistent with the chart (closeDate / ownerId / entityType) and re-fetch opportunities',
|
|
46
|
+
funcInParams: [
|
|
47
|
+
{
|
|
48
|
+
apiKey: 'filter',
|
|
49
|
+
label: 'Filter conditions (relation + filter array)',
|
|
50
|
+
type: 'Object',
|
|
51
|
+
required: false,
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
apiKey: 'refreshData',
|
|
57
|
+
label: 'Refresh data',
|
|
58
|
+
helpTextKey: 'Re-fetch opportunity list and stage list',
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
propsSchema = [
|
|
63
|
+
{
|
|
64
|
+
type: 'panelInput',
|
|
65
|
+
name: 'defaultEntityTypeApiKey',
|
|
66
|
+
label: 'Business type ApiKey (stage list API)',
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default StageSwitchModel;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# StageTimeChart Component
|
|
2
|
+
|
|
3
|
+
Stage time component that displays the average time spent in each sales stage.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import StageTimeChart from './components/stageTimeChart__c';
|
|
9
|
+
|
|
10
|
+
<StageTimeChart
|
|
11
|
+
title="Avg. Time in Stage"
|
|
12
|
+
items={[
|
|
13
|
+
{ stageName: 'Prospecting', actualTime: '8d 6h', actualPercent: 28, actualColor: '#22c55e', targetPercent: 33, limitPercent: 50 },
|
|
14
|
+
]}
|
|
15
|
+
onStageClick={(name) => console.log('Clicked:', name)}
|
|
16
|
+
/>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Props
|
|
20
|
+
|
|
21
|
+
| Property | Description | Type | Default |
|
|
22
|
+
|----------|-------------|------|---------|
|
|
23
|
+
| title | Title | string | 'Avg. Time in Stage' |
|
|
24
|
+
| items | Stage data | StageTimeItem[] | [] |
|
|
25
|
+
| showAiButton | Show AI button | boolean | true |
|
|
26
|
+
| onStageClick | Stage click callback | (stageName: string) => void | - |
|
|
27
|
+
|
|
28
|
+
## StageTimeItem
|
|
29
|
+
|
|
30
|
+
| Property | Description | Type |
|
|
31
|
+
|----------|-------------|------|
|
|
32
|
+
| stageName | Stage name | string |
|
|
33
|
+
| actualTime | Actual time | string |
|
|
34
|
+
| actualPercent | Actual percentage | number |
|
|
35
|
+
| actualColor | Actual color | string |
|
|
36
|
+
| targetPercent | Target percentage | number |
|
|
37
|
+
| limitPercent | Limit percentage | number |
|