Compare commits

...

34 Commits

Author SHA1 Message Date
Lei OT 9edfa0c5b8 fix: 市场数据导出, 格式优化 7 days ago
Lei OT 195d0facaa 2.14.7 2 weeks ago
Lei OT 0f2041cdc8 perf: +来源战点 `Thailand` 2 weeks ago
Lei OT ce4e23fe6c perf: 老客户: 状态字段 2 weeks ago
Lei OT 62ba5b5973 # 1 month ago
Lei OT 986f27eb28 2.14.6 1 month ago
Lei OT 9a59a5ac0e perf: 老客户-分析: 目的地城市的省/地区汇总, 去除重复计算的城市 1 month ago
Lei OT 11be41a446 perf: 老客户-分析: 国籍: 按语种汇总 2 months ago
Lei OT da3f4458ff 2.14.5 2 months ago
Lei OT e2d91d9df8 perf: 老客户: 显示原条件占比 2 months ago
Lei OT 594426048c 2.14.4 2 months ago
Lei OT d6c34f145e perf: 老客户: 明细: 走团国家 2 months ago
Lei OT 90714b6f82 perf: 老客户-分析: +目的地城市 2 months ago
Lei OT ae07f7593b feat: 外联业绩×页面类型 3 months ago
Lei OT 460dd47f1c perf: 老客户: 只显示市场占比 3 months ago
Lei OT 46f3a52784 2.14.3 3 months ago
Lei OT 93fa13ac9d perf: 老客户: +字段: 城市, 产品类型 3 months ago
Lei OT 5351239f5f perf: 数据透视: +分销客户 3 months ago
Lei OT a44a05de26 2.14.2 3 months ago
Lei OT c023dd5b68 perf: 老客户-分析: 国籍汇总计算 3 months ago
Lei OT 16e6ec574a feat: 老客户-分析: 增加`目的地国家` 3 months ago
Lei OT ead5b37ded perf: 分销: +显示字段 3 months ago
Lei OT 5f7a942842 fix: 数据透视: 目的地国家/城市 空值 不省略 3 months ago
Lei OT 1f5d6a9047 perf: 字段文字 3 months ago
Lei OT 7cb91abea2 2.14.1 3 months ago
ybc 8581038aeb 优化导游语种组件 3 months ago
Lei OT 3b24aa1373 perf: 分销订单统计: 表头字段 3 months ago
ybc cb994aff29 Merge branch 'main' of github.com:hainatravel-it/dashboard 3 months ago
ybc a48ed8ff03 增加小组, 导游语种, 对比日期 3 months ago
Lei OT 8cd446d8f8 2.14.0 3 months ago
Lei OT 559ba14e35 feat: 分销订单统计 3 months ago
Lei OT a2b920b313 refactor: RenderVSDataCell 组件 3 months ago
Lei OT 9793f62b34 perf: 老客户: 默认选项 + GH 3 months ago
Lei OT a66ed759f8 perf: 老客户. 明细; -->zustand 3 months ago

4
package-lock.json generated

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

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

@ -58,6 +58,9 @@ import Hotel from './views/Hotel';
import HostCaseCount from './views/HostCaseCount';
import TrainsUpsell from './views/biz/reports/TrainsUpsell';
import HostCaseReport from './views/HostCaseReport';
import ToBOrder from './views/toB/ToBOrder';
import ToBOrderSub from './views/toB/ToBOrderSub';
import MeetingSales from './views/reports/MeetingSales';
const App = () => {
const { Content, Footer, Sider, } = Layout;
@ -81,6 +84,10 @@ const App = () => {
key: 'meeting-2024-GH',
label: <NavLink to="/orders/meeting-2024-GH">GH区域数据</NavLink>, // GH-2024
},
{
key: 'sales-insight',
label: <NavLink to="/reports/sales-insight">顾问业绩</NavLink>,
},
],
},
{
@ -93,6 +100,11 @@ const App = () => {
label: <NavLink to="/orders">订单数据</NavLink>,
// icon: <FileProtectOutlined />,
},
{
key: 'tob_orders',
label: <NavLink to="/tob_orders">分销订单</NavLink>,
// icon: <FileProtectOutlined />,
},
{
key: 22,
label: <NavLink to="/dashboard">仪表盘</NavLink>,
@ -260,6 +272,8 @@ const App = () => {
<Route path="/:page/pivot" element={<DataPivot />} />
<Route path="/orders/meeting-2024-GH" element={<Meeting2024GH />} />
<Route path="/orders/meeting-2025-GH" element={<Meeting2025GH />} />
<Route path="/tob_orders" element={<ToBOrder />} />
<Route path="/tob_orders_sub/:ordertype/:ordertype_sub/:ordertype_title" element={<ToBOrderSub />} />
<Route path="/biz_orders" element={<BizOrder />} />
<Route path="/biz_orders_sub/:ordertype/:ordertype_sub/:ordertype_title" element={<BizOrderSub />} />
<Route path="/trains" element={<TrainsUpsell />} />
@ -295,6 +309,8 @@ const App = () => {
<Route path="/sales-crm/process" element={<OPProcess />} />
<Route path="/sales-crm/risk" element={<OPRisk />} />
<Route path="/sales-crm/risk/sales/:opisn" element={<OPRisk />} />
<Route path="/reports/sales-insight" element={<MeetingSales />} />
</Route>
</Routes>
</Content>

@ -11,13 +11,13 @@ import { TableExportBtn, RenderVSDataCell } from '../components/Data';
import useCustomerRelationsStore from '../zustand/CustomerRelations';
import { useShallow } from 'zustand/shallow';
import { fixTo2Decimals } from '@haina/utils-commons';
import { fixTo2Decimals, isEmpty } from '@haina/utils-commons';
const Customer_care_regular = () => {
const { date_picker_store, customer_store } = useContext(stores_Context);
const regular_data = customer_store.regular_data;
const [loading, loading2, searchValues, ] = useCustomerRelationsStore(useShallow((state) => [state.loading, state.loading2, state.searchValues,]));
const [loading, loading2, searchValues, searchValuesToSub] = useCustomerRelationsStore(useShallow((state) => [state.loading, state.loading2, state.searchValues, state.searchValuesToSub]));
const [setSearchValues] = useCustomerRelationsStore(useShallow((state) => [state.setSearchValues]));
const [regular] = useCustomerRelationsStore(useShallow((state) => [state.regular]));
@ -38,9 +38,9 @@ const Customer_care_regular = () => {
{
title: '订单状态',
width: '4rem',
dataIndex: 'OrderState1',
key: 'OrderState1',
render: (text, record) => record.OrderState === 1 ? '成行' : '未成行',
dataIndex: 'orderstate_name',
key: 'orderstate_name',
// render: (text, record) => record.OrderState === 1 ? '' : '',
sorter: (a, b) => b.OrderState - a.OrderState,
},
{
@ -70,10 +70,15 @@ const Customer_care_regular = () => {
},
{
title: '走团国家',
dataIndex: 'recommend_country',
key: 'recommend_country',
dataIndex: 'PassCountry_This',
key: 'PassCountry_This',
width: '4em',
},
{
title: '经过城市',
dataIndex: 'PassCity_This',
key: 'PassCity_This',
},
{
title: '小组',
dataIndex: 'Department',
@ -109,6 +114,11 @@ const Customer_care_regular = () => {
dataIndex: 'COLI_LineClass',
key: 'COLI_LineClass',
},
{
title: '产品类型',
dataIndex: 'TourType_Name',
key: 'TourType_Name',
},
{
title: '券额',
dataIndex: 'Voucher_amount',
@ -158,6 +168,14 @@ const Customer_care_regular = () => {
style: { backgroundColor: '#5B8FF9' + '1A' },
}),
},
{
title: '上次经过城市',
dataIndex: 'PassCity_Last',
key: 'PassCity_Last',
onCell: (r) => ({
style: { backgroundColor: '#5B8FF9' + '1A' },
}),
},
{
title: '复购周期',
dataIndex: 'Repurchase_cycle',
@ -284,14 +302,15 @@ const Customer_care_regular = () => {
key: 'OrderNum',
render: (text, record, index) => (
<>
<RenderVSDataCell showDiffData={true} data1={record.OrderNum} data2={record.diff?.OrderNum} />
{/* <span>{text}</span> */}
&nbsp;&nbsp;
{
<Tooltip key="total_data_tips" title={regular.total_data_tips}>
{index === 0 && regular.total_data_tips !== '' && <InfoCircleOutlined className="ant-tag-gold" />}
</Tooltip>
}
<RenderVSDataCell showDiffData={!isEmpty(searchValuesToSub.DateDiff1)} data1={record.OrderNum} data2={record.diff?.OrderNum} />
{index === 0 && regular.total_data_tips !== '' && (
<>
&nbsp;&nbsp;
<Tooltip key="total_data_tips" title={regular.total_data_tips}>
<InfoCircleOutlined className="ant-tag-gold" />
</Tooltip>
</>
)}
</>
),
},
@ -299,49 +318,84 @@ const Customer_care_regular = () => {
title: '订单数占比',
dataIndex: 'OrderRate',
key: 'OrderRate',
render: (text, record) => <RenderVSDataCell showDiffData={true} data1={fixTo2Decimals(record.OrderRate*100)} data2={fixTo2Decimals(record.diff?.OrderRate*100)} dataSuffix='%' />
},
{
title: '订单数占比(市场)',
dataIndex: 'OrderRate2',
key: 'OrderRate2',
render: (text, record) => <RenderVSDataCell showDiffData={true} data1={fixTo2Decimals(record.OrderRate2*100)} data2={fixTo2Decimals(record.diff?.OrderRate2*100)} dataSuffix='%' />
render: (text, record) => (
<RenderVSDataCell
showDiffData={!isEmpty(searchValuesToSub.DateDiff1)}
data1={fixTo2Decimals(record.OrderRate * 100)}
data2={fixTo2Decimals(record.diff?.OrderRate * 100)}
dataSuffix="%"
/>
),
},
// {
// title: '()',
// dataIndex: 'OrderRate2',
// key: 'OrderRate2',
// render: (text, record) => (
// <RenderVSDataCell
// showDiffData={!isEmpty(searchValuesToSub.DateDiff1)}
// data1={fixTo2Decimals(record.OrderRate2 * 100)}
// data2={fixTo2Decimals(record.diff?.OrderRate2 * 100)}
// dataSuffix="%"
// />
// ),
// },
{
title: '成行数',
dataIndex: 'SUCOrderNum',
key: 'SUCOrderNum',
render: (text, record) => <RenderVSDataCell showDiffData={true} data1={(record.SUCOrderNum)} data2={(record.diff?.SUCOrderNum)} />
render: (text, record) => <RenderVSDataCell showDiffData={!isEmpty(searchValuesToSub.DateDiff1)} data1={record.SUCOrderNum} data2={record.diff?.SUCOrderNum} />,
},
{
title: '成行率',
dataIndex: 'SUCRate',
key: 'SUCRate',
render: (text, record) => <RenderVSDataCell showDiffData={true} data1={fixTo2Decimals(record.SUCRate*100)} data2={fixTo2Decimals(record.diff?.SUCRate*100)} dataSuffix='%' />
render: (text, record) => (
<RenderVSDataCell
showDiffData={!isEmpty(searchValuesToSub.DateDiff1)}
data1={fixTo2Decimals(record.SUCRate * 100)}
data2={fixTo2Decimals(record.diff?.SUCRate * 100)}
dataSuffix="%"
/>
),
},
{
title: '毛利',
dataIndex: 'ML',
key: 'ML',
render: (text, record) => <RenderVSDataCell showDiffData={true} data1={(record.ML)} data2={(record.diff?.ML)} />
render: (text, record) => <RenderVSDataCell showDiffData={!isEmpty(searchValuesToSub.DateDiff1)} data1={record.ML} data2={record.diff?.ML} />,
},
{
title: '毛利占比',
dataIndex: 'OrderMLRate',
key: 'OrderMLRate',
render: (text, record) => <RenderVSDataCell showDiffData={true} data1={fixTo2Decimals(record.OrderMLRate*100)} data2={fixTo2Decimals(record.diff?.OrderMLRate*100)} dataSuffix='%' />
},
{
title: '毛利占比(市场)',
dataIndex: 'OrderMLRate2',
key: 'OrderMLRate2',
render: (text, record) => <RenderVSDataCell showDiffData={true} data1={fixTo2Decimals(record.OrderMLRate2*100)} data2={fixTo2Decimals(record.diff?.OrderMLRate2*100)} dataSuffix='%' />
render: (text, record) => (
<RenderVSDataCell
showDiffData={!isEmpty(searchValuesToSub.DateDiff1)}
data1={fixTo2Decimals(record.OrderMLRate * 100)}
data2={fixTo2Decimals(record.diff?.OrderMLRate * 100)}
dataSuffix="%"
/>
),
},
// {
// title: '()',
// dataIndex: 'OrderMLRate2',
// key: 'OrderMLRate2',
// render: (text, record) => (
// <RenderVSDataCell
// showDiffData={!isEmpty(searchValuesToSub.DateDiff1)}
// data1={fixTo2Decimals(record.OrderMLRate2 * 100)}
// data2={fixTo2Decimals(record.diff?.OrderMLRate2 * 100)}
// dataSuffix="%"
// />
// ),
// },
{
title: '人数(含成人+儿童)',
dataIndex: 'PersonNum',
key: 'PersonNum',
render: (text, record) => <RenderVSDataCell showDiffData={true} data1={(record.PersonNum)} data2={(record.diff?.PersonNum)} />
render: (text, record) => <RenderVSDataCell showDiffData={!isEmpty(searchValuesToSub.DateDiff1)} data1={record.PersonNum} data2={record.diff?.PersonNum} />,
},
]}
size="small"

@ -1,9 +1,11 @@
import { useContext, useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { toJS } from 'mobx';
import { stores_Context } from '../config';
import { Table, Row, Col, Divider, Switch, Space, Tabs } from 'antd';
import { Table, Row, Col, Divider, Switch, Space, Tabs, Tooltip } from 'antd';
import { InfoCircleOutlined } from '@ant-design/icons';
import SearchForm from '../components/search/SearchForm';
import { TableExportBtn, VSTag } from '../components/Data';
import { RenderVSDataCell, TableExportBtn, VSTag } from '../components/Data';
import { fixTo2Decimals, isEmpty } from '@haina/utils-commons';
// TdCellDataTable
@ -16,6 +18,32 @@ const TdCell = (tdprops) => {
const pivotOptions = [
{ key: 'operatorName', label: '顾问' },
{ key: 'country', label: '国籍' },
{
key: 'destinationCountry',
value: 'destinationCountry',
labelX: '目的地国家',
label: (
<>
目的地国家&nbsp;&nbsp;
<Tooltip key="total_data_tips" title={'途径的国家将会重复计算'}>
<InfoCircleOutlined className="ant-tag-gold" />
</Tooltip>
</>
),
},
{
key: 'destinations',
value: 'destinations',
labelX: '目的地城市',
label: (
<>
目的地城市&nbsp;&nbsp;
<Tooltip key="total_data_tips" title={'途径的城市将会重复计算'}>
<InfoCircleOutlined className="ant-tag-gold" />
</Tooltip>
</>
),
},
];
const pivotColOptions = [
{ key: 'hasOld', value: 'hasOld', label: '老客户+推荐' },
@ -25,8 +53,8 @@ const pivotColOptions = [
const CustomerCareRegularPivot = (props) => {
const { date_picker_store: searchFormStore, customer_store } = useContext(stores_Context);
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const { loading, pivotResult, filterColValues, rawData } = customer_store.sales_regular_data;
const { loading, pivotResult, filterColValues, rawData, countrySummary, citySummary } = customer_store.sales_regular_data;
// console.log('', countrySummary);
const [pivotRow, setPivotRow] = useState('operatorName');
const [pivotCol, setPivotCol] = useState('hasOld');
const [pivotColLabel, setPivotColLabel] = useState('老客户+推荐');
@ -43,36 +71,10 @@ const CustomerCareRegularPivot = (props) => {
useEffect(() => {
if ( ! ifmerge) {
// setDataSource(pageData[pivotRow].data);
setDataSource(pivotResult);
// setDataForExport(
// // pageData[pivotRow].data.reduce(
// pivotResult.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(pivotResult.reduce((r, c) => r.concat([{...c, children: undefined}], [{ ...c.vsData, vsData: {} }]), []).filter((ele) => ele.SumOrder !== undefined));
} else {
// 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 () => {};
@ -116,9 +118,10 @@ const CustomerCareRegularPivot = (props) => {
{ 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: 'ConfirmRates', title: '成交率', dataIndex: 'ConfirmRates_txt', width: '5em', render: (v, r) => renderVS(v, r, 'ConfirmRates'), suffix: '%' },
{ key: 'SingleML', title: '单团毛利', dataIndex: 'SingleML', width: '5em', render: (v, r) => renderVS(v, r, 'SingleML') },
].map(c => ({...c, sorter: (a, b) => (a[c.key] - b[c.key])}));
return (
<>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
@ -153,37 +156,18 @@ const CustomerCareRegularPivot = (props) => {
...ele,
children: (
<>
{/* <h2>{ele.label}-老客户, 含推荐</h2> */}
<>
<Divider orientation={'right'} style={{backgroundColor: '#fff', margin: 0, padding: '10px 0'}} >
{/* {dataSource.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: rawData }}
{...{ columns: [{ title: ele.labelX || ele.label, dataIndex: pivotRow, key: pivotRow }, ...rowColumns], dataSource: 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}.${pivotColLabel}`}
{...{ columns: [{ title: ele.label, dataIndex: pivotRow, key: pivotRow }, ...columns], dataSource: dataForExportS }}
{...{ columns: [{ title: ele.labelX || ele.label, dataIndex: pivotRow, key: pivotRow }, ...columns], dataSource: dataForExportS }}
/>
</Divider>
</>
@ -195,14 +179,14 @@ const CustomerCareRegularPivot = (props) => {
customer_store.regular_data_pivot(pivotRow, sub);
}}
items={pivotColOptions.map((col, i) => {
// const SubjectTableComponent = subjectComponents[ele.key];
return {
...col,
children: (
<Table
sticky
sticky={{ offsetHeader: 88 }}
dataSource={dataSource}
loading={loading}
components={{ body: { cell: TdCell } }}
columns={[
{
key: ele.key,
@ -215,8 +199,35 @@ const CustomerCareRegularPivot = (props) => {
},
...columns,
]}
summary={() => {
return (
pivotRow === 'country' ?
countrySummary.map((srow) => (
<Table.Summary.Row key={srow.key}>
<Table.Summary.Cell index1={0}><b>{srow.country}</b></Table.Summary.Cell>
{columns.map((td) => (
<Table.Summary.Cell key={td.key}>
<RenderVSDataCell data1={srow[td.dataIndex]} data2={srow.vsData?.[td.dataIndex]} showDiffData={toJS(formValuesToSub.DateDiff1)} dataSuffix={td.suffix} />
</Table.Summary.Cell>
))}
</Table.Summary.Row>
))
: pivotRow === 'destinations' ? citySummary.map((srow) => (
<Table.Summary.Row key={srow.key}>
<Table.Summary.Cell index1={0}><b>{srow.destinations}</b></Table.Summary.Cell>
{columns.map((td) => (
<Table.Summary.Cell key={td.key}>
<RenderVSDataCell data1={srow[td.dataIndex]} data2={srow.vsData?.[td.dataIndex]} showDiffData={toJS(formValuesToSub.DateDiff1)} dataSuffix={td.suffix} />
</Table.Summary.Cell>
))}
</Table.Summary.Row>
)) : null
);
}}
pagination={false}
/>)
/>
),
};
})}
/>

@ -68,7 +68,7 @@ export const VSDataTag = ({ diffPercent=0, diffData=0, data1=0, data2=0, dataSuf
* @property {number} data2
* @property {string} dataSuffix
*/
export const RenderVSDataCell = ({ showDiffData=false, data1, data2, dataSuffix = '', ...props }) => {
export const RenderVSDataCell = ({ showDiffData=false, data1=0, data2=0, dataSuffix = '', ...props }) => {
if (showDiffData) {
return <VSDataTag data1={data1} data2={data2} dataSuffix={dataSuffix} {...props} />;
}
@ -119,9 +119,10 @@ export const TableExportBtn = ({label, columns, dataSource, btnTxt, ...props}) =
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 data_val_str = data_val ? String(data_val) : '';
const x_val = item[`${kset.dataIndex}_X`];
// const _title = kset.title.replace('-[object Object]', '');
const v = { [kset.title]: x_val || export_val || data_val || render_val };
const v = { [kset.title]: x_val || export_val || data_val_str || render_val };
return { ...sv, ...v };
}, {});
return itemMapped;

@ -0,0 +1,40 @@
import React from 'react';
import { Select } from 'antd';
import { observer } from 'mobx-react';
//
export const languageOptions = [
{ value: '', label: '所有语种' },
{ value: '102001', label: '英语' },
{ value: '102002', label: '普通话' },
{ value: '102003', label: '日语' },
{ value: '102004', label: '韩语' },
{ value: '102005', label: '德语' },
{ value: '102006', label: '法语' },
{ value: '102007', label: '意大利语' },
{ value: '102008', label: '西班牙语' },
{ value: '102009', label: '俄语' },
{ value: '102010', label: '粤语' },
{ value: '102011', label: '印尼语' },
{ value: '102012', label: '泰国语' },
{ value: '102013', label: '葡萄牙语' }
];
export const GuideLanguageSelect = ({ value, onChange, ...props }) => {
return (
<div>
<Select
style={{ width: '100%' }}
defaultValue={['']}
placeholder="选择导游语种"
value={value}
onChange={onChange}
allowClear={true}
options={languageOptions}
{...props}
/>
</div>
);
};
export default observer(GuideLanguageSelect);

@ -0,0 +1,23 @@
import React from 'react';
import { Select } from 'antd';
import { observer } from 'mobx-react';
import { lineClass } from './../../libs/ht';
export const LineClassSelector = ({ value, onChange, ...props }) => {
return (
<div>
<Select
style={{ width: '100%' }}
placeholder="选择来源类型"
value={value}
onChange={onChange}
allowClear
labelInValue
{...props}
options={lineClass}
/>
</div>
);
};
export default observer(LineClassSelector);

@ -13,6 +13,8 @@ import SiteSelect from './SiteSelect';
import DateTypeSelect from './DataTypeSelect';
import DatePickerCharts from './DatePickerCharts';
import YearPickerCharts from './YearPickerCharts';
import GuideLanguageSelect from './GuideLanguageSelect';
import LineClassSeletor from './LineClassSeletor';
import SearchInput from './Input';
import { objectMapper, at, empty, isEmpty } from '@haina/utils-commons';
import { departureDateTypes } from './../../libs/ht';
@ -80,6 +82,13 @@ export default observer((props) => {
},
default: '',
},
'lineClass': {
key: 'lineClass',
transform: (value) => {
return isEmpty(value) ? '': Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.key : '';
},
default: '',
},
'WebCode': {
key: 'WebCode',
transform: (value) => {
@ -100,7 +109,7 @@ export default observer((props) => {
'operator': {
key: 'operator',
// transform: (value) => value?.key || '',
transform: (value) => Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? (!isNaN(parseInt(value.key), 10) ? value.key : '') : '',
transform: (value) => isEmpty(value) ? '': Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? (!isNaN(parseInt(value.key), 10) ? value.key : '') : '',
default: '',
},
'date': {
@ -360,6 +369,13 @@ function getFields(props) {
</Form.Item>,
fieldProps?.DepartmentList?.col
),
item(
'guide_lgc',
99,
<Form.Item name={`guide_lgc`}>
<GuideLanguageSelect />
</Form.Item>,
),
item(
'WebCode',
99,
@ -498,7 +514,7 @@ function getFields(props) {
item(
'operator',
99,
<Form.Item name={'operator'} dependencies={['DepartmentList']}>
<Form.Item name={'operator'} dependencies={['DepartmentList']} initialValue={at(props, 'initialValue.operator')[0]}>
<SearchInput
{...fieldProps.operator}
autoGet
@ -665,6 +681,13 @@ function getFields(props) {
<HotelStarSelect {...fieldProps.hotelStar} labelInValue={true} />
</Form.Item>
),
item(
'lineClass',
99,
<Form.Item name={`lineClass`} initialValue={at(props, 'initialValue.lineClass')[0] || undefined}>
<LineClassSeletor {...fieldProps.lineClass} labelInValue={true} />
</Form.Item>
),
];
baseChildren = baseChildren
.map((x) => {

@ -6,4 +6,4 @@ export const stores_Context = React.createContext();
export const DATE_FORMAT = "YYYY-MM-DD";
export const SMALL_DATETIME_FORMAT = 'YYYY-MM-DD 23:59:00';
export const DATETIME_FORMAT = 'YYYY-MM-DD 23:59:59';
export const HT_HOST = process.env.NODE_ENV === "production" ? "https://p9axztuwd7x8a7.mycht.cn" : "http://202.103.68.144:890";
export const HT_HOST = process.env.NODE_ENV === "production" ? "https://p9axztuwd7x8a7.mycht.cn" : "http://202.103.68.144:889";

@ -75,6 +75,7 @@ export const sites = [
{ value: '187', key: '187', label: 'HTravel', code: 'HTravel' },
{ value: '186', key: '186', label: 'JH', code: 'JH' },
{ value: '163', key: '163', label: 'GH', code: 'GH' },
{ value: '188', key: '188', label: 'Thailand', code: 'Thailand' },
{ value: '184', key: '184', label: 'GH站外渠道 (中国)', code: 'ZWQD' },
{ value: '185', key: '185', label: 'GH站外渠道 (海外)', code: 'GH_ZWQD_HW' },
{ value: '28', key: '28', label: '客运中国', code: 'GHKYZG' },
@ -116,6 +117,43 @@ export const departureDateTypes = [
{ key: 'departureDate', value: 'departureDate', label: '抵达日期' },
];
/**
* 附加来源类型
*/
export const lineClass = [
{key:78001, value: 78001, label:"Google PPC"},
{key:78002, value: 78002, label:"页面推荐订单"},
{key:78003, value: 78003, label:"Bing PPC"},
// {key:78004, value: 78004, label:null},
{key:78005, value: 78005, label:"Newsletter"},
{key:78006, value: 78006, label:"Facebook订单"},
{key:78007, value: 78007, label:"travelchinacheaper"},
{key:78008, value: 78008, label:"farwestchina"},
{key:78009, value: 78009, label:"petel.bg"},
{key:78010, value: 78010, label:"Instagram订单"},
{key:78011, value: 78011, label:"Pinterest"},
// {key:78012, value: 78012, label:null},
{key:78013, value: 78013, label:"网前自然订单"},
{key:78014, value: 78014, label:"Youtube"},
{key:78015, value: 78015, label:"国际站内广告"},
{key:78016, value: 78016, label:"WhatsApp"},
{key:78017, value: 78017, label:"Reddit"},
{key:78018, value: 78018, label:"邮件订单"},
{key:78019, value: 78019, label:"老客户网前订单"},
{key:78020, value: 78020, label:"1v1"},
{key:78021, value: 78021, label:"Facebook广告"},
{key:78022, value: 78022, label:"来自GH的订单"},
{key:78023, value: 78023, label:"小红书"},
{key:78024, value: 78024, label:"Yandex PPC"},
// {key:78025, value: 78025, label:null},
{key:78026, value: 78026, label:"TikTok"},
{key:78027, value: 78027, label:"Instagram广告"},
{key:78028, value: 78028, label:"Facebook再营销广告"},
{key:78029, value: 78029, label:"AI推荐"},
{key:78030, value: 78030, label:"合作平台"},
{key:78031, value: 78031, label:"客运前端获取"},
{key:78032, value: 78032, label:"CT站群营销"}];
/**
* 结果字段
*/
@ -318,7 +356,7 @@ export const pivotBy = (_data, [rows, columns, date]) => {
ele.isCusCommend_txt = ele.isCusCommend === '1' ? '老客户推荐' : '否';
const hasOld = (ele.IsOld === '1' || ele.isCusCommend === '1') ? 1 : 0;
ele.hasOld = hasOld;
ele.hasOld_txt = hasOld === 1 ? '老客户(推荐)' : '';
ele.hasOld_txt = hasOld === 1 ? '老客户(推荐)' : '';
// ele.SumML_ctxt1 = ele.ML > 10000 ? '1W+' : '1W-';
// ele.SumML_ctxt1_5 = ele.ML > 15000 ? '1.5W+' : '1.5W-';
// ele.SumML_ctxt2 = ele.ML > 20000 ? '2W+' : '2W-';
@ -332,7 +370,7 @@ export const pivotBy = (_data, [rows, columns, date]) => {
// 数组的字段值, 拆分处理
if (groupbyKeys.includes('destinationCountry')) {
data = data.reduce((r, v, i) => {
const vjson = isEmpty(v.destinationCountry) ? [] : v.destinationCountry;
const vjson = isEmpty(v.destinationCountry) ? [""] : v.destinationCountry;
const xv = (vjson).reduce((rv, cv, vi) => {
rv.push({...v, destinationCountry: cv, key: vi === 0 ? v.key : `${v.key}@${cv}`});
return rv;
@ -344,7 +382,7 @@ export const pivotBy = (_data, [rows, columns, date]) => {
}
if (groupbyKeys.includes('destinations')) {
data = data.reduce((r, v, i) => {
const vjson = isEmpty(v.destinations) ? [] : v.destinations;
const vjson = isEmpty(v.destinations) ? [""] : v.destinations;
const xv = (vjson).reduce((rv, cv, vi) => {
rv.push({...v, destinations: cv, key: vi === 0 ? v.key : `${v.key}@${cv}`});
return rv;

@ -2,7 +2,7 @@ import {makeAutoObservable, runInAction, toJS } from "mobx";
import { fetchJSON } from '@haina/utils-request';
import * as config from "../config";
import { groupsMappedByKey, sitesMappedByCode, pivotBy } from './../libs/ht';
import { sortBy, formatPercent, groupBy, isEmpty, uniqWith, formatPercentToFloat } from "@haina/utils-commons";
import { sortBy, formatPercent, groupBy, isEmpty, uniqWith, formatPercentToFloat, fixTo2Decimals, unique } from "@haina/utils-commons";
import { show_vs_tag, } from "./../utils/commons";
import moment from 'moment';
@ -14,6 +14,108 @@ const getDetailData = async (param) => {
return json.errcode === 0 ? json.result : [];
};
const initialSummaryRow = {
SumOrder: 0,
ResumeOrder: 0,
ResumeConfirmOrder: 0,
SumPersonNum: 0,
ConfirmPersonNum: 0,
ConfirmOrder: 0,
transactions: 0,
SumML: 0,
SumML_txt: '',
quotePrice: 0,
tourdays: 0,
applyDays: 0,
confirmDays: 0,
SingleML: 0,
OrderValue: 0,
PPPrice: 0,
AvgPPPrice: 0,
confirmTourdays: 0,
PPPriceRange: '',
unitPPPriceRange: '',
};
const calcSummaryRow = (data1, data2) => {
const summaryFields = ['ConfirmOrder', 'SumOrder', 'SumML', 'transactions', 'SumPersonNum', 'ConfirmPersonNum', 'ConfirmOrderKPIvalue', 'OrderKPIvalue', 'MLKPIvalue'];
const xSummary = summaryFields.reduce((r, skey) => ({ ...r, [skey]: data1.reduce((a, c) => a + c[skey], 0) }), {});
xSummary.ConfirmRates = xSummary.SumOrder ? fixTo2Decimals((xSummary.ConfirmOrder / xSummary.SumOrder) * 100) : 0;
xSummary.ConfirmRates_txt = xSummary.ConfirmRates; // + '%';
xSummary.confirmTourdays = '-';
xSummary.SingleML = '-';
xSummary._data = data1;
const xSummary2 = summaryFields.reduce((r, skey) => ({ ...r, [skey]: data2.reduce((a, c) => a + c[skey], 0) }), {});
xSummary2.ConfirmRates = xSummary2.SumOrder ? fixTo2Decimals((xSummary2.ConfirmOrder / xSummary2.SumOrder) * 100) : 0;
xSummary2.ConfirmRates_txt = xSummary2.ConfirmRates; // + '%';
xSummary2.confirmTourdays = '-';
xSummary2.SingleML = '-';
xSummary2._data = data2;
xSummary.vsData = xSummary2;
return xSummary;
};
const calcSummaryUniqueRow = (_data1, _data2, rawData = [[], []]) => {
// console.log('calcSummaryRow', _data1, _data2, rawData);
const uniqueKeysData1 = unique(_data1.reduce((a, r) => a.concat(r.key.split('_').reduce((a1, r1) => a1.concat(r1.split('@')[0]), [])), [])).map(x => Number(x));
const uniqueKeysData2 = unique(_data2.reduce((a, r) => a.concat(r.key.split('_').reduce((a1, r1) => a1.concat(r1.split('@')[0]), [])), [])).map(x => Number(x));
// console.log(uniqueKeysData1, uniqueKeysData2);
const data1 = rawData[0].filterHasOld.filter(row => uniqueKeysData1.includes((row.key)));
const data2 = rawData[1].filterHasOld.filter(row => uniqueKeysData2.includes((row.key)));
// console.log(data1, data2);
const xSummary = data1.reduce((r, v) => {
r.SumOrder += 1;
r.SumPersonNum += v.personNum;
r.ConfirmPersonNum += Number(v.orderState) === 1 ? v.personNum : 0;
r.ConfirmOrder += Number(v.orderState) === 1 ? 1 : 0;
r.ResumeOrder += v.hasOld === 1 ? 1 : 0;
r.ResumeConfirmOrder += Number(v.orderState) === 1 && v.hasOld === 1 ? 1 : 0;
r.transactions += v.transactions;
r.SumML += Number(v.orderState) === 1 ? v.ML : 0;
r.quotePrice += Number(v.orderState) === 1 ? v.quotePrice : 0;
r.tourdays += v.tourdays;
r.applyDays += v.applyDays;
r.confirmDays += v.confirmDays;
// r.PPPrice += Number(v.orderState) === 1 ? v.PPPrice : 0;
r.confirmTourdays += Number(v.orderState) === 1 ? v.tourdays : 0;
return r;
}, structuredClone(initialSummaryRow));
const xSummary2 = data2.reduce((r, v) => {
r.SumOrder += 1;
r.SumPersonNum += v.personNum;
r.ConfirmPersonNum += Number(v.orderState) === 1 ? v.personNum : 0;
r.ConfirmOrder += Number(v.orderState) === 1 ? 1 : 0;
r.ResumeOrder += v.hasOld === 1 ? 1 : 0;
r.ResumeConfirmOrder += Number(v.orderState) === 1 && v.hasOld === 1 ? 1 : 0;
r.transactions += v.transactions;
r.SumML += Number(v.orderState) === 1 ? v.ML : 0;
r.quotePrice += Number(v.orderState) === 1 ? v.quotePrice : 0;
r.tourdays += v.tourdays;
r.applyDays += v.applyDays;
r.confirmDays += v.confirmDays;
// r.PPPrice += Number(v.orderState) === 1 ? v.PPPrice : 0;
r.confirmTourdays += Number(v.orderState) === 1 ? v.tourdays : 0;
return r;
}, structuredClone(initialSummaryRow));
xSummary.ConfirmRates = xSummary.SumOrder ? fixTo2Decimals((xSummary.ConfirmOrder / xSummary.SumOrder) * 100) : 0;
xSummary.ConfirmRates_txt = xSummary.ConfirmRates; // + '%';
xSummary.confirmTourdays = '-';
xSummary.SingleML = '-';
xSummary._data = data1;
xSummary2.ConfirmRates = xSummary2.SumOrder ? fixTo2Decimals((xSummary2.ConfirmOrder / xSummary2.SumOrder) * 100) : 0;
xSummary2.ConfirmRates_txt = xSummary2.ConfirmRates; // + '%';
xSummary2.confirmTourdays = '-';
xSummary2.SingleML = '-';
xSummary2._data = data2;
xSummary.vsData = xSummary2;
return xSummary;
};
class CustomerStore {
constructor(rootStore) {
@ -466,8 +568,8 @@ class CustomerStore {
// mergedData: [],
rawData: [],
searchValues: {
DepartmentList: ['1', '2', '28', '7'].map(kk => groupsMappedByKey[kk]),
WebCode: ['CHT','AH','JH','GH','ZWQD','GH_ZWQD_HW','GHKYZG','GHKYHW'].map(kk => sitesMappedByCode[kk]),
DepartmentList: ['1', '2', '28', '7', '33'].map(kk => groupsMappedByKey[kk]),
WebCode: ['CHT','AH','HTravel','JH','GH','ZWQD','GH_ZWQD_HW','GHKYZG','GHKYHW'].map(kk => sitesMappedByCode[kk]),
DateType: { key: 'applyDate', label: '提交日期'},
IncludeTickets: { key: '0', label: '不含门票' },
},
@ -476,6 +578,8 @@ class CustomerStore {
// country: { loading: false, data: [], rawData: [], mergedData: [], filterColValues: [] },
// },
pivotResult: [],
countrySummary: [],
citySummary: [],
filterColValues: [],
rawDataArr: [],
};
@ -554,38 +658,156 @@ class CustomerStore {
// console.log('regular_data_pivot -------------------------------------------------------------- rawData 000 ', toJS(this.sales_regular_data.rawData));
// console.log('regular_data_pivot ---- ', pivotRow, pivotCol);
const [result1, result2] = this.sales_regular_data.rawDataArr;
const allRows = Array.from(new Set([...result1.filterHasOld.map(row=>row[pivotRow]), ...result2.filterHasOld.map(row=>row[pivotRow])]));
// console.log(' ------ allRows', allRows);
const [pivot1, pivot2] = [result1, result2].map(_result => {
// const allRows = Array.from(new Set([...result1.filterHasOld.map(row=>row[pivotRow]), ...result2.filterHasOld.map(row=>row[pivotRow])]));
// console.log(' ------ allRows', result1, result2);
const [{ pivotResult: pivot1, rowValues: rowValues1 }, { pivotResult: pivot2, rowValues: rowValues2 }] = [result1, result2].map((_result) => {
const dataColField = pivotCol.replace('_txt', '');
const rawData = pivotCol === 'hasOld' ? _result.filterHasOld : _result.filterHasOld.filter((ele) => ele[dataColField] === '1');
const { data: pivotResult } = pivotBy(rawData, [[pivotRow, pivotCol], [], []]);
return pivotResult;
const {
data: pivotResult,
columnValues: [[rowValues], columnsKeys, dateKeys],
} = pivotBy(rawData, [[pivotRow, pivotCol], [], []]);
return { pivotResult, rowValues };
});
const allRowValues = Array.from(new Set([...rowValues1, ...rowValues2]));
// console.log(' ------ xx', rowValues1);
const rows1 = groupBy(pivot1, pivotRow);
const rows2 = groupBy(pivot2, pivotRow);
const pivotResultWithCompare = isEmpty(pivot2) ? pivot1 : allRows.reduce((r, rowName) => {
const _default = { [pivotRow]: rowName, rowLabel: rows1?.[rowName]?.rowLabel || rows2?.[rowName]?.rowLabel, children: rows1?.[rowName], key: rowName};
const operatorRow = {...(rows1?.[rowName]?.[0] || _default), vsData: rows2?.[rowName]?.[0] || {}};
// 展开的两项: '老客户', '老客户推荐'
// const series1Children = rows1?.[rowName]?.[0]?.children || [];
// const series2Children = rows2?.[rowName]?.[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);
}, []);
const pivotResultWithCompare = isEmpty(pivot2)
? pivot1
: allRowValues.reduce((r, rowName) => {
const _default = { [pivotRow]: rowName, rowLabel: rows1?.[rowName]?.rowLabel || rows2?.[rowName]?.rowLabel, children: rows1?.[rowName], key: rowName };
const operatorRow = { ...(rows1?.[rowName]?.[0] || _default), vsData: rows2?.[rowName]?.[0] || {} };
// 展开的两项: '老客户', '老客户推荐'
// const series1Children = rows1?.[rowName]?.[0]?.children || [];
// const series2Children = rows2?.[rowName]?.[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);
}, []);
// console.log(' ------ pivot1', pivot1, '\npivot2', pivot2, '\npivotResultWithCompare', pivotResultWithCompare);
const filterColValues = uniqWith(
allRows.map((rr) => ({ text: rr, value: rr })),
allRowValues.map((rr) => ({ text: rr, value: rr })),
(a, b) => JSON.stringify(a) === JSON.stringify(b)
).sort((a, b) => a.text.localeCompare(b.text, 'zh-CN'));
if (pivotRow === 'country') {
const aseanKK = ['新加坡', '马来西亚', '印度尼西亚', '菲律宾', '越南', '泰国', '文莱', '柬埔寨', '东帝汶', '老挝', '缅甸'];
const [asean1, asean2] = [
pivot1.filter((ele) => aseanKK.includes(ele.country)),
pivot2.filter((ele) => aseanKK.includes(ele.country)),
];
const aseanSummary = calcSummaryRow(asean1, asean2);
aseanSummary.key = '东南亚11国';
aseanSummary.country = '东南亚11国';
const eurusdKK = ['美国', '加拿大', '澳大利亚', '英国'];
const [eurusd1, eurusd2] = [
pivot1.filter((ele) => eurusdKK.includes(ele.country)),
pivot2.filter((ele) => eurusdKK.includes(ele.country)),
];
const eurusdSummary = calcSummaryRow(eurusd1, eurusd2);
eurusdSummary.key = '欧美4国';
eurusdSummary.country = '欧美4国';
// 西班牙语国家/地区
const esLgc = ['阿根廷','玻利维亚','智利','哥伦比亚','哥斯达黎加','古巴','多米尼加共和国','厄瓜多尔','萨尔瓦多','赤道几内亚','危地马拉','洪都拉斯','墨西哥','尼加拉瓜','巴拿马','巴拉圭','秘鲁','波多黎各','西班牙','乌拉圭','委内瑞拉'];
const [esLgc1, esLgc2] = [
pivot1.filter((ele) => esLgc.includes(ele.country)),
pivot2.filter((ele) => esLgc.includes(ele.country)),
];
const esLgcSummary = calcSummaryRow(esLgc1, esLgc2);
esLgcSummary.key = '西班牙语国家/地区';
esLgcSummary.country = '西班牙语国家/地区';
// 意大利语国家/地区
const itLgc = ['意大利','圣马力诺','梵蒂冈','瑞士',];
const [itLgc1, itLgc2] = [
pivot1.filter((ele) => itLgc.includes(ele.country)),
pivot2.filter((ele) => itLgc.includes(ele.country)),
];
const itLgcSummary = calcSummaryRow(itLgc1, itLgc2);
itLgcSummary.key = '意大利语国家/地区';
itLgcSummary.country = '意大利语国家/地区';
// 德语国家/地区
const deLgc = ['德国','奥地利','瑞士','列支敦士登','卢森堡','比利时',];
const [deLgc1, deLgc2] = [
pivot1.filter((ele) => deLgc.includes(ele.country)),
pivot2.filter((ele) => deLgc.includes(ele.country)),
];
const deLgcSummary = calcSummaryRow(deLgc1, deLgc2);
deLgcSummary.key = '德语国家/地区';
deLgcSummary.country = '德语国家/地区';
// 葡萄牙语国家
const ptLgc = ['葡萄牙','巴西','安哥拉','莫桑比克','几内亚比绍','佛得角','圣多美和普林西比','东帝汶',];
const [ptLgc1, ptLgc2] = [
pivot1.filter((ele) => ptLgc.includes(ele.country)),
pivot2.filter((ele) => ptLgc.includes(ele.country)),
];
const ptLgcSummary = calcSummaryRow(ptLgc1, ptLgc2);
ptLgcSummary.key = '葡萄牙语国家/地区';
ptLgcSummary.country = '葡萄牙语国家/地区';
this.sales_regular_data.countrySummary = [
aseanSummary, eurusdSummary,
esLgcSummary, itLgcSummary, deLgcSummary, ptLgcSummary,
];
}
if (pivotRow === 'destinations') {
const city1 = ['昆明','大理','丽江','中甸','德钦','西双版纳','普洱','泸沽湖','腾冲'];
const [city11, city12] = [
pivot1.filter((ele) => city1.includes(ele.destinations)),
pivot2.filter((ele) => city1.includes(ele.destinations)),
];
const city1Summary = calcSummaryUniqueRow(city11, city12, toJS(this.sales_regular_data.rawDataArr));
city1Summary.key = '云南';
city1Summary.destinations = '云南';
const city2 = ['上海','苏州','杭州','黄山','婺源','上饶','景德镇'];
const [city21, city22] = [
pivot1.filter((ele) => city2.includes(ele.destinations)),
pivot2.filter((ele) => city2.includes(ele.destinations)),
];
const city2Summary = calcSummaryUniqueRow(city21, city22, toJS(this.sales_regular_data.rawDataArr));
city2Summary.key = '华东';
city2Summary.destinations = '华东';
const city3 = ['乌鲁木齐','喀什','伊宁','昭苏','霍城','那拉提','喀纳斯','禾木','布尔津','吐鲁番','库尔勒','赛里木湖','和田','库车'];
const [city31, city32] = [
pivot1.filter((ele) => city3.includes(ele.destinations)),
pivot2.filter((ele) => city3.includes(ele.destinations)),
];
const city3Summary = calcSummaryUniqueRow(city31, city32, toJS(this.sales_regular_data.rawDataArr));
city3Summary.key = '新疆';
city3Summary.destinations = '新疆';
const city4 = ['拉萨','江孜','林芝','鲁朗','巴松措','然乌','波密','日喀则','定日','泽当','塔钦'];
const [city41, city42] = [
pivot1.filter((ele) => city4.includes(ele.destinations)),
pivot2.filter((ele) => city4.includes(ele.destinations)),
];
const city4Summary = calcSummaryUniqueRow(city41, city42, toJS(this.sales_regular_data.rawDataArr));
city4Summary.key = '西藏';
city4Summary.destinations = '西藏';
const city5 = ['贵阳','安顺','黄果树','榕江','从江','毕节','织金','铜仁','梵净山','荔波','平塘','兴义'];
const [city51, city52] = [
pivot1.filter((ele) => city5.includes(ele.destinations)),
pivot2.filter((ele) => city5.includes(ele.destinations)),
];
const city5Summary = calcSummaryUniqueRow(city51, city52, toJS(this.sales_regular_data.rawDataArr));
city5Summary.key = '贵州';
city5Summary.destinations = '贵州';
this.sales_regular_data.citySummary = [city1Summary, city2Summary, city3Summary, city4Summary, city5Summary];
}
this.sales_regular_data.pivotResult = pivotResultWithCompare;
this.sales_regular_data.filterColValues = filterColValues;

@ -28,7 +28,7 @@ const filterFields = [
options: [
{ key: 'productType', value: 'productType', label: '产品类型' },
{ key: 'CLI_NO', value: 'CLI_NO', label: '线路' },
{ key: 'destinationCountry', value: 'destinationCountry', label: '目的地国' },
{ key: 'destinationCountry', value: 'destinationCountry', label: '目的地国' },
{ key: 'destinations', value: 'destinations', label: '目的地城市' },
{ key: 'HotelStar', value: 'HotelStar', label: '酒店星级' },
],
@ -40,20 +40,21 @@ const filterFields = [
{ key: 'travelMotivation', value: 'travelMotivation', label: '出行目的' },
{ key: 'IsOld_txt', value: 'IsOld_txt', label: '是否老客户' },
{ key: 'isCusCommend_txt', value: 'isCusCommend_txt', label: '是否老客户推荐' },
{ key: 'hasOld_txt', value: 'hasOld_txt', label: '老客户(推荐)' },
{ key: 'hasOld_txt', value: 'hasOld_txt', label: '老客户(推荐)' },
{ key: 'RTXF_WB_range', value: 'RTXF_WB_range', label: '人天消费(外币)' },
{ key: 'customer_types', value: 'customer_types', label: '分销客户' },
],
},
{
label: '业绩',
options: [
{ key: 'operatorName', value: 'operatorName', label: '顾问' },
{ key: 'SumML_ctxt', value: 'SumML_ctxt', label: '毛利' },
{ key: 'startMonth', value: 'startMonth', label: '出行日期-月份' },
{ key: 'startYearMonth', value: 'startYearMonth', label: '出行日期-年月' },
{ key: 'applyMonth', value: 'applyMonth', label: '预订日期-月份' },
{ key: 'applyYearMonth', value: 'applyYearMonth', label: '预订日期-年月' },
{ key: 'operatorName', value: 'operatorName', label: '顾问' },
{ key: 'PPPriceRange', value: 'PPPriceRange', label: '人均天/单(外币)' },
{ key: 'SumML_ctxt', value: 'SumML_ctxt', label: '毛利' },
{ key: 'dealDays_ctxt', value: 'dealDays_ctxt', label: '成团周期(天)' },
{ key: 'applyDays_ctxt', value: 'applyDays_ctxt', label: '预订周期(天)' },
],
@ -74,7 +75,7 @@ const quickOptions = [
{ label: ' 来源站点 ', fields: [['WebCode'], []] },
{ label: '[ 产品×客群 ]', fields: [['productType', 'guestGroupType'], []] },
{ label: '[ 国籍×客群 ]', fields: [['country', 'guestGroupType'], []] },
{ label: '[ 客群×目的地国 ]', fields: [['guestGroupType', 'destinationCountry'], []] },
{ label: '[ 客群×目的地国 ]', fields: [['guestGroupType', 'destinationCountry'], []] },
// { label: '[ × ]×[ ]', fields: [['country', 'guestGroupType'], []] },
];

@ -7,6 +7,7 @@ import SearchForm from '../components/search/SearchForm';
import useHostCaseStore from '../zustand/HostCase';
import { useShallow } from 'zustand/shallow';
import { exportDoc } from '../components/TemplateLetter2025/Index';
import { RenderVSDataCell } from './../components/Data';
import ExportDocxBtn from '../components/TemplateLetter2025/ExportDocxBtn';
const sorter = (a, b, key) => parseInt(a[key]) - parseInt(b[key]);
@ -14,8 +15,8 @@ const HostCaseReport = ({ ...props }) => {
const { date_picker_store } = useContext(stores_Context);
// const host_case_data = customer_store.host_case_data;
const [loading, reset, searchValues, setSearchValues, forExport] = useHostCaseStore(
useShallow((state) => [state.loading, state.reset, state.searchValues, state.setSearchValues, state.forExport])
const [loading, reset, searchValues, setSearchValues, forExport, searchValuesToSub] = useHostCaseStore(
useShallow((state) => [state.loading, state.reset, state.searchValues, state.setSearchValues, state.forExport, state.searchValuesToSub])
);
const [caseSummary, caseSummaryByGuide, caseFeatured, getCaseReport] = useHostCaseStore(
useShallow((state) => [state.caseSummary, state.caseSummaryByGuide, state.caseFeatured, state.getCaseReport])
@ -26,35 +27,46 @@ const HostCaseReport = ({ ...props }) => {
await getCaseReport(formVal);
};
const summaryCols = [
{ title: '接团数', dataIndex: 'group_count', width: '6rem',
sorter: (a, b) => sorter(a,b, 'group_count'),
render: (text, r) => <RenderVSDataCell data1={r.group_count} data2={r.diff?.group_count} showDiffData={searchValuesToSub.DateDiff1} />,
},
// { title: 'feedback', dataIndex: 'feedbak_group', width: '6rem' },
{ title: '东道主团数', dataIndex: 'group_count_dongdaozhu', width1: '8rem',
sorter: (a, b) => sorter(a,b, 'group_count_dongdaozhu'),
render: (text, r) => <RenderVSDataCell data1={r.group_count_dongdaozhu} data2={r.diff?.group_count_dongdaozhu} showDiffData={searchValuesToSub.DateDiff1} />,
},
{ title: '东道主个数', dataIndex: 'case_count_dongdaozhu', width1: '8rem',
sorter: (a, b) => sorter(a,b, 'case_count_dongdaozhu'),
render: (text, r) => <RenderVSDataCell data1={r.case_count_dongdaozhu} data2={r.diff?.case_count_dongdaozhu} showDiffData={searchValuesToSub.DateDiff1} />,
},
{ title: '东道主实施比例', dataIndex: 'dongdaozhu_rate',
render: (text, r) => <RenderVSDataCell data1={parseFloat(r.dongdaozhu_rate)} data2={parseFloat(r.diff?.dongdaozhu_rate)} dataSuffix='%' showDiffData={searchValuesToSub.DateDiff1} />,
},
{ title: '东道主实施比例', dataIndex: 'dongdaozhu_rate' },
{
title: '各类型个数',
children: [
{ title: 'Live There', dataIndex: 'live_there_count',
sorter: (a, b) => sorter(a, b, 'live_there_count'),
render: (text, r) => <RenderVSDataCell data1={r.live_there_count} data2={r.diff?.live_there_count} showDiffData={searchValuesToSub.DateDiff1} />,
},
{ title: '动机圆梦', dataIndex: 'dream_fulfillment_count',
sorter: (a, b) => sorter(a, b, 'dream_fulfillment_count'),
render: (text, r) => <RenderVSDataCell data1={r.dream_fulfillment_count} data2={r.diff?.dream_fulfillment_count} showDiffData={searchValuesToSub.DateDiff1} />,
},
{ title: '仪式感创造', dataIndex: 'ceremony_creation_count',
sorter: (a, b) => sorter(a, b, 'ceremony_creation_count'),
render: (text, r) => <RenderVSDataCell data1={r.ceremony_creation_count} data2={r.diff?.ceremony_creation_count} showDiffData={searchValuesToSub.DateDiff1} />,
},
{ title: '遗憾弥补', dataIndex: 'regret_compensation_count',
sorter: (a, b) => sorter(a, b, 'regret_compensation_count'),
render: (text, r) => <RenderVSDataCell data1={r.regret_compensation_count} data2={r.diff?.regret_compensation_count} showDiffData={searchValuesToSub.DateDiff1} />,
},
{ title: '力挽狂澜', dataIndex: 'rescue_mission_count',
sorter: (a, b) => sorter(a, b, 'rescue_mission_count'),
render: (text, r) => <RenderVSDataCell data1={r.rescue_mission_count} data2={r.diff?.rescue_mission_count} showDiffData={searchValuesToSub.DateDiff1} />,
},
],
},
@ -78,11 +90,11 @@ const HostCaseReport = ({ ...props }) => {
...toJS(date_picker_store.formValues),
...searchValues,
},
shows: ['years', 'agency'],
shows: ['years', 'agency','DepartmentList', 'guide_lgc'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
dates: { hide_vs: true },
years: { hide_vs: true },
years: { hide_vs: false },
agency: { rules: [{ required: true, message: '请选择地接社' }] },
},
}}

@ -71,9 +71,11 @@ class Orders extends Component {
</div> : null}
</span>
),
titleX: showDiff ? `${date_picker_store.start_date.format(config.DATE_FORMAT)}~${date_picker_store.end_date.format(config.DATE_FORMAT)} vs ${date_picker_store.start_date_cp.format(
config.DATE_FORMAT
)}~${date_picker_store.end_date_cp.format(config.DATE_FORMAT)}` : `${date_picker_store.start_date.format(config.DATE_FORMAT)}~${date_picker_store.end_date.format(config.DATE_FORMAT)}`,
titleX: showDiff ? `${
date_picker_store.start_date.format(config.DATE_FORMAT)}~${date_picker_store.end_date.format(config.DATE_FORMAT)
} vs ${
date_picker_store.start_date_cp.format(config.DATE_FORMAT)}~${date_picker_store.end_date_cp.format(config.DATE_FORMAT)
}` : `${date_picker_store.start_date.format(config.DATE_FORMAT)}~${date_picker_store.end_date.format(config.DATE_FORMAT)}`,
dataIndex: 'OrderType',
fixed: 'left',
render: (text, record) => <NavLink to={`/orders_sub/${orders_store.active_tab_key}/${record.OrderTypeSN}/${encodeURIComponent(record.OrderType)}`}>{text}</NavLink>,
@ -93,7 +95,7 @@ class Orders extends Component {
diffData={ordercountTotal1?.OrderCount_diff}
/>
),
titleX: [ordercountTotal1.OrderCount, ordercountTotal2.OrderCount].join(' vs '),
titleX: [ordercountTotal1.OrderCount, ordercountTotal2.OrderCount].filter(s => s).filter(s => s).join(' vs '),
dataIndex: 'OrderCount',
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.OrderCount} diffPercent={r.OrderCount_vs} diffData={r.OrderCount_diff} />,
},
@ -112,7 +114,7 @@ class Orders extends Component {
diffData={ordercountTotal1?.CJCount_diff}
/>
),
titleX: [ordercountTotal1.CJCount, ordercountTotal2.CJCount].join(' vs '),
titleX: [ordercountTotal1.CJCount, ordercountTotal2.CJCount].filter(s => s).join(' vs '),
dataIndex: 'CJCount',
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.CJCount} diffPercent={r.CJCount_vs} diffData={r.CJCount_diff} />,
},
@ -131,7 +133,7 @@ class Orders extends Component {
diffData={ordercountTotal1?.CJPersonNum_diff}
/>
),
titleX: [ordercountTotal1.CJPersonNum, ordercountTotal2.CJPersonNum].join(' vs '),
titleX: [ordercountTotal1.CJPersonNum, ordercountTotal2.CJPersonNum].filter(s => s).join(' vs '),
dataIndex: 'CJPersonNum',
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.CJPersonNum} diffPercent={r.CJPersonNum_vs} diffData={r.CJPersonNum_diff} />,
},
@ -150,7 +152,7 @@ class Orders extends Component {
diffData={ordercountTotal1?.CJrate_diff}
/>
),
titleX: [ordercountTotal1.CJrate, ordercountTotal2.CJrate].join(' vs '),
titleX: [ordercountTotal1.CJrate, ordercountTotal2.CJrate].filter(s => s).join(' vs '),
dataIndex: 'CJrate',
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.CJrate} diffPercent={r.CJrate_vs} diffData={r.CJrate_diff} />,
},
@ -169,7 +171,7 @@ class Orders extends Component {
diffData={ordercountTotal1?.YJLY_diff}
/>
),
titleX: [ordercountTotal1.YJLY, ordercountTotal2.YJLY].join(' vs '),
titleX: [ordercountTotal1.YJLY, ordercountTotal2.YJLY].filter(s => s).join(' vs '),
dataIndex: 'YJLY',
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.YJLY} diffPercent={r.YJLY_vs} diffData={r.YJLY_diff} />,
},
@ -189,7 +191,7 @@ class Orders extends Component {
diffData={ordercountTotal1?.Ordervalue_diff}
/>
),
titleX: [ordercountTotal1.Ordervalue, ordercountTotal2.Ordervalue].join(' vs '),
titleX: [ordercountTotal1.Ordervalue, ordercountTotal2.Ordervalue].filter(s => s).join(' vs '),
dataIndex: 'Ordervalue',
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.Ordervalue} diffPercent={r.Ordervalue_vs} diffData={r.Ordervalue_diff} />,
},

@ -7,7 +7,7 @@ import * as comm from '@haina/utils-commons';
import DateGroupRadio from '../../components/DateGroupRadio';
import SearchForm from '../../components/search/SearchForm';
import { TableExportBtn } from '../../components/Data';
import { VSDataTag, } from './../../components/Data';
import { RenderVSDataCell } from './../../components/Data';
import { observer } from 'mobx-react';
import { toJS } from 'mobx';
@ -15,13 +15,6 @@ import { stores_Context } from '../../config';
import { useShallow } from 'zustand/shallow';
import useBizOrderStore, { orderCountDataMapper, orderCountDataFieldMapper } from '../../zustand/BizOrder';
const DataRenderCell = ({ data1, data2, dataSuffix = '', showDiffData, ...props }) => {
if (showDiffData) {
return <VSDataTag data1={data1} data2={data2} dataSuffix={dataSuffix} {...props} />;
}
return <div>{data1}{dataSuffix}</div>;
};
const BizOrder = observer(() => {
const { date_picker_store: searchFormStore } = useContext(stores_Context);
@ -151,7 +144,7 @@ const BizOrder = observer(() => {
children: [
{
title: (
<DataRenderCell
<RenderVSDataCell
showDiffData={showDiff}
data1={result.ordercountTotal1?.OrderCount}
data2={result.ordercountTotal2?.OrderCount}
@ -161,7 +154,7 @@ const BizOrder = observer(() => {
),
titleX: [result.ordercountTotal1?.OrderCount, result.ordercountTotal2?.OrderCount].join(' vs '),
dataIndex: 'OrderCount',
render: (text, r) => <DataRenderCell showDiffData={showDiff} data1={text} data2={r.diff?.OrderCount} diffPercent={r.OrderCount_vs} diffData={r.OrderCount_diff} />,
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.OrderCount} diffPercent={r.OrderCount_vs} diffData={r.OrderCount_diff} />,
},
],
},
@ -170,7 +163,7 @@ const BizOrder = observer(() => {
children: [
{
title: (
<DataRenderCell
<RenderVSDataCell
showDiffData={showDiff}
data1={result.ordercountTotal1?.CJCount}
data2={result.ordercountTotal2?.CJCount}
@ -180,7 +173,7 @@ const BizOrder = observer(() => {
),
titleX: [result.ordercountTotal1?.CJCount, result.ordercountTotal2?.CJCount].join(' vs '),
dataIndex: 'CJCount',
render: (text, r) => <DataRenderCell showDiffData={showDiff} data1={text} data2={r.diff?.CJCount} diffPercent={r.CJCount_vs} diffData={r.CJCount_diff} />,
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.CJCount} diffPercent={r.CJCount_vs} diffData={r.CJCount_diff} />,
},
],
},
@ -189,7 +182,7 @@ const BizOrder = observer(() => {
children: [
{
title: (
<DataRenderCell
<RenderVSDataCell
showDiffData={showDiff}
data1={result.ordercountTotal1?.CJPersonNum}
data2={result.ordercountTotal2?.CJPersonNum}
@ -199,7 +192,7 @@ const BizOrder = observer(() => {
),
titleX: [result.ordercountTotal1?.CJPersonNum, result.ordercountTotal2?.CJPersonNum].join(' vs '),
dataIndex: 'CJPersonNum',
render: (text, r) => <DataRenderCell showDiffData={showDiff} data1={text} data2={r.diff?.CJPersonNum} diffPercent={r.CJPersonNum_vs} diffData={r.CJPersonNum_diff} />,
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.CJPersonNum} diffPercent={r.CJPersonNum_vs} diffData={r.CJPersonNum_diff} />,
},
],
},
@ -208,7 +201,7 @@ const BizOrder = observer(() => {
children: [
{
title: (
<DataRenderCell
<RenderVSDataCell
showDiffData={showDiff}
data1={result.ordercountTotal1?.CJrate}
data2={result.ordercountTotal2?.CJrate}
@ -218,7 +211,7 @@ const BizOrder = observer(() => {
),
titleX: [result.ordercountTotal1?.CJrate, result.ordercountTotal2?.CJrate].join(' vs '),
dataIndex: 'CJrate',
render: (text, r) => <DataRenderCell showDiffData={showDiff} data1={text} data2={r.diff?.CJrate} diffPercent={r.CJrate_vs} diffData={r.CJrate_diff} />,
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.CJrate} diffPercent={r.CJrate_vs} diffData={r.CJrate_diff} />,
},
],
},
@ -227,7 +220,7 @@ const BizOrder = observer(() => {
children: [
{
title: (
<DataRenderCell
<RenderVSDataCell
showDiffData={showDiff}
data1={result.ordercountTotal1?.YJLY}
data2={result.ordercountTotal2?.YJLY}
@ -237,7 +230,7 @@ const BizOrder = observer(() => {
),
titleX: [result.ordercountTotal1?.YJLY, result.ordercountTotal2?.YJLY].join(' vs '),
dataIndex: 'YJLY',
render: (text, r) => <DataRenderCell showDiffData={showDiff} data1={text} data2={r.diff?.YJLY} diffPercent={r.YJLY_vs} diffData={r.YJLY_diff} />,
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.YJLY} diffPercent={r.YJLY_vs} diffData={r.YJLY_diff} />,
},
],
},
@ -247,7 +240,7 @@ const BizOrder = observer(() => {
children: [
{
title: (
<DataRenderCell
<RenderVSDataCell
showDiffData={showDiff}
data1={result.ordercountTotal1?.Ordervalue}
data2={result.ordercountTotal2?.Ordervalue}
@ -257,7 +250,7 @@ const BizOrder = observer(() => {
),
titleX: [result.ordercountTotal1?.Ordervalue, result.ordercountTotal2?.Ordervalue].join(' vs '),
dataIndex: 'Ordervalue',
render: (text, r) => <DataRenderCell showDiffData={showDiff} data1={text} data2={r.diff?.Ordervalue} diffPercent={r.Ordervalue_vs} diffData={r.Ordervalue_diff} />,
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.Ordervalue} diffPercent={r.Ordervalue_vs} diffData={r.Ordervalue_diff} />,
},
],
},

@ -4,7 +4,7 @@ import { Funnel, Pie, Sunburst } from '@ant-design/charts';
import { isEmpty } from '@haina/utils-commons';
import SearchForm from '../../../components/search/SearchForm';
import { VSDataTag, } from './../../../components/Data';
import { RenderVSDataCell } from './../../../components/Data';
import { observer } from 'mobx-react';
import { toJS } from 'mobx';
@ -63,13 +63,6 @@ const buildPieData = (data1, data2) => {
return data01.concat(data02);
};
const DataRenderCell = ({ data1, data2, dataSuffix = '', showDiffData, ...props }) => {
if (showDiffData) {
return <VSDataTag data1={data1} data2={data2} dataSuffix={dataSuffix} {...props} />;
}
return <div>{data1}{dataSuffix}</div>;
};
const TrainsUpsell = observer(({ ...props }) => {
const { date_picker_store: searchFormStore } = useContext(stores_Context);
const [searchValues, setSearchValues] = useTrainsStore(useShallow((state) => [state.searchValues, state.setSearchValues]));
@ -175,33 +168,33 @@ const TrainsUpsell = observer(({ ...props }) => {
{
title: '数量',
dataIndex: 'OrderCount',
render: (text, r) => <DataRenderCell showDiffData={showDiff} data1={text} data2={r.diff?.OrderCount} diffPercent={r.OrderCount_vs} diffData={r.OrderCount_diff} />,
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.OrderCount} diffPercent={r.OrderCount_vs} diffData={r.OrderCount_diff} />,
},
{
title: '成交数',
dataIndex: 'CJCount',
render: (text, r) => <DataRenderCell showDiffData={showDiff} data1={text} data2={r.diff?.CJCount} diffPercent={r.CJCount_vs} diffData={r.CJCount_diff} />,
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.CJCount} diffPercent={r.CJCount_vs} diffData={r.CJCount_diff} />,
},
{
title: '成交人数',
dataIndex: 'CJPersonNum',
render: (text, r) => <DataRenderCell showDiffData={showDiff} data1={text} data2={r.diff?.CJPersonNum} diffPercent={r.CJPersonNum_vs} diffData={r.CJPersonNum_diff} />,
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.CJPersonNum} diffPercent={r.CJPersonNum_vs} diffData={r.CJPersonNum_diff} />,
},
{
title: '成交率',
dataIndex: 'CJrate',
render: (text, r) => <DataRenderCell showDiffData={showDiff} data1={text} data2={r.diff?.CJrate} diffPercent={r.CJrate_vs} diffData={r.CJrate_diff} />,
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.CJrate} diffPercent={r.CJrate_vs} diffData={r.CJrate_diff} />,
},
{
title: '成交毛利(预计)',
dataIndex: 'YJLY',
render: (text, r) => <DataRenderCell showDiffData={showDiff} data1={text} data2={r.diff?.YJLY} diffPercent={r.YJLY_vs} diffData={r.YJLY_diff} />,
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.YJLY} diffPercent={r.YJLY_vs} diffData={r.YJLY_diff} />,
},
{
title: '单个订单价值',
dataIndex: 'Ordervalue',
render: (text, r) => <DataRenderCell showDiffData={showDiff} data1={text} data2={r.diff?.Ordervalue} diffPercent={r.Ordervalue_vs} diffData={r.Ordervalue_diff} />,
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.Ordervalue} diffPercent={r.Ordervalue_vs} diffData={r.Ordervalue_diff} />,
},
];
return (

@ -0,0 +1,273 @@
import { useContext } from 'react';
import { Row, Col, Table, Button } from 'antd';
import { SwapOutlined } from '@ant-design/icons';
import * as comm from '@haina/utils-commons';
import SearchForm from '../../components/search/SearchForm';
import { RenderVSDataCell } from '../../components/Data';
import { observer } from 'mobx-react';
import { toJS } from 'mobx';
import { stores_Context } from '../../config';
import { useShallow } from 'zustand/shallow';
import useSalesInsightStore from '../../zustand/SalesInsight';
// TdCellDataTable
const TdCell = (tdprops) => {
// onMouseEnter, onMouseLeave
const { onMouseEnter, onMouseLeave, ...restProps } = tdprops;
return <td {...restProps} />;
};
const MeetingSales = observer(() => {
const { date_picker_store: searchFormStore } = useContext(stores_Context);
const [searchValues, setSearchValues] = useSalesInsightStore(useShallow((state) => [state.searchValues, state.setSearchValues]));
const [loading, typeLoading, ] = useSalesInsightStore(useShallow((state) => [state.loading, state.typeLoading, ]));
const [tableMajorKey, tableData, salesDataTotal] = useSalesInsightStore(useShallow((state) => [state.matrixtableMajorKey, state.matrixTableData, state.salesDataTotal]));
const getMeetingDataSales = useSalesInsightStore((state) => state.getMeetingDataSales);
const onMatrixChange = useSalesInsightStore((state) => state.onMatrixChange);
// const showDiff = !comm.isEmpty(searchFormStore.start_date_cp);
const columns = [
{
title: (
<>
<Button icon={<SwapOutlined />} onClick={onMatrixChange} />
</>
),
key: 'matrix',
children: [
{
// title: '',
title: tableMajorKey === 'opi' ? '顾问' : '页面类型',
fixed: 'left',
dataIndex: tableMajorKey === 'opi' ? 'OPI_Name' : 'LineClass',
key: 'pk',
onCell: (record) => ({
rowSpan: record.rowSpan,
}),
},
{
title: tableMajorKey === 'opi' ? '页面类型' : '顾问',
dataIndex: tableMajorKey === 'opi' ? 'LineClass' : 'OPI_Name',
key: 'k2',
},
],
},
// { // title: '',</div><div></div><div>
{
title: (
<>
本期<div>订单数按订单提交日期; 成团数按订单确认日期; 入境按团抵达日期</div>
</>
),
key: 'current',
children: [
{
title: '订单数',
dataIndex: 'OrderNum',
key: 'OrderNum',
},
{
title: '成团数',
dataIndex: 'GroupNum',
key: 'GroupNum',
},
{
title: '成团率',
dataIndex: 'SuccessRate',
key: 'SuccessRate',
render: (text, r) => <RenderVSDataCell showDiffData={false} data1={comm.fixTo2Decimals(text * 100)} data2={r.diff?.SuccessRate} dataSuffix="%" />,
},
{
title: '总人数',
dataIndex: 'TotalPersonNum',
key: 'TotalPersonNum',
},
{
title: '总报价',
dataIndex: 'PreTotalPrice',
key: 'PreTotalPrice',
},
{
title: '总利润',
dataIndex: 'PreTotalProfit',
key: 'PreTotalProfit',
},
{
title: '利润率',
dataIndex: 'PreProfitRate',
key: 'PreProfitRate',
render: (text, r) => <RenderVSDataCell showDiffData={false} data1={comm.fixTo2Decimals(text * 100)} data2={r.diff?.PreProfitRate} dataSuffix="%" />,
},
{
title: '入境团数',
dataIndex: 'EntranceGroupNum',
key: 'EntranceGroupNum',
},
{
title: '入境人数',
dataIndex: 'EntrancePersonNum',
key: 'EntrancePersonNum',
},
],
},
{
title: (
<>
到目前<div>按订单提交日期</div>
</>
),
key: 'until',
children: [
{
title: '订单数',
dataIndex: 'LYOrderNum',
key: 'LYOrderNum',
onCell: (r) => ({
style: { backgroundColor: '#5B8FF9' + '1A' },
}),
},
{
title: '订单中的成团数',
dataIndex: 'LYGroupNum',
key: 'LYGroupNum',
onCell: (r) => ({
style: { backgroundColor: '#5B8FF9' + '1A' },
}),
},
{
title: '成团率',
dataIndex: 'LYSuccessRate',
key: 'LYSuccessRate',
render: (text, r) => <RenderVSDataCell showDiffData={false} data1={comm.fixTo2Decimals(text * 100)} data2={r.diff?.LYSuccessRate} dataSuffix="%" />,
onCell: (r) => ({
style: { backgroundColor: '#5B8FF9' + '1A' },
}),
},
],
},
{
title: (
<>
本年<div>按团抵达日期</div>
</>
),
key: 'toyear',
children: [
{
title: '团队数',
dataIndex: 'LYTotalGroupNum',
key: 'LYTotalGroupNum',
onCell: (r) => ({
style: { backgroundColor: '#9FB40F' + '1A' },
}),
},
{
title: '总人数',
dataIndex: 'LYTotalPersonNum',
key: 'LYTotalPersonNum',
onCell: (r) => ({
style: { backgroundColor: '#9FB40F' + '1A' },
}),
},
{
title: '总报价',
dataIndex: 'LYPreTotalPrice',
key: 'LYPreTotalPrice',
onCell: (r) => ({
style: { backgroundColor: '#9FB40F' + '1A' },
}),
},
{
title: '总利润',
dataIndex: 'LYPreTotalProfit',
key: 'LYPreTotalProfit',
onCell: (r) => ({
style: { backgroundColor: '#9FB40F' + '1A' },
}),
},
{
title: '利润率',
dataIndex: 'LYPreProfitRate',
key: 'LYPreProfitRate',
render: (text, r) => <RenderVSDataCell showDiffData={false} data1={comm.fixTo2Decimals(text * 100)} data2={r.diff?.LYPreProfitRate} dataSuffix="%" />,
onCell: (r) => ({
style: { backgroundColor: '#9FB40F' + '1A' },
}),
},
],
},
];
const columnsTotal = (toJS(columns)).slice(1);
const tableProps = {
size: 'small',
pagination: false,
scroll: { x: 100 * 7 },
loading,
};
return (
<>
<div>
<Row gutter={16} className={toJS(searchFormStore.siderBroken) ? '' : 'sticky-top'}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...toJS(searchFormStore.formValues),
...searchValues,
},
//
shows: ['WebCode', 'IncludeTickets', 'DepartmentList', 'dates', 'operator', 'lineClass'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
dates: { hide_vs: true },
operator: { param: {} },
},
}}
onSubmit={(_err, obj, form, str) => {
setSearchValues(obj, form);
getMeetingDataSales({ ...obj, IsDetail: 0 });
getMeetingDataSales({ ...obj, IsDetail: 1 });
// onTabChange(activeTab);
}}
/>
</Col>
</Row>
<Row gutter={[16, { sm: 16, lg: 32 }]}>
<Col span={24}>
<h2 key={'t1'}>业绩</h2>
<Table
sticky={{ offsetHeader: 56 }}
key={`table_to_xlsx_total`}
{...tableProps}
columns={columnsTotal}
loading={typeLoading}
dataSource={salesDataTotal}
bordered
components={{ body: { cell: TdCell } }}
/>
<h2 key={'t2'}>顾问×页面类型业绩</h2>
<Table
sticky={{ offsetHeader: 56 }}
key={`table_to_xlsx_lineclass`}
{...tableProps}
loading={typeLoading}
dataSource={tableData}
columns={columns}
bordered
components={{ body: { cell: TdCell } }}
/>
</Col>
</Row>
</div>
</>
);
});
export default MeetingSales;

@ -0,0 +1,367 @@
import { useContext } from 'react';
import { Row, Col, Tabs, Table, Divider, Spin, Checkbox, Space } from 'antd';
import { ContainerOutlined, BlockOutlined, SmileOutlined, MobileOutlined, CustomerServiceOutlined, IeOutlined } from '@ant-design/icons';
import { Line, Pie } from '@ant-design/charts';
import { NavLink } from 'react-router-dom';
import * as comm from '@haina/utils-commons';
import DateGroupRadio from '../../components/DateGroupRadio';
import SearchForm from '../../components/search/SearchForm';
import { TableExportBtn } from '../../components/Data';
import { RenderVSDataCell } from './../../components/Data';
import { observer } from 'mobx-react';
import { toJS } from 'mobx';
import { stores_Context } from '../../config';
import { useShallow } from 'zustand/shallow';
import useToBOrderStore, { orderCountDataMapper, orderCountDataFieldMapper } from '../../zustand/ToBOrder';
const ToBOrder = observer(() => {
const { date_picker_store: searchFormStore } = useContext(stores_Context);
const [searchValues, setSearchValues] = useToBOrderStore(useShallow((state) => [state.searchValues, state.setSearchValues]));
const [activeTab, setActiveTab] = useToBOrderStore(useShallow((state) => [state.activeTab, state.setActiveTab]));
const [loading, typeLoading, onTabChange] = useToBOrderStore(useShallow((state) => [state.loading, state.typeLoading, state.onTabChange]));
const orderCountDataRaw = useToBOrderStore((state) => state.orderCountDataRaw);
const [orderCountDataLines, avgLineValue] = useToBOrderStore(useShallow((state) => [state.orderCountDataLines, state.avgLineValue]));
const [onChangeDateGroup, activeDateGroupRadio] = useToBOrderStore(useShallow((state) => [state.onChangeDateGroup, state.activeDateGroupRadio]));
const orderCountDataByType = useToBOrderStore((state) => state.orderCountDataByType);
const result = orderCountDataByType[activeTab] || {};
const getToBOrderCount = useToBOrderStore((state) => state.getToBOrderCount);
const showDiff = !comm.isEmpty(searchFormStore.start_date_cp);
const avg_line_y = Math.round(avgLineValue);
const lineConfig = {
data: orderCountDataLines,
padding: 'auto',
xField: 'xField',
yField: 'yField',
seriesField: 'seriesField',
// xAxis: {
// type: "timeCat",
// },
point: {
size: 4,
shape: 'cicle',
},
annotations: [
{
type: 'text',
position: ['start', avg_line_y],
content: avg_line_y,
offsetX: -15,
style: {
fill: '#F4664A',
textBaseline: 'bottom',
},
},
{
type: 'line',
start: [-10, avg_line_y],
end: ['max', avg_line_y],
style: {
stroke: '#F4664A',
lineDash: [2, 2],
},
},
],
label: {}, //
legend: {
itemValue: {
formatter: (text, item) => {
const items = orderCountDataLines.filter((d) => d.seriesField === item.value); //
return items.length ? items.reduce((a, b) => a + b.yField, 0) : ''; //
},
},
},
tooltip: {
customItems: (originalItems) => {
// process originalItems,
return originalItems.map((ele) => ({ ...ele, name: ele.data?.seriesField || ele.data?.xField }));
},
title: (title, datum) => {
let ret = title;
switch (activeDateGroupRadio) {
case 'day':
ret = `${title} ${comm.getWeek(datum.xField)}`; //
break;
default:
break;
}
return ret;
},
},
// smooth: true,
};
const pieConfig = {
appendPadding: 10,
data: [],
angleField: 'OrderCount',
colorField: 'OrderType',
radius: 0.8,
label: {
type: 'outer',
content: '{name} {value} \t {percentage}',
},
legend: false, //
interactions: [
{
type: 'element-selected',
},
{
type: 'element-active',
},
],
};
const tableProps = {
dataSource: result?.ordercount1 || [], // table_data.dataSource,
columns: [
{
title: '#',
fixed: 'left',
children: [
{
title: (
<span>
<div>{result.ordercountTotal1?.groups}</div>
{showDiff ? <div>{result.ordercountTotal2?.groups}</div> : null}
</span>
),
titleX: `${result.ordercountTotal1?.groups}` + (showDiff ? ` vs ${result.ordercountTotal2?.groups}` : ''),
dataIndex: 'OrderType',
fixed: 'left',
render: (text, record) => <NavLink to={`/tob_orders_sub/${activeTab}/${record.OrderTypeSN}/${encodeURIComponent(record.OrderType)}`}>{text}</NavLink>,
},
],
},
{
title: '数量',
children: [
{
title: (
<RenderVSDataCell
showDiffData={showDiff}
data1={result.ordercountTotal1?.OrderCount}
data2={result.ordercountTotal2?.OrderCount}
diffPercent={result.ordercountTotal1?.OrderCount_vs}
diffData={result.ordercountTotal1?.OrderCount_diff}
/>
),
titleX: [result.ordercountTotal1?.OrderCount, result.ordercountTotal2?.OrderCount].join(' vs '),
dataIndex: 'OrderCount',
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.OrderCount} diffPercent={r.OrderCount_vs} diffData={r.OrderCount_diff} />,
},
],
},
{
title: '成交数',
children: [
{
title: (
<RenderVSDataCell
showDiffData={showDiff}
data1={result.ordercountTotal1?.CJCount}
data2={result.ordercountTotal2?.CJCount}
diffPercent={result.ordercountTotal1?.CJCount_vs}
diffData={result.ordercountTotal1?.CJCount_diff}
/>
),
titleX: [result.ordercountTotal1?.CJCount, result.ordercountTotal2?.CJCount].join(' vs '),
dataIndex: 'CJCount',
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.CJCount} diffPercent={r.CJCount_vs} diffData={r.CJCount_diff} />,
},
],
},
{
title: '成交人数',
children: [
{
title: (
<RenderVSDataCell
showDiffData={showDiff}
data1={result.ordercountTotal1?.CJPersonNum}
data2={result.ordercountTotal2?.CJPersonNum}
diffPercent={result.ordercountTotal1?.CJPersonNum_vs}
diffData={result.ordercountTotal1?.CJPersonNum_diff}
/>
),
titleX: [result.ordercountTotal1?.CJPersonNum, result.ordercountTotal2?.CJPersonNum].join(' vs '),
dataIndex: 'CJPersonNum',
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.CJPersonNum} diffPercent={r.CJPersonNum_vs} diffData={r.CJPersonNum_diff} />,
},
],
},
{
title: '成交率',
children: [
{
title: (
<RenderVSDataCell
showDiffData={showDiff}
data1={result.ordercountTotal1?.CJrate}
data2={result.ordercountTotal2?.CJrate}
diffPercent={result.ordercountTotal1?.CJrate_vs}
diffData={result.ordercountTotal1?.CJrate_diff}
/>
),
titleX: [result.ordercountTotal1?.CJrate, result.ordercountTotal2?.CJrate].join(' vs '),
dataIndex: 'CJrate',
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.CJrate} diffPercent={r.CJrate_vs} diffData={r.CJrate_diff} />,
},
],
},
{
title: '成交毛利(预计)',
children: [
{
title: (
<RenderVSDataCell
showDiffData={showDiff}
data1={result.ordercountTotal1?.YJLY}
data2={result.ordercountTotal2?.YJLY}
diffPercent={result.ordercountTotal1?.YJLY_vs}
diffData={result.ordercountTotal1?.YJLY_diff}
/>
),
titleX: [result.ordercountTotal1?.YJLY, result.ordercountTotal2?.YJLY].join(' vs '),
dataIndex: 'YJLY',
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.YJLY} diffPercent={r.YJLY_vs} diffData={r.YJLY_diff} />,
},
],
},
{
title: '单个订单价值',
children: [
{
title: (
<RenderVSDataCell
showDiffData={showDiff}
data1={result.ordercountTotal1?.Ordervalue}
data2={result.ordercountTotal2?.Ordervalue}
diffPercent={result.ordercountTotal1?.Ordervalue_vs}
diffData={result.ordercountTotal1?.Ordervalue_diff}
/>
),
titleX: [result.ordercountTotal1?.Ordervalue, result.ordercountTotal2?.Ordervalue].join(' vs '),
dataIndex: 'Ordervalue',
render: (text, r) => <RenderVSDataCell showDiffData={showDiff} data1={text} data2={r.diff?.Ordervalue} diffPercent={r.Ordervalue_vs} diffData={r.Ordervalue_diff} />,
},
],
},
],
size: 'small',
pagination: false,
scroll: { x: 100 * 7 },
loading,
};
return (
<>
<div>
<Row gutter={16} className={toJS(searchFormStore.siderBroken) ? '' : 'sticky-top'}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...toJS(searchFormStore.formValues),
...searchValues,
},
//
shows: ['DateType', 'WebCode', 'IncludeTickets', 'DepartmentList', 'dates'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
years: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
setSearchValues(obj, form);
getToBOrderCount(obj);
onTabChange(activeTab);
}}
/>
</Col>
</Row>
<Row gutter={[16, { sm: 16, lg: 32 }]}>
<Col span={24} style={{ textAlign: 'right' }}>
<DateGroupRadio
visible={orderCountDataLines.length !== 0}
dataRaw={orderCountDataRaw}
onChange={onChangeDateGroup}
value={activeDateGroupRadio}
dataMapper={orderCountDataMapper}
fieldMapper={orderCountDataFieldMapper}
/>
</Col>
<Col span={24}>
<Spin spinning={loading}>
<Line {...lineConfig} />
</Spin>
</Col>
<Col span={24}>
<Tabs
activeKey={activeTab}
onChange={(active_key) => onTabChange(active_key)}
items={[
{
key: 'customer_types',
label: (
<span>
<CustomerServiceOutlined />
分销客户
</span>
),
},
].map((ele) => {
return {
...ele,
children: (
<>
<Table sticky key={`table_to_xlsx_${ele.key}`} {...tableProps} loading={typeLoading} />
<Divider orientation="right" plain>
<TableExportBtn label={ele.key} {...{ columns: tableProps.columns, dataSource: tableProps.dataSource }} />
</Divider>
</>
),
};
})}
/>
</Col>
</Row>
<div>
<h3>各项占比</h3>
{/* <Checkbox
checked={true}
// onChange={(e) => setIsShowEmpty(e.target.checked)}
>
包含空值
</Checkbox> */}
</div>
<Spin spinning={typeLoading}>
<Row>
<Col sm={24} lg={12}>
<Pie {...pieConfig} data={result?.ordercount1 || []} innerRadius={0.6} statistic={{ title: false, content: { content: '数量' } }} />
<Pie {...pieConfig} data={result?.ordercount1 || []} angleField="YJLYx" innerRadius={0.6} statistic={{ title: false, content: { content: '预计毛利' } }} />
</Col>
{showDiff && (
<Col sm={24} lg={12}>
<Pie {...pieConfig} data={result?.ordercount2 || []} innerRadius={0.6} statistic={{ title: false, content: { content: '数量' } }} />
<Pie {...pieConfig} data={result?.ordercount2 || []} angleField="YJLYx" innerRadius={0.6} statistic={{ title: false, content: { content: '预计毛利' } }} />
</Col>
)}
</Row>
</Spin>
</div>
</>
);
});
export default ToBOrder;

@ -0,0 +1,295 @@
import { useContext, useEffect } from 'react';
import { Row, Col, Tabs, Table, Divider, Spin, Space } from 'antd';
import { ContainerOutlined, BlockOutlined, SmileOutlined, MobileOutlined } from '@ant-design/icons';
import { Line } from '@ant-design/charts';
import { NavLink, useParams } from 'react-router-dom';
import { getWeek } from '@haina/utils-commons';
import DateGroupRadio from '../../components/DateGroupRadio';
import SearchForm from '../../components/search/SearchForm';
import { TableExportBtn } from '../../components/Data';
import { observer } from 'mobx-react';
import { stores_Context } from '../../config';
import { useShallow } from 'zustand/shallow';
import useToBOrderStore, { orderCountDataMapper, orderCountDataFieldMapper } from '../../zustand/ToBOrder';
//
const addLineBreaksAtCommas = (text) => {
if (!text) return '';
return text.replace(/&amp;/g, '&').replace(//g, '\n').replace(/,/g, ',\n').replace(//g, '\n').replace(/;/g, ';\n');
};
const OrderDetailTable = ({ caption, dataSource, loading, ...props }) => {
const columns = [
{
title: '订单号',
dataIndex: 'COLI_ID',
key: 'COLI_ID',
},
{
title: '网站',
dataIndex: 'COLI_WebCode',
key: 'COLI_WebCode',
},
{
title: '国籍',
dataIndex: 'MEI_Country',
key: 'MEI_Country',
},
{
title: '成行',
dataIndex: 'COLI_Success',
key: 'COLI_Success',
render: (text, record) => <span>{text == 1 ? '是' : '否'}</span>,
sorter: (a, b) => b.COLI_Success - a.COLI_Success,
},
{
title: '人数(成/童/婴)',
dataIndex: 'COLI_PersonNum',
key: 'COLI_PersonNum',
render: (text, record) => (
<span>
{record.COLI_PersonNum}/{record.COLI_ChildNum}/{record.COLI_BabyNum}
</span>
),
},
{
title: '天数',
dataIndex: 'COLI_Days',
key: 'COLI_Days',
},
{
title: '预计利润',
dataIndex: 'CGI_YJLY',
key: 'CGI_YJLY',
},
{
title: '预定时间',
dataIndex: 'COLI_ApplyDate',
key: 'COLI_ApplyDate',
},
{
title: '确认日期',
dataIndex: 'COLI_ConfirmDate',
key: 'COLI_ConfirmDate',
},
{
title: '出发日期',
dataIndex: 'CGI_ArriveDate',
key: 'CGI_ArriveDate',
},
// {
// title: '',
// dataIndex: 'COLI_CustomerRequest',
// key: 'COLI_CustomerRequest',
// ellipsis: true,
// },
{
title: '订单内容',
dataIndex: 'COLI_OrderDetailText',
key: 'COLI_OrderDetailText',
ellipsis: true,
},
Table.EXPAND_COLUMN,
];
return (
<div>
<Divider orientation="left" plain>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '10px' }}>
<div>{caption}</div>
<TableExportBtn label={caption} columns={columns} dataSource={dataSource} />
</div>
</Divider>
<Table
id="table_to_xlsx_form"
dataSource={dataSource}
columns={columns}
loading={loading}
size="small"
// pagination={false}
rowKey={(record) => record.key}
expandable={{
expandedRowRender: (record) => (
<pre style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word', fontSize: '16px' }}>
{/* <Divider orientation="left" plain>
客户需求
</Divider>
{record.COLI_CustomerRequest} */}
<Divider orientation="left" plain>
订单内容
</Divider>
{record.COLI_OrderDetailText}
</pre>
),
}}
/>
</div>
);
};
const ToBOrderSub = observer(({ ...props }) => {
const { ordertype, ordertype_sub, ordertype_title } = useParams();
const { date_picker_store: searchFormStore } = useContext(stores_Context);
const [searchValues, setSearchValues] = useToBOrderStore(useShallow((state) => [state.searchValues, state.setSearchValues]));
const [searchValuesToSub] = useToBOrderStore(useShallow((state) => [state.searchValuesToSub]));
const [loading, typeLoading] = useToBOrderStore(useShallow((state) => [state.loading, state.typeLoading]));
const orderCountDataRawSub = useToBOrderStore((state) => state.orderCountDataRawSub);
const [orderCountDataLinesSub, avgLineValueSub] = useToBOrderStore(useShallow((state) => [state.orderCountDataLinesSub, state.avgLineValueSub]));
const [onChangeDateGroupSub, activeDateGroupRadioSub] = useToBOrderStore(useShallow((state) => [state.onChangeDateGroupSub, state.activeDateGroupRadioSub]));
const orderDetails = useToBOrderStore((state) => state.orderDetails);
const getToBOrderCount = useToBOrderStore((state) => state.getToBOrderCount);
const getToBOrderDetailByType = useToBOrderStore((state) => state.getToBOrderDetailByType);
useEffect(() => {
getToBOrderCount(searchValuesToSub, ordertype, ordertype_sub);
getToBOrderDetailByType(searchValuesToSub, ordertype, ordertype_sub);
}, []);
const avg_line_y = Math.round(avgLineValueSub);
const lineConfig = {
data: orderCountDataLinesSub,
padding: 'auto',
xField: 'xField',
yField: 'yField',
seriesField: 'seriesField',
// xAxis: {
// type: "timeCat",
// },
// point: {
// size: 4,
// shape: 'cicle',
// },
annotations: [
{
type: 'text',
position: ['start', avg_line_y],
content: avg_line_y,
offsetX: -15,
style: {
fill: '#F4664A',
textBaseline: 'bottom',
},
},
{
type: 'line',
start: [-10, avg_line_y],
end: ['max', avg_line_y],
style: {
stroke: '#F4664A',
lineDash: [2, 2],
},
},
],
label: {}, //
legend: {
itemValue: {
formatter: (text, item) => {
const items = orderCountDataLinesSub.filter((d) => d.seriesField === item.value); //
return items.length ? items.reduce((a, b) => a + b.yField, 0) : ''; //
},
},
},
tooltip: {
customItems: (originalItems) => {
// process originalItems,
return originalItems.map((ele) => ({ ...ele, name: ele.data?.seriesField || ele.data?.xField }));
},
title: (title, datum) => {
let ret = title;
switch (activeDateGroupRadioSub) {
case 'day':
ret = `${title} ${getWeek(datum.xField)}`; //
break;
default:
break;
}
return ret;
},
},
// smooth: true,
};
const tab_items = [
{
key: 'detail',
label: (
<span>
<ContainerOutlined />
订单内容
</span>
),
title: '订单内容',
children: (
<>
<Space direction="vertical">
<OrderDetailTable caption={orderDetails[0]?.dateRangeStr} dataSource={orderDetails[0]?.data || []} loading={typeLoading} />
<OrderDetailTable caption={orderDetails[1]?.dateRangeStr} dataSource={orderDetails[1]?.data || []} loading={typeLoading} />
</Space>
</>
),
},
];
return (
<div>
<Row gutter={{ sm: 16, lg: 32 }} className={searchFormStore.siderBroken ? '' : 'sticky-top'}>
<Col md={24} lg={12} xxl={14}>
<NavLink to={`/tob_orders`}>返回</NavLink>
</Col>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...searchFormStore.formValues,
...searchValues,
},
//
shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'dates'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
// dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
setSearchValues(obj, form);
getToBOrderCount(obj, ordertype, ordertype_sub);
getToBOrderDetailByType(obj, ordertype, ordertype_sub);
}}
/>
</Col>
</Row>
<Row gutter={[16, { xs: 8, sm: 16, md: 24, lg: 32 }]}>
<Col span={24} style={{ textAlign: 'right' }}>
<DateGroupRadio
visible={orderCountDataLinesSub.length !== 0}
dataRaw={orderCountDataRawSub}
onChange={onChangeDateGroupSub}
value={activeDateGroupRadioSub}
dataMapper={orderCountDataMapper}
fieldMapper={orderCountDataFieldMapper}
/>
</Col>
<Col className="gutter-row" span={24}>
<Spin spinning={loading}>
<Line {...lineConfig} />
</Spin>
</Col>
<Col className="gutter-row" span={24}>
<Tabs
activeKey={'detail'}
// onChange={onTabsChange}
items={tab_items}
/>
</Col>
</Row>
</div>
);
});
export default ToBOrderSub;

@ -26,7 +26,7 @@ export const fetchRegularCustomer = async (params) => {
...params,
};
const { WebCode, DepartmentList, DateType, Date1, Date2, ...readyParams } = _params;
const [result1, result2] = await Promise.all([
const [result1=[], result2=[]] = await Promise.all([
fetchJSON(HT_HOST + '/service-tourdesign/RegularCusOrder', { ...defaultParams, ...readyParams }),
...(params.DateDiff1 && params.IsDetail === 0
? [
@ -55,7 +55,6 @@ export const fetchRegularCustomer = async (params) => {
x[key] = { ...(result1Mapped?.[key] || { ItemName: key }), diff: result2Mapped[key] || {} };
});
ret.result1 = Object.values(x);
console.log(ret);
return { result1: ret.result1 }; // { result1, result2 };
};
@ -109,7 +108,7 @@ const initialState = {
loading: false,
loading2: false,
searchValues: {
DepartmentList: ['1', '2', '28', '7'].map((kk) => groupsMappedByKey[kk]),
DepartmentList: ['1', '2', '28', '7', '33'].map((kk) => groupsMappedByKey[kk]),
WebCode: ['CHT', 'AH', 'JH', 'GH', 'ZWQD', 'GH_ZWQD_HW', 'GHKYZG', 'GHKYHW', 'HTravel'].map((kk) => sitesMappedByCode[kk]),
DateType: { key: 'applyDate', label: '提交日期' },
IncludeTickets: { key: '0', label: '不含门票' },
@ -154,10 +153,12 @@ const useCustomerRelationsStore = create(
state.regular.pivotX = pivotByDate;
state.regular.pivotY = pivotByOrder;
} else {
console.log('0000');
state.regular.data = result1;
}
});
} catch (error) {
console.error(error);
} finally {
setLoading(false);
IsDetail === 1 && setLoading2(false);

@ -22,28 +22,60 @@ export const transformRows = (cols, rows) => {
});
};
export const fetchCaseSummary = async (params) => {
const searchParams = {
export const transSearchParams = (params) => {
const newsearchParams = {
...params,
vei_sn: params.agency,
DepartmentList: params.DepartmentList === 'ALL' ? undefined : params.DepartmentList,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-Analyse2/dong_dao_zhu_total`, searchParams);
return errcode !== 0 ? [] : (result || []);
};
export const fetchCaseSummaryByGuide = async (params) => {
const searchParams = {
const oldseachPararms = {
...params,
Date1: params.DateDiff1,
Date2: params.DateDiff2,
vei_sn: params.agency,
DepartmentList: params.DepartmentList === 'ALL' ? undefined : params.DepartmentList,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-Analyse2/dong_dao_zhu_tour_guide`, searchParams);
return errcode !== 0 ? [] : (result || []); // .sort(sortDescBy('case_count_dongdaozhu'));
return {
newsearchParams,
oldseachPararms,
};
};
export const fetchCaseSummary = async (params) => {
const {newsearchParams, oldseachPararms} = transSearchParams(params);
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-Analyse2/dong_dao_zhu_total`, newsearchParams);
let response2;
if(oldseachPararms.DateDiff1){
response2 = await fetchJSON(`${HT_HOST}/service-Analyse2/dong_dao_zhu_total`, oldseachPararms);
}else{
response2 = { errcode: 0, result: [] };
}
const caseSummary = result.map((item, index) => ({
...item,
diff: response2.errcode !== 0 ? {} : (response2.result[index] || {dongdaozhu_rate: "0%"})
}));
return errcode !== 0 ? [] : (caseSummary || []);
};
export const fetchCaseSummaryByGuide = async (params) => {
const {newsearchParams, oldseachPararms} = transSearchParams(params);
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-Analyse2/dong_dao_zhu_tour_guide`, newsearchParams);
let response2;
if(oldseachPararms.DateDiff1){
response2 = await fetchJSON(`${HT_HOST}/service-Analyse2/dong_dao_zhu_tour_guide`, oldseachPararms);
}else{
response2 = { errcode: 0, result: [] };
}
const caseSummaryByGuide = result.map((item, index) => ({
...item,
diff: response2.errcode !== 0 ? {} : (response2.result.find(r => r.TGI_SN === item.TGI_SN) || {dongdaozhu_rate: "0%"})
}));
return errcode !== 0 ? [] : (caseSummaryByGuide || []); // .sort(sortDescBy('case_count_dongdaozhu'));
};
export const fetchCaseFeatured = async (params) => {
const searchParams = {
...params,
vei_sn: params.agency,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-Analyse2/dong_dao_zhu_case`, searchParams);
const {newsearchParams, oldseachPararms} = transSearchParams(params);
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-Analyse2/dong_dao_zhu_case`, newsearchParams);
return errcode !== 0 ? [] : (result || []);
};
@ -75,6 +107,7 @@ const useHostCaseStore = create(
setLoading: (loading) => set({ loading }),
setSearchValues: (obj, values) => set((state) => ({ searchValues: values, searchValuesToSub: obj })),
setSearchValuesToSub: (values) => set((state) => ({ searchValuesToSub: values })),
setLoadingCase: (loadingCase) => set({ loadingCase }),
setCaseFeatured: (caseFeatured) => set({ caseFeatured }),

@ -0,0 +1,122 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { groupsMappedByCode } from '../libs/ht';
import { fetchJSON } from '@haina/utils-request';
import { HT_HOST } from '../config';
import { groupBy, isEmpty, } from '@haina/utils-commons';
/**
* 顾问业绩 (例会数据)
*/
const defaultParams = { OrderType: 227001, IsDYTJ: '', IncludeTickets: 1, Team: '', WebCodeFX: '', CusType: '', OldCus: 0, lineClass: '', IsDetail: -1 };
export const fetchMeetingDataSales = async (params) => {
const { errcode, errmsg, result } = await fetchJSON(HT_HOST + '/service-web/QueryData/WLCountForMeetingNew', {
...defaultParams,
...params,
WebCode: (params.WebCode || '').replace('all', ''),
OPI_SN: params.operator || '',
});
const ret =
errcode !== 0
? []
: (result || [])
// .filter((ele) =>
// Object.keys(ele)
// .filter((col) => !['OPI_SN', 'OPI_Name', 'COLI_LineClass', 'LineClass', 'vi'].includes(col))
// .some((col) => !isEmpty(ele[col])),
// )
.map((ele) => ({ ...ele, key: `${ele.OPI_SN}_${ele.COLI_LineClass || ''}_${ele.vi || ''}` }));
const byOPI = groupBy(structuredClone(ret), 'OPI_SN');
const OPIValue = Object.keys(byOPI).reduce((r, opisn) => {
byOPI[opisn].forEach((ele, xi) => {
ele.rowSpan = xi === 0 ? byOPI[opisn].length : 0;
});
return [...r, ...byOPI[opisn]];
}, []);
const byLineClass = groupBy(structuredClone(ret), 'LineClass');
const LineClassValue = Object.keys(byLineClass).reduce((r, gkey) => {
byLineClass[gkey].forEach((ele, xi) => {
ele.rowSpan = xi === 0 ? byLineClass[gkey].length : 0;
});
return [...r, ...byLineClass[gkey]];
}, []);
return { result: ret, OPIValue, LineClassValue };
};
/**
* --------------------------------------------------------------------------------------------------------
*/
const initialState = {
loading: false,
typeLoading: false,
searchValues: {
// DateType: { key: 'applyDate', label: '提交日期' },
WebCode: { key: 'all', label: '所有来源' },
IncludeTickets: { key: '1', label: '含门票' },
DepartmentList: groupsMappedByCode.GH, // { key: 'All', label: '所有来源' }, //
},
searchValuesToSub: {
// DateType: 'applyDate',
WebCode: 'all',
IncludeTickets: '1',
DepartmentList: -1, // -1: All
},
salesDataTotal: [],
salesData: [],
matrixData: {},
matrixtableMajorKey: 'opi',
matrixTableData: [],
// 二级页面
};
const useSalesInsightStore = create(
devtools(
immer((set, get) => ({
...initialState,
reset: () => set(initialState),
setLoading: (loading) => set({ loading }),
setTypeLoading: (typeLoading) => set({ typeLoading }),
setSearchValues: (obj, values) => set((state) => ({ searchValues: values, searchValuesToSub: obj })),
setSearchValuesToSub: (values) => set((state) => ({ searchValuesToSub: values })),
setActiveTab: (tab) => set({ activeTab: tab }),
// site effects
getMeetingDataSales: async (params) => {
const { setTypeLoading, } = get();
setTypeLoading(true);
try {
const res = await fetchMeetingDataSales(params);
if (params.IsDetail === 1) {
set({ matrixData: res, matrixTableData: res.OPIValue, matrixtableMajorKey: 'opi', salesData: res.result });
}
else {
set({ salesDataTotal: res.result });
}
} catch (error) {
console.error(error);
} finally {
setTypeLoading(false);
}
},
onMatrixChange: () => {
const { matrixtableMajorKey, matrixData } = get();
const newKey = matrixtableMajorKey === 'opi' ? 'lineclass' : 'opi';
const dataKey = matrixtableMajorKey === 'opi' ? 'LineClassValue' : 'OPIValue' ;
set({ matrixtableMajorKey: newKey, matrixTableData: matrixData?.[dataKey] });
},
// sub
})),
{ name: 'SalesInsight' }
)
);
export default useSalesInsightStore;

@ -0,0 +1,241 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { immer } from 'zustand/middleware/immer';
import { groupsMappedByCode } from '../libs/ht';
import { fetchJSON } from '@haina/utils-request';
import { HT_HOST } from '../config';
import { resultDataCb } from '../components/DateGroupRadio/date';
import { isEmpty, price_to_number } from '@haina/utils-commons';
/**
* 分销(ToB)订单
*/
const defaultParams = {};
export const fetchToBOrderCount = async (params, type = '', typeVal = '') => {
const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCount_FX', {
...defaultParams,
...params,
COLI_ApplyDate1: params.Date1,
COLI_ApplyDate2: params.Date2,
COLI_ApplyDateOld1: params.DateDiff1 || '',
COLI_ApplyDateOld2: params.DateDiff2 || '',
OrderType: type,
OrderType_val: typeVal,
});
return errcode !== 0 ? {} : (result || {});
};
const _typeRes = {
'ordercount1': [],
'ordercount2': [],
'ordercountTotal1': {
'OrderType': '合计',
'OrderCount': 0,
'CJCount': 0,
'CJPersonNum': 0,
'YJLY': '',
'CJrate': '0%',
'Ordervalue': '',
'groups': '',
'key': 1,
},
'ordercountTotal2': {},
};
export const fetchToBOrderCountByType = async (type, params) => {
const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType_FX', {
...defaultParams,
...params,
COLI_ApplyDate1: params.Date1,
COLI_ApplyDate2: params.Date2,
COLI_ApplyDateOld1: params.DateDiff1 || '',
COLI_ApplyDateOld2: params.DateDiff2 || '',
OrderType: type,
});
const res = errcode !== 0 ? _typeRes : (result || _typeRes);
const rows1Map = res.ordercount1.reduce((a, row1) => ({ ...a, [row1.OrderTypeSN]: {...row1, YJLYx: price_to_number(row1.YJLY)} }), {});
const rows2Map = res.ordercount2.reduce((a, row2) => ({ ...a, [row2.OrderTypeSN]: {...row2, YJLYx: price_to_number(row2.YJLY)} }), {});
const mixRow1 = res.ordercount1.map((row1) => ({ ...row1, YJLYx: price_to_number(row1.YJLY), diff: rows2Map[row1.OrderTypeSN] || {} }));
// Diff: elements in rows2 but not in rows1
const diff = [...new Set(Object.keys(rows2Map).filter((x) => !new Set(Object.keys(rows1Map)).has(x)))];
mixRow1.push(...diff.map((sn) => ({ diff: rows2Map[sn], OrderType: rows2Map[sn].OrderType, OrderTypeSN: rows2Map[sn].OrderTypeSN })));
return { ...res, ordercount1: mixRow1, ordercount2: res.ordercount2.map((row1) => ({ ...row1, YJLYx: price_to_number(row1.YJLY), })) };
};
const _detailRes = { ordercount1: [], ordercount2: [] };
export const fetchToBOrderDetailByType = async (params, type = '', typeVal = '', orderContent = 'detail') => {
const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType_Sub_FX', {
...defaultParams,
SubOrderType: orderContent,
...params,
COLI_ApplyDate1: params.Date1,
COLI_ApplyDate2: params.Date2,
COLI_ApplyDateOld1: params.DateDiff1 || '',
COLI_ApplyDateOld2: params.DateDiff2 || '',
OrderType: type,
OrderType_val: typeVal,
});
const res = errcode !== 0 ? _detailRes : (result || _detailRes);
const dateStr = [params.Date1, params.Date2].map((d) => d.substring(0, 10)).join('~');
const dateDiffStr = isEmpty(params.DateDiff1) ? '' : [params.DateDiff1, params.DateDiff2].map((d) => d.substring(0, 10)).join('~');
const ret = [
{ dateRangeStr: dateStr, data: res.ordercount1 },
{ dateRangeStr: dateDiffStr, data: res.ordercount2 },
];
return ret;
};
const calculateLineData = (value, data, avg1) => {
const groupByDate = data.reduce((r, v) => {
(r[v.ApplyDate] || (r[v.ApplyDate] = [])).push(v);
return r;
}, {});
const _data = Object.keys(groupByDate)
.reduce((r, _d) => {
const xAxisGroup = groupByDate[_d].reduce((a, v) => {
(a[v.groups] || (a[v.groups] = [])).push(v);
return a;
}, {});
Object.keys(xAxisGroup).map((_group) => {
const summaryVal = xAxisGroup[_group].reduce((rows, row) => rows + row.orderCount, 0);
r.push({ ...xAxisGroup[_group][0], orderCount: summaryVal });
return _group;
});
return r;
}, [])
.map((row) => ({ xField: row.ApplyDate, yField: row.orderCount, seriesField: row.groups }));
return { lines: _data, dateRadioValue: value, avgLineValue: avg1 };
};
export const orderCountDataMapper = { 'data1': 'ordercount1', data2: 'ordercount2' };
export const orderCountDataFieldMapper = { 'dateKey': 'ApplyDate', 'valueKey': 'orderCount', 'seriesKey': 'id', _f: 'sum' };
/**
* --------------------------------------------------------------------------------------------------------
*/
const initialState = {
loading: false,
typeLoading: false,
searchValues: {
DateType: { key: 'applyDate', label: '提交日期' },
WebCode: { key: 'all', label: '所有来源' },
IncludeTickets: { key: '1', label: '含门票' },
DepartmentList: groupsMappedByCode.GH, // { key: 'All', label: '所有来源' }, //
},
searchValuesToSub: {
DateType: 'applyDate',
WebCode: 'all',
IncludeTickets: '1',
DepartmentList: -1, // -1: All
},
activeTab: 'customer_types',
activeDateGroupRadio: 'day',
orderCountDataRaw: {},
orderCountDataLines: [],
avgLineValue: 0,
orderCountDataByType: {},
// 二级页面
orderCountDataRawSub: {},
orderCountDataLinesSub: [],
avgLineValueSub: 0,
activeDateGroupRadioSub: 'day',
orderDetails: [],
};
const useToBOrderStore = create(
devtools(
immer((set, get) => ({
...initialState,
reset: () => set(initialState),
setLoading: (loading) => set({ loading }),
setTypeLoading: (typeLoading) => set({ typeLoading }),
setSearchValues: (obj, values) => set((state) => ({ searchValues: values, searchValuesToSub: obj })),
setSearchValuesToSub: (values) => set((state) => ({ searchValuesToSub: values })),
setActiveTab: (tab) => set({ activeTab: tab }),
setOrderCountDataLines: (data) => set({ orderCountDataLines: data }),
setOrderCountDataByType: (type, data) =>
set((state) => {
state.orderCountDataByType[type] = data;
}),
// data ----
onChangeDateGroup: (value, data, avg1) => {
const { lines, dateRadioValue, avgLineValue } = calculateLineData(value, data, avg1);
set({ orderCountDataLines: lines, avgLineValue, activeDateGroupRadio: dateRadioValue });
},
onChangeDateGroupSub: (value, data, avg1) => {
const { lines, dateRadioValue, avgLineValue } = calculateLineData(value, data, avg1);
set({ orderCountDataLinesSub: lines, avgLineValueSub: avgLineValue, activeDateGroupRadioSub: dateRadioValue });
},
// site effects
getToBOrderCount: async (params, type, typeVal) => {
const { setLoading } = get();
setLoading(true);
try {
const res = await fetchToBOrderCount(params, type, typeVal);
// 第一次得到数据
const { lines, dateRadioValue, avgLineValue } = resultDataCb(res, 'day', orderCountDataMapper, orderCountDataFieldMapper, calculateLineData);
if (isEmpty(type)) {
// index page
set({ orderCountDataRaw: res });
set({ orderCountDataLines: lines, avgLineValue, activeDateGroupRadio: dateRadioValue });
} else {
// sub page
set({ orderCountDataRawSub: res });
set({ orderCountDataLinesSub: lines, avgLineValueSub: avgLineValue, activeDateGroupRadioSub: dateRadioValue });
}
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
},
getToBOrderCount_type: async (type) => {
const { setTypeLoading, searchValuesToSub, setOrderCountDataByType } = get();
setTypeLoading(true);
try {
const res = await fetchToBOrderCountByType(type, searchValuesToSub);
setOrderCountDataByType(type, res);
} catch (error) {
console.error(error);
} finally {
setTypeLoading(false);
}
},
onTabChange: async (tab) => {
const { setActiveTab, getToBOrderCount_type } = get();
setActiveTab(tab);
await getToBOrderCount_type(tab);
},
// sub
getToBOrderDetailByType: async (params, type, typeVal) => {
const { setTypeLoading } = get();
try {
setTypeLoading(true);
const res = await fetchToBOrderDetailByType(params, type, typeVal, 'detail');
set({ orderDetails: res });
} catch (error) {
console.error(error);
} finally {
setTypeLoading(false);
}
},
})),
{ name: 'ToBOrder' }
)
);
export default useToBOrderStore;
Loading…
Cancel
Save