diff --git a/src/App.jsx b/src/App.jsx index 9e22aee..ff950f8 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, @@ -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: 销售进度 }, { key: 'distribution', label: 统计分布 }, { key: 'trade-pivot', label: 数据透视 }, + { key: 'xx', type: 'divider' }, + { key: 'op_dashboard', label: 顾问-看板 }, + { key: 'op_activity', label: 顾问-沟通 }, + { key: 'op_risk', label: 顾问-提升 }, ], }, { @@ -143,7 +151,7 @@ const App = () => { { key: 6, label: '客服', - icon: , + icon: , children: [ { key: 61, @@ -255,6 +263,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 e821323..5f878c5 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 0579adc..1e303e0 100644 --- a/src/stores/Index.js +++ b/src/stores/Index.js @@ -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); } 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..1fb9cd9 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' }, 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); + }} + /> + + +
+

>24H回复

+
+ +
+

首次报价周期>48h

+
+ +
+

报价次数<1

+
+ +
+

报价次数<2

+
+ + + ); +});