diff --git a/src/App.jsx b/src/App.jsx
index 4a04b1c..dbc25a1 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,6 +1,6 @@
import './App.css';
import React, { useContext, useState } from 'react';
-import {
+import Icon, {
HomeOutlined,
TeamOutlined,
DashboardOutlined,
@@ -49,6 +49,10 @@ import Meeting2024GH from './views/Meeting2024-GH';
import SalesCustomerCareRegular from './views/SalesCustomerCareRegular';
import { stores_Context, APP_VERSION } from './config';
import { WaterMark } from '@ant-design/pro-components';
+import CooperationIcon from './components/icons/CooperationIcon';
+import OPDashboard from './views/OPDashboard';
+import OPActivity from './views/OPActivity';
+import OPRisk from './views/OPRisk';
const App = () => {
const { Content, Footer, Sider, } = Layout;
@@ -93,6 +97,10 @@ const App = () => {
{ key: 52, label: 销售进度 },
{ key: 'distribution', label: 统计分布 },
{ key: 'trade-pivot', label: 数据透视 },
+ { key: 'xx', type: 'divider' },
+ { key: 'op_dashboard', label: 顾问-看板 },
+ { key: 'op_activity', label: 顾问-沟通 },
+ { key: 'op_risk', label: 顾问-提升 },
],
},
{
@@ -131,7 +139,7 @@ const App = () => {
{
key: 6,
label: '客服',
- icon: ,
+ icon: ,
children: [
{
key: 61,
@@ -242,6 +250,9 @@ const App = () => {
} />
} />
} />
+ } />
+ } />
+ } />
diff --git a/src/components/Column.jsx b/src/components/Column.jsx
index 301de04..d5a8e40 100644
--- a/src/components/Column.jsx
+++ b/src/components/Column.jsx
@@ -35,6 +35,12 @@ export default observer((props) => {
// xField: 'value',
// yField: 'year',
// seriesField: 'type',
+ xAxis: {
+ label: {
+ autoHide: false,
+ autoRotate: true,
+ },
+ },
label: {
// 可手动配置 label 数据标签位置
position: 'middle',
@@ -62,5 +68,6 @@ export default observer((props) => {
},
annotations: [...annotationsLine],
}, extProps);
+ // console.log(config);
return ;
});
diff --git a/src/components/MixFieldsDetail.jsx b/src/components/MixFieldsDetail.jsx
new file mode 100644
index 0000000..d0e3f88
--- /dev/null
+++ b/src/components/MixFieldsDetail.jsx
@@ -0,0 +1,265 @@
+import { observer } from 'mobx-react';
+import { message } from 'antd';
+import { Mix, getCanvasPattern, } from '@ant-design/plots';
+import { merge, isEmpty, cloneDeep } from '../utils/commons';
+import { dataFieldAlias } from '../libs/ht';
+
+const COLOR_SETS = [
+ "#FF6B3B",
+ "#9FB40F",
+ "#76523B",
+ "#DAD5B5",
+ "#E19348",
+ "#F383A2",
+];
+const COLOR_SETS2 = [
+ "#5B8FF9",
+ "#61DDAA",
+ "#65789B",
+];
+
+/**
+ * 订单数, 团数: 柱形图
+ * 成交率: 折线图
+ */
+export default observer((props) => {
+ const { dataSource, summaryData: areaData, ...config } = props;
+ const { xField, yFields, colFields, lineFields, seriesField, tooltip, ...extConfig } = config;
+ const diffData0 = dataSource.reduce((r, row) => {
+ r.push({ ...row, yField: row[colFields[0]], yGroup: dataFieldAlias[colFields[0]].alias });
+ r.push({ ...row, yField: row[colFields[1]], yGroup: dataFieldAlias[colFields[1]].alias });
+ return r;
+ }, []);
+ const diffData1 = dataSource.reduce((r, row) => {
+ r.push({ ...row, yField: row[lineFields[1]], yGroup: dataFieldAlias[lineFields[1]].alias });
+ return r;
+ }, []);
+ const calcAxis = isEmpty(diffData0) ? 300 : (Math.max(...diffData0.map(ele => ele.yField))) * 3;
+ // const calcAxisC = isEmpty(diffData0) ? 300 : (Math.max(...diffDataPercent.map(ele => ele.yField))) * 3;
+ const diffLine = [
+ // {
+ // type: 'text',
+ // position: ['start', 0],
+ // content: `同比, 环比`,
+ // offsetX: -15,
+ // style: {
+ // fill: COLOR_SETS[0],
+ // textBaseline: 'bottom',
+ // },
+ // },
+ // {
+ // type: 'line',
+ // start: [-10, 0],
+ // end: ['max', 0],
+ // style: {
+ // stroke: COLOR_SETS[0],
+ // // lineDash: [2, 2],
+ // lineWidth: 0.5,
+ // },
+ // },
+ ];
+
+ const pattern = (datum, color) => {
+ return getCanvasPattern({
+ type: String(datum.yGroup).includes(' ') ? 'line' : '',
+ cfg: {
+ backgroundColor: color,
+ },
+ });
+ };
+
+ const MixConfig = {
+ appendPadding: 15,
+ height: 400,
+ syncViewPadding: true,
+ tooltip: {
+ shared: true,
+ // customItems: (originalItems) => {
+ // // process originalItems,
+ // const items = originalItems.map((ele) => ({ ...ele, name: ele.data?.extraLine ? ele.name : `${ele.name} ${dataFieldAlias[yField]?.alias || yField}` }));
+ // return items;
+ // },
+ },
+ legend: {
+ position: 'top',
+ layout: 'horizontal',
+ custom: true,
+ items: [
+ ...['团数', '订单数'].map((ele, ei) => ({
+ name: `${ele}`,
+ value: `${ele}`,
+ marker: {
+ symbol: 'square',
+ style: {
+ fill: COLOR_SETS2[ei],
+ r: 5,
+ },
+ },
+ })),
+ ...['', '成行率'].map((ele, ei) => ({ // '业绩',
+ name: `${ele}`,
+ value: `${ele}`,
+ marker: {
+ symbol: 'hyphen',
+ style: {
+ stroke: COLOR_SETS[ei],
+ r: 5,
+ lineWidth: 2
+ },
+ },
+ })),
+ ],
+ },
+ // event: (chart, e) => {
+ // console.log('mix', chart, e);
+ // if (e.type === 'click') {
+ // props.itemClick(e);
+ // }
+ // },
+ onReady: (plot) => {
+ plot.on('plot:click', (...args) => {
+ // message.info('请在柱状图上点击, 显示详情');
+ });
+ plot.on('element:click', (e) => {
+ const {
+ data: { data },
+ } = e;
+ // console.log('plot element', data);
+ props.itemClick(data);
+ });
+ // axis-label 添加点击事件
+ plot.on('axis-label:click', (e, ...args) => {
+ const { text } = e.target.attrs;
+ // console.log(text);
+ props.itemClick({ [xField]: text });
+ });
+ },
+ plots: [
+ {
+ type: 'column',
+ options: {
+ data: diffData0,
+ isGroup: true,
+ xField,
+ yField: 'yField',
+ seriesField: 'yGroup',
+ // xAxis: false,
+ meta: merge({
+ ...cloneDeep(dataFieldAlias),
+ }),
+ // color: '#b32b19',
+ // color: '#f58269',
+ legend: false, // {},
+ // smooth: true,
+ yAxis: {
+ type: 'linear',
+ tickCount: 4,
+ min: 0,
+ max: calcAxis,
+ title: { text: '团数', autoRotate: false, position: 'end' },
+ },
+ xAxis: {
+ label: {
+ autoHide: false,
+ autoRotate: true,
+ },
+ },
+ label: false,
+ color: COLOR_SETS2,
+ pattern,
+ },
+ },
+ {
+ type: 'line',
+ options: {
+ data: diffData1,
+ isGroup: true,
+ xField,
+ yField: 'yField',
+ seriesField: 'yGroup',
+ xAxis: false,
+ legend: false, // {},
+ meta: merge(
+ {
+ ...cloneDeep(dataFieldAlias),
+ },
+ { yField: dataFieldAlias[lineFields[1]] }
+ ),
+ // color: '#1AAF8B',
+ color: COLOR_SETS[1],
+ // smooth: true,
+ point: {
+ size: 4,
+ shape: 'cicle',
+ },
+ yAxis: {
+ type: 'linear',
+ // tickCount: 4,
+ min: 0,
+ position: 'right',
+ line: null,
+ grid: null,
+ title: { text: dataFieldAlias[lineFields[1]].label, autoRotate: false, position: 'end' },
+ },
+ label: {
+ style: {
+ fontWeight: 700,
+ stroke: '#fff',
+ lineWidth: 1,
+ },
+ },
+ lineStyle: (datum) => {
+ if (String(datum.yGroup).includes(' ')) {
+ return {
+ lineDash: [4, 4],
+ opacity: 0.75,
+ };
+ }
+ return {
+ opacity: 1,
+ };
+ },
+ },
+ },
+ // {
+ // type: 'column',
+ // options: {
+ // data: diffData2,
+ // xField,
+ // yField: 'yField',
+ // seriesField: 'yGroup',
+ // columnWidthRatio: 0.28,
+ // meta: {
+ // // yField: {
+ // // formatter: (v) => `${v}%`,
+ // // },
+ // },
+ // isGroup: true,
+ // xAxis: false,
+ // yAxis: {
+ // line: null,
+ // grid: null,
+ // label: false,
+ // position: 'left',
+ // // min: -calcAxisC,
+ // // max: calcAxisC/4,
+ // min: -3000,
+ // max: 250,
+ // tickCount: 4,
+ // },
+ // legend: false, // {},
+ // color: COLOR_SETS,
+ // // annotations: diffLine,
+
+ // minColumnWidth: 5,
+ // maxColumnWidth: 5,
+ // // 分组柱状图 组内柱子间的间距 (像素级别)
+ // dodgePadding: 1,
+ // // 分组柱状图 组间的间距 (像素级别)
+ // // intervalPadding: 20,
+ // },
+ // },
+ ],
+ };
+ return ;
+});
diff --git a/src/components/icons/CooperationIcon.jsx b/src/components/icons/CooperationIcon.jsx
new file mode 100644
index 0000000..c4c4013
--- /dev/null
+++ b/src/components/icons/CooperationIcon.jsx
@@ -0,0 +1,9 @@
+import Icon, {} from '@ant-design/icons';
+
+const CooperationSvg = () => (
+
+);
+
+const CooperationIcon = (props) => ;
+
+export default CooperationIcon;
diff --git a/src/libs/ht.js b/src/libs/ht.js
index d591b05..f687cea 100644
--- a/src/libs/ht.js
+++ b/src/libs/ht.js
@@ -271,6 +271,8 @@ export const pivotBy = (_data, [rows, columns, date]) => {
_b:dataObj[colKey].map((v) => `${v.orderState}: ${v.quotePrice}/ ${v.tourdays}/ ${v.personNum}`).join(', '),
SumOrder: _len,
+ ResumeOrder: 0,
+ ResumeConfirmOrder: 0,
SumPersonNum: 0,
ConfirmPersonNum: 0,
@@ -295,6 +297,8 @@ export const pivotBy = (_data, [rows, columns, date]) => {
r.SumPersonNum += v.personNum;
r.ConfirmPersonNum += Number(v.orderState) === 1 ? v.personNum : 0;
r.ConfirmOrder += Number(v.orderState) === 1 ? 1 : 0;
+ r.ResumeOrder += v.hasOld === 1 ? 1 : 0;
+ r.ResumeConfirmOrder += Number(v.orderState) === 1 && v.hasOld === 1 ? 1 : 0;
r.transactions += v.transactions;
r.SumML += Number(v.orderState) === 1 ? v.ML : 0;
r.quotePrice += Number(v.orderState) === 1 ? v.quotePrice : 0;
@@ -356,6 +360,7 @@ export const pivotBy = (_data, [rows, columns, date]) => {
const summaryCalc = [
'ConfirmOrder',
'SumOrder',
+ 'ResumeOrder', 'ResumeConfirmOrder',
'SumML',
'transactions',
'SumPersonNum',
@@ -376,6 +381,7 @@ export const pivotBy = (_data, [rows, columns, date]) => {
summaryCalc.applyDays = Math.ceil(summaryCalc.applyDays / allColumns.length);
summaryCalc.confirmDays = Math.ceil(summaryCalc.confirmDays / allColumns.length);
summaryCalc.ConfirmRates = summaryCalc.ConfirmOrder ? fixTo2Decimals(summaryCalc.ConfirmOrder / summaryCalc.SumOrder*100) : 0;
+ summaryCalc.ResumeConfirmRates = summaryCalc.ResumeConfirmOrder ? fixTo2Decimals(summaryCalc.ResumeConfirmOrder / summaryCalc.ResumeOrder*100) : 0;
summaryCalc.OrderValue = summaryCalc.SumOrder ? fixToInt(summaryCalc.SumML / summaryCalc.SumOrder) : 0;
summaryCalc.SingleML = summaryCalc.ConfirmOrder ? fixTo2Decimals(summaryCalc.SumML / summaryCalc.ConfirmOrder) : 0;
summaryCalc.AvgPPPrice = Math.ceil(summaryCalc.AvgPPPrice / allColumns.length);
diff --git a/src/stores/Index.js b/src/stores/Index.js
index d8161f6..2c87532 100644
--- a/src/stores/Index.js
+++ b/src/stores/Index.js
@@ -16,6 +16,7 @@ import DictData from "./DictData";
import Distribution from "./Distribution";
import DataPivot from './DataPivot';
import MeetingData from './MeetingData';
+import SalesCRMData from './SalesCRMData';
class Index {
constructor() {
this.dashboard_store = new DashboardStore(this);
@@ -35,6 +36,7 @@ class Index {
this.DistributionStore = new Distribution(this);
this.DataPivotStore = new DataPivot(this);
this.MeetingDataStore = new MeetingData(this);
+ this.SalesCRMDataStore = new SalesCRMData(this);
makeAutoObservable(this);
}
diff --git a/src/stores/SalesCRMData.js b/src/stores/SalesCRMData.js
new file mode 100644
index 0000000..da4cbc6
--- /dev/null
+++ b/src/stores/SalesCRMData.js
@@ -0,0 +1,105 @@
+import { makeAutoObservable, runInAction, toJS } from 'mobx';
+import { fetchJSON } from '../utils/request';
+import { isEmpty, sortDescBy, objectMapper, groupBy, pick, unique } from '../utils/commons';
+import { groupsMappedByCode, dataFieldAlias } from './../libs/ht';
+import { DATE_FORMAT, SMALL_DATETIME_FORMAT } from './../config';
+import moment from 'moment';
+
+const fetchResultsData = async (param) => {
+ const defaultParam = {
+ WebCode: 'All',
+ DepartmentList: '',
+ opisn: -1,
+ Date1: '',
+ Date2: '',
+ groupType: '',
+ groupDateType: '',
+ };
+ const json = await fetchJSON('/service-Analyse2/sales_crm_results', { ...defaultParam, ...param });
+ return json.errcode === 0 ? json.result : [];
+};
+
+class SalesCRMData {
+ constructor(appStore) {
+ this.appStore = appStore;
+ makeAutoObservable(this);
+ }
+
+ async get90n180Data(param = {}) {
+ const retProps = param?.retLabel || '';
+
+ const retKey = param.groupDateType === '' ? (param.groupType === 'overview' ? 'dataSource' : 'details') : 'byDate';
+ this.results.loading = true;
+ const date90={
+ Date1: moment().subtract(90, 'days').format(DATE_FORMAT),
+ Date2: moment().subtract(30, 'days').format(SMALL_DATETIME_FORMAT),
+ };
+ const date180={
+ Date1: moment().subtract(180, 'days').format(DATE_FORMAT),
+ Date2: moment().subtract(50, 'days').format(SMALL_DATETIME_FORMAT),
+ };
+ const [result90, result180] = await Promise.all([
+ fetchResultsData({ ...this.searchValues, ...date90, ...param }),
+ fetchResultsData({ ...this.searchValues, ...date180, ...param }),
+ ]);
+ const _90O = groupBy(result90, 'groupsKey');
+ const _180O = groupBy(result180, 'groupsKey');
+ const result2 = unique(Object.keys(_90O).concat(Object.keys(_180O))).map((key) => {
+ return {
+ ...pick(_90O[key]?.[0] || _180O[key][0], ['groupsKey', 'groupsLabel', 'groupType']),
+ ...(retProps && retKey === 'dataSource' ? { groupsLabel: retProps, retProps } : { retProps }),
+ key: `${param.groupType}-${key}`,
+ result90: _90O[key]?.[0] || {},
+ result180: _180O[key]?.[0] || {},
+ };
+ });
+ // console.log(result2, '+++++ +++', retKey);
+ // console.log(this.results[retKey].length);
+
+ runInAction(() => {
+ this.results.loading = false;
+ this.results[retKey] = [].concat(this.results[retKey], result2);
+ });
+ return this.results;
+ }
+
+
+ async getResultData(param = {}) {
+ const retKey = param.groupDateType === '' ? 'byOperator' : 'byDate';
+ this.results.loading = true;
+ this.results[retKey] = [];
+ const res = await fetchResultsData({ ...this.searchValues, ...param });
+ runInAction(() => {
+ this.results.loading = false;
+ this.results[retKey] = retKey === 'byOperator' ? res.filter(ele => ele.SumML > 0).sort(sortDescBy('SumML')) : res;
+ });
+ return this.results;
+ };
+
+ searchValues = {
+ // DateType: { key: 'confirmDate', label: '确认日期'},
+ WebCode: { key: 'All', label: '所有来源' },
+ // IncludeTickets: { key: '1', label: '含门票'},
+ DepartmentList: [groupsMappedByCode.GH], // test: GH
+ operator: '-1',
+ opisn: '-1',
+ };
+
+ searchValuesToSub = {};
+
+ setSearchValues(obj, values) {
+ this.searchValues = { ...this.searchValues, ...values };
+ this.searchValuesToSub = obj;
+ }
+
+ results = { loading: false, dataSource: [], details: [], byDate: [], byOperator: [] };
+ resetData = () => {
+ this.results.loading = false;
+ for (const key of Object.keys(this.results)) {
+ if (key !== 'loading') {
+ this.results[key] = [];
+ }
+ }
+ };
+}
+export default SalesCRMData;
diff --git a/src/utils/commons.js b/src/utils/commons.js
index 647b9f2..2849681 100644
--- a/src/utils/commons.js
+++ b/src/utils/commons.js
@@ -296,6 +296,10 @@ export const sortBy = (key) => {
return (a, b) => (a[key] > b[key]) ? 1 : ((b[key] > a[key]) ? -1 : 0);
};
+export const sortDescBy = (key) => {
+ return (a, b) => (a[key] < b[key]) ? 1 : ((b[key] < a[key]) ? -1 : 0);
+};
+
/**
* Object排序keys
*/
diff --git a/src/views/DataPivot.jsx b/src/views/DataPivot.jsx
index 888f161..825f291 100644
--- a/src/views/DataPivot.jsx
+++ b/src/views/DataPivot.jsx
@@ -88,6 +88,7 @@ const pageSetting = {
{ key: 'SumOrder', title: '订单数', dataIndex: 'SumOrder', width: '5em' },
{ key: 'ConfirmOrder', title: '成交数', dataIndex: 'ConfirmOrder', width: '5em' },
{ key: 'ConfirmRates', title: '成交率', dataIndex: 'ConfirmRates_txt', width: '5em' },
+ // { key: 'ResumeOrder', title: '老客户订单数', dataIndex: 'ResumeOrder', width: '5em', render: (_, r) => `${r.ResumeConfirmOrder}/${r.ResumeOrder}` },
{ key: 'SingleML', title: '单团毛利', dataIndex: 'SingleML', width: '5em' },
{ key: 'OrderValue', title: '单个订单价值', dataIndex: 'OrderValue', width: '5em' },
{ key: 'PPPriceRange', title: '人均天(外币)', dataIndex: 'PPPriceRange', width: '5em' },
@@ -174,7 +175,7 @@ export default observer((props) => {
const calcDataByDate = (_rawData) => {
// console.log(';;;;;', pivotDateColumns);
const { data, columnValues, summaryRows, summaryColumns, pivotKeys, summaryMix } = pivotBy(_rawData || rawData, [].concat(pivotDateColumns, [curXfield]));
- // console.log('data====', data, '\ncolumnValues', columnValues, '\nsummaryRows', summaryRows, '\nsummaryColumns', summaryColumns, '\nsummaryMix', summaryMix);
+ console.log('data====', data, '\ncolumnValues', columnValues, '\nsummaryRows', summaryRows, '\nsummaryColumns', summaryColumns, '\nsummaryMix', summaryMix);
setShowPassCountryTips(pivotKeys.includes('destinationCountry_AsJOSN'));
setDataBeforePick(data.sort(sortBy(curXfield)));
// 折线图汇总数据, 排序
diff --git a/src/views/OPActivity.jsx b/src/views/OPActivity.jsx
new file mode 100644
index 0000000..9c42e41
--- /dev/null
+++ b/src/views/OPActivity.jsx
@@ -0,0 +1,85 @@
+import React, { Children, useContext, useState } from 'react';
+import { observer } from 'mobx-react';
+import { stores_Context } from '../config';
+import moment from 'moment';
+import { SlackOutlined, SketchOutlined, AntCloudOutlined, RedditOutlined, GithubOutlined } from '@ant-design/icons';
+import { Row, Col, Table, Select, Space, Typography, Progress, Spin, Divider, Button, Switch } from 'antd';
+import SearchForm from './../components/search/SearchForm';
+
+export default observer((props) => {
+ const { sale_store, date_picker_store: searchFormStore } = useContext(stores_Context);
+
+ const { formValues, siderBroken } = searchFormStore;
+
+ const activityTableProps = {
+ columns: [
+ { title: '', dataIndex: 'op', key: 'op' }, // 维度: 顾问
+ {
+ title: '顾问动作',
+ key: 'date',
+ children: [
+ { title: '首次响应率24H', dataIndex: 'action', key: 'action' },
+ { title: '48H内报价率', dataIndex: 'action', key: 'action' },
+ { title: '一次报价率', dataIndex: 'action', key: 'action' },
+ { title: '二次报价率', dataIndex: 'action', key: 'action' },
+ { title: '>50条会话', dataIndex: 'action', key: 'action' },
+ { title: '违规数', dataIndex: 'action', key: 'action' },
+ ],
+ },
+ {
+ title: '客人回复',
+ key: 'department',
+ children: [
+ { title: '首次回复率24H', dataIndex: 'action', key: 'action' },
+ { title: '48H内报价回复率', dataIndex: 'action', key: 'action' },
+ { title: '一次报价回复率', dataIndex: 'action', key: 'action' },
+ { title: '二次报价回复率', dataIndex: 'action', key: 'action' },
+ ],
+ },
+ ],
+ };
+
+ const riskTableProps = {
+ columns: [
+ { title: '', dataIndex: 'date', key: 'date' }, // 维度
+ { title: '>24H回复', dataIndex: 'action', key: 'action' },
+ { title: '首次报价周期>48h', dataIndex: 'action', key: 'action' },
+ { title: '报价次数<1次', dataIndex: 'action' },
+ { title: '报价次数<2次', dataIndex: 'action' },
+ ],
+ };
+
+ return (
+ <>
+
+
+ {
+ sale_store.setSearchValues(obj, form);
+ // pageRefresh(obj);
+ }}
+ />
+
+
+
+
+ >
+ );
+});
diff --git a/src/views/OPDashboard.jsx b/src/views/OPDashboard.jsx
new file mode 100644
index 0000000..772e50a
--- /dev/null
+++ b/src/views/OPDashboard.jsx
@@ -0,0 +1,298 @@
+import React, { useContext, useState, useEffect } from 'react';
+import { observer } from 'mobx-react';
+import { stores_Context, DATE_FORMAT, SMALL_DATETIME_FORMAT } from '../config';
+import moment from 'moment';
+import { SlackOutlined, SketchOutlined, AntCloudOutlined, RedditOutlined, GithubOutlined } from '@ant-design/icons';
+import { Row, Col, Table, Select, Space, Typography, Progress, Spin, Divider, Button, Switch, Tag } from 'antd';
+import SearchForm from './../components/search/SearchForm';
+import MixFieldsDetail from '../components/MixFieldsDetail';
+import { cloneDeep, fixTo2Decimals, sortBy, isEmpty, pick } from '../utils/commons';
+import Column from '../components/Column';
+import { groupsMappedByKey } from './../libs/ht';
+
+const numberConvert10K = (number, scale = 10) => {
+ return fixTo2Decimals((number/(1000*scale)));
+};
+
+export default observer((props) => {
+ const { SalesCRMDataStore, date_picker_store: searchFormStore } = useContext(stores_Context);
+
+ const { formValues, formValuesToSub, siderBroken } = searchFormStore;
+ const _formValuesToSub = pick(formValuesToSub, ['DepartmentList', 'WebCode']);
+
+ const { resetData, results } = SalesCRMDataStore;
+ const operatorObjects = results.details.map((v) => ({ key: v.groupsKey, value: v.groupsKey, label: v.groupsLabel, text: v.groupsLabel }));
+ // console.log(operatorObjects);
+
+ const pageRefresh = async (obj) => {
+ resetData();
+ const deptList = obj.DepartmentList.split(',');
+ const includeCH = ['1', '2', '7'].some(ele => deptList.includes(ele));
+ const includeAH = ['28'].every(ele => deptList.includes(ele));
+ const includeGH = ['33'].every(ele => deptList.includes(ele));
+ const otherDept = deptList.filter(ele => !['1', '2', '7', '28', '33'].includes(ele));
+ const separateParam = [];
+ if (includeCH) {
+ const inCH = deptList.filter(k => ['1', '2', '7'].includes(k)).join(',');
+ separateParam.push({ DepartmentList: inCH, retLabel: 'CH'});
+ }
+ if (includeAH) {
+ separateParam.push({ DepartmentList: '28', retLabel: 'AH'});
+ }
+ if (includeGH) {
+ separateParam.push({ DepartmentList: '33', retLabel: 'GH'});
+ }
+ if (!isEmpty(otherDept) && (!isEmpty(includeAH) || !isEmpty(includeCH) || !isEmpty(includeGH))) {
+ separateParam.push({ DepartmentList: otherDept.join(','), retLabel: otherDept.map(k => groupsMappedByKey[k].label).join(', ') }); // '其它组'
+ }
+ if (!includeAH && !includeCH && !includeGH) {
+ separateParam.push({ DepartmentList: obj.DepartmentList });
+ }
+ // console.log('separateParam', separateParam, otherDept);
+ // console.log('formValuesToSub --- pageRefresh', formValuesToSub.DepartmentList);
+ // return;
+ for await (const subParam of separateParam) {
+ // console.log(subParam);
+ await SalesCRMDataStore.get90n180Data({
+ ...(obj || _formValuesToSub),
+ ...subParam,
+ groupType: 'overview',
+ // groupType: 'dept', // todo:
+ groupDateType: '',
+ });
+ await SalesCRMDataStore.get90n180Data({
+ ...(obj || _formValuesToSub),
+ ...subParam,
+ groupType: 'operator',
+ groupDateType: '',
+ });
+ }
+ };
+
+ const getFullYearDiagramData = async (obj) => {
+ // console.log('invoke --- getFullYearDiagramData');
+ // console.log('formValuesToSub --- getFullYearDiagramData', formValuesToSub.DepartmentList);
+ await SalesCRMDataStore.getResultData({
+ ..._formValuesToSub,
+ Date1: moment().startOf('year').format(DATE_FORMAT),
+ Date2: moment().endOf('year').format(SMALL_DATETIME_FORMAT),
+ groupType: 'overview',
+ // groupType: 'operator',
+ groupDateType: 'month',
+ ...(obj),
+ });
+ };
+ const getDiagramData = async (obj) => {
+ // console.log('invoke --- getDiagramData');
+ // console.log(_formValuesToSub, SalesCRMDataStore.searchValuesToSub);
+ await SalesCRMDataStore.getResultData({
+ ..._formValuesToSub,
+ ...SalesCRMDataStore.searchValuesToSub,
+ // Date1: moment().startOf('year').format(DATE_FORMAT),
+ // Date2: moment().endOf('year').format(SMALL_DATETIME_FORMAT),
+ // groupType: 'overview',
+ groupType: 'operator',
+ groupDateType: '',
+ ...(obj),
+ });
+ };
+
+ const retPropsMinRatesSet = { 'CH': 12, 'AH': 8, 'default': 10 }; //
+
+ /**
+ * 业绩数据列
+ * ! 成行率: CH个人成行率<12%, AH个人成行率<8%
+ */
+ const dataFields = (suffix) => [
+ {
+ key: 'ConfirmRates' + suffix,
+ title: '成行率',
+ dataIndex: [suffix, 'ConfirmRates'],
+ width: '5em',
+ // CH个人成行率<12%, AH个人成行率<8%刷红
+ render: (val, r) => ({ props: { style: { color: val < (retPropsMinRatesSet?.[r?.retProps || 'default'] || retPropsMinRatesSet.default) ? 'red' : 'green' } }, children: val ? `${val}%` : '' }),
+ sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].ConfirmRates - b[suffix].ConfirmRates),
+ },
+ {
+ key: 'SumML' + suffix,
+ title: '业绩/万',
+ dataIndex: [suffix, 'SumML'],
+ width: '5em',
+ render: (_, r) => numberConvert10K(_),
+ sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].SumML - b[suffix].SumML),
+ },
+ {
+ key: 'ConfirmOrder' + suffix,
+ title: '团数',
+ dataIndex: [suffix, 'ConfirmOrder'],
+ width: '5em',
+ sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].ConfirmOrder - b[suffix].ConfirmOrder),
+ },
+ {
+ key: 'SumOrder' + suffix,
+ title: '订单数',
+ dataIndex: [suffix, 'SumOrder'],
+ width: '5em',
+ sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].SumOrder - b[suffix].SumOrder),
+ },
+ {
+ key: 'ResumeOrder' + suffix,
+ title: '老客户团数',
+ dataIndex: [suffix, 'ResumeOrder'],
+ width: '5em',
+ sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].ResumeOrder - b[suffix].ResumeOrder),
+ },
+ {
+ key: 'ResumeRates' + suffix,
+ title: '老客户成行率',
+ dataIndex: [suffix, 'ResumeRates'],
+ width: '5em',
+ render: (val, r) => ({ children: val ? `${val}%` : '' }),
+ sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].ResumeRates - b[suffix].ResumeRates),
+ },
+ ];
+
+ const dashboardTableProps = {
+ pagination: false,
+ size: 'small',
+ columns: [
+ {
+ title: '',
+ dataIndex: 'groupsLabel',
+ key: 'name',
+ width: '5em',
+ filterSearch: true,
+ filters: operatorObjects.sort((a, b) => a.text.localeCompare(b.text)),
+ onFilter: (value, record) => record.groupsKey === value || record.groupType === 'overview',
+ },
+ {
+ title: '前90 -30天',
+ key: 'date',
+ children: dataFields('result90'),
+ },
+ {
+ title: '前180 -50天',
+ key: 'department',
+ children: dataFields('result180'),
+ },
+ ],
+ rowClassName: (record, rowIndex) => {
+ return record.groupType === 'overview' ? 'ant-tag-blue' : '';
+ },
+ };
+
+ const columnConfig = {
+ xField: 'groupsLabel',
+ yField: 'SumML',
+ label: { position: 'top' },
+ };
+
+ const [clickColumn, setClickColumn] = useState({});
+ const [clickColumnTitle, setClickColumnTitle] = useState('');
+ const onChartItemClick = (colData) => {
+ // console.log('onChartItemClick', colData);
+ if (colData.groupType === 'operator') {
+ // test: 0
+ return false; // 单人趋势数据上的点击, 不做钻取
+ }
+ setClickColumn(colData);
+ setClickColumnTitle(moment(colData.groupDateVal).format('YYYY-MM'));
+ };
+ const chartsConfig = {
+ colFields: ['ConfirmOrder', 'SumOrder'],
+ lineFields: ['SumML', 'ConfirmRates'],
+ seriesField: null,
+ xField: 'groupDateVal',
+ itemClick: onChartItemClick,
+ };
+
+ useEffect(() => {
+ if (isEmpty(clickColumnTitle)) {
+ return () => {};
+ }
+ getDiagramData({
+ Date1: moment(clickColumn.groupDateVal).startOf('month').format(DATE_FORMAT),
+ Date2: moment(clickColumn.groupDateVal).endOf('month').format(SMALL_DATETIME_FORMAT),
+ groupType: 'operator', // test: overview
+ groupDateType: '',
+ });
+
+ return () => {};
+ }, [clickColumnTitle]);
+
+
+ return (
+ <>
+
+
+ {
+ SalesCRMDataStore.setSearchValues(obj, form);
+ pageRefresh(obj);
+ getFullYearDiagramData({ groupType: 'overview', ...obj });
+ }}
+ />
+
+
+
+
+
+ 每月数据
+
+ */}
+
+
+
+ {/* 小组每月; x轴: 日期; y轴: [订单数, ...] */}
+
+
+
+ 点击上方图表的柱状图, 查看当月 业绩数据: {clickColumnTitle}
+
+ {/* 显示小组的详情: 所有顾问? */}
+
+
+
+ {/* */}
+
+
+ {/* 月份×小组的详情; x轴: 顾问; y轴: [订单数, ...] */}
+ {/* */}
+
+ >
+ );
+});
diff --git a/src/views/OPRisk.jsx b/src/views/OPRisk.jsx
new file mode 100644
index 0000000..d966a24
--- /dev/null
+++ b/src/views/OPRisk.jsx
@@ -0,0 +1,69 @@
+import React, { useContext, useState } from 'react';
+import { observer } from 'mobx-react';
+import { stores_Context } from '../config';
+import moment from 'moment';
+import { SlackOutlined, SketchOutlined, AntCloudOutlined, RedditOutlined, GithubOutlined } from '@ant-design/icons';
+import { Row, Col, Table, Select, Space, Typography, Progress, Spin, Divider, Button, Switch } from 'antd';
+import SearchForm from './../components/search/SearchForm';
+
+export default observer((props) => {
+ const { sale_store, date_picker_store: searchFormStore } = useContext(stores_Context);
+
+ const { formValues, siderBroken } = searchFormStore;
+
+ const riskTableProps = {
+ loading: false,
+ // sticky: true,
+ // scroll: { x: 1000, y: 400 },
+ pagination: false,
+ columns: [
+ { title: '客人姓名', dataIndex: 'WebCode', key: 'WebCode' },
+ { title: '团号', dataIndex: 'o_id', key: 'o_id' },
+ { title: '表单内容', dataIndex: 'COLI_LineClass', key: 'COLI_LineClass' },
+ { title: '顾问', dataIndex: 'SourceType', key: 'SourceType' },
+ { title: '预定时间', dataIndex: 'applyDate', key: 'applyDate' },
+ ],
+ };
+ return (
+ <>
+
+
+ {
+ sale_store.setSearchValues(obj, form);
+ // pageRefresh(obj);
+ }}
+ />
+
+
+
+
+
+
+ >
+ );
+});