feat: CRM-统计数据
parent
b2ce88e8aa
commit
2be734a591
@ -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 <Mix {...MixConfig} />;
|
||||||
|
});
|
@ -0,0 +1,9 @@
|
|||||||
|
import Icon, {} from '@ant-design/icons';
|
||||||
|
|
||||||
|
const CooperationSvg = () => (
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11.8611 2.39057C12.8495 1.73163 14.1336 1.71797 15.1358 2.35573L19.291 4.99994H20.9998C21.5521 4.99994 21.9998 5.44766 21.9998 5.99994V14.9999C21.9998 15.5522 21.5521 15.9999 20.9998 15.9999H19.4801C19.5396 16.9472 19.0933 17.9102 18.1955 18.4489L13.1021 21.505C12.4591 21.8907 11.6609 21.8817 11.0314 21.4974C10.3311 22.1167 9.2531 22.1849 8.47104 21.5704L3.33028 17.5312C2.56387 16.9291 2.37006 15.9003 2.76579 15.0847C2.28248 14.7057 2 14.1254 2 13.5109V6C2 5.44772 2.44772 5 3 5H7.94693L11.8611 2.39057ZM4.17264 13.6452L4.86467 13.0397C6.09488 11.9632 7.96042 12.0698 9.06001 13.2794L11.7622 16.2518C12.6317 17.2083 12.7903 18.6135 12.1579 19.739L17.1665 16.7339C17.4479 16.5651 17.5497 16.2276 17.4448 15.9433L13.0177 9.74551C12.769 9.39736 12.3264 9.24598 11.9166 9.36892L9.43135 10.1145C8.37425 10.4316 7.22838 10.1427 6.44799 9.36235L6.15522 9.06958C5.58721 8.50157 5.44032 7.69318 5.67935 7H4V13.5109L4.17264 13.6452ZM14.0621 4.04306C13.728 3.83047 13.3 3.83502 12.9705 4.05467L7.56943 7.65537L7.8622 7.94814C8.12233 8.20827 8.50429 8.30456 8.85666 8.19885L11.3419 7.45327C12.5713 7.08445 13.8992 7.53859 14.6452 8.58303L18.5144 13.9999H19.9998V6.99994H19.291C18.9106 6.99994 18.5381 6.89148 18.2172 6.68727L14.0621 4.04306ZM6.18168 14.5448L4.56593 15.9586L9.70669 19.9978L10.4106 18.7659C10.6256 18.3897 10.5738 17.9178 10.2823 17.5971L7.58013 14.6247C7.2136 14.2215 6.59175 14.186 6.18168 14.5448Z"></path></svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const CooperationIcon = (props) => <Icon component={CooperationSvg} {...props} />;
|
||||||
|
|
||||||
|
export default CooperationIcon;
|
@ -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;
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
|
||||||
|
<Col md={24} lg={24} xxl={24}>
|
||||||
|
<SearchForm
|
||||||
|
defaultValue={{
|
||||||
|
initialValue: {
|
||||||
|
...formValues,
|
||||||
|
...sale_store.searchValues,
|
||||||
|
},
|
||||||
|
shows: ['DepartmentList', 'WebCode', 'dates'],
|
||||||
|
fieldProps: {
|
||||||
|
DepartmentList: { show_all: false, mode: 'multiple', col: 5 },
|
||||||
|
WebCode: { show_all: false, mode: 'multiple', col: 5 },
|
||||||
|
dates: { hide_vs: true },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onSubmit={(_err, obj, form, str) => {
|
||||||
|
sale_store.setSearchValues(obj, form);
|
||||||
|
// pageRefresh(obj);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<section>
|
||||||
|
<Table {...activityTableProps} bordered />
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>未成行订单</h3>
|
||||||
|
<Table {...riskTableProps} bordered />
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
|
||||||
|
<Col md={24} lg={24} xxl={24}>
|
||||||
|
<SearchForm
|
||||||
|
defaultValue={{
|
||||||
|
initialValue: {
|
||||||
|
...formValues,
|
||||||
|
...SalesCRMDataStore.searchValues,
|
||||||
|
},
|
||||||
|
shows: ['DepartmentList', 'WebCode'],
|
||||||
|
fieldProps: {
|
||||||
|
DepartmentList: { show_all: false, mode: 'multiple', col: 5 },
|
||||||
|
WebCode: { show_all: false, mode: 'multiple', col: 5 },
|
||||||
|
dates: { hide_vs: true },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onSubmit={(_err, obj, form, str) => {
|
||||||
|
SalesCRMDataStore.setSearchValues(obj, form);
|
||||||
|
pageRefresh(obj);
|
||||||
|
getFullYearDiagramData({ groupType: 'overview', ...obj });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<section>
|
||||||
|
<Table {...dashboardTableProps} bordered dataSource={[...results.dataSource, ...results.details]} loading={results.loading} sticky />
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col flex={'12em'}><h3>每月数据</h3></Col>
|
||||||
|
<Col flex={'auto'}>
|
||||||
|
<Select
|
||||||
|
labelInValue
|
||||||
|
// mode={'multiple'}
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
placeholder={'选择顾问'}
|
||||||
|
// onChange={sale_store.setPickSales}
|
||||||
|
// onSelect={}
|
||||||
|
onChange={(labelInValue) => labelInValue ? getFullYearDiagramData({ groupType: 'operator', opisn: labelInValue.value, }) : false}
|
||||||
|
onClear={() => getFullYearDiagramData({ groupType: 'overview', opisn: -1, })}
|
||||||
|
// value={sale_store.salesTrade.pickSales}
|
||||||
|
// maxTagCount={1}
|
||||||
|
// maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`}
|
||||||
|
allowClear={true}
|
||||||
|
options={operatorObjects}
|
||||||
|
/>
|
||||||
|
{/* {operatorObjects.map((ele) => (
|
||||||
|
<Select.Option key={ele.key} value={ele.value}>
|
||||||
|
{ele.label}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select> */}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Spin spinning={results.loading}>
|
||||||
|
{/* 小组每月; x轴: 日期; y轴: [订单数, ...] */}
|
||||||
|
<MixFieldsDetail {...chartsConfig} dataSource={results.byDate} />
|
||||||
|
</Spin>
|
||||||
|
<h3>
|
||||||
|
点击上方图表的柱状图, 查看当月 <Tag color='orange'>业绩</Tag>数据: <Tag color='orange'>{clickColumnTitle}</Tag>
|
||||||
|
</h3>
|
||||||
|
{/* 显示小组的详情: 所有顾问? */}
|
||||||
|
<Spin spinning={results.loading}>
|
||||||
|
<Column {...columnConfig} dataSource={results.byOperator} />
|
||||||
|
</Spin>
|
||||||
|
{/* <Table columns={[{ title: '', dataIndex: 'date', key: 'date', width: '5em' }, ...dataFields]} bordered size='small' /> */}
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
{/* 月份×小组的详情; x轴: 顾问; y轴: [订单数, ...] */}
|
||||||
|
{/* <MixFieldsDetail {...chartsConfig} xField={'label'} dataSource={[]} /> */}
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
|
||||||
|
<Col md={24} lg={24} xxl={24}>
|
||||||
|
<SearchForm
|
||||||
|
defaultValue={{
|
||||||
|
initialValue: {
|
||||||
|
...formValues,
|
||||||
|
...sale_store.searchValues,
|
||||||
|
},
|
||||||
|
shows: ['DepartmentList', 'WebCode', 'dates'],
|
||||||
|
fieldProps: {
|
||||||
|
DepartmentList: { show_all: false, mode: 'multiple', col: 5 },
|
||||||
|
WebCode: { show_all: false, mode: 'multiple', col: 5 },
|
||||||
|
dates: { hide_vs: true },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onSubmit={(_err, obj, form, str) => {
|
||||||
|
sale_store.setSearchValues(obj, form);
|
||||||
|
// pageRefresh(obj);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<section>
|
||||||
|
<h3>>24H回复</h3>
|
||||||
|
<Table {...riskTableProps} bordered />
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>首次报价周期>48h</h3>
|
||||||
|
<Table {...riskTableProps} bordered />
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>报价次数<1</h3>
|
||||||
|
<Table {...riskTableProps} bordered />
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>报价次数<2</h3>
|
||||||
|
<Table {...riskTableProps} bordered />
|
||||||
|
</section>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
Loading…
Reference in New Issue