Merge branch 'feature/crm'

# Conflicts:
#	src/stores/Index.js
feature/hotel-cruise
Lei OT 4 months ago
commit 557e4ac104

@ -1,6 +1,6 @@
import './App.css';
import React, { useContext, useState } from 'react';
import {
import Icon, {
HomeOutlined,
TeamOutlined,
DashboardOutlined,
@ -50,6 +50,10 @@ import Meeting2025GH from './views/Meeting2025-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;
@ -105,6 +109,10 @@ const App = () => {
{ key: 52, label: <NavLink to="/sale_kpi">销售进度</NavLink> },
{ key: 'distribution', label: <NavLink to="/distribution">统计分布</NavLink> },
{ key: 'trade-pivot', label: <NavLink to="/trade/pivot">数据透视</NavLink> },
{ key: 'xx', type: 'divider' },
{ key: 'op_dashboard', label: <NavLink to="/op_dashboard">顾问-看板</NavLink> },
{ key: 'op_activity', label: <NavLink to="/op_activity">顾问-沟通</NavLink> },
{ key: 'op_risk', label: <NavLink to="/op_risk">顾问-提升</NavLink> },
],
},
{
@ -143,7 +151,7 @@ const App = () => {
{
key: 6,
label: '客服',
icon: <WechatOutlined />,
icon: <CooperationIcon/>,
children: [
{
key: 61,
@ -255,6 +263,9 @@ const App = () => {
<Route path="/sale_kpi" element={<Sale_KPI />} />
<Route path="/distribution" element={<Distribution />} />
<Route path="/:page/pivot" element={<DataPivot />} />
<Route path="/op_dashboard" element={<OPDashboard />} />
<Route path="/op_activity" element={<OPActivity />} />
<Route path="/op_risk" element={<OPRisk />} />
</Route>
</Routes>
</Content>

@ -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 <Column {...config} data={dataSource} />;
});

@ -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;

@ -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);

@ -17,6 +17,7 @@ import Distribution from "./Distribution";
import DataPivot from './DataPivot';
import MeetingData from './MeetingData2024';
import MeetingData2025 from './MeetingData2025';
import SalesCRMData from './SalesCRMData';
class Index {
constructor() {
this.dashboard_store = new DashboardStore(this);
@ -37,6 +38,7 @@ class Index {
this.DataPivotStore = new DataPivot(this);
this.MeetingDataStore = new MeetingData(this);
this.MeetingData2025Store = new MeetingData2025(this);
this.SalesCRMDataStore = new SalesCRMData(this);
makeAutoObservable(this);
}

@ -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;

@ -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
*/

@ -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' },

@ -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>&gt;24H回复</h3>
<Table {...riskTableProps} bordered />
</section>
<section>
<h3>首次报价周期&gt;48h</h3>
<Table {...riskTableProps} bordered />
</section>
<section>
<h3>报价次数&lt;1</h3>
<Table {...riskTableProps} bordered />
</section>
<section>
<h3>报价次数&lt;2</h3>
<Table {...riskTableProps} bordered />
</section>
</>
);
});
Loading…
Cancel
Save