feat: 外联业绩×页面类型

main
Lei OT 3 months ago
parent 460dd47f1c
commit ae07f7593b

@ -60,6 +60,7 @@ import TrainsUpsell from './views/biz/reports/TrainsUpsell';
import HostCaseReport from './views/HostCaseReport'; import HostCaseReport from './views/HostCaseReport';
import ToBOrder from './views/toB/ToBOrder'; import ToBOrder from './views/toB/ToBOrder';
import ToBOrderSub from './views/toB/ToBOrderSub'; import ToBOrderSub from './views/toB/ToBOrderSub';
import MeetingSales from './views/reports/MeetingSales';
const App = () => { const App = () => {
const { Content, Footer, Sider, } = Layout; const { Content, Footer, Sider, } = Layout;
@ -83,6 +84,10 @@ const App = () => {
key: 'meeting-2024-GH', key: 'meeting-2024-GH',
label: <NavLink to="/orders/meeting-2024-GH">GH区域数据</NavLink>, // GH-2024 label: <NavLink to="/orders/meeting-2024-GH">GH区域数据</NavLink>, // GH-2024
}, },
{
key: 'sales-insight',
label: <NavLink to="/reports/sales-insight">顾问业绩</NavLink>,
},
], ],
}, },
{ {
@ -304,6 +309,8 @@ const App = () => {
<Route path="/sales-crm/process" element={<OPProcess />} /> <Route path="/sales-crm/process" element={<OPProcess />} />
<Route path="/sales-crm/risk" element={<OPRisk />} /> <Route path="/sales-crm/risk" element={<OPRisk />} />
<Route path="/sales-crm/risk/sales/:opisn" element={<OPRisk />} /> <Route path="/sales-crm/risk/sales/:opisn" element={<OPRisk />} />
<Route path="/reports/sales-insight" element={<MeetingSales />} />
</Route> </Route>
</Routes> </Routes>
</Content> </Content>

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

@ -14,6 +14,7 @@ import DateTypeSelect from './DataTypeSelect';
import DatePickerCharts from './DatePickerCharts'; import DatePickerCharts from './DatePickerCharts';
import YearPickerCharts from './YearPickerCharts'; import YearPickerCharts from './YearPickerCharts';
import GuideLanguageSelect from './GuideLanguageSelect'; import GuideLanguageSelect from './GuideLanguageSelect';
import LineClassSeletor from './LineClassSeletor';
import SearchInput from './Input'; import SearchInput from './Input';
import { objectMapper, at, empty, isEmpty } from '@haina/utils-commons'; import { objectMapper, at, empty, isEmpty } from '@haina/utils-commons';
import { departureDateTypes } from './../../libs/ht'; import { departureDateTypes } from './../../libs/ht';
@ -81,6 +82,13 @@ export default observer((props) => {
}, },
default: '', default: '',
}, },
'lineClass': {
key: 'lineClass',
transform: (value) => {
return isEmpty(value) ? '': Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.key : '';
},
default: '',
},
'WebCode': { 'WebCode': {
key: 'WebCode', key: 'WebCode',
transform: (value) => { transform: (value) => {
@ -101,7 +109,7 @@ export default observer((props) => {
'operator': { 'operator': {
key: 'operator', key: 'operator',
// transform: (value) => value?.key || '', // 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: '', default: '',
}, },
'date': { 'date': {
@ -506,7 +514,7 @@ function getFields(props) {
item( item(
'operator', 'operator',
99, 99,
<Form.Item name={'operator'} dependencies={['DepartmentList']}> <Form.Item name={'operator'} dependencies={['DepartmentList']} initialValue={at(props, 'initialValue.operator')[0]}>
<SearchInput <SearchInput
{...fieldProps.operator} {...fieldProps.operator}
autoGet autoGet
@ -673,6 +681,13 @@ function getFields(props) {
<HotelStarSelect {...fieldProps.hotelStar} labelInValue={true} /> <HotelStarSelect {...fieldProps.hotelStar} labelInValue={true} />
</Form.Item> </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 baseChildren = baseChildren
.map((x) => { .map((x) => {

@ -116,6 +116,43 @@ export const departureDateTypes = [
{ key: 'departureDate', value: 'departureDate', label: '抵达日期' }, { 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站群营销"}];
/** /**
* 结果字段 * 结果字段
*/ */

@ -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,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;
Loading…
Cancel
Save