feat: CRM-统计: 过程指标

main
Lei OT 4 months ago
parent cc3d0c19ad
commit e5ff51ae63

@ -53,7 +53,7 @@ import { stores_Context, APP_VERSION } from './config';
import { WaterMark } from '@ant-design/pro-components'; import { WaterMark } from '@ant-design/pro-components';
import CooperationIcon from './components/icons/CooperationIcon'; import CooperationIcon from './components/icons/CooperationIcon';
import OPDashboard from './views/OPDashboard'; import OPDashboard from './views/OPDashboard';
import OPActivity from './views/OPActivity'; import OPProcess from './views/OPProcess';
import OPRisk from './views/OPRisk'; import OPRisk from './views/OPRisk';
const App = () => { const App = () => {
@ -167,7 +167,7 @@ const App = () => {
children: [ children: [
// { key: 'xx', type: 'divider' }, // { key: 'xx', type: 'divider' },
{ key: 'op_dashboard', label: <NavLink to="/op_dashboard">结果</NavLink> }, { key: 'op_dashboard', label: <NavLink to="/op_dashboard">结果</NavLink> },
// { key: 'op_activity', label: <NavLink to="/op_activity"></NavLink> }, { key: 'op_process', label: <NavLink to="/op_process">过程</NavLink> },
// { key: 'op_risk', label: <NavLink to="/op_risk"></NavLink> }, // { key: 'op_risk', label: <NavLink to="/op_risk"></NavLink> },
], ],
}, },
@ -272,7 +272,7 @@ const App = () => {
<Route path="/distribution" element={<Distribution />} /> <Route path="/distribution" element={<Distribution />} />
<Route path="/:page/pivot" element={<DataPivot />} /> <Route path="/:page/pivot" element={<DataPivot />} />
<Route path="/op_dashboard" element={<OPDashboard />} /> <Route path="/op_dashboard" element={<OPDashboard />} />
<Route path="/op_activity" element={<OPActivity />} /> <Route path="/op_process" element={<OPProcess />} />
<Route path="/op_risk" element={<OPRisk />} /> <Route path="/op_risk" element={<OPRisk />} />
</Route> </Route>
</Routes> </Routes>

@ -19,6 +19,20 @@ const fetchResultsData = async (param) => {
return json.errcode === 0 ? json.result : []; return json.errcode === 0 ? json.result : [];
}; };
const fetchProcessData = async (param) => {
const defaultParam = {
WebCode: 'All',
DepartmentList: '',
opisn: -1,
Date1: '',
Date2: '',
groupType: '',
groupDateType: '',
};
const json = await fetchJSON('/service-Analyse2/sales_crm_process', { ...defaultParam, ...param });
return json.errcode === 0 ? json.result : [];
};
class SalesCRMData { class SalesCRMData {
constructor(appStore) { constructor(appStore) {
this.appStore = appStore; this.appStore = appStore;
@ -63,7 +77,6 @@ class SalesCRMData {
return this.results; return this.results;
} }
async getResultData(param = {}) { async getResultData(param = {}) {
const retKey = param.groupDateType === '' ? 'byOperator' : 'byDate'; const retKey = param.groupDateType === '' ? 'byOperator' : 'byDate';
this.results.loading = true; this.results.loading = true;
@ -76,8 +89,21 @@ class SalesCRMData {
return this.results; return this.results;
}; };
async getProcessData(param = {}) {
// const retKey = param.groupDateType === '' ? 'byOperator' : 'byDate';
const retKey = param.groupDateType === '' ? (param.groupType !== 'operator' ? 'dataSource' : 'details') : 'byDate';
this.process.loading = true;
this.process[retKey] = [];
const res = await fetchProcessData({ ...this.searchValues, ...param });
runInAction(() => {
this.process.loading = false;
this.process[retKey] = [].concat(this.results[retKey], res);
// this.process[retKey] =retKey === 'byOperator' ? res.filter(ele => ele.)
});
}
searchValues = { searchValues = {
// DateType: { key: 'confirmDate', label: '确认日期'}, DateType: { key: 'applyDate', label: '提交日期'},
WebCode: { key: 'All', label: '所有来源' }, WebCode: { key: 'All', label: '所有来源' },
// IncludeTickets: { key: '1', label: '含门票'}, // IncludeTickets: { key: '1', label: '含门票'},
DepartmentList: [groupsMappedByCode.GH], // test: GH DepartmentList: [groupsMappedByCode.GH], // test: GH
@ -89,10 +115,21 @@ class SalesCRMData {
setSearchValues(obj, values) { setSearchValues(obj, values) {
this.searchValues = { ...this.searchValues, ...values }; this.searchValues = { ...this.searchValues, ...values };
if (values.date) {
this.searchValues.date90 = {
Date1: (values.date.clone()).subtract(90, 'days').format(DATE_FORMAT),
Date2: (values.date.clone()).subtract(30, 'days').format(SMALL_DATETIME_FORMAT),
};
this.searchValues.date180 = {
Date1: (values.date.clone()).subtract(180, 'days').format(DATE_FORMAT),
Date2: (values.date.clone()).subtract(50, 'days').format(SMALL_DATETIME_FORMAT),
};
}
this.searchValuesToSub = obj; this.searchValuesToSub = obj;
} }
results = { loading: false, dataSource: [], details: [], byDate: [], byOperator: [] }; results = { loading: false, dataSource: [], details: [], byDate: [], byOperator: [] };
process = { loading: false, dataSource: [], details: [], byDate: [], byOperator: [] };
resetData = () => { resetData = () => {
this.results.loading = false; this.results.loading = false;
for (const key of Object.keys(this.results)) { for (const key of Object.keys(this.results)) {
@ -100,6 +137,11 @@ class SalesCRMData {
this.results[key] = []; this.results[key] = [];
} }
} }
for (const key of Object.keys(this.process)) {
if (key !== 'loading') {
this.process[key] = [];
}
}
}; };
} }
export default SalesCRMData; export default SalesCRMData;

@ -1,85 +0,0 @@
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,186 @@
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';
import { cloneDeep, fixTo2Decimals, sortBy, isEmpty, pick } from '../utils/commons';
const COLOR_SETS = [
"#FFFFFF",
// "#5B8FF9",
// "#FF6B3B",
"#9FB40F",
"#76523B",
"#DAD5B5",
"#E19348",
"#F383A2",
];
const transparentHex = '1A';
export default observer((props) => {
const { SalesCRMDataStore, date_picker_store: searchFormStore } = useContext(stores_Context);
// const { formValues, siderBroken } = searchFormStore;
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const _formValuesToSub = pick(formValuesToSub, ['DepartmentList', 'WebCode']);
const { resetData, process } = SalesCRMDataStore;
const operatorObjects = process.details.map((v) => ({ key: v.groupsKey, value: v.groupsKey, label: v.groupsLabel, text: v.groupsLabel }));
const pageRefresh = async (obj) => {
resetData();
Promise.allSettled([
SalesCRMDataStore.getProcessData({
...(obj || _formValuesToSub),
// ...subParam,
// groupType: 'overview',
groupType: 'dept',
groupDateType: '',
}),
SalesCRMDataStore.getProcessData({
...(obj || _formValuesToSub),
// ...subParam,
groupType: 'operator',
groupDateType: '',
}),
]);
};
const tableSorter = (a, b, colName) => (a.groupType !== 'operator' ? -1 : b.groupType !== 'operator' ? 0 : a[colName] - b[colName]);
const percentageRender = val => val ? `${val}%` : '';
const activityTableProps = {
rowKey: 'groupsKey',
pagination: { pageSize: 10, showSizeChanger: false },
size: 'small',
rowClassName: (record, rowIndex) => {
return record.groupType === 'operator' ? '': 'ant-tag-blue';
},
columns: [
{ title: '', dataIndex: 'groupsLabel', key: 'groupsLabel', width: '6rem',
filterSearch: true,
filters: operatorObjects.sort((a, b) => a.text.localeCompare(b.text)),
onFilter: (value, record) => record.groupsKey === value || record.groupType !== 'operator',
}, // :
{
title: '顾问动作',
key: 'date',
children: [
{ title: '首次响应率24H', dataIndex: 'firstTouch24', key: 'firstTouch24', render: percentageRender,
sorter: (a, b) => tableSorter(a, b, 'firstTouch24'),
},
{ title: '48H内报价率', dataIndex: 'firstQuote48', key: 'firstQuote48',render: percentageRender ,
sorter: (a, b) => tableSorter(a, b, 'firstQuote48'), },
{ title: '一次报价率', dataIndex: 'quote1', key: 'quote1',render: percentageRender ,
sorter: (a, b) => tableSorter(a, b, 'quote1'), },
{ title: '二次报价率', dataIndex: 'quote2', key: 'quote2',render: percentageRender ,
sorter: (a, b) => tableSorter(a, b, 'quote2'), },
{ title: '>50条会话', dataIndex: 'turnsGT50', key: 'turnsGT50', render: percentageRender ,
sorter: (a, b) => tableSorter(a, b, 'turnsGT50'), },
{ title: '违规数', dataIndex: 'violations', key: 'violations',
sorter: (a, b) => tableSorter(a, b, 'violations'), },
],
},
{
title: '客人回复',
key: 'department',
children: [
{
title: '首次回复率24H', dataIndex: 'firstReply24', key: 'firstReply24',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1]+transparentHex } },
children: percentageRender(_),
}),
sorter: (a, b) => tableSorter(a, b, 'firstReply24'), },
{
title: '48H内报价回复率', dataIndex: 'replyQuote48', key: 'replyQuote48',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1]+transparentHex } },
children: percentageRender(_),
}),
sorter: (a, b) => tableSorter(a, b, 'replyQuote48'), },
{
title: '一次报价回复率', dataIndex: 'replyQuote1', key: 'replyQuote1',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1]+transparentHex } },
children: percentageRender(_),
}),
sorter: (a, b) => tableSorter(a, b, 'replyQuote1'), },
{
title: '二次报价回复率', dataIndex: 'replyQuote2', key: 'replyQuote2',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1]+transparentHex } },
children: percentageRender(_),
}),
sorter: (a, b) => tableSorter(a, b, 'replyQuote2'), },
],
},
],
};
const riskTableProps = {
rowKey: 'groupsKey',
pagination: { pageSize: 10, showSizeChanger: false },
size: 'small',
rowClassName: (record, rowIndex) => {
return record.groupType === 'operator' ? '': 'ant-tag-blue';
},
columns: [
{ title: '', dataIndex: 'groupsLabel', key: 'groupsLabel', width: '6rem',
filterSearch: true,
filters: operatorObjects.sort((a, b) => a.text.localeCompare(b.text)),
onFilter: (value, record) => record.groupsKey === value || record.groupType !== 'operator',
// render: (text, record) => { // todo:
// },
}, // :
{ title: '>24H回复', dataIndex: 'lostTouch24', key: 'lostTouch24',
sorter: (a, b) => tableSorter(a, b, 'lostTouch24'),
},
{ title: '首次报价周期>48h', dataIndex: 'lostQuote48', key: 'lostQuote48',
sorter: (a, b) => tableSorter(a, b, 'lostQuote48'),
},
{ title: '报价次数<1次', dataIndex: 'lostQuote1', key: 'lostQuote1',
sorter: (a, b) => tableSorter(a, b, 'lostQuote1'),
},
{ title: '报价次数<2次', dataIndex: 'lostQuote2', key: 'lostQuote2',
sorter: (a, b) => tableSorter(a, b, 'lostQuote2'),
},
],
};
return (
<>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col md={24} lg={24} xxl={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
...SalesCRMDataStore.searchValues,
},
shows: ['DepartmentList', 'WebCode', 'DateType', '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) => {
SalesCRMDataStore.setSearchValues(obj, form);
pageRefresh(obj);
}}
/>
</Col>
</Row>
<section>
<Table {...activityTableProps} bordered dataSource={[...process.dataSource, ...process.details]} loading={process.loading} sticky />
</section>
<section>
<h3>未成行订单 数量</h3>
<Table {...riskTableProps} bordered dataSource={[...process.dataSource, ...process.details]} loading={process.loading} sticky />
</section>
</>
);
});
Loading…
Cancel
Save