Compare commits

...

58 Commits

Author SHA1 Message Date
Lei OT 021fb429ce perf: 销售进度: 表格优化 4 days ago
ZJYHX a9f45e334d perf:客运-老客户:订单数占比、毛利占比值保留小数 1 month ago
ZJYHX 488b33ae50 perf:客运-老客户:增加订单数占比、毛利占比 1 month ago
Lei OT 6dbf9cfb56 2.11.6 1 month ago
ZJYHX 108becd2cd perf:客服-目的地:增加国籍统计 1 month ago
Lei OT 73e1dcdb4c Revert "perf: 数据透视: + 预定的出发日期"
This reverts commit 16b08311fa.
1 month ago
Lei OT baae505831 perf: 老客户-分析: 移动端显示 1 month ago
Lei OT 16b08311fa perf: 数据透视: + 预定的出发日期 1 month ago
Lei OT e0e7c3c84f 2.11.5 2 months ago
ZJYHX 87f24c40fa fix:东道主项目:加载动画 2 months ago
ZJYHX bed6b81753 feat:东道主项目:单团明细 2 months ago
ZJYHX 171dd7514a fix:错误id命名 2 months ago
Lei OT a0ea6507a3 feat: 老客户-分析 2 months ago
Lei OT 40cd81f9a7 2.11.4 2 months ago
Lei OT 876216458e perf: 数据透视: 增加日期 2 months ago
Lei OT 8a0f919a42 fix: 2 months ago
Lei OT 020c9fc3e9 Merge remote-tracking branch 'origin/main' 2 months ago
Lei OT b2e539a4d2 perf: 导出表格: render 显示不正确; 酒店/三峡导出 2 months ago
ZJYHX fee1185406 feat:东道主项目页 2 months ago
Lei OT e297d36256 perf: 老客户: 默认参数 2 months ago
Lei OT 667574ec34 perf: 销售-老客户: 默认参数 2 months ago
Lei OT 185db1ec80 2.11.3 2 months ago
Lei OT 6dc82a0a01 perf: 销售-老客户: + 对比 2 months ago
Lei OT 4724b13a2d fix: 客服-目的地接团: 明细 2 months ago
Lei OT f532f138ff perf: 三峡,酒店: 合计 2 months ago
Lei OT 3b51ae8d08 2.11.2 2 months ago
Lei OT a6f7884e6e perf: 三峡: 常用的地接社列表; fix: 地接社 参数 2 months ago
Lei OT 928e27913d perf: 三峡: 人数 2 months ago
Lei OT 94363df0fc 2.11.1 2 months ago
Lei OT 621d3a2446 Merge remote-tracking branch 'origin/main' 2 months ago
ZJYHX 8f12fc8747 fix:老客户:修复数据量大时加载状态不提前消失 2 months ago
Lei OT 397c886aea perf: 三峡: 预订类型 2 months ago
ZJYHX 459db55453 fix:老客户:修复数据量大时加载状态不提前消失 2 months ago
ZJYHX 6317c24a75 perf:老客户:添加总计对比折线;修改折线颜色 2 months ago
Lei OT c9e13385dc 2.11.0 2 months ago
Lei OT a02fa37f17 fear: CRM统计: 个人看板; 违规明细 2 months ago
Lei OT ccbd0be1cd Merge remote-tracking branch 'origin/main' 2 months ago
Lei OT c7d3cf5746 fix: 三峡游船, 酒店 参数 2 months ago
ZJYHX 23b1d2c81b fix:老客户:切换页面折线颜色变换 2 months ago
Lei OT 7c08e8bfe6 Merge branch 'feature/hotel-cruise' 2 months ago
Lei OT 1e46cf6e63 feat: 客服: 三峡游船: 人数 2 months ago
Lei OT 17cedf14d9 feat: 客服: 酒店 2 months ago
ZJYHX 2fae92fa2a perf:老客户:增加折线虚实颜色 2 months ago
Lei OT 08aa01e33c feat: 客服: 三峡游船 2 months ago
ZJYHX 17a85122b1 fix:老客户:折线显示 2 months ago
ZJYHX 1b86ab1192 perf:老客户:增加对比折线 2 months ago
ZJYHX be31c3983d perf:老客户:添加数据对比、对比折线 2 months ago
ZJYHX 5551b2e362 perf:客服:添加目的地接数据对比 3 months ago
Lei OT 7cfd79a0d2 Merge branch 'main' into feature/hotel-cruise 3 months ago
Lei OT dcb185be5a Merge remote-tracking branch 'origin/main' 3 months ago
ZJYHX c3547538fd perf:客服:增加对比 3 months ago
Lei OT cb598ecba1 perf: CRM统计: tips 3 months ago
Lei OT a32943fffb Merge branch 'main' into feature/hotel-cruise
# Conflicts:
#	src/stores/CustomerServices.js
3 months ago
Lei OT a7b24d206d perf: 过程指标: tips 3 months ago
Lei OT f8eef38944 站点来源: + JH 3 months ago
Lei OT 89d151f388 2.10.7 3 months ago
Lei OT 6ea43a58e2 perf: 老客户: 订单走势 3 months ago
Lei OT d1ee551a4e todo: 三峡, 酒店 4 months ago

4
package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "haina-dashboard",
"version": "2.10.6",
"version": "2.11.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "haina-dashboard",
"version": "2.10.6",
"version": "2.11.6",
"dependencies": {
"@ant-design/charts": "^1.4.2",
"@ant-design/pro-components": "^2.6.16",

@ -1,6 +1,6 @@
{
"name": "haina-dashboard",
"version": "2.10.6",
"version": "2.11.6",
"private": true,
"dependencies": {
"@ant-design/charts": "^1.4.2",

@ -52,9 +52,12 @@ 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 OPProcess from './views/OPProcess';
import OPRisk from './views/OPRisk';
import OPDashboard from './views/sales-crm/Dashboard';
import OPProcess from './views/sales-crm/Process';
import OPRisk from './views/sales-crm/Risk';
import Cruise from './views/Cruise';
import Hotel from './views/Hotel';
import HostCaseCount from './views/HostCaseCount';
const App = () => {
const { Content, Footer, Sider, } = Layout;
@ -125,7 +128,7 @@ const App = () => {
key: 32,
label: <NavLink to="/customer_care_regular">老客户</NavLink>,
},
{ key: 'customer_care_regular_sales', label: <NavLink to="/customer_care_regular_sales">销售-老客户</NavLink> },
{ key: 'customer_care_regular_sales', label: <NavLink to="/customer_care_regular_sales">老客户-分析</NavLink> },
{
key: 33,
label: <NavLink to="/customer_care_inchina">在华客户</NavLink>,
@ -158,6 +161,9 @@ const App = () => {
key: 62,
label: <NavLink to="/destination/group/count">目的地接团</NavLink>,
},
{ key: 'cruise', label: <NavLink to="/cruise">三峡游船</NavLink> },
{ key: 'hotel', label: <NavLink to="/hotel">酒店</NavLink> },
{ key: 'hostcase', label: <NavLink to="/hostcase/count">东道主项目</NavLink> },
],
},
{
@ -259,7 +265,10 @@ const App = () => {
<Route path="/destination/group/count" element={<DestinationGroupCount />} />
<Route path="/agent/:agentId/group/list" element={<AgentGroupList />} />
<Route path="/destination/:destinationId/group/list" element={<DestinationGroupList />} />
</Route>
<Route path="/cruise" element={<Cruise />} />
<Route path="/hotel" element={<Hotel />} />
<Route path="/hostcase/count" element={<HostCaseCount />} />
</Route>
<Route element={<ProtectedRoute auth={['admin', 'director_bu', 'financial']} />}>
<Route path="/credit_card_bill" element={<Credit_card_bill />} />
<Route path="/exchange_rate" element={<ExchangeRate />} />
@ -274,6 +283,7 @@ const App = () => {
<Route path="/op_dashboard" element={<OPDashboard />} />
<Route path="/op_process" element={<OPProcess />} />
<Route path="/op_risk" element={<OPRisk />} />
<Route path="/op_risk/sales/:opisn" element={<OPRisk />} />
</Route>
</Routes>
</Content>

@ -1,16 +1,18 @@
import React, { useContext, useEffect } from 'react';
import React, { useContext, useState } from 'react';
import { Row, Col, Divider, Table, Tooltip } from 'antd';
import { InfoCircleOutlined } from '@ant-design/icons';
import { utils, writeFileXLSX } from 'xlsx';
import { stores_Context } from '../config';
import { observer } from 'mobx-react';
import SearchForm from './../components/search/SearchForm';
import LineWithAvg from '../components/LineWithAvg';
import { flow } from 'mobx';
const Customer_care_regular = () => {
const { orders_store, date_picker_store, customer_store } = useContext(stores_Context);
const regular_data = customer_store.regular_data;
useEffect(() => {}, []);
// useEffect(() => {}, []);
return (
<div>
@ -26,13 +28,26 @@ const Customer_care_regular = () => {
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
dates: { hide_vs: true },
years: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
onSubmit={async (_err, obj, form, str) => {
customer_store.setSearchValues(obj, form, 'regular_data');
customer_store.regular_customer_order();
customer_store.regular_customer_order(true);
regular_data.data_compare=[];
if (obj.DateDiff1 && obj.DateDiff2){
regular_data.isCompareLine=true;
regular_data.showCompareSum=true;
await customer_store.regular_customer_order();
customer_store.regular_customer_order(false,true);
customer_store.regular_customer_order(true,false,true);
customer_store.regular_customer_order(true,true,true);
}
else{
regular_data.isCompareLine=false;
regular_data.showCompareSum=false;
customer_store.regular_customer_order();
customer_store.regular_customer_order(true);
}
}}
/>
</Col>
@ -66,11 +81,17 @@ const Customer_care_regular = () => {
<>
<span>{text}</span>&nbsp;&nbsp;
{<Tooltip key='total_data_tips' title={regular_data.total_data_tips}>
{index === 0 && regular_data.total_data_tips!=='' && <InfoCircleOutlined />}
{index === 0 && regular_data.total_data_tips!=='' && <InfoCircleOutlined className='ant-tag-gold' />}
</Tooltip>}
</>
),
},
{
title: '订单数占比',
dataIndex: 'OrderRate',
key: 'OrderRate',
render: (text) => typeof text === 'number'?<span>{parseFloat((text * 100).toFixed(2))}%</span>:text,
},
{
title: '成行数',
dataIndex: 'SUCOrderNum',
@ -80,13 +101,19 @@ const Customer_care_regular = () => {
title: '成行率',
dataIndex: 'SUCRate',
key: 'SUCRate',
render: (text, record) => <span>{Math.round(text * 100)}%</span>,
render: (text) => typeof text === 'number'?<span>{Math.round(text * 100)}%</span>:text,
},
{
title: '毛利',
dataIndex: 'ML',
key: 'ML',
},
{
title: '毛利占比',
dataIndex: 'OrderMLRate',
key: 'OrderMLRate',
render: (text) => typeof text === 'number'?<span>{parseFloat((text * 100).toFixed(2))}%</span>:text,
},
{
title: '人数(含成人+儿童)',
dataIndex: 'PersonNum',
@ -98,6 +125,13 @@ const Customer_care_regular = () => {
rowKey={(record) => record.ItemName}
/>
</Col>
<Col span={24}>
<LineWithAvg dataSource={regular_data.pivotData} loading={regular_data.detail_loading} xField={regular_data.pivotX} yField={regular_data.pivotY}
seriesField='_ylabel' showCompareSum={regular_data.showCompareSum} solidLineTime={regular_data.solidLineTime} solidLineCompareTime={regular_data.solidLineCompareTime}
solidLineDash={regular_data.solidLineDash} isCompareLine={regular_data.isCompareLine}/>
</Col>
<Col span={24}>
<Divider orientation="right" plain>
<a
@ -112,7 +146,7 @@ const Customer_care_regular = () => {
<Table
id="table_to_xlsx"
pagination={false}
loading={regular_data.loading}
loading={regular_data.detail_loading}
dataSource={regular_data.data_detail}
scroll={{ x: 1200 }}
columns={[
@ -127,7 +161,7 @@ const Customer_care_regular = () => {
key: 'COLI_ApplyDate',
},
{
title: '订单状态',
title: '订单状态',width: '4rem',
dataIndex: 'OrderState',
key: 'OrderState',
render: (text, record) => <span>{text == 1 ? '成行' : '未成行'}</span>,
@ -188,17 +222,17 @@ const Customer_care_regular = () => {
dataIndex: 'COLI_LineClass',
key: 'COLI_LineClass',
},
{ title: '上次 订单号', dataIndex: 'coli_id_Last', key: 'coli_id_Last', width: '2em',
{ title: '上次 订单号', dataIndex: 'coli_id_Last', key: 'coli_id_Last', width: '4em',
render: (_, r) => ({
props: { style: { backgroundColor: '#5B8FF9'+'1A' } },
children: _,
}), },
{ title: '上次 走团日期', dataIndex: 'COLI_OrderStartDate_Last', key: 'COLI_OrderStartDate_Last',width: '2em',
{ title: '上次 走团日期', dataIndex: 'COLI_OrderStartDate_Last', key: 'COLI_OrderStartDate_Last',width: '4em',
render: (_, r) => ({
props: { style: { backgroundColor: '#5B8FF9'+'1A' } },
children: _,
}), },
{ title: '上次 小组', dataIndex: 'Department_Last', key: 'Department_Last',width: '2em',
{ title: '上次 小组', dataIndex: 'Department_Last', key: 'Department_Last',width: '4em',
render: (_, r) => ({
props: { style: { backgroundColor: '#5B8FF9'+'1A' } },
children: _,

@ -30,21 +30,25 @@ export const VSTag = (props) => {
/**
* 导出表格数据存为xlsx
* @property label 文件名字
* @property columns 表格列
* @property dataSource 表格数据
* @property btnTxt 按钮文字
*/
export const TableExportBtn = (props) => {
const output_name = `${props.label}`;
export const TableExportBtn = ({label, columns, dataSource, btnTxt, ...props}) => {
const output_name = `${label}`;
const [columnsMap, setColumnsMap] = useState([]);
const [summaryRow, setSummaryRow] = useState({});
useEffect(() => {
const r1 = props.columns.reduce((r, v) => ({
const r1 = columns.reduce((r, v) => ({
...r,
...(v.children ? v.children.reduce((rc, vc, ci) => ({
...rc,
...(vc?.titleX ? {[`${v?.titleX || v.title},${vc.titleX}`]: vc.titleX } : {[(v?.titleX || v.title) + (ci || '')]: `${vc?.titleX || vc?.title || ''}`}),
...(vc?.titleX ? {[`${v?.titleX || v.title},${vc.titleX}`]: vc.titleX } : {[(v?.titleX || v.title) + (vc.key || ci || '')]: `${vc?.titleX || vc?.title || ''}`}),
}), {}) : {})
}), {});
const flatCols = props.columns.flatMap((v, k) =>
v.children ? v.children.map((vc, ci) => ({ ...vc, title: `${v?.titleX || v.title}` + (vc?.titleX ? `,${vc.titleX}` : (ci || '')) })) : {...v, title: `${v?.titleX || v.title}`}
const flatCols = columns.flatMap((v, k) =>
v.children ? v.children.map((vc, ci) => ({ ...vc, title: `${v?.titleX || v.title}` + (vc?.titleX ? `,${vc.titleX}` : (vc.key || ci || '')) })) : {...v, title: `${v?.titleX || v.title}`}
);
// .filter((c) => c.dataIndex)
// !['string', 'number'].includes(typeof vc.title) ? `${v?.titleX || v.title}` : `${v?.titleX || v.title}-${vc.title || ''}`
@ -56,20 +60,21 @@ export const TableExportBtn = (props) => {
// console.log('summaryRow', r1);
return () => {};
}, [props.columns]);
}, [columns]);
const onExport = () => {
if (isEmpty(props.dataSource)) {
if (isEmpty(dataSource)) {
message.warning('无结果.');
return false;
}
const data = props.dataSource.map((item) => {
const data = dataSource.map((item) => {
const itemMapped = columnsMap.reduce((sv, kset) => {
const export_val = typeof kset?.dataExport === 'function' ? kset.dataExport('', item) : null;
const render_val = typeof kset?.render === 'function' ? kset.render('', item) : null;
const data_val = kset?.dataIndex ? (Array.isArray(kset.dataIndex) ? getNestedValue(item, kset.dataIndex) : item[kset.dataIndex]) : undefined;
const x_val = item[`${kset.dataIndex}_X`];
// const _title = kset.title.replace('-[object Object]', '');
const v = { [kset.title]: x_val || data_val || render_val };
const v = { [kset.title]: x_val || export_val || data_val || render_val };
return { ...sv, ...v };
}, {});
return itemMapped;
@ -88,7 +93,7 @@ export const TableExportBtn = (props) => {
disabled={false}
onClick={onExport}
>
{props.btnTxt || '导出excel'}
{btnTxt || '导出excel'}
</Button>
);
};

@ -0,0 +1,240 @@
import React, { useEffect, useState } from 'react';
import { Row, Col, Spin } from 'antd';
import { Line } from '@ant-design/plots';
import { observer } from 'mobx-react';
import { dataFieldAlias } from '../libs/ht';
import DateGroupRadio from '../components/DateGroupRadio';
import { cloneDeep, groupBy, sortBy } from '../utils/commons';
export default observer((props) => {
const { dataSource: rawData, showAVG, showCompareSum, loading, solidLineTime,
solidLineDash, isCompareLine,solidLineCompareTime, ...config } = props;
const { xField, yField, yFieldAlias, seriesField } = config;
const [dataBeforeXChange, setDataBeforeXChange] = useState([]);
const [dataSource, setDataSource] = useState([]);
const [sumSeries, setSumSeries] = useState([]);
const line_config = {
// data: dataSource,
padding: 'auto',
xField,
yField,
seriesField,
// seriesField: 'rowLabel',
// xAxis: {
// type: 'timeCat',
// },
color: isCompareLine?['#17f485', '#1890ff','#17f485', '#1890ff',"#181414","#181414"]:undefined,
point: {
size: 4,
shape: "cicle",
},
lineStyle: (datum) => {
return {
stroke: isCompareLine?datum._ylabel.includes("总计")?"#181414":datum._ylabel.includes(solidLineDash) ? '#1890ff':'#17f485' : undefined, //
lineDash: isCompareLine?datum._ylabel.includes(solidLineTime) ? undefined:[4, 4] : undefined, // 线
};
},
yAxis: {
min: 0,
maxTickInterval: 5,
},
meta: {
...cloneDeep(dataFieldAlias),
},
// smooth: true,
label: {}, //
legend: {
position: 'right-top',
// title: {
// text: ' ' + dataSource.reduce((a, b) => a + b.SumOrder, 0),
// },
itemMarginBottom: 12, //
},
tooltip: {
customItems: (originalItems) => {
return originalItems
.map((ele) => ({ ...ele, valueR: ele.data[yField] }))
.sort(sortBy('valueR'))
.reverse();
},
},
};
const [lineConfig, setLineConfig] = useState(cloneDeep(line_config));
useEffect(() => {
resetX();
return () => {};
}, [rawData]);
useEffect(() => {
setLineConfig(cloneDeep(line_config));
return () => {};
}, [isCompareLine,solidLineTime]);
useEffect(() => {
if (lineChartX === 'day') {
setDataBeforeXChange(dataSource);
}
return () => {};
}, [dataSource]);
//
const [lineChartX, setLineChartX] = useState('day');
const orderCountDataMapper = { data1: 'data1', data2: undefined };
const orderCountDataFieldMapper = { 'dateKey': xField, 'valueKey': yField, 'seriesKey': seriesField, _f: 'sum' };
const resetX = () => {
setLineChartX('day');
setDataSource(rawData);
setDataBeforeXChange(rawData);
// ``线, ``线
const byDays = groupBy(rawData, xField);
const sumY = rawData.reduce((a, b) => a + b[yField], 0);
// const avgVal = Math.round(sumY / (Object.keys(byDays).length));
// const avgLine = [
// { type: 'text', position: ['start', avgVal], content: avgVal, offsetX: -15, style: { fill: '#F4664A', textBaseline: 'bottom' } },
// { type: 'line', start: [-10, avgVal], end: ['max', avgVal], style: { stroke: '#F4664A', lineDash: [2, 2] } },
// ];
// setLineConfig({ ...lineConfig, yField, xField, annotations: avgLine });
setLineConfig({ ...lineConfig, yField, xField,});
if (showCompareSum) {
const _sumLine = Object.keys(byDays).reduce((r, _d) => {
const summaryVal = byDays[_d].reduce((rows, row) =>
{
if (row[seriesField].includes(solidLineTime)){
return rows + row[yField];
}
else{
return rows;
}
}
, 0);
const summaryCompareVal = byDays[_d].reduce((rows, row) =>
{
if (row[seriesField].includes(solidLineCompareTime)){
return rows + row[yField];
}
else{
return rows;
}
}
, 0);
r.push({ ...byDays[_d][0], [yField]: summaryVal, [seriesField]: solidLineTime+'总计' });
r.push({ ...byDays[_d][0], [yField]: summaryCompareVal, [seriesField]: solidLineCompareTime+'总计' });
return r;
}, []);
setSumSeries(_sumLine);
}
else{
const _sumLine = Object.keys(byDays).reduce((r, _d) => {
const summaryVal = byDays[_d].reduce((rows, row) => rows + row[yField], 0);
r.push({ ...byDays[_d][0], [yField]: summaryVal, [seriesField]: '总计' });
return r;
}, []);
// console.log(_sumLine.map((ele) => ele[yField]));
setSumSeries(_sumLine);
}
};
const onChangeXDateFieldGroup = (value, data, avg1) => {
// console.log(value, data, avg1);
const _sumLine = [];
const { xField, yField, seriesField } = lineConfig;
const groupByDate = data.reduce((r, v) => {
(r[v[xField]] || (r[v[xField]] = [])).push(v);
return r;
}, {});
// console.log(groupByDate);
const _data = Object.keys(groupByDate).reduce((r, _d) => {
if (showCompareSum) {
const summaryVal = groupByDate[_d].reduce((rows, row) =>
{
if (row[seriesField].includes(solidLineTime)){
return rows + row[yField];
}
else{
return rows;
}
}
, 0);
const summaryCompareVal = groupByDate[_d].reduce((rows, row) =>
{
if (row[seriesField].includes(solidLineCompareTime)){
return rows + row[yField];
}
else{
return rows;
}
}
, 0);
_sumLine.push({ ...groupByDate[_d][0], [yField]: summaryVal, [seriesField]: solidLineTime+'总计' });
_sumLine.push({ ...groupByDate[_d][0], [yField]: summaryCompareVal, [seriesField]: solidLineCompareTime+'总计' });
}
else{
const summaryVal = groupByDate[_d].reduce((rows, row) => rows + row[yField], 0);
_sumLine.push({ ...groupByDate[_d][0], [yField]: summaryVal, [seriesField]: '总计' });
}
const xAxisGroup = groupByDate[_d].reduce((a, v) => {
(a[v[seriesField]] || (a[v[seriesField]] = [])).push(v);
return a;
}, {});
// console.log(xAxisGroup);
Object.keys(xAxisGroup).map((_group) => {
const summaryVal = xAxisGroup[_group].reduce((rows, row) => rows + row[yField], 0);
r.push({ ...xAxisGroup[_group][0], [yField]: summaryVal, });
return _group;
});
return r;
}, []);
// const _sum = Object.values(groupBy(_data, 'dateGroup')).reduce((ac, b) => ({...b, [yField]: 0}), {});
// console.log(xField, avg1);
// console.log('date source=====', _data);
setLineChartX(value);
setDataSource(_data);
setSumSeries(_sumLine);
// setAvgLine1(avg1);
// const avg1Int = Math.round(avg1);
// const mergedConfig = { ...lineConfig,
// annotations: [
// { type: 'text', position: ['start', avg1Int], content: avg1Int, offsetX: -15, style: { fill: '#F4664A', textBaseline: 'bottom' } },
// { type: 'line', start: [-10, avg1Int], end: ['max', avg1Int], style: { stroke: '#F4664A', lineDash: [2, 2] } },
// ],
// };
// console.log(mergedConfig);
// setLineConfig(mergedConfig);
setLineConfig(cloneDeep(line_config));
};
return (
<section>
<Row gutter={16} justify={'space-between'} className="mb-1">
<Col flex={'auto'}>
<h3>
走势: <span style={{ fontSize: 'smaller' }}>{dataFieldAlias[lineConfig.yField].label}</span>
</h3>
</Col>
<Col style={{ textAlign: 'right' }} align={'end'}>
<DateGroupRadio
visible={true}
dataRaw={{ data1: dataBeforeXChange }}
onChange={onChangeXDateFieldGroup}
value={lineChartX}
dataMapper={orderCountDataMapper}
fieldMapper={orderCountDataFieldMapper}
/>
</Col>
</Row>
<Spin spinning={loading}>
<Line {...lineConfig} data={[].concat(dataSource, sumSeries)} />
</Spin>
</section>
);
});

@ -0,0 +1,40 @@
import React from 'react';
import { Select } from 'antd';
import { observer } from 'mobx-react';
import { HotelStars as options } from '../../libs/ht';
const HotelStars = (props) => {
const { mode, value, onChange, show_all, ...extProps } = props;
const _show_all = ['tags', 'multiple'].includes(mode) ? false : show_all;
return (
<div>
<Select
mode={mode || null}
allowClear
style={{ width: '100%' }}
placeholder="星级"
value={value || undefined}
onChange={(value) => {
if (typeof onChange === 'function') {
onChange(value);
}
}}
labelInValue={false}
{...extProps}
options={options}
>
{_show_all ? (
<Select.Option key="-1" value="ALL">
ALL
</Select.Option>
) : (
''
)}
</Select>
</div>
);
};
/**
* 酒店星级
*/
export default observer(HotelStars);

@ -3,7 +3,7 @@ import { toJS } from 'mobx';
import { observer } from 'mobx-react';
import { DATE_FORMAT, SMALL_DATETIME_FORMAT, stores_Context } from './../../config';
import { SearchOutlined } from '@ant-design/icons';
import { Form, Row, Col, Select, Button, Space, DatePicker } from 'antd';
import { Form, Row, Col, Select, Button, Space, DatePicker, Input, InputNumber } from 'antd';
import moment from 'moment';
// import locale from 'antd/es/date-picker/locale/zh_CN';
import BusinessSelect from './BusinessSelect';
@ -18,6 +18,7 @@ import { objectMapper, at, empty, isEmpty } from './../../utils/commons';
import { departureDateTypes } from './../../libs/ht';
import './search.css';
import HotelStarSelect from './HotelStarSelect';
const EditableContext = createContext();
const Option = Select.Option;
@ -180,6 +181,31 @@ export default observer((props) => {
transform: (value) => value?.value || value?.key || '',
default: '',
},
'cruiseDirection': {
key: 'cruiseDirection',
transform: (value) => value?.value || '',
default: '',
},
'cruiseBookType': {
key: 'cruiseBookType',
transform: (value) => value?.value || '',
default: '',
},
'hotelBookType': {
key: 'hotelBookType',
transform: (value) => value?.value || '',
default: '',
},
'hotelRecommandRate': {
key: 'hotelRecommandRate',
transform: (value) => value?.value || '',
default: '',
},
'hotelStar': {
key: 'hotelStar',
transform: (value) => value?.value || '',
default: '',
},
};
let dest = {};
const { departureDateType, applyDate, applyDate2, year, yearDiff, dates, months, date, ...omittedValue } = values;
@ -274,11 +300,19 @@ function getFields(props) {
};
let baseChildren = [];
baseChildren = [
item(
'keyword', // {...fieldComProps.keyword}
99,
<Form.Item name="keyword" {...fieldProps.keyword}>
<Input allowClear {...fieldProps.keyword} />
</Form.Item>,
fieldProps?.keyword?.col || 6
),
item(
'agency',
99,
<Form.Item name={'agency'}>
<SearchInput autoGet url="/service-web/QueryData/GetVEIName" map={{ 'CAV_VEI_SN': 'key', 'VEI2_CompanyBN': 'label' }} resultkey={'result1'} placeholder="所有地接社" />
<SearchInput autoGet url="/service-web/QueryData/GetVEIName" map={{ 'CAV_VEI_SN': 'key', 'VEI2_CompanyBN': 'label' }} resultkey={'result1'} placeholder="所有地接社" {...fieldProps.agency} />
</Form.Item>
),
item(
@ -348,11 +382,11 @@ function getFields(props) {
item(
'countryArea',
99,
<Form.Item name={`countryArea`} initialValue={at(props, 'initialValue.countryArea')[0] || (fieldProps?.countryArea?.show_all ? { key: 'all', label: '所有国家' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="所有国家" labelInValue>
<Form.Item name={`countryArea`} initialValue={at(props, 'initialValue.countryArea')[0] || (fieldProps?.countryArea?.show_all ? { key: 'all', label: '国内外' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="国内外" labelInValue allowClear={fieldProps?.countryArea?.show_all || false}>
{fieldProps?.countryArea?.show_all && (
<Option key="all" value="">
所有国家
<Option key="all" value="" disabled>
国内外
</Option>
)}
<Option key="china" value="china">
@ -368,11 +402,11 @@ function getFields(props) {
item(
'orderStatus',
99,
<Form.Item name={`orderStatus`} initialValue={at(props, 'initialValue.orderStatus')[0] || (fieldProps?.orderStatus?.show_all ? { key: '-1', label: '所有' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="所有状态" labelInValue>
<Form.Item name={`orderStatus`} initialValue={at(props, 'initialValue.orderStatus')[0] || (fieldProps?.orderStatus?.show_all ? { key: '-1', label: '成行状态' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="成行状态" labelInValue allowClear>
{fieldProps?.orderStatus?.show_all && (
<Select.Option key="所有" value="-1">
所有
<Select.Option key="-1" value="-1" disabled>
成行状态
</Select.Option>
)}
<Select.Option key="已成行" value="1">
@ -398,10 +432,7 @@ function getFields(props) {
'departureDateType',
99,
<Form.Item name={`departureDateType`} initialValue={at(props, 'initialValue.departureDateType')[0] || { key: 'departureDate', label: '抵达日期' }}>
<Select labelInValue={true}
style={{ width: '100%' }}
placeholder="选择日期类型"
>
<Select labelInValue={true} style={{ width: '100%' }} placeholder="选择日期类型">
{departureDateTypes.map((ele) => (
<Select.Option key={ele.key} value={ele.key} disabled={fieldProps?.departureDateType?.disabledKeys.includes(ele.key)}>
{ele.label}
@ -492,6 +523,128 @@ function getFields(props) {
<SearchInput autoGet url="/service-Analyse2/GetGlobalDestinationInfo" map={{ 'c_id': 'key', 'cn_name': 'label' }} resultkey={'result'} placeholder="输入搜索城市: 中/英名字" />
</Form.Item>
),
item(
'cruiseDirection',
99,
<Form.Item name={`cruiseDirection`} initialValue={at(props, 'initialValue.cruiseDirection')[0] || undefined}>
<Select style={{ width: '100%' }} placeholder="上下水" labelInValue allowClear>
{fieldProps?.cruiseDirection?.show_all && (
<Option key="all" value="" disabled>
上下水
</Option>
)}
<Option key="1" value="1">
上水
</Option>
<Option key="2" value="2">
下水
</Option>
{/* <Option key="long" value="long">
长线
</Option> */}
</Select>
</Form.Item>,
3
),
item(
'cruiseBookType',
99,
<Form.Item name={`cruiseBookType`} initialValue={at(props, 'initialValue.cruiseBookType')[0] || (fieldProps?.cruiseBookType?.show_all ? { key: '-1', label: '预定类型' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="预定类型" labelInValue allowClear>
{fieldProps?.cruiseBookType?.show_all && (
<Option key="-1" value="-1" disabled>
预定类型
</Option>
)}
<Option key="1" value="1">
单订三峡
</Option>
<Option key="0" value="0">
含行程
</Option>
</Select>
</Form.Item>,
3
),
item(
'roomsRange',
99,
<Form.Item noStyle>
<Input.Group compact>
<Form.Item name={'RoomNumStart'} noStyle>
<InputNumber style={{ width: 'calc(50% - 15px)', textAlign: 'center' }} placeholder="房间数" />
</Form.Item>
<Input style={{ width: 30, borderLeft: 0, borderRight: 0, pointerEvents: 'none', }} placeholder="~" disabled />
<Form.Item name={'RoomNumEnd'} noStyle>
<InputNumber style={{ width: 'calc(50% - 15px)', textAlign: 'center', borderLeft: 0 }} placeholder="房间数" />
</Form.Item>
</Input.Group>
</Form.Item>,
fieldProps?.roomsRange?.col || 4
),
item(
'personRange',
99,
<Form.Item noStyle>
<Input.Group compact>
<Form.Item name={'PersonNumStart'} noStyle>
<InputNumber style={{ width: 'calc(50% - 15px)', textAlign: 'center' }} placeholder="人数" />
</Form.Item>
<Input style={{ width: 30, borderLeft: 0, borderRight: 0, pointerEvents: 'none', }} placeholder="~" disabled />
<Form.Item name={'PersonNumEnd'} noStyle>
<InputNumber style={{ width: 'calc(50% - 15px)', textAlign: 'center', borderLeft: 0 }} placeholder="人数" />
</Form.Item>
</Input.Group>
</Form.Item>,
fieldProps?.personRange?.col || 4
),
item(
'hotelBookType',
99,
<Form.Item name={`hotelBookType`} initialValue={at(props, 'initialValue.hotelBookType')[0] || (fieldProps?.hotelBookType?.show_all ? { key: 'all', label: '预定类型' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="预定类型" labelInValue allowClear={fieldProps?.hotelBookType?.show_all || false}>
{fieldProps?.hotelBookType?.show_all && (
<Option key="all" value="" disabled>
预定类型
</Option>
)}
<Option key="1" value="1">
代订
</Option>
<Option key="0" value="0">
自订
</Option>
</Select>
</Form.Item>,
3
),
item(
'hotelRecommandRate',
99,
<Form.Item name={`hotelRecommandRate`} initialValue={at(props, 'initialValue.hotelRecommandRate')[0] || (fieldProps?.hotelRecommandRate?.show_all ? { key: 'all', label: '推荐等级' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="推荐等级" labelInValue allowClear={fieldProps?.hotelRecommandRate?.show_all || false}>
{fieldProps?.hotelRecommandRate?.show_all && (
<Option key="all" value="" disabled>
推荐等级
</Option>
)}
<Option key="1" value="1">
主推
</Option>
<Option key="0" value="0">
非主推
</Option>
</Select>
</Form.Item>,
3
),
item(
'hotelStar',
99,
<Form.Item name={`hotelStar`} initialValue={at(props, 'initialValue.hotelStar')[0] || undefined}>
<HotelStarSelect {...fieldProps.hotelStar} labelInValue={true} />
</Form.Item>
),
];
baseChildren = baseChildren
.map((x) => {

@ -1,3 +1,4 @@
import moment from 'moment';
import { fixTo4Decimals, fixTo1Decimals, fixToInt, groupBy, sortBy, cloneDeep, pick, unique, flush, fixTo2Decimals, isEmpty } from '../utils/commons';
/**
@ -66,6 +67,7 @@ export const overviewGroup = groups.slice(0, 3); // todo: 花梨鹰 APP Trippest
export const sites = [
{ value: '2', key: '2', label: 'CHT', code: 'CHT' },
{ value: '8', key: '8', label: 'AH', code: 'AH' },
{ value: '186', key: '186', label: 'JH', code: 'JH' },
{ value: '163', key: '163', label: 'GH', code: 'GH' },
{ value: '184', key: '184', label: 'GH站外渠道 (中国)', code: 'ZWQD' },
{ value: '185', key: '185', label: 'GH站外渠道 (海外)', code: 'GH_ZWQD_HW' },
@ -183,6 +185,30 @@ export const KPISubjects = [
// { key: 'sum_person_num', value: 'sum_person_num', label: '人数' },
];
export const HotelStars = [
{ key: '1', value: '1', label: '五星' },
{ key: '2', value: '2', label: '四星' },
{ key: '3', value: '3', label: '三星' },
{ key: '4', value: '4', label: '二星' },
{ key: '8', value: '8', label: '准五星 ' },
{ key: '9', value: '9', label: '准四星' },
{ key: '10', value: '10', label: '客栈' },
{ key: '11', value: '11', label: '公寓' },
{ key: '12', value: '12', label: '四合院酒店' },
{ key: '13', value: '13', label: '豪华五星' },
];
export const CruiseAgency = [
{ key: '14193', value: '14193', label: '长江海外游轮旅游有限公司' },
{ key: '1067', value: '1067', label: '湖北东方皇家游船公司(待删除)' },
{ key: '70', value: '70', label: '湖北皇家长江旅游船有限公司' },
{ key: '159', value: '159', label: '文嘉船务' },
{ key: '77', value: '77', label: '武汉扬子江' },
{ key: '1337', value: '1337', label: '重庆薇灿' },
{ key: '4378', value: '4378', label: '重庆新世纪国旅' },
{ key: '-1', value: '-1', label: '自订' },
];
/**
* 计算指标值的分段区间
* @param {number} value
@ -269,10 +295,14 @@ export const pivotBy = (_data, [rows, columns, date]) => {
// if (groupbyKeys.includes('PPPriceRange')) {
// }
// 补充计算的字段
const RTXF_WB_values = cloneDeep(_data).map(ele => ele.RTXF_WB);
const RTXF_WB_values = cloneDeep(_data).map(ele => ele.RTXF_WB); // 人天消费
// const max_RTXF_WB = Math.max(...RTXF_WB_values);
const RTXF_WB_range = calculateRangeScale(RTXF_WB_values);
let data = cloneDeep(_data).map(ele => {
ele.startYearMonth = ele.startDate ? moment(ele.startDate).format('YYYY-MM') : '';
ele.startMonth = ele.startDate ? moment(ele.startDate).format('MM') : '';
ele.applyYearMonth = ele.applyDate ? moment(ele.applyDate).format('YYYY-MM') : '';
ele.applyMonth = ele.applyDate ? moment(ele.applyDate).format('MM') : '';
ele.PPPrice = (Number(ele.orderState) === 1 && ele.tourdays && ele.personNum) ? fixToInt(ele.quotePrice / ele.tourdays / ele.personNum) : -1; // 报价: 人均天
ele.PPPriceRange = calcPPPriceRange(ele.PPPrice);
ele.RTXF_WB_range = findRange(ele.RTXF_WB, RTXF_WB_range);
@ -291,7 +321,7 @@ export const pivotBy = (_data, [rows, columns, date]) => {
});
// 数组的字段值, 拆分处理
if (groupbyKeys.includes('destinationCountry_AsJOSN')) {
data = _data.reduce((r, v, i) => {
data = data.reduce((r, v, i) => {
const vjson = isEmpty(v.destinationCountry_AsJOSN) ? [] : v.destinationCountry_AsJOSN;
const xv = (vjson).reduce((rv, cv, vi) => {
rv.push({...v, destinationCountry_AsJOSN: cv, key: vi === 0 ? v.key : `${v.key}@${cv}`});
@ -303,7 +333,7 @@ export const pivotBy = (_data, [rows, columns, date]) => {
}, []);
}
if (groupbyKeys.includes('destinations_AsJOSN')) {
data = _data.reduce((r, v, i) => {
data = data.reduce((r, v, i) => {
const vjson = isEmpty(v.destinations_AsJOSN) ? [] : v.destinations_AsJOSN;
const xv = (vjson).reduce((rv, cv, vi) => {
rv.push({...v, destinations_AsJOSN: cv, key: vi === 0 ? v.key : `${v.key}@${cv}`});

@ -3,7 +3,7 @@ import moment from "moment";
import { NavLink } from "react-router-dom";
import * as config from "../config";
import * as req from '../utils/request';
import { groupBy, prepareUrl } from '../utils/commons';
import { groupBy, prepareUrl, isEmpty, show_vs_tag, formatPercent, percentToDecimal } from '../utils/commons';
class CustomerServices {
@ -53,113 +53,272 @@ class CustomerServices {
.then(json => {
if (json.errcode === 0) {
runInAction(() => {
const splitTotalList = groupBy(json.result1, row => row.EOI_ObjSN === -1 ? '0' : '1');
this.agentGroupList = splitTotalList['1'];
const total1 = splitTotalList['0']?.[0] || {}; // json.total1;
this.agentGroupListColumns = [
{
title: '地接社名称',
dataIndex: 'VendorName',
fixed: 'left',
children: [{
// title: this.startDate.format(config.DATE_FORMAT) + '~' + this.endDate.format(config.DATE_FORMAT),
dataIndex: 'VendorName',
fixed: 'left',
render: (text, record) => <NavLink to={`/agent/${record.EOI_ObjSN}/group/list`}>{record.VendorName}</NavLink>
}
]
},
{
title: '团数',
dataIndex: 'GroupCount',
sorter: (a, b) => a.GroupCount - b.GroupCount,
children: [{
title: total1.GroupCount,
dataIndex: 'GroupCount'
}
]
},
{
title: '人数',
dataIndex: 'PersonNum',
sorter: (a, b) => a.PersonNum - b.PersonNum,
children: [{
title: total1.PersonNum,
dataIndex: 'PersonNum'
}
]
},
{
title: '团天数',
dataIndex: 'GroupDays',
sorter: (a, b) => a.GroupDays - b.GroupDays,
children: [{
title: total1.GroupDays,
dataIndex: 'GroupDays'
}
]
},
{
title: '交易额',
dataIndex: 'totalcost',
sorter: (a, b) => a.totalcost - b.totalcost,
children: [{
title: total1.totalcost,
dataIndex: 'totalcost'
}
]
},
{
title: '前勤分',
dataIndex: 'qianqin',
sorter: (a, b) => a.qianqin - b.qianqin,
children: [{
title: total1.qianqin,
dataIndex: 'qianqin'
}
]
},
{
title: '好评数',
dataIndex: 'GoodCount',
sorter: (a, b) => a.GoodCount - b.GoodCount,
children: [{
title: total1.GoodCount,
dataIndex: 'GoodCount'
}
]
},
{
title: '好评率',
dataIndex: 'GoodRate',
sorter: (a, b) => parseInt(a.GoodRate) - parseInt(b.GoodRate),
children: [{
title: total1.GoodRate,
dataIndex: 'GoodRate'
}
]
},
{
title: '差评数',
dataIndex: 'BadCount',
sorter: (a, b) => a.BadCount - b.BadCount,
children: [{
title: total1.BadCount,
dataIndex: 'BadCount'
}
]
},
{
title: '差评率',
dataIndex: 'BadRate',
sorter: (a, b) => parseInt(a.BadRate) - parseInt(b.BadRate),
children: [{
title: total1.BadRate,
dataIndex: 'BadRate'
if (isEmpty(json.result2)){
const splitTotalList = groupBy(json.result1, row => row.EOI_ObjSN === -1 ? '0' : '1');
this.agentGroupList = splitTotalList['1'];
const total1 = splitTotalList['0']?.[0] || {}; // json.total1;
this.agentGroupListColumns = [
{
title: '地接社名称',
dataIndex: 'VendorName',
fixed: 'left',
children: [{
// title: this.startDate.format(config.DATE_FORMAT) + '~' + this.endDate.format(config.DATE_FORMAT),
dataIndex: 'VendorName',
fixed: 'left',
render: (text, record) => <NavLink to={`/agent/${record.EOI_ObjSN}/group/list`}>{record.VendorName}</NavLink>
}
]
},
{
title: '团数',
dataIndex: 'GroupCount',
sorter: (a, b) => a.GroupCount - b.GroupCount,
children: [{
title: total1.GroupCount,
dataIndex: 'GroupCount'
}
]
},
{
title: '人数',
dataIndex: 'PersonNum',
sorter: (a, b) => a.PersonNum - b.PersonNum,
children: [{
title: total1.PersonNum,
dataIndex: 'PersonNum'
}
]
},
{
title: '团天数',
dataIndex: 'GroupDays',
sorter: (a, b) => a.GroupDays - b.GroupDays,
children: [{
title: total1.GroupDays,
dataIndex: 'GroupDays'
}
]
},
{
title: '交易额',
dataIndex: 'totalcost',
sorter: (a, b) => a.totalcost - b.totalcost,
children: [{
title: total1.totalcost,
dataIndex: 'totalcost'
}
]
},
{
title: '前勤分',
dataIndex: 'qianqin',
sorter: (a, b) => a.qianqin - b.qianqin,
children: [{
title: total1.qianqin,
dataIndex: 'qianqin'
}
]
},
{
title: '好评数',
dataIndex: 'GoodCount',
sorter: (a, b) => a.GoodCount - b.GoodCount,
children: [{
title: total1.GoodCount,
dataIndex: 'GoodCount'
}
]
},
{
title: '好评率',
dataIndex: 'GoodRate',
sorter: (a, b) => parseInt(a.GoodRate) - parseInt(b.GoodRate),
children: [{
title: total1.GoodRate,
dataIndex: 'GoodRate'
}
]
},
{
title: '差评数',
dataIndex: 'BadCount',
sorter: (a, b) => a.BadCount - b.BadCount,
children: [{
title: total1.BadCount,
dataIndex: 'BadCount'
}
]
},
{
title: '差评率',
dataIndex: 'BadRate',
sorter: (a, b) => parseInt(a.BadRate) - parseInt(b.BadRate),
children: [{
title: total1.BadRate,
dataIndex: 'BadRate'
}
]
}
];
}
else{
const splitTotalList1 = groupBy(json.result1, row => row.EOI_ObjSN === -1 ? '0' : '1');
const splitTotalList2 = groupBy(json.result2, row => row.EOI_ObjSN === -1 ? '0' : '1');
const result = [];
for (const item1 of splitTotalList1['1']) {
for (const item2 of splitTotalList2['1']) {
if (item1.EOI_ObjSN === item2.EOI_ObjSN) {
const goodRate1 = percentToDecimal(item1.GoodRate);
const goodRate2 = percentToDecimal(item2.GoodRate);
const badRate1 = percentToDecimal(item1.BadRate);
const badRate2 = percentToDecimal(item2.BadRate);
result.push({
EOI_ObjSN: item1.EOI_ObjSN,
VendorName: item1.VendorName,
GroupCount: show_vs_tag(formatPercent((item1.GroupCount-item2.GroupCount)/(item2.GroupCount===0?1:item2.GroupCount)),
item1.GroupCount-item2.GroupCount,item1.GroupCount,item2.GroupCount),
PersonNum: show_vs_tag(formatPercent((item1.PersonNum-item2.PersonNum)/(item2.PersonNum===0?1:item2.PersonNum)),
item1.PersonNum-item2.PersonNum,item1.PersonNum,item2.PersonNum),
GroupDays: show_vs_tag(formatPercent((item1.GroupDays-item2.GroupDays)/(item2.GroupDays===0?1:item2.GroupDays)),
item1.GroupDays-item2.GroupDays,item1.GroupDays,item2.GroupDays),
totalcost: show_vs_tag(formatPercent((item1.totalcost-item2.totalcost)/(item2.totalcost===0?1:item2.totalcost)),
(item1.totalcost-item2.totalcost).toFixed(2),item1.totalcost,item2.totalcost),
GoodCount: show_vs_tag(formatPercent((item1.GoodCount-item2.GoodCount)/(item2.GoodCount===0?1:item2.GoodCount)),
item1.GoodCount-item2.GoodCount,item1.GoodCount,item2.GoodCount),
GoodRate: show_vs_tag(formatPercent((goodRate1-goodRate2)/(goodRate2===0?1:goodRate2)),
formatPercent(goodRate1-goodRate2),item1.GoodRate,item2.GoodRate),
BadCount: show_vs_tag(formatPercent((item1.BadCount-item2.BadCount)/(item2.BadCount===0?1:item2.BadCount)),
item1.BadCount-item2.BadCount,item1.BadCount,item2.BadCount),
BadRate: show_vs_tag(formatPercent((badRate1-badRate2)/(badRate2===0?1:badRate2)),
formatPercent(badRate1-badRate2),item1.BadRate,item2.BadRate),
qianqin: show_vs_tag(formatPercent((item1.qianqin-item2.qianqin)/(item2.qianqin===0?1:item2.qianqin)),
(item1.qianqin-item2.qianqin).toFixed(2),item1.qianqin,item2.qianqin),
key:item1.key,
});
}
]
}
}
];
this.agentGroupList = result;
const total1 = splitTotalList1['0']?.[0] || {};
const total2 = splitTotalList2['0']?.[0] || {};
this.agentGroupListColumns = [
{
title: '地接社名称',
dataIndex: 'VendorName',
fixed: 'left',
children: [{
// title: this.startDate.format(config.DATE_FORMAT) + '~' + this.endDate.format(config.DATE_FORMAT),
dataIndex: 'VendorName',
fixed: 'left',
render: (text, record) => <NavLink to={`/agent/${record.EOI_ObjSN}/group/list`}>{record.VendorName}</NavLink>
}
]
},
{
title: '团数',
dataIndex: 'GroupCount',
sorter: (a, b) => a.GroupCount - b.GroupCount,
children: [{
title: show_vs_tag(formatPercent((total1.GroupCount-total2.GroupCount)/total2.GroupCount),total1.GroupCount-total2.GroupCount,total1.GroupCount,total2.GroupCount),
titleX: [total1.GroupCount, total2.GroupCount].join(' vs '),
dataIndex: 'GroupCount'
}
]
},
{
title: '人数',
dataIndex: 'PersonNum',
sorter: (a, b) => a.PersonNum - b.PersonNum,
children: [{
title: show_vs_tag(formatPercent((total1.PersonNum-total2.PersonNum)/total2.PersonNum),total1.PersonNum-total2.PersonNum,total1.PersonNum,total2.PersonNum),
titleX: [total1.PersonNum, total2.PersonNum].join(' vs '),
dataIndex: 'PersonNum'
}
]
},
{
title: '团天数',
dataIndex: 'GroupDays',
sorter: (a, b) => a.GroupDays - b.GroupDays,
children: [{
title: show_vs_tag(formatPercent((total1.GroupDays-total2.GroupDays)/total2.GroupDays),total1.GroupDays-total2.GroupDays,total1.GroupDays,total2.GroupDays),
titleX: [total1.GroupDays, total2.GroupDays].join(' vs '),
dataIndex: 'GroupDays'
}
]
},
{
title: '交易额',
dataIndex: 'totalcost',
sorter: (a, b) => a.totalcost - b.totalcost,
children: [{
title: show_vs_tag(formatPercent((total1.totalcost-total2.totalcost)/total2.totalcost),(total1.totalcost-total2.totalcost).toFixed(2),total1.totalcost,total2.totalcost),
titleX: [total1.totalcost, total2.totalcost].join(' vs '),
dataIndex: 'totalcost'
}
]
},
{
title: '前勤分',
dataIndex: 'qianqin',
sorter: (a, b) => a.qianqin - b.qianqin,
children: [{
title: show_vs_tag(formatPercent((total1.qianqin-total2.qianqin)/total2.qianqin),(total1.qianqin-total2.qianqin).toFixed(2),total1.qianqin,total2.qianqin),
titleX: [total1.qianqin, total2.qianqin].join(' vs '),
dataIndex: 'qianqin'
}
]
},
{
title: '好评数',
dataIndex: 'GoodCount',
sorter: (a, b) => a.GoodCount - b.GoodCount,
children: [{
title: show_vs_tag(formatPercent((total1.GoodCount-total2.GoodCount)/total2.GoodCount),total1.GoodCount-total2.GoodCount,total1.GoodCount,total2.GoodCount),
titleX: [total1.GoodCount, total2.GoodCount].join(' vs '),
dataIndex: 'GoodCount'
}
]
},
{
title: '好评率',
dataIndex: 'GoodRate',
sorter: (a, b) => parseInt(a.GoodRate) - parseInt(b.GoodRate),
children: [{
title: show_vs_tag(formatPercent((percentToDecimal(total1.GoodRate)-percentToDecimal(total2.GoodRate))/percentToDecimal(total2.GoodRate)),
formatPercent(percentToDecimal(total1.GoodRate)-percentToDecimal(total2.GoodRate)),total1.GoodRate,total2.GoodRate),
titleX: [total1.GoodRate, total2.GoodRate].join(' vs '),
dataIndex: 'GoodRate'
}
]
},
{
title: '差评数',
dataIndex: 'BadCount',
sorter: (a, b) => a.BadCount - b.BadCount,
children: [{
title: show_vs_tag(formatPercent((total1.BadCount-total2.BadCount)/total2.BadCount),total1.BadCount-total2.BadCount,total1.BadCount,total2.BadCount),
titleX: [total1.BadCount, total2.BadCount].join(' vs '),
dataIndex: 'BadCount'
}
]
},
{
title: '差评率',
dataIndex: 'BadRate',
sorter: (a, b) => parseInt(a.BadRate) - parseInt(b.BadRate),
children: [{
title: show_vs_tag(formatPercent((percentToDecimal(total1.BadRate)-percentToDecimal(total2.BadRate))/percentToDecimal(total2.GoodRate)),
formatPercent(percentToDecimal(total1.BadRate)-percentToDecimal(total2.BadRate)),total1.BadRate,total2.BadRate),
titleX: [total1.BadRate, total2.BadRate].join(' vs '),
dataIndex: 'BadRate'
}
]
}
];
}
});
}
})
@ -278,9 +437,10 @@ class CustomerServices {
});
}
fetchDestinationGroupCount() {
this.inProgress = true;
const fetchUrl = prepareUrl(config.HT_HOST + '/service-web/QueryData/GetdistGroupInfoAll')
fetchDistGroupInfoByCountry(destinationId) {
this.nationality_count_data.loading = true;
const fetchUrl = prepareUrl(config.HT_HOST + '/service-web/QueryData/GetDistGroupInfoALLByCountry')
.append('city', destinationId)
.append('DateType', this.dateType)
.append('Date1', this.startDateString)
.append('Date2', this.endDateString)
@ -291,22 +451,19 @@ class CustomerServices {
.append('OrderStatus', this.selectedOrderStatus)
.build();
req.fetchJSON(fetchUrl)
.then(json => {
.then((json) => {
if (json.errcode === 0) {
runInAction(() => {
const splitTotalList = groupBy(json.result1, row => row.COLD_ServiceCity === -1 ? '0' : '1');
this.agentGroupList = splitTotalList['1'];
const total1 = splitTotalList['0']?.[0] || {}; // json.total1;
this.destinationGroupCount = splitTotalList['1'];
this.destinationGroupCountColumns = [
this.nationality_count_data.destinationGroupByCountryList = splitTotalList['1'];
const total1 = splitTotalList['0']?.[0] || {};
this.nationality_count_data.destinationGroupByCountryListColumns =[
{
title: '城市',
title: '国籍',
dataIndex: 'COLD_ServiceCityName',
fixed: 'left',
children: [{
dataIndex: 'COLD_ServiceCityName',
fixed: 'left',
render: (text, record) => <NavLink to={`/destination/${record.COLD_ServiceCity}/group/list`}>{record.COLD_ServiceCityName}</NavLink>
title: total1.COLD_ServiceCityName,
dataIndex: 'COLD_ServiceCityName'
}
]
},
@ -362,6 +519,193 @@ class CustomerServices {
}
];
});
}
})
.then(() => {
this.nationality_count_data.loading = false;
});
}
fetchDestinationGroupCount() {
this.inProgress = true;
const fetchUrl = prepareUrl(config.HT_HOST + '/service-web/QueryData/GetdistGroupInfoAll')
.append('DateType', this.dateType)
.append('Date1', this.startDateString)
.append('Date2', this.endDateString)
.append('OldDate1', this.startDateDiffString)
.append('OldDate2', this.endDateDiffString)
.append('DepList', this.selectedTeam)
.append('Country', this.selectedCountry)
.append('OrderStatus', this.selectedOrderStatus)
.build();
req.fetchJSON(fetchUrl)
.then(json => {
if (json.errcode === 0) {
runInAction(() => {
if (isEmpty(json.result2)){
const splitTotalList = groupBy(json.result1, row => row.COLD_ServiceCity === -1 ? '0' : '1');
this.agentGroupList = splitTotalList['1'];
const total1 = splitTotalList['0']?.[0] || {}; // json.total1;
this.destinationGroupCount = splitTotalList['1'];
this.destinationGroupCountColumns = [
{
title: '城市',
dataIndex: 'COLD_ServiceCityName',
fixed: 'left',
children: [{
dataIndex: 'COLD_ServiceCityName',
fixed: 'left',
render: (text, record) => <NavLink to={`/destination/${record.COLD_ServiceCity}/group/list`}>{record.COLD_ServiceCityName}</NavLink>
}
]
},
{
title: '团数',
dataIndex: 'GroupCount',
sorter: (a, b) => a.GroupCount - b.GroupCount,
children: [{
title: total1.GroupCount,
dataIndex: 'GroupCount'
}
]
},
{
title: '人数',
dataIndex: 'PersonNum',
sorter: (a, b) => a.PersonNum - b.PersonNum,
children: [{
title: total1.PersonNum,
dataIndex: 'PersonNum'
}
]
},
{
title: '团天数',
dataIndex: 'GroupDays',
sorter: (a, b) => a.GroupDays - b.GroupDays,
children: [{
title: total1.GroupDays,
dataIndex: 'GroupDays'
}
]
},
{
title: '交易额',
dataIndex: 'TotalCost',
sorter: (a, b) => a.TotalCost - b.TotalCost,
children: [{
title: total1.TotalCost,
dataIndex: 'TotalCost'
}
]
},
{
title: '报价',
dataIndex: 'TotalPrice',
sorter: (a, b) => a.TotalPrice - b.TotalPrice,
children: [{
title: total1.TotalPrice,
dataIndex: 'TotalPrice'
}
]
}
];
}
else{
const splitTotalList1 = groupBy(json.result1, row => row.COLD_ServiceCity === -1 ? '0' : '1');
const splitTotalList2 = groupBy(json.result2, row => row.COLD_ServiceCity === -1 ? '0' : '1');
const result = [];
for (const item1 of splitTotalList1['1']) {
for (const item2 of splitTotalList2['1']) {
if (item1.COLD_ServiceCity === item2.COLD_ServiceCity) {
result.push({
COLD_ServiceCity: item1.COLD_ServiceCity,
COLD_ServiceCityName: item1.COLD_ServiceCityName,
GroupCount: show_vs_tag(formatPercent((item1.GroupCount-item2.GroupCount)/(item2.GroupCount===0?1:item2.GroupCount)),
item1.GroupCount-item2.GroupCount,item1.GroupCount,item2.GroupCount),
PersonNum: show_vs_tag(formatPercent((item1.PersonNum-item2.PersonNum)/(item2.PersonNum===0?1:item2.PersonNum)),
item1.PersonNum-item2.PersonNum,item1.PersonNum,item2.PersonNum),
GroupDays: show_vs_tag(formatPercent((item1.GroupDays-item2.GroupDays)/(item2.GroupDays===0?1:item2.GroupDays)),
item1.GroupDays-item2.GroupDays,item1.GroupDays,item2.GroupDays),
TotalCost: show_vs_tag(formatPercent((item1.TotalCost-item2.TotalCost)/(item2.TotalCost===0?1:item2.TotalCost)),
(item1.TotalCost-item2.TotalCost).toFixed(2),item1.TotalCost,item2.TotalCost),
TotalPrice: show_vs_tag(formatPercent((item1.TotalPrice-item2.TotalPrice)/(item2.TotalPrice===0?1:item2.TotalPrice)),
item1.TotalPrice-item2.TotalPrice,item1.TotalPrice,item2.TotalPrice),
key:item1.key,
});
}
}
}
this.destinationGroupCount = result;
const total1 = splitTotalList1['0']?.[0] || {};
const total2 = splitTotalList2['0']?.[0] || {};
this.destinationGroupCountColumns = [
{
title: '城市',
dataIndex: 'COLD_ServiceCityName',
fixed: 'left',
children: [{
dataIndex: 'COLD_ServiceCityName',
fixed: 'left',
render: (text, record) => <NavLink to={`/destination/${record.COLD_ServiceCity}/group/list`}>{record.COLD_ServiceCityName}</NavLink>
}
]
},
{
title: '团数',
dataIndex: 'GroupCount',
sorter: (a, b) => a.GroupCount - b.GroupCount,
children: [{
title: show_vs_tag(formatPercent((total1.GroupCount-total2.GroupCount)/total2.GroupCount),total1.GroupCount-total2.GroupCount,total1.GroupCount,total2.GroupCount),
dataIndex: 'GroupCount'
}
]
},
{
title: '人数',
dataIndex: 'PersonNum',
sorter: (a, b) => a.PersonNum - b.PersonNum,
children: [{
title: show_vs_tag(formatPercent((total1.PersonNum-total2.PersonNum)/total2.PersonNum),total1.PersonNum-total2.PersonNum,total1.PersonNum,total2.PersonNum),
dataIndex: 'PersonNum'
}
]
},
{
title: '团天数',
dataIndex: 'GroupDays',
sorter: (a, b) => a.GroupDays - b.GroupDays,
children: [{
title: show_vs_tag(formatPercent((total1.GroupDays-total2.GroupDays)/total2.GroupDays),total1.GroupDays-total2.GroupDays,total1.GroupDays,total2.GroupDays),
dataIndex: 'GroupDays'
}
]
},
{
title: '交易额',
dataIndex: 'TotalCost',
sorter: (a, b) => a.TotalCost - b.TotalCost,
children: [{
title: show_vs_tag(formatPercent((total1.TotalCost-total2.TotalCost)/total2.TotalCost),
(total1.TotalCost-total2.TotalCost).toFixed(2),total1.TotalCost,total2.TotalCost),
dataIndex: 'TotalCost'
}
]
},
{
title: '报价',
dataIndex: 'TotalPrice',
sorter: (a, b) => a.TotalPrice - b.TotalPrice,
children: [{
title: show_vs_tag(formatPercent((total1.TotalPrice-total2.TotalPrice)/total2.TotalPrice),
(total1.TotalPrice-total2.TotalPrice).toFixed(2),total1.TotalPrice,total2.TotalPrice),
dataIndex: 'TotalPrice'
}
]
}
];
}
});
}
})
.then(() => {
@ -372,8 +716,6 @@ class CustomerServices {
fetchGroupListByDestinationId(destinationId) {
this.inProgress = true;
this.destinationName = '...';
this.destinationGroupList = [];
this.destinationGroupListColumns = [];
const fetchUrl = prepareUrl(config.HT_HOST + '/service-web/QueryData/GetdistGroupInfo')
.append('city', destinationId)
.append('DateType', this.dateType)
@ -383,6 +725,7 @@ class CustomerServices {
.append('OldDate2', this.endDateString)
.append('DepList', this.selectedTeam)
.append('Country', this.selectedCountry)
.append('OrderStatus', this.selectedOrderStatus)
.build();
req.fetchJSON(fetchUrl)
.then(json => {
@ -473,7 +816,7 @@ class CustomerServices {
this.startDateDiffString = obj.DateDiff1;
this.endDateDiffString = obj.DateDiff2;
this.selectedCountry = obj.countryArea;
this.selectedTeam = obj.DepartmentList.replace('ALL', '');
this.selectedTeam = (obj.DepartmentList || '').replace('ALL', '');
this.selectedOrderStatus = obj.orderStatus;
}
@ -524,6 +867,15 @@ class CustomerServices {
destinationGroupCount = [];
destinationGroupCountColumns =[];
destinationGroupList = [];
destinationGroupListColumns = [];
// 国籍统计
nationality_count_data = {
loading: false,
destinationGroupByCountryList:[],
destinationGroupByCountryListColumns:[]
};
agentGroupList = [{
EOI_ObjSN: 1,
VendorName: '---',

@ -2,6 +2,8 @@ import {makeAutoObservable, runInAction} from "mobx";
import { fetchJSON } from '../utils/request';
import * as config from "../config";
import { groupsMappedByKey, sitesMappedByCode, pivotBy } from './../libs/ht';
import { sortBy, show_vs_tag, formatPercent, groupBy, isEmpty, uniqWith, formatPercentToFloat } from "../utils/commons";
import moment from 'moment';
/**
* 用于透视的数据
@ -99,8 +101,12 @@ class CustomerStore {
// 老客户 beign
regular_customer_order(get_detail = false) {
// isCompare对比数据boolean isCompareRender对比折线boolean
regular_customer_order(get_detail = false, isCompare = false, isCompareRender=false) {
let pivotByOrder = 'SumOrder';
let pivotByDate = 'applyDate';
this.regular_data.loading = true;
this.regular_data.detail_loading = get_detail;
const date_picker_store = this.rootStore.date_picker_store;
let url = '/service-tourdesign/RegularCusOrder';
url += '?Website=' + this.regular_data.webcode.toString() + '&DEI_SNList=' + this.regular_data.groups.toString();
@ -108,37 +114,162 @@ class CustomerStore {
url += '&ApplydateCheck=1&EntrancedateCheck=0&ConfirmDateCheck=0';
} else if(String(this.regular_data.date_type).toLowerCase() === 'confirmdate'){
url += '&ApplydateCheck=0&EntrancedateCheck=0&ConfirmDateCheck=1';
pivotByOrder = 'ConfirmOrder';
pivotByDate = 'confirmDate';
}else {
url += '&ApplydateCheck=0&EntrancedateCheck=1&ConfirmDateCheck=0';
pivotByOrder = 'ConfirmOrder';
pivotByDate = 'startDate';
}
if (isCompare){
url += '&ApplydateStart=' + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + '&ApplydateEnd=' + date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT);
url += '&EntrancedateStart=' + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + '&EntrancedateEnd=' + date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT);
url += '&ConfirmdateStart=' + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + '&ConfirmdateEnd=' + date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT);
}
else{
url += '&ApplydateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&ApplydateEnd=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
url += '&EntrancedateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&EntrancedateEnd=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
url += '&ConfirmdateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&ConfirmdateEnd=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
}
url += '&ApplydateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&ApplydateEnd=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
url += '&EntrancedateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&EntrancedateEnd=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
url += '&ConfirmdateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&ConfirmdateEnd=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
if (get_detail) {
url += '&IsDetail=1';
} else {
url += '&IsDetail=0';
}
url += `&IncludeTickets=${this.regular_data.include_tickets}`;
return new Promise((resolve, reject) => {
fetch(config.HT_HOST + url)
.then((response) => response.json())
.then((json) => {
runInAction(() => {
if (get_detail) {
this.regular_data.data_detail = json;
const dump_l = (json || []).filter(ele => ele.COLI_IsOld !== '' && ele.COLI_IsCusCommend !== '').length;
this.regular_data.total_data_tips = dump_l > 0 ? `包含 ${dump_l} 条同时勾选的数据` : '';
if (!isCompare){
this.regular_data.data_detail = json;
}
if (isCompareRender){
this.regular_data.solidLineTime=date_picker_store.start_date.format(config.DATE_FORMAT)+ '-' +date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
this.regular_data.solidLineCompareTime=date_picker_store.start_date_cp.format(config.DATE_FORMAT)+ '-' +date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT);
const dump_l = (json || []).filter(ele => ele.COLI_IsOld !== '' && ele.COLI_IsCusCommend !== '').length;
this.regular_data.total_data_tips = dump_l > 0 ? `包含 ${dump_l} 条同时勾选的数据` : '';
/** 使用明细数据画图 */
const data_detail = (json || []).map((ele) => ({
...ele,
key: ele.COLI_ID,
orderState: ele.OrderState,
applyDate: moment(ele.COLI_ApplyDate).format('YYYY-MM-DD'),
startDate: ele.COLI_OrderStartDate,
confirmDate: moment(ele.COLI_ConfirmDate).format('YYYY-MM-DD'),
}));
const { data: IsOldData, } = pivotBy(data_detail.filter(ele => ele.COLI_IsOld === '是'), [['COLI_IsOld', ], [], pivotByDate]);
const { data: isCusCommendData, } = pivotBy(data_detail.filter(ele => ele.COLI_IsCusCommend === '是'), [['COLI_IsCusCommend', ], [], pivotByDate]);
// console.log('IsOldData====', IsOldData, '\nisCusCommend', isCusCommendData);
// 合并成两个系列
const seriesData = [].concat(IsOldData.map(ele => ({...ele, _ylabel: '老客户'})), isCusCommendData.map(ele => ({...ele, _ylabel: '老客户推荐'})),).sort(sortBy(pivotByDate));
const seriesNewData = seriesData.map(item => {
if (isCompare){
return {
...item,
_ylabel: date_picker_store.start_date_cp.format(config.DATE_FORMAT)+ '-' +date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT) + item._ylabel
};
}
else{
return {
...item,
_ylabel: date_picker_store.start_date.format(config.DATE_FORMAT)+ '-' +date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT) + item._ylabel
};
}
});
// console.log('seriesData====', seriesNewData);
if (this.regular_data.data_compare.length===0){
this.regular_data.data_compare=seriesNewData;
}
else{
let seriesCompareData = [];
const fistCompareDetail = this.regular_data.data_compare;
if (fistCompareDetail.length>seriesNewData.length){
seriesCompareData = fistCompareDetail;
for (let i = 0; i < seriesNewData.length; i++) {
seriesNewData[i][pivotByDate] = fistCompareDetail[i][pivotByDate];
}
seriesCompareData.push(...seriesNewData);
}
else{
seriesCompareData=seriesNewData;
for (let i = 0; i < fistCompareDetail.length; i++) {
fistCompareDetail[i][pivotByDate] = seriesNewData[i][pivotByDate];
}
seriesCompareData.push(...fistCompareDetail);
}
this.regular_data.detail_loading = false;
this.regular_data.pivotData = seriesCompareData; // { IsOldData, isCusCommendData, };
this.regular_data.pivotY = pivotByOrder;
this.regular_data.pivotX = pivotByDate;
}
}
else{
this.regular_data.detail_loading = false;
const dump_l = (json || []).filter(ele => ele.COLI_IsOld !== '' && ele.COLI_IsCusCommend !== '').length;
this.regular_data.total_data_tips = dump_l > 0 ? `包含 ${dump_l} 条同时勾选的数据` : '';
/** 使用明细数据画图 */
const data_detail = (json || []).map((ele) => ({
...ele,
key: ele.COLI_ID,
orderState: ele.OrderState,
applyDate: moment(ele.COLI_ApplyDate).format('YYYY-MM-DD'),
startDate: ele.COLI_OrderStartDate,
confirmDate: moment(ele.COLI_ConfirmDate).format('YYYY-MM-DD'),
}));
const { data: IsOldData, } = pivotBy(data_detail.filter(ele => ele.COLI_IsOld === '是'), [['COLI_IsOld', ], [], pivotByDate]);
const { data: isCusCommendData, } = pivotBy(data_detail.filter(ele => ele.COLI_IsCusCommend === '是'), [['COLI_IsCusCommend', ], [], pivotByDate]);
// console.log('IsOldData====', IsOldData, '\nisCusCommend', isCusCommendData);
// 合并成两个系列
const seriesData = [].concat(IsOldData.map(ele => ({...ele, _ylabel: '老客户'})), isCusCommendData.map(ele => ({...ele, _ylabel: '老客户推荐'})),).sort(sortBy(pivotByDate));
this.regular_data.pivotData = seriesData; // { IsOldData, isCusCommendData, };
this.regular_data.pivotY = pivotByOrder;
this.regular_data.pivotX = pivotByDate;
}
} else {
this.regular_data.data = json;
if (isCompare){
const result = [];
const firstCompareData = this.regular_data.data;
for (const item1 of firstCompareData) {
for (const item2 of json) {
if (item1.ItemName === item2.ItemName) {
result.push({
ItemName: item1.ItemName,
OrderNum: show_vs_tag(formatPercent((item1.OrderNum-item2.OrderNum)/(item2.OrderNum===0?1:item2.OrderNum)),
item1.OrderNum-item2.OrderNum,item1.OrderNum,item2.OrderNum),
SUCOrderNum: show_vs_tag(formatPercent((item1.SUCOrderNum-item2.SUCOrderNum)/(item2.SUCOrderNum===0?1:item2.SUCOrderNum)),
item1.SUCOrderNum-item2.SUCOrderNum,item1.SUCOrderNum,item2.SUCOrderNum),
OrderRate: show_vs_tag(formatPercent((item1.OrderRate-item2.OrderRate)/item2.OrderRate),
formatPercentToFloat(item1.OrderRate-item2.OrderRate),formatPercentToFloat(item1.OrderRate),formatPercentToFloat(item2.OrderRate)),
SUCRate: show_vs_tag(formatPercent((item1.SUCRate-item2.SUCRate)/(item2.SUCRate===0?1:item2.SUCRate)),
formatPercent(item1.SUCRate-item2.SUCRate),formatPercent(item1.SUCRate),formatPercent(item2.SUCRate)),
ML: show_vs_tag(formatPercent((item1.ML-item2.ML)/(item2.ML===0?1:item2.ML)),
(item1.ML-item2.ML).toFixed(2),item1.ML,item2.ML),
OrderMLRate: show_vs_tag(formatPercent((item1.OrderMLRate-item2.OrderMLRate)/item2.OrderMLRate),
formatPercentToFloat(item1.OrderMLRate-item2.OrderMLRate),formatPercentToFloat(item1.OrderMLRate),formatPercentToFloat(item2.OrderMLRate)),
PersonNum: show_vs_tag(formatPercent((item1.PersonNum-item2.PersonNum)/(item2.PersonNum===0?1:item2.PersonNum)),
item1.PersonNum-item2.PersonNum,item1.PersonNum,item2.PersonNum),
});
}
}
};
this.regular_data.data = result;
}
else{
this.regular_data.data = json;
}
}
this.regular_data.loading = false;
resolve();
});
})
.catch((error) => {
this.regular_data.loading = false;
console.log('fetch data failed', error);
});
});
}
handleChange_webcode_regular = (value) => {
@ -159,8 +290,15 @@ class CustomerStore {
regular_data = {
loading: false,
detail_loading: false,
data: [],
data_detail: [],
data_compare: [],
showCompareSum:false,
solidLineTime:'',
solidLineCompareTime:'',
solidLineDash:'老客户推荐',
isCompareLine:false,
total_data_tips: '',
webcode: 'ALL',
site_select_mode: 'multiple',// 站点是否多选
@ -176,9 +314,14 @@ class CustomerStore {
searchValues: {
DepartmentList: ['1', '2', '28', '7'].map(kk => groupsMappedByKey[kk]),
WebCode: undefined, // ['ALL'].map(kk => sitesMappedByCode[kk]),
WebCode: ['CHT','AH','JH','GH','ZWQD','GH_ZWQD_HW','GHKYZG','GHKYHW'].map(kk => sitesMappedByCode[kk]),
DateType: { key: 'applyDate', label: '提交日期'},
IncludeTickets: { key: '0', label: '不含门票' },
},
pivotData: [],
pivotY: 'SumOrder',
pivotX: 'applyDate',
};
// 老客户 end
@ -261,6 +404,57 @@ class CustomerStore {
};
// 在华客人 end
// 东道主项目 begin
host_case_data = {
loading: false,
summaryData: [], // 汇总数据
groupData: [], // 小组数据
counselorData: [], // 顾问数据
singleDetailData:[], // 单团详细数据
group_select_mode: 'multiple',
groups: ['1', '2', '28', '7'],
searchValues: {
DepartmentList: ['1', '2', '28', '7'].map(kk => groupsMappedByKey[kk]),
},
};
getHostCaseData(groupBy) {
this.host_case_data.loading = true;
const date_picker_store = this.rootStore.date_picker_store;
let url = '/service-Analyse2/DDZCount';
url += '?DEI_SN=' + this.host_case_data.groups.toString();
url += '&ArriveDateStart=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&ArriveDateEnd=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
url += '&GroupBy=' + groupBy;
fetch(config.HT_HOST + url)
.then((response) => response.json())
.then((json) => {
runInAction(() => {
switch(groupBy){
case "1":
this.host_case_data.summaryData = json.result?json.result:[];
break;
case "2":
this.host_case_data.counselorData = json.result?json.result:[];
break;
case "3":
this.host_case_data.groupData = json.result?json.result:[];
break;
case "4":
this.host_case_data.singleDetailData = json.result?json.result:[];
this.host_case_data.loading = false;
break;
}
});
})
.catch((error) => {
this.host_case_data.loading = false;
console.log('fetch data failed', error);
});
};
// 东道主项目 end
// 销售-老客户
sales_regular_data = {
loading: false,
data: [],
@ -268,48 +462,122 @@ class CustomerStore {
rawData: [],
searchValues: {
DepartmentList: ['1', '2', '28', '7'].map(kk => groupsMappedByKey[kk]),
WebCode: undefined, // ['ALL'].map(kk => sitesMappedByCode[kk]),
WebCode: ['CHT','AH','JH','GH','ZWQD','GH_ZWQD_HW','GHKYZG','GHKYHW'].map(kk => sitesMappedByCode[kk]),
DateType: { key: 'applyDate', label: '提交日期'},
IncludeTickets: { key: '0', label: '不含门票' },
},
pivotData: {
operatorName: { loading: false, data: [], rawData: [], mergedData: [], filterColValues: [] },
country: { loading: false, data: [], rawData: [], mergedData: [], filterColValues: [] },
},
};
get_sales_regular_data = async (param) => {
this.sales_regular_data.loading = true;
const rawData = await getDetailData({...param, IncludeTickets: 1});
const filterHasOld = rawData.filter(ele => (ele.IsOld === '1' || ele.isCusCommend === '1')).map(e => ({...e, operatorNameB: e.operatorName.replace(/\([^)]*\)/gi, '').toLowerCase()}));
get_sales_regular_data_vs = async (param, pivotRow = 'operatorName') => {
this.sales_regular_data.pivotData[pivotRow].loading = true;
const hasCompare = !isEmpty(param.DateDiff1);
const [result1, result2] = await Promise.all([
this.get_sales_regular_data(param, pivotRow),
hasCompare ? this.get_sales_regular_data({...param, Date1: param.DateDiff1, Date2: param.DateDiff2}, pivotRow) : { mergeDataBySales: [], mergeDataBySalesAccount: [], filterHasOld: []},
]);
const allTypes = ['老客户', '老客户推荐'];
// 独立的账户
const allSales = Array.from(new Set([...result1.mergeDataBySales.map(row=>row[pivotRow]), ...result2.mergeDataBySales.map(row=>row[pivotRow])]));
const sales1 = groupBy(result1.mergeDataBySales, pivotRow);
const sales2 = groupBy(result2.mergeDataBySales, pivotRow);
const mergeDataBySales = allSales.reduce((r, sale) => {
const _default = { [pivotRow]: sale, rowLabel: sales1?.[sale]?.rowLabel || sales2?.[sale]?.rowLabel, children: sales1?.[sale]?.children || [], key: sale};
const operatorRow = {...(sales1?.[sale]?.[0] || _default), vsData: sales2?.[sale]?.[0] || {}};
// 展开的两项: '老客户', '老客户推荐'
const series1Children = sales1?.[sale]?.[0]?.children || [];
const series2Children = sales2?.[sale]?.[0]?.children || [];
const children = allTypes.reduce((r, type) => {
const _default = { [pivotRow]: type, rowLabel: type, key: type};
const _typeRow = series1Children.find(sc => sc[pivotRow] === type) || _default;
const _typeVSRow = series2Children.find(sc => sc[pivotRow] === type) || {};
return r.concat({..._typeRow, vsData: _typeVSRow});
}, []);
operatorRow.children = children;
return r.concat(operatorRow);
}, []);
// 合并顾问的账户
let mergeDataBySalesAccount = [];
if (pivotRow === 'operatorName') {
const allSalesMerged = Array.from(new Set([...result1.mergeDataBySalesAccount.map(row=>row.operatorName), ...result2.mergeDataBySalesAccount.map(row=>row.operatorName)]));
const salesM1 = groupBy(result1.mergeDataBySalesAccount, 'operatorName');
const salesM2 = groupBy(result2.mergeDataBySalesAccount, 'operatorName');
mergeDataBySalesAccount = allSalesMerged.reduce((r, sale) => {
const _default = { operatorName: sale, rowLabel: salesM1?.[sale]?.rowLabel || salesM2?.[sale]?.rowLabel, children: salesM1?.[sale]?.children || [], key: sale};
const operatorRow = {...(salesM1?.[sale]?.[0] || _default), vsData: salesM2?.[sale]?.[0] || {}};
// 展开的两项: '老客户', '老客户推荐'
const series1Children = salesM1?.[sale]?.[0]?.children || [];
const series2Children = salesM2?.[sale]?.[0]?.children || [];
const children = allTypes.reduce((r, type) => {
const _default = { operatorName: type, rowLabel: type, key: type};
const _typeRow = series1Children.find(sc => sc.operatorName === type) || _default;
const _typeVSRow = series2Children.find(sc => sc.operatorName === type) || {};
return r.concat({..._typeRow, vsData: _typeVSRow});
}, []);
operatorRow.children = children;
return r.concat(operatorRow);
}, []);
}
const filterColValues = uniqWith(
mergeDataBySales.map((rr) => ({ text: rr[pivotRow], value: rr[pivotRow] })),
(a, b) => JSON.stringify(a) === JSON.stringify(b)
).sort((a, b) => a.text.localeCompare(b.text, 'zh-CN'));
this.sales_regular_data.pivotData[pivotRow].loading = false;
this.sales_regular_data.pivotData[pivotRow].rawData = [].concat(result1.filterHasOld, result2.filterHasOld);
this.sales_regular_data.pivotData[pivotRow].data = mergeDataBySales;
this.sales_regular_data.pivotData[pivotRow].mergedData = isEmpty(mergeDataBySalesAccount) ? mergeDataBySales : mergeDataBySalesAccount;
this.sales_regular_data.pivotData[pivotRow].filterColValues = filterColValues;
const { data: hasOldData, } = pivotBy(filterHasOld, [['hasOld', 'operatorName'], [], []]);
const { data: IsOldData, } = pivotBy(filterHasOld.filter(ele => ele.IsOld === '1'), [['operatorName', 'IsOld_txt', ], [], []]);
const { data: isCusCommendData, } = pivotBy(filterHasOld.filter(ele => ele.isCusCommend === '1'), [['operatorName', 'isCusCommend_txt', ], [], []]);
};
get_sales_regular_data = async (param, pivotRow = 'operatorName') => {
const seriesKey = `${param.Date1}${param.Date2}`;
const rawData = await getDetailData({...param, });
const filterHasOld = rawData.filter(ele => (ele.IsOld === '1' || ele.isCusCommend === '1')).map(e => ({
...e,
seriesKey,
operatorNameB: e.operatorName.replace(/\([^)]*\)/gi, '').toLowerCase(),
}));
const { data: hasOldData, } = pivotBy(filterHasOld, [['hasOld', pivotRow], [], []]);
const { data: IsOldData, } = pivotBy(filterHasOld.filter(ele => ele.IsOld === '1'), [[pivotRow, 'IsOld_txt', ], [], []]);
const { data: isCusCommendData, } = pivotBy(filterHasOld.filter(ele => ele.isCusCommend === '1'), [[pivotRow, 'isCusCommend_txt', ], [], []]);
// console.log('IsOldData====', IsOldData, '\nisCusCommend', isCusCommendData);
// console.log('data====', rawData, '\ncolumnValues', columnValues, '\nsummaryRows', summaryRows, '\nsummaryColumns', summaryColumns, '\nsummaryMix', summaryMix, '\nhasOld',filterHasOld);
const mergeDataBySales = hasOldData.map((ele) => ({
...ele,
seriesKey,
children: [].concat(
IsOldData.filter((ele1) => ele1.operatorName === ele.operatorName).map(o => ({...o, operatorName: o.IsOld_txt, key: o.rowLabel})),
isCusCommendData.filter((ele2) => ele2.operatorName === ele.operatorName).map(o => ({...o, operatorName: o.isCusCommend_txt, key: o.rowLabel}))
IsOldData.filter((ele1) => ele1[pivotRow] === ele[pivotRow]).map(o => ({...o, [pivotRow]: o.IsOld_txt, key: o.rowLabel, seriesKey,})),
isCusCommendData.filter((ele2) => ele2[pivotRow] === ele[pivotRow]).map(o => ({...o, [pivotRow]: o.isCusCommend_txt, key: o.rowLabel, seriesKey,}))
),
}));
// 合并顾问的账户
const { data: hasOldDataSales, } = pivotBy(filterHasOld, [['hasOld', 'operatorNameB'], [], []]);
const { data: IsOldDataSales, } = pivotBy(filterHasOld.filter(ele => ele.IsOld === '1'), [['operatorNameB', 'IsOld_txt', ], [], []]);
const { data: isCusCommendDataSales, } = pivotBy(filterHasOld.filter(ele => ele.isCusCommend === '1'), [['operatorNameB', 'isCusCommend_txt', ], [], []]);
const mergeDataBySalesAccount = hasOldDataSales.map((ele) => ({
...ele,
operatorName: ele.operatorNameB,
children: [].concat(
IsOldDataSales.filter((ele1) => ele1.operatorNameB === ele.operatorNameB).map(o => ({...o, operatorName: o.IsOld_txt, key: o.rowLabel})),
isCusCommendDataSales.filter((ele2) => ele2.operatorNameB === ele.operatorNameB).map(o => ({...o, operatorName: o.isCusCommend_txt, key: o.rowLabel}))
),
}));
let mergeDataBySalesAccount = [];
if (pivotRow === 'operatorName') {
const { data: hasOldDataSales, } = pivotBy(filterHasOld, [['hasOld', 'operatorNameB'], [], []]);
const { data: IsOldDataSales, } = pivotBy(filterHasOld.filter(ele => ele.IsOld === '1'), [['operatorNameB', 'IsOld_txt', ], [], []]);
const { data: isCusCommendDataSales, } = pivotBy(filterHasOld.filter(ele => ele.isCusCommend === '1'), [['operatorNameB', 'isCusCommend_txt', ], [], []]);
mergeDataBySalesAccount = hasOldDataSales.map((ele) => ({
...ele,
operatorName: ele.operatorNameB,
seriesKey,
children: [].concat(
IsOldDataSales.filter((ele1) => ele1.operatorNameB === ele.operatorNameB).map(o => ({...o, operatorName: o.IsOld_txt, key: o.rowLabel, seriesKey,})),
isCusCommendDataSales.filter((ele2) => ele2.operatorNameB === ele.operatorNameB).map(o => ({...o, operatorName: o.isCusCommend_txt, key: o.rowLabel, seriesKey,}))
),
}));
}
// console.log('IsOldDataSales====', IsOldDataSales, '\nisCusCommendDataSales', isCusCommendDataSales);
// console.log('mergeDataBySalesAccount====', mergeDataBySalesAccount);
this.sales_regular_data.loading = false;
this.sales_regular_data.data = mergeDataBySales;
this.sales_regular_data.mergedData = mergeDataBySalesAccount;
this.sales_regular_data.rawData = filterHasOld;
return { mergeDataBySales, mergeDataBySalesAccount, filterHasOld };
};
setSearchValues(obj, values, target) {

@ -0,0 +1,200 @@
import { makeAutoObservable, runInAction, toJS } from 'mobx';
import { fetchJSON } from '../utils/request';
import { isEmpty, sortDescBy, objectMapper, groupBy, pick, unique, cloneDeep, omit, fixTo2Decimals } from '../utils/commons';
import { groupsMappedByCode, dataFieldAlias } from './../libs/ht';
import { DATE_FORMAT, SMALL_DATETIME_FORMAT } from './../config';
import moment from 'moment';
const fetchHotelData = async (param) => {
const defaultParam = {
DEI_SN: '',
City: '',
OrderState: '',
BookingType: '-1',
RecommendedLevel: '-1',
Star: '-1',
ArriveDateCheck: '0',
ArriveDateStart: '',
ArriveDateEnd: '',
ConfirmDateCheck: '0',
ConfirmDateStart: '',
ConfirmDateEnd: '',
Compare: '0',
CompareDateStart: '',
CompareDateEnd: '',
Area: '-1',
};
const json = await fetchJSON('/service-Analyse2/HotelReservation', { ...defaultParam, ...param });
return json.errcode === 0 ? json.result || [] : [];
};
const fetchCruiseData = async (param) => {
const defaultParam = {
DEI_SN: '',
OrderState: '', // 0: 不成行 1: 成行
ArriveDateStart: '',
ArriveDateEnd: '',
Compare: '',
CompareDateStart: '',
CompareDateEnd: '',
BookingType: '', // 0: 非单订三峡1: 单订三峡
ProductName: '',
Direction: '', // 1: 上水 2: 下水
VEI_SN: '-1', // 只要列出常用游船供应商选择
RoomNumStart: '0',
RoomNumEnd: '',
PersonNumStart: '0',
PersonNumEnd: '',
Country: '-1',
};
const json = await fetchJSON('/service-Analyse2/CruiseReservation', { ...defaultParam, ...param });
return json.errcode === 0 ? json.result || [] : [];
};
const paramKeyMapped = {
'DateType': [
{ key: 'ArriveDateCheck', transform: (val) => (val === 'startDate' ? '1' : '0') },
{ key: 'ConfirmDateCheck', transform: (val) => (val === 'confirmDate' ? '1' : '0') },
// { key: 'ApplyDateCheck', transform: (val) => (val === 'applyDate' ? '1' : '0') },
],
'DepartmentList': { key: 'DEI_SN' },
'orderStatus': { key: 'OrderState' },
'Date1': [{ key: 'ArriveDateStart' }, { key: 'ConfirmDateStart' }],
'Date2': [{ key: 'ArriveDateEnd' }, { key: 'ConfirmDateEnd' }],
'DateDiff1': { key: 'CompareDateStart' },
'DateDiff2': { key: 'CompareDateEnd' },
'keyword': { key: 'ProductName' },
'agency': { key: 'VEI_SN' },
'cruiseDirection': { key: 'Direction' },
'cruiseBookType': { key: 'BookingType' },
'hotelStar': { key: 'Star' },
'hotelRecommandRate': { key: 'RecommendedLevel' },
'hotelBookType': { key: 'BookingType' },
'country': { key: 'Country' },
'countryArea': { key: 'Area', transform: (val) => (val === 'china' ? '1' : val === 'foreign' ? '0' : '-1') },
};
class HotelCruise {
constructor(appStore) {
this.appStore = appStore;
makeAutoObservable(this);
}
async getCruiseData(param = {}) {
this.cruise.loading = true;
this.cruise.dataSource = [];
const _queryParam = objectMapper(param, paramKeyMapped);
const queryParam = omit({ ...this.searchValuesToSub, ..._queryParam }, Object.keys(paramKeyMapped));
queryParam.Compare = isEmpty(param.DateDiff1) ? '' : '1';
const res = await fetchCruiseData(queryParam);
const resCP =
queryParam.Compare === ''
? res
: (res || []).map((ele) => ({
...ele,
// 计算 增长率 = (当前值 - 上次值) / 上次值 * 100
TotalNumPercent: ele.CPTotalNum ? fixTo2Decimals(((ele.TotalNum - ele.CPTotalNum) / ele.CPTotalNum) * 100) : '-',
TotalPersonNumPercent: ele.CPTotalPersonNum ? fixTo2Decimals(((ele.TotalPersonNum - ele.CPTotalPersonNum) / ele.CPTotalPersonNum) * 100) : '-',
TotalProfitPercent: ele.CPTotalProfit ? fixTo2Decimals(((ele.TotalProfit - ele.CPTotalProfit) / ele.CPTotalProfit) * 100) : '-',
}));
const summaryRow = ['TotalNum', 'TotalPersonNum', 'TotalProfit', 'CPTotalNum', 'CPTotalPersonNum', 'CPTotalProfit'].reduce(
(r, skey) => ({
...r,
[skey]: resCP.reduce((a, c) => a + c[skey], 0),
}),
{ ProductName: '合计' }
);
const summaryDelta = ['TotalNum', 'TotalPersonNum', 'TotalProfit'].reduce(
(r, skey) => ({
...r,
[`${skey}Percent`]: queryParam.Compare === '' ? null : fixTo2Decimals(((summaryRow[skey] - summaryRow[`CP${skey}`]) / summaryRow[`CP${skey}`]) * 100),
}),
{}
);
runInAction(() => {
this.cruise.loading = false;
this.cruise.dataSource = resCP;
this.cruise.summaryRow = { ...summaryRow, ...summaryDelta };
});
return this.cruise;
}
async getHotelData(param = {}) {
this.hotel.loading = true;
this.hotel.dataSource = [];
const _queryParam = objectMapper(param, paramKeyMapped);
const queryParam = omit({ ...this.searchValuesToSub, ..._queryParam }, Object.keys(paramKeyMapped));
queryParam.Compare = isEmpty(param.DateDiff1) ? '0' : '1';
const _res = await fetchHotelData(queryParam);
const res = (_res || []).map((ele) => ({ ...ele, RecommendRate_100: fixTo2Decimals(ele.RecommendRate * 100) + '%' }));
const resCP =
queryParam.Compare === '0'
? res
: (res || []).map((ele) => ({
...ele,
TotalNumPercent: ele.CPTotalNum ? fixTo2Decimals(((ele.TotalNum - ele.CPTotalNum) / ele.CPTotalNum) * 100) : '-',
RecomendNumPercent: ele.CPRecomendNum ? fixTo2Decimals(((ele.RecomendNum - ele.CPRecomendNum) / ele.CPRecomendNum) * 100) : '-',
RecommendRateDelta: fixTo2Decimals((ele.RecommendRate - (ele.CPRecommendRate || 0)) * 100),
CPRecommendRate_100: fixTo2Decimals(ele.CPRecommendRate * 100) + '%',
}));
const summaryRow = ['TotalNum', 'RecomendNum', ].reduce(
(r, skey) => ({
...r,
[skey]: resCP.reduce((a, c) => a + c[skey], 0),
[`CP${skey}`]: resCP.reduce((a, c) => a + c[`CP${skey}`], 0),
}),
{ CityName: '合计' }
);
summaryRow.RecommendRate = fixTo2Decimals(summaryRow.RecomendNum/summaryRow.TotalNum);
summaryRow.RecommendRate_100 = fixTo2Decimals(summaryRow.RecommendRate * 100) + '%';
summaryRow.CPRecommendRate = fixTo2Decimals(summaryRow.CPRecomendNum/summaryRow.CPTotalNum);
summaryRow.CPRecommendRate_100 = fixTo2Decimals(summaryRow.CPRecommendRate * 100) + '%';
const summaryDelta = ['TotalNum', 'RecomendNum', ].reduce(
(r, skey) => ({
...r,
[`${skey}Percent`]: queryParam.Compare === '0' ? null : fixTo2Decimals(((summaryRow[skey] - summaryRow[`CP${skey}`]) / summaryRow[`CP${skey}`]) * 100),
}),
{}
);
summaryDelta.RecommendRateDelta = queryParam.Compare === '0' ? undefined : fixTo2Decimals((summaryRow.RecommendRate - (summaryRow.CPRecommendRate || 0)) * 100);
runInAction(() => {
this.hotel.loading = false;
this.hotel.dataSource = resCP;
this.hotel.summaryRow = { ...summaryRow, ...summaryDelta };
});
}
searchValues = {
date: moment(),
DateType: { key: 'confirmDate', label: '确认日期' },
WebCode: { key: '', label: '所有来源' },
// IncludeTickets: { key: '1', label: '含门票'},
DepartmentList: [{ key: '', label: '所有小组' }],
operator: '-1',
opisn: '-1',
};
searchValuesToSub = {};
setSearchValues(obj, values) {
this.searchValues = { ...this.searchValues, ...values };
this.searchValuesToSub = obj;
}
cruise = { loading: false, dataSource: [], summaryRow: {} };
hotel = { loading: false, dataSource: [], summaryRow: {} };
resetData = () => {
this.results.loading = false;
for (const key of Object.keys(this.results)) {
if (key !== 'loading') {
this.results[key] = [];
}
}
for (const key of Object.keys(this.hotel)) {
if (key !== 'loading') {
this.hotel[key] = [];
}
}
};
}
export default HotelCruise;

@ -18,6 +18,7 @@ import DataPivot from './DataPivot';
import MeetingData from './MeetingData2024';
import MeetingData2025 from './MeetingData2025';
import SalesCRMData from './SalesCRMData';
import HotelCruise from './HotelCruise';
class Index {
constructor() {
this.dashboard_store = new DashboardStore(this);
@ -39,6 +40,7 @@ class Index {
this.MeetingDataStore = new MeetingData(this);
this.MeetingData2025Store = new MeetingData2025(this);
this.SalesCRMDataStore = new SalesCRMData(this);
this.HotelCruiseStore = new HotelCruise(this);
makeAutoObservable(this);
}

@ -1,7 +1,7 @@
import { makeAutoObservable, runInAction, toJS } from 'mobx';
import { makeAutoObservable, runInAction } from 'mobx';
import { fetchJSON } from '../utils/request';
import { isEmpty, sortDescBy, objectMapper, groupBy, pick, unique, cloneDeep } from '../utils/commons';
import { groupsMappedByCode, dataFieldAlias } from './../libs/ht';
import { isEmpty, sortDescBy, groupBy, pick, unique } from '../utils/commons';
import { groupsMappedByCode } from './../libs/ht';
import { DATE_FORMAT, SMALL_DATETIME_FORMAT } from './../config';
import moment from 'moment';
@ -33,6 +33,20 @@ const fetchProcessData = async (param) => {
return json.errcode === 0 ? json.result : [];
};
const fetchRiskDetailData = async (param) => {
const defaultParam = {
opisn: -1,
DateType: '',
WebCode: 'All',
DepartmentList: '',
Date1: '',
Date2: '',
IncludeTickets: '1',
};
const json = await fetchJSON('/service-Analyse2/sales_crm_process_detail', {...defaultParam, ...param});
return json.errcode === 0 ? json.result : [];
};
class SalesCRMData {
constructor(appStore) {
this.appStore = appStore;
@ -42,7 +56,18 @@ class SalesCRMData {
async get90n180Data(param = {}) {
const retProps = param?.retLabel || '';
const retKey = param.groupDateType === '' ? (param.groupType === 'overview' ? 'dataSource' : 'details') : 'byDate';
let retKey = param.groupDateType === '' ? (param.groupType === 'overview' ? 'dataSource' : 'details') : 'byDate';
retKey = param.opisn ? `operator_${param.opisn}`: retKey;
if (param.opisn ) {
if (!isEmpty(this.results.details)) {
const _this_opi_row = this.results.details.filter(ele => ele.groupsKey === param.opisn);
this.results[retKey] = _this_opi_row;
return;
}
if (!isEmpty(this.results[retKey])) {
return;
}
}
this.results.loading = true;
const date90=this.searchValues.date90;
const date180=this.searchValues.date180;
@ -62,17 +87,21 @@ class SalesCRMData {
};
});
// console.log(result2, '+++++ +++', retKey);
// console.log(this.results[retKey].length);
// console.log(this.results[retKey]?.length);
runInAction(() => {
this.results.loading = false;
this.results[retKey] = [].concat(this.results[retKey], result2);
this.results[retKey] = [].concat((this.results[retKey] || []), result2);
});
return this.results;
}
async getResultData(param = {}) {
const retKey = param.groupDateType === '' ? 'byOperator' : 'byDate';
let retKey = param.groupDateType === '' ? 'byOperator' : 'byDate';
retKey = param.opisn ? `operator_byDate_${param.opisn}`: retKey;
if (!isEmpty(this.results[retKey])) {
return;
}
this.results.loading = true;
this.results[retKey] = [];
const res = await fetchResultsData({ ...this.searchValuesToSub, ...param });
@ -85,19 +114,42 @@ class SalesCRMData {
async getProcessData(param = {}) {
// const retKey = param.groupDateType === '' ? 'byOperator' : 'byDate';
const retKey = param.groupDateType === '' ? (param.groupType !== 'operator' ? 'dataSource' : 'details') : 'byDate';
let retKey = param.groupDateType === '' ? (param.groupType !== 'operator' ? 'dataSource' : 'details') : 'byDate';
retKey = param.opisn ? `operator_${param.opisn}`: retKey;
if (param.opisn) {
if (!isEmpty(this.process.details)) {
const _this_opi_row = this.process.details.filter(ele => ele.groupsKey === param.opisn);
this.process[retKey] = _this_opi_row;
return;
}
if (!isEmpty(this.process[retKey])) {
return;
}
}
this.process.loading = true;
this.process[retKey] = [];
const res = await fetchProcessData({ ...this.searchValuesToSub, ...param });
runInAction(() => {
this.process.loading = false;
this.process[retKey] = [].concat(this.process[retKey], res);
// this.process[retKey] =retKey === 'byOperator' ? res.filter(ele => ele.)
});
}
async getRiskDetailData(param = {}) {
this.risk.loading = true;
this.risk.dataSource = [];
const res = await fetchRiskDetailData({ ...this.searchValuesToSub, ...param });
runInAction(() => {
this.risk.loading = false;
this.risk.dataSource = res;
this.risk.byLostType = groupBy(res, 'lost_type');
});
}
searchValues = {
date: moment(),
Date1: moment().startOf("week").subtract(7, "days"),
Date2: moment().endOf("week").subtract(7, "days"),
DateType: { key: 'applyDate', label: '提交日期'},
WebCode: { key: 'All', label: '所有来源' },
// IncludeTickets: { key: '1', label: '含门票'},
@ -114,7 +166,16 @@ class SalesCRMData {
}
};
searchValuesToSub = {};
searchValuesToSub = {
date: moment().format(DATE_FORMAT),
Date1: moment().startOf("week").subtract(7, "days").format(DATE_FORMAT),
Date2: moment().endOf("week").subtract(7, "days").format(SMALL_DATETIME_FORMAT),
DateType: 'applyDate',
DepartmentList: groupsMappedByCode.GH.value,
WebCode: 'All',
operator: '-1',
opisn: '-1',
};
setSearchValues(obj, values) {
this.searchValues = { ...this.searchValues, ...values };
@ -128,21 +189,20 @@ class SalesCRMData {
Date2: (values.date.clone()).subtract(50, 'days').format(SMALL_DATETIME_FORMAT),
};
}
this.searchValuesToSub = obj;
this.searchValuesToSub = {...this.searchValuesToSub, ...obj};
}
results = { loading: false, dataSource: [], details: [], byDate: [], byOperator: [] };
process = { 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] = [];
}
risk = { loading: false, dataSource: [], byLostType: {}, };
resetData = (rootKey = '') => {
if (rootKey === '') {
return false;
}
for (const key of Object.keys(this.process)) {
this[rootKey].loading = false;
for (const key of Object.keys(this[rootKey])) {
if (key !== 'loading') {
this.process[key] = [];
this[rootKey][key] = [];
}
}
};

@ -72,6 +72,14 @@ export function formatPercent(number) {
return Math.round(number * 100) + "%";
}
export function formatPercentToFloat(number) {
return parseFloat((number * 100).toFixed(2)) + "%";
}
export function percentToDecimal(number) {
return parseFloat(number) / 100;
}
export function formatDate(date) {
if (isEmpty(date)) {
return "NaN";

@ -32,7 +32,7 @@ const AgentGroupCount = () => {
fieldProps: {
DepartmentList: { show_all: true, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
dates: { hide_vs: true },
years: { hide_vs: true },
departureDateType: { disabledKeys: ['applyDate'] },
},
}}

@ -0,0 +1,181 @@
import React, { useContext } from 'react';
import { observer } from 'mobx-react';
import { stores_Context } from '../config';
import { Row, Col, Table, Space, Typography, Divider } from 'antd';
import SearchForm from './../components/search/SearchForm';
import { VSTag, TableExportBtn } from './../components/Data';
import { CruiseAgency } from './../libs/ht';
const { Text } = Typography;
export default observer((props) => {
const { date_picker_store: searchFormStore } = useContext(stores_Context);
const { HotelCruiseStore, date_picker_store } = useContext(stores_Context);
const { loading, dataSource, summaryRow } = HotelCruiseStore.cruise;
const { formValues, siderBroken } = searchFormStore;
const tableSorter = (a, b, colName) => a[colName] - b[colName];
const tableExportDataRow = (col1, col2) => [col1, col2].filter((r) => r).join(' VS ');
const tableProps = {
size: 'small',
bordered: true,
pagination: false,
columns: [
{ title: '产品',
sorter: (a, b) => a.ProductName.localeCompare(b.ProductName, 'zh-CN'),children: [{ title: summaryRow.ProductName, dataIndex: 'ProductName', key: 'ProductName' }] },
{
title: '房间数',
sorter: (a, b) => tableSorter(a, b, 'TotalNum'),
children: [
{
title: (
<>
<Space direction={'vertical'}>
<Text strong>
{summaryRow.TotalNum}
{summaryRow.TotalNumPercent ? <Text type="secondary"> VS {summaryRow.CPTotalNum}</Text> : null}
</Text>
{summaryRow.TotalNumPercent && <VSTag diffPercent={summaryRow.TotalNumPercent} />}
</Space>
</>
),
titleX: tableExportDataRow(summaryRow.TotalNum, summaryRow.CPTotalNum),
dataExport: (v, r) => tableExportDataRow(r.TotalNum, r.CPTotalNum),
dataIndex: 'TotalNum',
key: 'TotalNum',
render: (v, r) => (
<>
<Space direction={'vertical'}>
<Text strong>
{v}
{r.CPTotalNum ? <Text type="secondary"> VS {r.CPTotalNum}</Text> : null}
</Text>
{r.CPTotalNum && <VSTag diffPercent={r.TotalNumPercent} />}
</Space>
</>
),
},
],
},
{
title: '人数',
sorter: (a, b) => tableSorter(a, b, 'TotalPersonNum'),
children: [
{
title: (
<>
<Space direction={'vertical'}>
<Text strong>
{summaryRow.TotalPersonNum}
{summaryRow.TotalPersonNumPercent ? <Text type="secondary"> VS {summaryRow.CPTotalPersonNum}</Text> : null}
</Text>
{summaryRow.TotalPersonNumPercent && <VSTag diffPercent={summaryRow.TotalPersonNumPercent} />}
</Space>
</>
),
titleX: tableExportDataRow(summaryRow.TotalPersonNum, summaryRow.CPTotalPersonNum),
dataExport: (v, r) => tableExportDataRow(r.TotalPersonNum, r.CPTotalPersonNum),
dataIndex: 'TotalPersonNum',
key: 'TotalPersonNum',
render: (v, r) => (
<>
<Space direction={'vertical'}>
<Text strong>
{v}
{r.CPTotalPersonNum ? <Text type="secondary"> VS {r.CPTotalPersonNum}</Text> : null}
</Text>
{r.CPTotalNum && <VSTag diffPercent={r.TotalPersonNumPercent} />}
</Space>
</>
),
},
],
},
{
title: '总利润',
sorter: (a, b) => tableSorter(a, b, 'TotalProfit'),
children: [
{
title: (
<>
<Space direction={'vertical'}>
<Text strong>
{summaryRow.TotalProfit}
{summaryRow.TotalProfitPercent ? <Text type="secondary"> VS {summaryRow.CPTotalProfit}</Text> : null}
</Text>
{summaryRow.TotalProfitPercent && <VSTag diffPercent={summaryRow.TotalProfitPercent} />}
</Space>
</>
),
titleX: tableExportDataRow(summaryRow.TotalProfit, summaryRow.CPTotalProfit),
dataExport: (v, r) => tableExportDataRow(r.TotalProfit, r.CPTotalProfit),
dataIndex: 'TotalProfit',
key: 'TotalProfit',
render: (v, r) => (
<>
<Space direction={'vertical'}>
<Text strong>
{v}
{r.CPTotalProfit ? <Text type="secondary"> VS {r.CPTotalProfit}</Text> : null}
</Text>
{r.CPTotalNum && <VSTag diffPercent={r.TotalProfitPercent} />}
</Space>
</>
),
},
],
},
// { title: '', dataIndex: '', key: '' },
// { title: '', dataIndex: '', key: '' },
// { title: '', dataIndex: '', key: '' },
],
};
return (
<>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col md={24} lg={24} xxl={24}>
<SearchForm
defaultValue={{
initialValue: {
...date_picker_store.formValues,
...HotelCruiseStore.searchValues,
},
// 'countryArea',
shows: [
'DepartmentList',
'orderStatus',
'dates',
'keyword',
'cruiseDirection',
'agency',
'cruiseBookType',
'country', // 'roomsRange', 'personRange'
],
sort: { keyword: 101, agency: 110, cruiseDirection: 102, country: 104 },
fieldProps: {
keyword: { placeholder: '产品名', col: 4 },
DepartmentList: { show_all: true, mode: 'multiple' },
orderStatus: { show_all: true },
cruiseDirection: { show_all: true },
agency: { defaultOptions: CruiseAgency, autoGet: false },
// years: { hide_vs: false },
},
}}
onSubmit={(_err, obj, form) => {
HotelCruiseStore.setSearchValues(obj, form);
HotelCruiseStore.getCruiseData(obj);
}}
/>
</Col>
</Row>
<section>
<Divider orientation="right" >
<TableExportBtn label={'游船'} {...{ columns: tableProps.columns, dataSource }} />
</Divider>
<Table {...tableProps} {...{ loading, dataSource }} rowKey={(record) => record.ProductName} />
</section>
</>
);
});

@ -21,6 +21,10 @@ const filterFields = [
{ key: 'COLI_LineClass', label: '页面渠道' },
{ key: 'guestGroupType', label: '客群类别' },
{ key: 'travelMotivation', label: '出行目的' },
{ key: 'startMonth', label: '出行日期-月份' },
{ key: 'startYearMonth', label: '出行日期-年月' },
{ key: 'applyMonth', label: '预订日期-月份' },
{ key: 'applyYearMonth', label: '预订日期-年月' },
{ key: 'operatorName', label: '顾问' },
{ key: 'WebCode', label: '来源站点' },
{ key: 'IsOld_txt', label: '是否老客户' },
@ -195,7 +199,10 @@ export default observer((props) => {
setPivotRow({});
setPivotRowDataSource([]);
//
const sortColData = summaryColumns.sort(sortBy(defaultValKey)).reverse();
const _col1 = pivotDateColumns[1][0] || '';
const _sortByDateOrVal = (_col1.includes('Month') || _col1.includes('Date')) ? _col1 : defaultValKey;
let sortColData = summaryColumns.sort(sortBy(_sortByDateOrVal));
sortColData = _sortByDateOrVal === defaultValKey ? sortColData.reverse() : sortColData;
const colDataMapped = isEmpty(pivotDateColumns[1]) ? sortColData[0] : sortColData.reduce((r, v) => ({...r, [v[pivotDateColumns[1][0]]]: v}), {});
setPivotTableColumnSummary(colDataMapped);
//
@ -459,11 +466,11 @@ export default observer((props) => {
dataIndex: 'applyDate',
key: 'applyDate',
},
// {
// title: '',
// dataIndex: 'CGI_ArriveDate',
// key: 'CGI_ArriveDate',
// },
{
title: '出发日期',
dataIndex: 'startDate',
key: 'startDate',
},
],
};
@ -709,7 +716,6 @@ export default observer((props) => {
/>
</Spin>
</section>
{ !pivotDateColumns[0].includes('destinationCountry_AsJOSN') && !pivotDateColumns[0].includes('destinations_AsJOSN') &&
<section>
<Spin spinning={loading}>
<h3>
@ -725,7 +731,6 @@ export default observer((props) => {
<Table {...detailsTableProps} dataSource={pivotRowDataSource} components={{ body: { cell: TdCell } }} />
</Spin>
</section>
}
</div>
);
});

@ -30,7 +30,7 @@ const DestinationGroupCount = () => {
DepartmentList: { show_all: true, mode: 'multiple' },
orderStatus: { show_all: true },
countryArea: { show_all: false },
dates: { hide_vs: true },
years: { hide_vs: true },
departureDateType: { disabledKeys: ['applyDate'] },
},
}}

@ -1,21 +1,24 @@
import { useContext, useEffect } from 'react';
import { Row, Col, Space, Table, List } from 'antd';
import { Row, Col, Space, Table, List, Typography, Divider } from 'antd';
import { stores_Context } from '../config';
import { observer } from 'mobx-react';
import { NavLink, useParams } from 'react-router-dom';
import 'moment/locale/zh-cn';
import SearchForm from './../components/search/SearchForm';
import { TableExportBtn } from './../components/Data';
const DestinationGroupList = () => {
const { destinationId } = useParams();
const { customerServicesStore, date_picker_store } = useContext(stores_Context);
useEffect(() => {
customerServicesStore.fetchDistGroupInfoByCountry(destinationId);
customerServicesStore.fetchGroupListByDestinationId(destinationId);
}, []);
const destinationGroupList = customerServicesStore.destinationGroupList;
const destinationGroupListColumns = customerServicesStore.destinationGroupListColumns;
const nationality_count_data = customerServicesStore.nationality_count_data;
const { inProgress } = customerServicesStore;
return (
@ -33,7 +36,7 @@ const DestinationGroupList = () => {
countryArea: { key: 'china', label: '国内' },
...customerServicesStore.searchValues,
},
shows: ['departureDateType', 'DepartmentList', 'dates'],
shows: ['departureDateType', 'DepartmentList', 'dates', 'orderStatus'],
fieldProps: {
DepartmentList: { show_all: true },
orderStatus: { show_all: true },
@ -45,11 +48,31 @@ const DestinationGroupList = () => {
onSubmit={(_err, obj, form) => {
customerServicesStore.setSearchValues(obj, form);
customerServicesStore.fetchGroupListByDestinationId(destinationId);
customerServicesStore.fetchDistGroupInfoByCountry(destinationId);
customerServicesStore.fetchDestinationGroupCount();
}}
/>
</Col>
</Row>
<Row>
<Col span={24}>
<Typography.Title level={3}>国籍统计</Typography.Title>
<Divider orientation="right" plain>
<TableExportBtn label={'国籍统计'} {...{ columns: nationality_count_data.destinationGroupByCountryListColumns, dataSource: nationality_count_data.destinationGroupByCountryList }} />
</Divider>
<Table
sticky
id="destinationGroupByCountry"
dataSource={nationality_count_data.destinationGroupByCountryList}
columns={nationality_count_data.destinationGroupByCountryListColumns}
size="small"
rowKey={(record) => record.key}
loading={nationality_count_data.loading}
pagination={false}
scroll={{ x: 1000 }}
/>
</Col>
</Row>
<Row>
<Col span={24}>
<Table

@ -0,0 +1,147 @@
import { useContext } from 'react';
import { Row, Col, Typography, Space, Table, Divider } from 'antd';
import { stores_Context } from '../config';
import { observer } from 'mobx-react';
import 'moment/locale/zh-cn';
import SearchForm from '../components/search/SearchForm';
import { TableExportBtn } from '../components/Data';
import * as comm from '../utils/commons';
const HostCaseCount = () => {
const { customer_store, date_picker_store } = useContext(stores_Context);
const host_case_data = customer_store.host_case_data;
const columnsList = [
{
title: '团数',
dataIndex: 'TotalGroupNum',
key: 'TotalGroupNum',
sorter: (a, b) => parseInt(a.TotalGroupNum) - parseInt(b.TotalGroupNum),
},
{
title: '人数',
dataIndex: 'TotalPersonNum',
key: 'TotalPersonNum',
sorter: (a, b) => parseInt(a.TotalPersonNum) - parseInt(b.TotalPersonNum),
},
{
title: '计费团天数',
dataIndex: 'TotalDays',
key: 'TotalDays',
sorter: (a, b) => parseInt(a.TotalDays) - parseInt(b.TotalDays),
},
{
title: '交易额',
dataIndex: 'TotalPrice',
key: 'TotalPrice',
sorter: (a, b) => parseInt(a.TotalPrice) - parseInt(b.TotalPrice),
},
];
//
const getFiltersData=(filterData,filterKey)=>{
return comm.uniqWith(filterData.map(rr => ({ text: rr[filterKey], value: rr[filterKey] })),
(a, b) => JSON.stringify(a) === JSON.stringify(b)).sort((a, b) => a.text.localeCompare(b.text));
};
//
const allOPIGroup = getFiltersData(host_case_data.summaryData,"GroupBy");
//
const allOPIConsultant = getFiltersData(host_case_data.counselorData,"GroupBy");
//
const allOPIDetailConsultant = getFiltersData(host_case_data.singleDetailData,"OPI_Name");
const summaryColumnsList = [{
title: '',
dataIndex: 'GroupBy',
key: 'GroupBy',
}, ...columnsList];
const groupColumnsList = [{
title: '组名',
dataIndex: 'GroupBy',
key: 'GroupBy',
filters: allOPIGroup,
onFilter: (value, record) => record.GroupBy === value,
filterSearch: true,
}, ...columnsList];
const counselorColumnsList = [{
title: '顾问名',
dataIndex: 'GroupBy',
key: 'GroupBy',
filters: allOPIConsultant,
onFilter: (value, record) => record.GroupBy === value,
filterSearch: true,
}, ...columnsList];
const singleDetailColumnsList = [{
title: '团名',
dataIndex: 'GroupBy',
key: 'GroupBy',
},
{
title: '顾问名',
dataIndex: 'OPI_Name',
key: 'OPI_Name',
filters: allOPIDetailConsultant,
onFilter: (value, record) => record.OPI_Name === value,
filterSearch: true,
},
...columnsList.slice(1)];
const renderRow=(rowColumns,rowDataSource,title)=>{
return(
<Row>
<Col span={24}>
<Typography.Title level={3}>{title}</Typography.Title>
<Divider orientation="right" plain>
<TableExportBtn label={title} {...{ columns: rowColumns, dataSource: rowDataSource }} />
</Divider>
<Table
sticky
id={`${rowColumns}`}
dataSource={rowDataSource}
columns={rowColumns}
size="small"
rowKey={(record) => record.key}
loading={host_case_data.loading}
pagination={false}
scroll={{ x: 1000 }}
/>
</Col>
</Row>
);
};
return (
<>
<Space direction="vertical" style={{ width: '100%' }}>
<Row gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"} >
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...date_picker_store.formValues,
...host_case_data.searchValues,
},
shows: ['DepartmentList', 'dates'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form) => {
customer_store.setSearchValues(obj, form,"host_case_data");
customer_store.getHostCaseData("1");
customer_store.getHostCaseData("2");
customer_store.getHostCaseData("3");
customer_store.getHostCaseData("4");
}}
/>
</Col>
</Row>
{renderRow(summaryColumnsList,host_case_data.summaryData,'东道主项目汇总')}
{renderRow(groupColumnsList,host_case_data.groupData,'东道主项目小组统计')}
{renderRow(counselorColumnsList,host_case_data.counselorData,'东道主项目顾问统计')}
{renderRow(singleDetailColumnsList,host_case_data.singleDetailData,'单团明细')}
</Space>
</>
);
};
export default observer(HostCaseCount);

@ -0,0 +1,171 @@
import React, { useContext } from 'react';
import { observer } from 'mobx-react';
import { stores_Context } from '../config';
import { Row, Col, Table, Space, Typography, Divider } from 'antd';
import SearchForm from './../components/search/SearchForm';
import { VSTag, TableExportBtn } from './../components/Data';
const { Text } = Typography;
export default observer((props) => {
const { date_picker_store: searchFormStore } = useContext(stores_Context);
const { date_picker_store, HotelCruiseStore } = useContext(stores_Context);
const { loading, dataSource, summaryRow } = HotelCruiseStore.hotel;
const { formValues, siderBroken } = searchFormStore;
const tableSorter = (a, b, colName) => a[colName] - b[colName];
const tableExportDataRow = (col1, col2) => [col1, col2].filter((r) => r).join(' VS ');
const tableProps = {
size: 'small',
bordered: true,
pagination: false,
columns: [
{
title: '目的地',
sorter: (a, b) => a.CityName.localeCompare(b.CityName, 'zh-CN'),
children: [{ title: summaryRow.CityName, dataIndex: 'CityName', key: 'CityName' }],
},
{
title: '总间夜',
sorter: (a, b) => tableSorter(a, b, 'TotalNum'),
children: [
{
title: (
<>
<Space direction={'vertical'}>
<Text strong>
{summaryRow.TotalNum}
{summaryRow.TotalNumPercent ? <Text type="secondary"> VS {summaryRow.CPTotalNum}</Text> : null}
</Text>
{summaryRow.TotalNumPercent && <VSTag diffPercent={summaryRow.TotalNumPercent} />}
</Space>
</>
),
titleX: tableExportDataRow(summaryRow.TotalNum, summaryRow.CPTotalNum),
dataIndex: 'TotalNum',
key: 'TotalNum',
dataExport: (v, r) => tableExportDataRow(r.TotalNum, r.CPTotalNum),
render: (v, r) => (
<>
<Space direction={'vertical'}>
<Text strong>
{v}
{r.CPTotalNum ? <Text type="secondary"> VS {r.CPTotalNum}</Text> : null}
</Text>
{r.CPTotalNum && <VSTag diffPercent={r.TotalNumPercent} />}
</Space>
</>
),
},
],
},
{
title: '主推',
sorter: (a, b) => tableSorter(a, b, 'RecomendNum'),
children: [
{
title: (
<>
<Space direction={'vertical'}>
<Text strong>
{summaryRow.RecomendNum}
{summaryRow.RecomendNumPercent ? <Text type="secondary"> VS {summaryRow.CPRecomendNum}</Text> : null}
</Text>
{summaryRow.RecomendNumPercent && <VSTag diffPercent={summaryRow.RecomendNumPercent} />}
</Space>
</>
),
titleX: tableExportDataRow(summaryRow.RecomendNum, summaryRow.CPRecomendNum),
dataIndex: 'RecomendNum',
key: 'RecomendNum',
dataExport: (v, r) => tableExportDataRow(r.RecomendNum, r.CPRecomendNum),
render: (v, r) => (
<>
<Space direction={'vertical'}>
<Text strong>
{v}
{r.CPRecomendNum ? <Text type="secondary"> VS {r.CPRecomendNum}</Text> : null}
</Text>
{r.CPRecomendNum && <VSTag diffPercent={r.RecomendNumPercent} />}
</Space>
</>
),
},
],
},
{
title: '使用比例',
children: [
{
title: (
<>
<Space direction={'vertical'}>
<Text strong>
{summaryRow.RecommendRate_100}
{summaryRow.RecommendRateDelta ? <Text type="secondary"> VS {summaryRow.CPRecommendRate_100}</Text> : null}
</Text>
{summaryRow.RecommendRateDelta && <VSTag diffPercent={summaryRow.RecommendRateDelta} />}
</Space>
</>
),
titleX: tableExportDataRow(summaryRow.RecommendRate_100, summaryRow.CPRecommendRate_100),
dataIndex: 'RecommendRate_100',
key: 'RecommendRate_100',
dataExport: (v, r) => tableExportDataRow(r.RecommendRate_100, r.CPRecommendRate_100),
render: (v, r) => (
<>
<Space direction={'vertical'}>
<Text strong>
{v}
{r.RecommendRateDelta !== undefined && <Text type="secondary"> VS {r.CPRecommendRate_100}</Text>}
</Text>
{r.RecommendRateDelta !== undefined && <VSTag diffPercent={r.RecommendRateDelta} />}
</Space>
</>
),
},
],
},
],
};
return (
<>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col md={24} lg={24} xxl={24}>
<SearchForm
defaultValue={{
initialValue: {
...date_picker_store.formValues,
...HotelCruiseStore.searchValues,
},
// 'countryArea', 'DateType', 'dates', 'hotelRecommandRate',
shows: ['DepartmentList', 'countryArea', 'orderStatus', 'hotelBookType', 'hotelStar', 'DateType', 'dates'],
sort: { DateType: 101, dates: 102 },
fieldProps: {
DepartmentList: { show_all: true, mode: 'multiple' },
countryArea: { show_all: true },
orderStatus: { show_all: true },
hotelBookType: { show_all: true },
hotelRecommandRate: { show_all: true },
// years: { hide_vs: false },
DateType: { disabledKeys: ['applyDate'] },
},
}}
onSubmit={(_err, obj, form) => {
HotelCruiseStore.setSearchValues(obj, form);
HotelCruiseStore.getHotelData(obj);
}}
/>
</Col>
</Row>
<section>
<Divider orientation="right" >
<TableExportBtn label={'酒店'} {...{ columns: tableProps.columns, dataSource }} />
</Divider>
<Table {...tableProps} bordered {...{ loading, dataSource }} rowKey={(record) => record.CityName} />
</section>
</>
);
});

@ -1,69 +0,0 @@
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>
</>
);
});

@ -241,7 +241,7 @@ const Sale_KPI = () => {
<TableExportBtn label={'sales kpi'} {...{ columns: columnsForExport, dataSource: dataForExport }} />
</Divider>
<Table
sticky
sticky={{ offsetHeader: 64 }}
key={`salesTradeTable`}
loading={loading}
columns={columns}

@ -1,10 +1,10 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { useContext, useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { stores_Context } from '../config';
import { Table, Row, Col, Divider, Button, Switch } from 'antd';
import { Table, Row, Col, Divider, Switch, Space, Tabs } from 'antd';
import SearchForm from '../components/search/SearchForm';
import { TableExportBtn } from './../components/Data';
import { uniqWith, groupBy } from './../utils/commons';
import { TableExportBtn, VSTag } from './../components/Data';
import { fixTo2Decimals, isEmpty } from './../utils/commons';
// TdCellDataTable
const TdCell = (tdprops) => {
@ -16,14 +16,15 @@ const TdCell = (tdprops) => {
const SalesCustomerCareRegular = (props) => {
const { date_picker_store: searchFormStore, customer_store } = useContext(stores_Context);
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const { sales_regular_data: pageData } = customer_store;
const { pivotData: pageData } = customer_store.sales_regular_data;
const allOPI1 = uniqWith(
pageData.data.map((rr) => ({ text: rr.operatorName, value: rr.operatorName })),
(a, b) => JSON.stringify(a) === JSON.stringify(b)
).sort((a, b) => a.text.localeCompare(b.text));
const pivotOptions = [{key: 'operatorName', label: '顾问'}, {key: 'country', label: '国家'}];
const [pivotRow, setPivotRow] = useState('operatorName');
const onTabsChange = (key) => {
setPivotRow(key);
};
const [OPIFilters, setOPIFilters] = useState([]);
const [dataSource, setDataSource] = useState([]);
const [ifmerge, setIfmerge] = useState(false);
const [dataForExport, setDataForExport] = useState([]);
@ -31,32 +32,42 @@ const SalesCustomerCareRegular = (props) => {
useEffect(() => {
if ( ! ifmerge) {
// const allOPI1 = uniqWith(
// pageData.data.map((rr) => ({ text: rr.operatorName, value: rr.operatorName })),
// (a, b) => JSON.stringify(a) === JSON.stringify(b)
// ).sort((a, b) => a.text.localeCompare(b.text));
// setOPIFilters(allOPI1);
setDataSource(pageData.data);
setDataForExport(pageData.data.reduce((r, c) => r.concat([{...c, children: undefined}], c.children.map(ele => ({...ele, operatorName: ele.rowLabel}))), []));
setDataForExportS(pageData.data.reduce((r, c) => r.concat([{...c, children: undefined}]), []));
setDataSource(pageData[pivotRow].data);
setDataForExport(
pageData[pivotRow].data.reduce(
(r, c) =>
r.concat(
[{ ...c, children: undefined }],
c.children
.reduce((rc, ele) => rc.concat([{ ...ele, [pivotRow]: ele.rowLabel }], [{ ...ele.vsData, [pivotRow]: ele.vsData.rowLabel, vsData: {} }]), [])
.filter((ele) => ele.SumOrder !== undefined)
),
[]
)
);
setDataForExportS(pageData[pivotRow].data.reduce((r, c) => r.concat([{...c, children: undefined}], [{ ...c.vsData, vsData: {} }]), []).filter((ele) => ele.SumOrder !== undefined));
} else {
// const allOPI1 = uniqWith(
// pageData.mergedData.map((rr) => ({ text: rr.operatorName, value: rr.operatorName })),
// (a, b) => JSON.stringify(a) === JSON.stringify(b)
// ).sort((a, b) => a.text.localeCompare(b.text));
// setOPIFilters(allOPI1);
setDataSource(pageData.mergedData);
setDataForExport(pageData.mergedData.reduce((r, c) => r.concat([{...c, children: undefined}], c.children.map(ele => ({...ele, operatorName: ele.rowLabel}))), []));
setDataForExportS(pageData.mergedData.reduce((r, c) => r.concat([{...c, children: undefined}]), []));
setDataSource(pageData[pivotRow].mergedData);
setDataForExport(
pageData[pivotRow].mergedData.reduce(
(r, c) =>
r.concat(
[{ ...c, children: undefined }],
c.children.reduce((rc, ele) => rc.concat([{ ...ele, operatorName: ele.rowLabel }], [{ ...ele.vsData, operatorName: ele.vsData.rowLabel, vsData: {} }]), [])
.filter((ele) => ele.SumOrder !== undefined)
),
[]
)
);
setDataForExportS(pageData[pivotRow].mergedData.reduce((r, c) => r.concat([{...c, children: undefined}], [{ ...c.vsData, vsData: {} }]), []).filter((ele) => ele.SumOrder !== undefined));
}
return () => {
};
}, [ifmerge, pageData.data]);
return () => {};
}, [ifmerge, pageData[pivotRow].data]);
const rowColumns = [
{ title: '日期区间', dataIndex: 'seriesKey', key: 'seriesKey' },
{ title: '顾问', dataIndex: 'operatorName', key: 'operatorName' },
{ title: '订单号', dataIndex: 'o_id', key: 'o_id' },
{ title: '预定日期', dataIndex: 'applyDate', key: 'applyDate' },
@ -73,19 +84,27 @@ const SalesCustomerCareRegular = (props) => {
{ title: '来源', dataIndex: 'SourceType', key: 'SourceType' },
{ title: '页面类型', dataIndex: 'COLI_LineClass', key: 'COLI_LineClass' },
];
const calcDelta = (r, key) => !isEmpty(Number(r.vsData[key])) ? fixTo2Decimals((Number(r[key] || 0) - Number(r.vsData[key]))/Number(r.vsData[key]) *100) : null;
const renderVS = (v, r, key) => {
const delta = calcDelta(r, key);
return <>
<Space direction={'vertical'}>
<span>
{v || 0}
{r.vsData[key] ? <span type="secondary"> VS {r.vsData[key]}</span> : null}
</span>
{delta && <VSTag diffPercent={delta} />}
</Space>
</>;
};
const columns = [
{ key: 'operatorName', title: '顾问', dataIndex: 'operatorName', width: '6em', filters: allOPI1, onFilter: (value, record) => value.includes(record.operatorName), filterSearch: true },
{ key: 'SumOrder', title: '订单数', dataIndex: 'SumOrder', width: '5em' },
{ key: 'ConfirmOrder', title: '成交数', dataIndex: 'ConfirmOrder', width: '5em' },
{ key: 'ConfirmPersonNum', title: '✔人数(SUM)', dataIndex: 'ConfirmPersonNum', width: '5em' },
{ key: 'confirmTourdays', title: '✔团天数(AVG)', dataIndex: 'confirmTourdays', width: '5em' },
{ key: 'SumML', title: '预计毛利', dataIndex: 'SumML', width: '5em' }, // SumML_txt
{ key: 'ConfirmRates', title: '成交率', dataIndex: 'ConfirmRates_txt', width: '5em' },
{ key: 'SingleML', title: '单团毛利', dataIndex: 'SingleML', width: '5em' },
{ key: 'action', title: '', width: '5em', render: (_, r) => {
const rowChildren = pageData.rawData.filter(ele => ele.operatorName === r.operatorName);
return r.hasOld ? <TableExportBtn btnTxt='明细' label={`老客户明细-${r.operatorName}`} {...{ columns: rowColumns, dataSource: rowChildren }} /> : null;
} },
{ key: 'SumOrder', title: '订单数', dataIndex: 'SumOrder', width: '5em', render: (v, r) => renderVS(v, r, 'SumOrder') },
{ key: 'ConfirmOrder', title: '成交数', dataIndex: 'ConfirmOrder', width: '5em', render: (v, r) => renderVS(v, r, 'ConfirmOrder') },
{ key: 'ConfirmPersonNum', title: '✔人数(SUM)', dataIndex: 'ConfirmPersonNum', width: '5em', render: (v, r) => renderVS(v, r, 'ConfirmPersonNum') },
{ key: 'confirmTourdays', title: '✔团天数(AVG)', dataIndex: 'confirmTourdays', width: '5em', render: (v, r) => renderVS(v, r, 'confirmTourdays') },
{ key: 'SumML', title: '预计毛利', dataIndex: 'SumML', width: '5em', render: (v, r) => renderVS(v, r, 'SumML') }, // SumML_txt
{ key: 'ConfirmRates', title: '成交率', dataIndex: 'ConfirmRates_txt', width: '5em', render: (v, r) => renderVS(v, r, 'ConfirmRates') },
{ key: 'SingleML', title: '单团毛利', dataIndex: 'SingleML', width: '5em', render: (v, r) => renderVS(v, r, 'SingleML') },
];
return (
<>
@ -95,52 +114,87 @@ const SalesCustomerCareRegular = (props) => {
defaultValue={{
initialValue: {
...formValues,
...pageData.searchValues,
...customer_store.sales_regular_data.searchValues,
},
shows: ['DateType', 'DepartmentList', 'WebCode', 'dates'],
shows: ['DateType', 'DepartmentList', 'WebCode', 'dates', 'IncludeTickets'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
dates: { hide_vs: true },
dates: { hide_vs: false },
},
}}
onSubmit={(_err, obj, form, str) => {
customer_store.setSearchValues(obj, form, 'sales_regular_data');
customer_store.get_sales_regular_data(obj);
// MeetingDataStore.setSearchValues(form);
// dataRefresh(obj);
customer_store.get_sales_regular_data_vs(obj, pivotRow);
}}
/>
</Col>
</Row>
<h2>销售-老客户, 含推荐</h2>
<Divider orientation="right" plain>
{pageData.data.length > 0 && (
<Switch
unCheckedChildren="各账户"
checkedChildren="合并"
key={'openOrMerge'}
checked={ifmerge}
onChange={(e) => {
setIfmerge(e);
}}
/>
)}
<Divider type={'vertical'} />
<TableExportBtn btnTxt='导出明细' label={`${formValuesToSub.Date1}-销售.老客户-明细`} {...{ columns: rowColumns, dataSource: pageData.rawData }} />
<Divider type={'vertical'} />
<TableExportBtn btnTxt='导出下表-展开' label={`${formValuesToSub.Date1}-销售.老客户`} {...{ columns, dataSource: dataForExport }} />
<Divider type={'vertical'} />
<TableExportBtn btnTxt='导出下表-总' label={`${formValuesToSub.Date1}-销售.老客户`} {...{ columns, dataSource: dataForExportS }} />
</Divider>
<Table
sticky
dataSource={dataSource}
loading={pageData.loading}
columns={columns}
pagination={false}
// defaultExpandAllRows
// expandable={{ defaultExpandAllRows: true, expandedRowKeys: pageData.data.map((ele) => ele.key), }}
<Tabs
type={'card'}
activeKey={pivotRow}
onChange={onTabsChange}
items={pivotOptions.map((ele) => {
return {
...ele,
children: (
<>
{/* <h2>{ele.label}-老客户, 含推荐</h2> */}
<>
<Divider orientation={'right'} style={{backgroundColor: '#fff', margin: 0, padding: '10px 0'}} >
{pageData[pivotRow].data.length > 0 && pivotRow === 'operatorName' && (
<Switch
unCheckedChildren="各账户"
checkedChildren="合并"
key={'openOrMerge'}
checked={ifmerge}
onChange={(e) => {
setIfmerge(e);
}}
/>
)}
<Divider type={'vertical'} />
<TableExportBtn
btnTxt="导出明细"
label={`${formValuesToSub.Date1}-老客户-明细`}
{...{ columns: [{ title: ele.label, dataIndex: pivotRow, key: pivotRow }, ...rowColumns], dataSource: pageData[pivotRow].rawData }}
/>
<Divider type={'vertical'} />
<TableExportBtn
btnTxt="导出下表-展开"
label={`${formValuesToSub.Date1}-${ele.label}.老客户`}
{...{ columns: [{ title: ele.label, dataIndex: pivotRow, key: pivotRow }, ...columns], dataSource: dataForExport }}
/>
<Divider type={'vertical'} />
<TableExportBtn
btnTxt="导出下表-总"
label={`${formValuesToSub.Date1}-${ele.label}.老客户`}
{...{ columns: [{ title: ele.label, dataIndex: pivotRow, key: pivotRow }, ...columns], dataSource: dataForExportS }}
/>
</Divider>
</>
<Table
sticky
dataSource={dataSource}
loading={pageData[ele.key].loading}
columns={[
{
key: ele.key,
title: ele.label,
dataIndex: ele.key,
width: '6em',
filters: pageData[ele.key].filterColValues,
onFilter: (value, record) => value.includes(record[ele.key]),
filterSearch: true,
},
...columns,
]}
pagination={false}
/>
</>
),
};
})}
/>
</>
);

@ -1,14 +1,14 @@
import React, { useContext, useState, useEffect } from 'react';
import { observer } from 'mobx-react';
import { stores_Context, DATE_FORMAT, SMALL_DATETIME_FORMAT } from '../config';
import { Link } from 'react-router-dom';
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';
import { Row, Col, Table, Select, Spin, Tag } from 'antd';
import SearchForm from '../../components/search/SearchForm';
import MixFieldsDetail from '../../components/MixFieldsDetail';
import { fixTo2Decimals, isEmpty, pick } from '../../utils/commons';
import Column from '../../components/Column';
import { groupsMappedByKey } from '../../libs/ht';
const COLOR_SETS = [
"#FFFFFF",
@ -33,12 +33,12 @@ export default observer((props) => {
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const _formValuesToSub = pick(formValuesToSub, ['DepartmentList', 'WebCode']);
const { resetData, results } = SalesCRMDataStore;
const { searchValues, 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();
resetData('results');
const deptList = obj.DepartmentList.split(',');
const includeCH = ['1', '2', '7'].some(ele => deptList.includes(ele));
const includeAH = ['28'].every(ele => deptList.includes(ele));
@ -87,8 +87,8 @@ export default observer((props) => {
// 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),
Date1: moment(obj.date).startOf('year').format(DATE_FORMAT),
Date2: moment(obj.date).endOf('year').format(SMALL_DATETIME_FORMAT),
groupType: 'overview',
// groupType: 'operator',
groupDateType: 'month',
@ -189,6 +189,7 @@ export default observer((props) => {
const dashboardTableProps = {
pagination: false,
size: 'small',
showSorterTooltip: false,
columns: [
{
title: '',
@ -198,14 +199,15 @@ export default observer((props) => {
filterSearch: true,
filters: operatorObjects.sort((a, b) => a.text.localeCompare(b.text)),
onFilter: (value, record) => record.groupsKey === value || record.groupType === 'overview',
render: (text, record) => (record.groupType !== 'operator' ? text : <Link to={`/op_risk/sales/${record.groupsKey}`}>{text}</Link>),
},
{
title: '前90 -30天',
title: () => (<>前90 -30<br/>{searchValues.date90.Date1} {searchValues.date90.Date2}</>),
key: 'date',
children: dataFields('result90', 0),
},
{
title: '前180 -50天',
title: () => (<>前180 -50<br/>{searchValues.date180.Date1} {searchValues.date180.Date2}</>),
key: 'department',
children: dataFields('result180', 1),
},

@ -1,11 +1,11 @@
import React, { Children, useContext, useState } from 'react';
import React, { useContext } from 'react';
import { Link } from 'react-router-dom';
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';
import { stores_Context } from '../../config';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Row, Col, Table, Tooltip } from 'antd';
import SearchForm from '../../components/search/SearchForm';
import { pick } from '../../utils/commons';
const COLOR_SETS = [
"#FFFFFF",
@ -30,7 +30,7 @@ export default observer((props) => {
const operatorObjects = process.details.map((v) => ({ key: v.groupsKey, value: v.groupsKey, label: v.groupsLabel, text: v.groupsLabel }));
const pageRefresh = async (obj) => {
resetData();
resetData('process');
Promise.allSettled([
SalesCRMDataStore.getProcessData({
...(obj || _formValuesToSub),
@ -58,28 +58,72 @@ export default observer((props) => {
rowClassName: (record, rowIndex) => {
return record.groupType === 'operator' ? '': 'ant-tag-blue';
},
showSorterTooltip: false,
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) => (record.groupType !== 'operator' ? text : <Link to={`/op_risk/sales/${record.groupsKey}`}>{text}</Link>),
}, // :
{
title: '顾问动作',
key: 'date',
children: [
{ title: '首次响应率24H', dataIndex: 'firstTouch24', key: 'firstTouch24', render: percentageRender,
{ title: () => (
<>
首次响应率24H{' '}
<Tooltip title="达到24H内回复的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'firstTouch24', key: 'firstTouch24', render: percentageRender,
sorter: (a, b) => tableSorter(a, b, 'firstTouch24'),
},
{ title: '48H内报价率', dataIndex: 'firstQuote48', key: 'firstQuote48',render: percentageRender ,
{ title: () => (
<>
48H内报价率{' '}
<Tooltip title="达到48H内报价的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'firstQuote48', key: 'firstQuote48',render: percentageRender ,
sorter: (a, b) => tableSorter(a, b, 'firstQuote48'), },
{ title: '一次报价率', dataIndex: 'quote1', key: 'quote1',render: percentageRender ,
{ title: () => (
<>
一次报价率{' '}
<Tooltip title="首次报价的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'quote1', key: 'quote1',render: percentageRender ,
sorter: (a, b) => tableSorter(a, b, 'quote1'), },
{ title: '二次报价率', dataIndex: 'quote2', key: 'quote2',render: percentageRender ,
{ title: () => (
<>
二次报价率{' '}
<Tooltip title="二次报价的订单数/一次报价后回复数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'quote2', key: 'quote2',render: percentageRender ,
sorter: (a, b) => tableSorter(a, b, 'quote2'), },
{ title: '>50条会话', dataIndex: 'turnsGT50', key: 'turnsGT50', render: percentageRender ,
{ title: () => (
<>
&gt;50条会话{' '}
<Tooltip title=">50条会话的订单数/总订单">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'turnsGT50', key: 'turnsGT50', render: percentageRender ,
sorter: (a, b) => tableSorter(a, b, 'turnsGT50'), },
{ title: '违规数', dataIndex: 'violations', key: 'violations',
{ title: () => (
<>
违规数{' '}
<Tooltip title="未遵循24H回复的订单数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'violations', key: 'violations',
sorter: (a, b) => tableSorter(a, b, 'violations'), },
],
},
@ -88,28 +132,56 @@ export default observer((props) => {
key: 'department',
children: [
{
title: '首次回复率24H', dataIndex: 'firstReply24', key: 'firstReply24',
title: () => (
<>
首次回复率24H{' '}
<Tooltip title="达到24H内回复的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
), 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',
title: () => (
<>
48H内报价回复率{' '}
<Tooltip title="48H内报价后的回复数/报价的订单数">
<InfoCircleOutlined />
</Tooltip>
</>
), 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',
title: () => (
<>
一次报价回复率{' '}
<Tooltip title="一次报价后回复率/订单总数">
<InfoCircleOutlined />
</Tooltip>
</>
), 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',
title: () => (
<>
二次报价回复率{' '}
<Tooltip title="二次报价后回复数/一次报价后回复数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'replyQuote2', key: 'replyQuote2',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1]+transparentHex } },
children: percentageRender(_),
@ -127,13 +199,13 @@ export default observer((props) => {
rowClassName: (record, rowIndex) => {
return record.groupType === 'operator' ? '': 'ant-tag-blue';
},
showSorterTooltip: false,
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:
// },
render: (text, record) => (record.groupType !== 'operator' ? text : <Link to={`/op_risk/sales/${record.groupsKey}`}>{text}</Link>),
}, // :
{ title: '>24H回复', dataIndex: 'lostTouch24', key: 'lostTouch24',
sorter: (a, b) => tableSorter(a, b, 'lostTouch24'),

@ -0,0 +1,397 @@
import React, { useContext, useEffect } from 'react';
import { NavLink, useParams } from 'react-router-dom';
import { observer } from 'mobx-react';
import { stores_Context, DATE_FORMAT, SMALL_DATETIME_FORMAT } from '../../config';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Row, Col, Table, Divider, Button, Popover, Tooltip } from 'antd';
import { fixTo2Decimals } from '../../utils/commons';
import MixFieldsDetail from '../../components/MixFieldsDetail';
const COLOR_SETS = [
'#FFFFFF',
// "#5B8FF9",
// "#FF6B3B",
'#9FB40F',
'#76523B',
'#DAD5B5',
'#E19348',
'#F383A2',
];
const transparentHex = '1A';
const percentageRender = (val) => (val ? `${val}%` : '');
const numberConvert10K = (number, scale = 10) => {
return fixTo2Decimals((number/(1000*scale)));
};
const retPropsMinRatesSet = { 'CH': 12, 'AH': 8, 'default': 10 }; //
export default observer((props) => {
const { opisn, opi_name } = useParams();
const { sale_store, SalesCRMDataStore, date_picker_store: searchFormStore } = useContext(stores_Context);
const { formValues, siderBroken } = searchFormStore;
const { searchValues, searchValuesToSub, resetData, results, process, risk } = SalesCRMDataStore;
useEffect(() => {
Promise.allSettled([
//
SalesCRMDataStore.get90n180Data({ opisn, groupType: 'operator', groupDateType: '' }),
// :
SalesCRMDataStore.getResultData({
opisn,
Date1: searchValues.date.clone().startOf('year').format(DATE_FORMAT),
Date2: searchValues.date.clone().endOf('year').format(SMALL_DATETIME_FORMAT),
groupType: 'overview',
// groupType: 'operator',
groupDateType: 'month',
}),
//
SalesCRMDataStore.getProcessData({ opisn, groupType: 'operator', groupDateType: '' }),
//
SalesCRMDataStore.getRiskDetailData({ opisn }),
]);
}, [opisn]);
const dataFields = (suffix, colRootIndex) => [
{
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', backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
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) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: 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',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: _,
}),
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',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: _,
}),
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',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: _,
}),
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) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: val ? `${val}%` : ''
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].ResumeRates - b[suffix].ResumeRates),
},
];
const dashboardTableProps = {
rowKey: 'groupsKey',
pagination: false,
size: 'small',
showSorterTooltip: false,
columns: [
{
title: '',
dataIndex: 'groupsLabel',
key: 'name',
width: '5em',
},
{
title: () => (<>前90 -30<br/>{searchValues.date90.Date1} {searchValues.date90.Date2}</>),
key: 'date',
children: dataFields('result90', 0),
},
{
title: () => (<>前180 -50<br/>{searchValues.date180.Date1} {searchValues.date180.Date2}</>),
key: 'department',
children: dataFields('result180', 1),
},
],
rowClassName: (record, rowIndex) => {
return record.groupType === 'overview' ? 'ant-tag-blue' : '';
},
};
const activityTableProps = {
rowKey: 'groupsKey',
pagination: false,
size: 'small',
rowClassName: (record, rowIndex) => {
return record.groupType === 'operator' ? '' : 'ant-tag-blue';
},
showSorterTooltip: false,
columns: [
{ title: '', dataIndex: 'groupsLabel', key: 'groupsLabel', width: '6rem' }, // :
{
title: '顾问动作',
key: 'date',
children: [
{
title: () => (
<>
首次响应率24H{' '}
<Tooltip title="达到24H内回复的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'firstTouch24',
key: 'firstTouch24',
render: percentageRender,
},
{
title: () => (
<>
48H内报价率{' '}
<Tooltip title="达到48H内报价的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'firstQuote48',
key: 'firstQuote48',
render: percentageRender,
},
{
title: () => (
<>
一次报价率{' '}
<Tooltip title="首次报价的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'quote1',
key: 'quote1',
render: percentageRender,
},
{
title: () => (
<>
二次报价率{' '}
<Tooltip title="二次报价的订单数/一次报价后回复数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'quote2',
key: 'quote2',
render: percentageRender,
},
{
title: () => (
<>
&gt;50条会话{' '}
<Tooltip title=">50条会话的订单数/总订单">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'turnsGT50',
key: 'turnsGT50',
render: percentageRender,
},
{
title: () => (
<>
违规数{' '}
<Tooltip title="未遵循24H回复的订单数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'violations',
key: 'violations',
},
],
},
{
title: '客人回复',
key: 'department',
children: [
{
title: () => (
<>
首次回复率24H{' '}
<Tooltip title="达到24H内回复的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'firstReply24',
key: 'firstReply24',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1] + transparentHex } },
children: percentageRender(_),
}),
},
{
title: () => (
<>
48H内报价回复率{' '}
<Tooltip title="48H内报价后的回复数/报价的订单数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'replyQuote48',
key: 'replyQuote48',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1] + transparentHex } },
children: percentageRender(_),
}),
},
{
title: () => (
<>
一次报价回复率{' '}
<Tooltip title="一次报价后回复率/订单总数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'replyQuote1',
key: 'replyQuote1',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1] + transparentHex } },
children: percentageRender(_),
}),
},
{
title: () => (
<>
二次报价回复率{' '}
<Tooltip title="二次报价后回复数/一次报价后回复数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'replyQuote2',
key: 'replyQuote2',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1] + transparentHex } },
children: percentageRender(_),
}),
},
],
},
],
};
const riskTableProps = {
loading: risk.loading,
sticky: true,
// scroll: { x: 1000, y: 400 },
pagination: false,
rowKey: (row) => row.coli_id,
columns: [
{ title: '客人姓名', dataIndex: 'guest_name', key: 'guest_name', width: '6rem' },
{ title: '团号', dataIndex: 'coli_id', key: 'coli_id', width: '6rem' },
{
title: '表单内容',
dataIndex: 'coli_contents',
key: 'coli_contents',
width: '6rem',
render: (text, record) => (
<Popover
title={`${record.coli_id} ${record.guest_name}`}
content={<pre dangerouslySetInnerHTML={{ __html: text }} style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word' }} />}
trigger={['click']}
placement="right"
overlayStyle={{ width: '500px', maxHeight: '500px' }}
autoAdjustOverflow={false}
>
<Button type="link" size="small">
表单内容
</Button>
</Popover>
),
},
{ title: '顾问', dataIndex: 'opi_name', key: 'opi_name', width: '6rem' },
{ title: '预定时间', dataIndex: 'coli_applydate', key: 'coli_applydate', width: '6rem' },
],
};
const chartsConfig = {
colFields: ['ConfirmOrder', 'SumOrder'],
lineFields: ['SumML', 'ConfirmRates'],
seriesField: null,
xField: 'groupDateVal',
};
return (
<>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col md={24} lg={12} xxl={14}>
<NavLink to={-1}>返回</NavLink>
</Col>
</Row>
<section>
<h2>结果指标 @ {searchValues.date.format(DATE_FORMAT)}</h2>
<Table {...dashboardTableProps} bordered dataSource={results[`operator_${opisn}`]} loading={results.loading} sticky />
</section>
<section>
<h3>全年每月业绩 @ {searchValues.date.format('YYYY')}</h3>
<MixFieldsDetail {...chartsConfig} dataSource={results[`operator_byDate_${opisn}`] || []} />
</section>
<Divider />
<section>
<h2>过程指标 @ {searchValuesToSub.Date1} {searchValuesToSub.Date2}</h2>
<Table {...activityTableProps} bordered dataSource={process[`operator_${opisn}`]} loading={process.loading} sticky />
</section>
<section>
<h2>违规明细</h2>
<h3>&gt;24H回复</h3>
<Table {...riskTableProps} bordered dataSource={risk.byLostType?.lostTouch24 || []} />
</section>
<section>
<h3>首次报价周期&gt;48h</h3>
<Table {...riskTableProps} bordered dataSource={risk.byLostType?.lostQuote48 || []} />
</section>
<section>
<h3>报价次数&lt;1</h3>
<Table {...riskTableProps} bordered dataSource={risk.byLostType?.lostQuote1 || []} />
</section>
<section>
<h3>报价次数&lt;2</h3>
<Table {...riskTableProps} bordered dataSource={risk.byLostType?.lostQuote2 || []} />
</section>
</>
);
});
Loading…
Cancel
Save