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.
Files changed (182) hide show
  1. package/README.md +2 -1
  2. package/dist/index2.js +1 -1
  3. package/dist/main2.js +1 -1
  4. package/dist/neo/env.js +1 -1
  5. package/dist/neo/neoLogin.js +1 -1
  6. package/dist/neo/pushCmp.js +1 -1
  7. package/dist/package.json.js +1 -1
  8. package/package.json +3 -2
  9. package/template/antd-custom-cmp-template/package.json +1 -1
  10. package/template/asset-manage-template/docs/README.md +1 -232
  11. package/template/asset-manage-template/package.json +2 -2
  12. package/template/echarts-custom-cmp-template/package.json +1 -1
  13. package/template/empty-custom-cmp-template/package.json +2 -2
  14. package/template/map-custom-cmp-template/package.json +1 -1
  15. package/template/neo-bi-cmps/neo.config.js +7 -1
  16. package/template/neo-bi-cmps/package.json +8 -7
  17. package/template/neo-bi-cmps/public/403.html +77 -0
  18. package/template/neo-bi-cmps/public/demo.html +2453 -0
  19. package/template/neo-bi-cmps/src/assets/icon/barChart.svg +1 -0
  20. package/template/neo-bi-cmps/src/assets/icon/card.svg +1 -0
  21. package/template/neo-bi-cmps/src/assets/icon/filter.svg +1 -0
  22. package/template/neo-bi-cmps/src/assets/icon/funnel.svg +1 -0
  23. package/template/neo-bi-cmps/src/assets/icon/tab.svg +1 -0
  24. package/template/neo-bi-cmps/src/components/filterBar__c/README.md +3 -14
  25. package/template/neo-bi-cmps/src/components/filterBar__c/common.scss +29 -0
  26. package/template/neo-bi-cmps/src/components/filterBar__c/index.tsx +668 -146
  27. package/template/neo-bi-cmps/src/components/filterBar__c/model.ts +26 -48
  28. package/template/neo-bi-cmps/src/components/filterBar__c/style.scss +46 -139
  29. package/template/neo-bi-cmps/src/components/targetNumber__c/customStyleConfig/index.tsx +11 -10
  30. package/template/neo-bi-cmps/src/components/targetNumber__c/index.tsx +9 -16
  31. package/template/neo-bi-cmps/src/components/targetNumber__c/model.ts +1 -1
  32. package/template/neo-bi-cmps/src/utils/common.ts +231 -0
  33. package/template/neo-bi-cmps/src/utils/filter2chartFilter.ts +268 -0
  34. package/template/neo-bi-cmps/src/utils/filterBar.ts +140 -0
  35. package/template/neo-bi-cmps/src/utils/pipelineFunnel.ts +341 -0
  36. package/template/{neo-h5-cmps/src/utils/queryObjectData.ts → neo-bi-cmps/src/utils/queryByCustomSQL.ts} +18 -13
  37. package/template/neo-bi-cmps/src/utils/requestDebounce.ts +22 -0
  38. package/template/neo-bi-cmps/src/utils/simpleTable.tsx +344 -0
  39. package/template/neo-bi-cmps/src/utils/stageSwitch.ts +15 -0
  40. package/template/neo-bi-cmps/src/utils/stageTimeChart.ts +90 -0
  41. package/template/neo-bi-cmps/src/utils/targetNumber.ts +12 -0
  42. package/template/neo-custom-cmp-template/docs/README.md +0 -231
  43. package/template/neo-custom-cmp-template/package.json +2 -2
  44. package/template/neo-h5-cmps/package.json +2 -2
  45. package/template/neo-h5-cmps/src/components/entityList__c/index.tsx +1 -2
  46. package/template/neo-h5-cmps/src/components/entityTabs__c/index.tsx +1 -1
  47. package/template/neo-h5-cmps/src/components/globalSearchInput__c/index.tsx +1 -1
  48. package/template/neo-h5-cmps/src/components/openChatPageBtn__c/index.tsx +1 -2
  49. package/template/neo-order-cmps/package.json +2 -2
  50. package/template/neo-pipeline-cmps/.prettierrc.js +12 -0
  51. package/template/neo-pipeline-cmps/@types/neo-ui-common.d.ts +36 -0
  52. package/template/neo-pipeline-cmps/README.md +99 -0
  53. package/template/neo-pipeline-cmps/commitlint.config.js +59 -0
  54. package/template/neo-pipeline-cmps/neo.config.js +135 -0
  55. package/template/neo-pipeline-cmps/package.json +66 -0
  56. package/template/neo-pipeline-cmps/public/403.html +77 -0
  57. package/template/neo-pipeline-cmps/public/css/base.css +283 -0
  58. package/template/neo-pipeline-cmps/public/demo.html +2453 -0
  59. package/template/neo-pipeline-cmps/public/scripts/app/bluebird.js +6679 -0
  60. package/template/neo-pipeline-cmps/public/template.html +13 -0
  61. package/template/neo-pipeline-cmps/src/assets/css/common.scss +127 -0
  62. package/template/neo-pipeline-cmps/src/assets/css/mixin.scss +47 -0
  63. package/template/neo-pipeline-cmps/src/assets/icon/barChart.svg +1 -0
  64. package/template/neo-pipeline-cmps/src/assets/icon/card.svg +1 -0
  65. package/template/neo-pipeline-cmps/src/assets/icon/filter.svg +1 -0
  66. package/template/neo-pipeline-cmps/src/assets/icon/funnel.svg +1 -0
  67. package/template/neo-pipeline-cmps/src/assets/icon/tab.svg +1 -0
  68. package/template/neo-pipeline-cmps/src/assets/img/AIBtn.gif +0 -0
  69. package/template/neo-pipeline-cmps/src/assets/img/NeoCRM.jpg +0 -0
  70. package/template/neo-pipeline-cmps/src/assets/img/aiLogo.png +0 -0
  71. package/template/neo-pipeline-cmps/src/assets/img/card-list.svg +1 -0
  72. package/template/neo-pipeline-cmps/src/assets/img/contact-form.svg +1 -0
  73. package/template/neo-pipeline-cmps/src/assets/img/custom-form.svg +1 -0
  74. package/template/neo-pipeline-cmps/src/assets/img/custom-widget.svg +1 -0
  75. package/template/neo-pipeline-cmps/src/assets/img/data-list.svg +1 -0
  76. package/template/neo-pipeline-cmps/src/assets/img/detail.svg +1 -0
  77. package/template/neo-pipeline-cmps/src/assets/img/favicon.png +0 -0
  78. package/template/neo-pipeline-cmps/src/assets/img/map.svg +1 -0
  79. package/template/neo-pipeline-cmps/src/assets/img/search.svg +1 -0
  80. package/template/neo-pipeline-cmps/src/assets/img/table.svg +1 -0
  81. package/template/neo-pipeline-cmps/src/components/filterBar__c/README.md +24 -0
  82. package/template/neo-pipeline-cmps/src/components/filterBar__c/common.scss +29 -0
  83. package/template/neo-pipeline-cmps/src/components/filterBar__c/index.tsx +731 -0
  84. package/template/neo-pipeline-cmps/src/components/filterBar__c/model.ts +52 -0
  85. package/template/neo-pipeline-cmps/src/components/filterBar__c/style.scss +119 -0
  86. package/template/neo-pipeline-cmps/src/components/pipelineFunnel__c/README.md +39 -0
  87. package/template/neo-pipeline-cmps/src/components/pipelineFunnel__c/index.tsx +416 -0
  88. package/template/neo-pipeline-cmps/src/components/pipelineFunnel__c/model.ts +80 -0
  89. package/template/neo-pipeline-cmps/src/components/pipelineFunnel__c/style.scss +83 -0
  90. package/template/neo-pipeline-cmps/src/components/showHealthResult__c/index.tsx +470 -0
  91. package/template/neo-pipeline-cmps/src/components/showHealthResult__c/model.ts +45 -0
  92. package/template/neo-pipeline-cmps/src/components/showHealthResult__c/style.scss +137 -0
  93. package/template/neo-pipeline-cmps/src/components/simpleTable__c/README.md +89 -0
  94. package/template/neo-pipeline-cmps/src/components/simpleTable__c/common.scss +195 -0
  95. package/template/neo-pipeline-cmps/src/components/simpleTable__c/index.tsx +667 -0
  96. package/template/neo-pipeline-cmps/src/components/simpleTable__c/model.ts +124 -0
  97. package/template/neo-pipeline-cmps/src/components/simpleTable__c/style.scss +192 -0
  98. package/template/neo-pipeline-cmps/src/components/stageSwitch__c/README.md +36 -0
  99. package/template/neo-pipeline-cmps/src/components/stageSwitch__c/index.tsx +513 -0
  100. package/template/neo-pipeline-cmps/src/components/stageSwitch__c/model.ts +71 -0
  101. package/template/{neo-bi-cmps → neo-pipeline-cmps}/src/components/stageSwitch__c/style.scss +4 -2
  102. package/template/neo-pipeline-cmps/src/components/stageTimeChart__c/README.md +37 -0
  103. package/template/neo-pipeline-cmps/src/components/stageTimeChart__c/index.tsx +455 -0
  104. package/template/neo-pipeline-cmps/src/components/stageTimeChart__c/model.ts +106 -0
  105. package/template/{neo-bi-cmps → neo-pipeline-cmps}/src/components/stageTimeChart__c/style.scss +3 -2
  106. package/template/neo-pipeline-cmps/src/utils/common.ts +229 -0
  107. package/template/neo-pipeline-cmps/src/utils/filter2chartFilter.ts +266 -0
  108. package/template/neo-pipeline-cmps/src/utils/filterBar.ts +140 -0
  109. package/template/neo-pipeline-cmps/src/utils/pipelineFunnel.ts +343 -0
  110. package/template/neo-pipeline-cmps/src/utils/queryByCustomSQL.ts +121 -0
  111. package/template/neo-pipeline-cmps/src/utils/requestDebounce.ts +22 -0
  112. package/template/neo-pipeline-cmps/src/utils/simpleTable.tsx +349 -0
  113. package/template/neo-pipeline-cmps/src/utils/stageSwitch.ts +15 -0
  114. package/template/neo-pipeline-cmps/src/utils/stageTimeChart.ts +90 -0
  115. package/template/neo-pipeline-cmps/src/utils/targetNumber.ts +12 -0
  116. package/template/neo-pipeline-cmps/tsconfig.json +40 -0
  117. package/template/neo-web-entity-grid/package.json +2 -2
  118. package/template/neo-web-form/package.json +2 -2
  119. package/template/neo-web-form/src/components/batchAddTable__c/index.tsx +161 -41
  120. package/template/neo-web-form/src/components/batchAddTable__c/model.ts +4 -2
  121. package/template/react-custom-cmp-template/package.json +1 -1
  122. package/template/react-ts-custom-cmp-template/package.json +1 -1
  123. package/template/vue2-custom-cmp-template/package.json +1 -1
  124. package/template/asset-manage-template/src/utils/axiosFetcher.ts +0 -37
  125. package/template/asset-manage-template/src/utils/queryObjectData.ts +0 -112
  126. package/template/asset-manage-template/src/utils/xobjects.ts +0 -162
  127. package/template/neo-bi-cmps/.npmrc copy +0 -1
  128. package/template/neo-bi-cmps/src/components/aiCommitDrawer__c/README.md +0 -52
  129. package/template/neo-bi-cmps/src/components/aiCommitDrawer__c/index.tsx +0 -183
  130. package/template/neo-bi-cmps/src/components/aiCommitDrawer__c/model.ts +0 -90
  131. package/template/neo-bi-cmps/src/components/aiCommitDrawer__c/style.scss +0 -218
  132. package/template/neo-bi-cmps/src/components/forecastChart__c/README.md +0 -31
  133. package/template/neo-bi-cmps/src/components/forecastChart__c/index.tsx +0 -158
  134. package/template/neo-bi-cmps/src/components/forecastChart__c/model.ts +0 -40
  135. package/template/neo-bi-cmps/src/components/forecastChart__c/style.scss +0 -154
  136. package/template/neo-bi-cmps/src/components/forecastGrid__c/README.md +0 -36
  137. package/template/neo-bi-cmps/src/components/forecastGrid__c/index.tsx +0 -86
  138. package/template/neo-bi-cmps/src/components/forecastGrid__c/model.ts +0 -62
  139. package/template/neo-bi-cmps/src/components/forecastGrid__c/style.scss +0 -48
  140. package/template/neo-bi-cmps/src/components/gapCloser__c/README.md +0 -24
  141. package/template/neo-bi-cmps/src/components/gapCloser__c/index.tsx +0 -100
  142. package/template/neo-bi-cmps/src/components/gapCloser__c/model.ts +0 -46
  143. package/template/neo-bi-cmps/src/components/gapCloser__c/style.scss +0 -60
  144. package/template/neo-bi-cmps/src/components/kpiCards__c/README.md +0 -35
  145. package/template/neo-bi-cmps/src/components/kpiCards__c/index.tsx +0 -70
  146. package/template/neo-bi-cmps/src/components/kpiCards__c/model.ts +0 -50
  147. package/template/neo-bi-cmps/src/components/kpiCards__c/style.scss +0 -33
  148. package/template/neo-bi-cmps/src/components/oppList__c/README.md +0 -52
  149. package/template/neo-bi-cmps/src/components/oppList__c/index.tsx +0 -285
  150. package/template/neo-bi-cmps/src/components/oppList__c/model.ts +0 -86
  151. package/template/neo-bi-cmps/src/components/oppList__c/style.scss +0 -133
  152. package/template/neo-bi-cmps/src/components/pipelineFunnel__c/README.md +0 -39
  153. package/template/neo-bi-cmps/src/components/pipelineFunnel__c/index.tsx +0 -130
  154. package/template/neo-bi-cmps/src/components/pipelineFunnel__c/model.ts +0 -66
  155. package/template/neo-bi-cmps/src/components/pipelineFunnel__c/style.scss +0 -133
  156. package/template/neo-bi-cmps/src/components/stageSwitch__c/README.md +0 -36
  157. package/template/neo-bi-cmps/src/components/stageSwitch__c/index.tsx +0 -118
  158. package/template/neo-bi-cmps/src/components/stageSwitch__c/model.ts +0 -92
  159. package/template/neo-bi-cmps/src/components/stageTimeChart__c/README.md +0 -37
  160. package/template/neo-bi-cmps/src/components/stageTimeChart__c/index.tsx +0 -126
  161. package/template/neo-bi-cmps/src/components/stageTimeChart__c/model.ts +0 -57
  162. package/template/neo-bi-cmps/src/components/tabSwitch__c/README.md +0 -37
  163. package/template/neo-bi-cmps/src/components/tabSwitch__c/index.tsx +0 -80
  164. package/template/neo-bi-cmps/src/components/tabSwitch__c/model.ts +0 -45
  165. package/template/neo-bi-cmps/src/components/tabSwitch__c/style.scss +0 -37
  166. package/template/neo-bi-cmps/src/utils/axiosFetcher.ts +0 -37
  167. package/template/neo-bi-cmps/src/utils/queryObjectData.ts +0 -76
  168. package/template/neo-bi-cmps/src/utils/xobjects.ts +0 -162
  169. package/template/neo-custom-cmp-template/src/utils/axiosFetcher.ts +0 -37
  170. package/template/neo-custom-cmp-template/src/utils/queryObjectData.ts +0 -112
  171. package/template/neo-custom-cmp-template/src/utils/xobjects.ts +0 -162
  172. package/template/neo-h5-cmps/src/utils/axiosFetcher.ts +0 -37
  173. package/template/neo-h5-cmps/src/utils/xobjects.ts +0 -167
  174. package/template/neo-order-cmps/src/utils/axiosFetcher.ts +0 -37
  175. package/template/neo-order-cmps/src/utils/queryObjectData.ts +0 -112
  176. package/template/neo-order-cmps/src/utils/xobjects.ts +0 -162
  177. package/template/neo-web-entity-grid/src/utils/axiosFetcher.ts +0 -37
  178. package/template/neo-web-entity-grid/src/utils/queryObjectData.ts +0 -112
  179. package/template/neo-web-entity-grid/src/utils/xobjects.ts +0 -167
  180. package/template/neo-web-form/src/utils/axiosFetcher.ts +0 -37
  181. package/template/neo-web-form/src/utils/queryObjectData.ts +0 -112
  182. 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> &nbsp; </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;
@@ -1,8 +1,10 @@
1
- .stage-switch__c {
1
+ .stageSwitch__c {
2
+ margin-top: 12px;
3
+
2
4
  .stage-tabs {
3
5
  display: flex;
4
6
  gap: 0;
5
- margin-bottom: 20px;
7
+ margin-bottom: 12px;
6
8
  background: #fff;
7
9
  border-radius: 8px;
8
10
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
@@ -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 |