diff --git a/src/App.jsx b/src/App.jsx index a465a07..7e0c7ad 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -58,6 +58,8 @@ 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'; const App = () => { const { Content, Footer, Sider, } = Layout; @@ -93,6 +95,11 @@ const App = () => { label: 订单数据, // icon: , }, + { + key: 'tob_orders', + label: 分销订单, + // icon: , + }, { key: 22, label: 仪表盘, @@ -260,6 +267,8 @@ const App = () => { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/src/views/toB/ToBOrder.jsx b/src/views/toB/ToBOrder.jsx new file mode 100644 index 0000000..27fcaf4 --- /dev/null +++ b/src/views/toB/ToBOrder.jsx @@ -0,0 +1,366 @@ +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: ( + +
{result.ordercountTotal1?.groups}
+ {showDiff ?
{result.ordercountTotal2?.groups}
: null} +
+ ), + titleX: `${result.ordercountTotal1?.groups}` + (showDiff ? ` vs ${result.ordercountTotal2?.groups}` : ''), + dataIndex: 'OrderType', + fixed: 'left', + render: (text, record) => {text}, + }, + ], + }, + { + title: '数量', + children: [ + { + title: ( + + ), + titleX: [result.ordercountTotal1?.OrderCount, result.ordercountTotal2?.OrderCount].join(' vs '), + dataIndex: 'OrderCount', + render: (text, r) => , + }, + ], + }, + { + title: '成交数', + children: [ + { + title: ( + + ), + titleX: [result.ordercountTotal1?.CJCount, result.ordercountTotal2?.CJCount].join(' vs '), + dataIndex: 'CJCount', + render: (text, r) => , + }, + ], + }, + { + title: '成交人数', + children: [ + { + title: ( + + ), + titleX: [result.ordercountTotal1?.CJPersonNum, result.ordercountTotal2?.CJPersonNum].join(' vs '), + dataIndex: 'CJPersonNum', + render: (text, r) => , + }, + ], + }, + { + title: '成交率', + children: [ + { + title: ( + + ), + titleX: [result.ordercountTotal1?.CJrate, result.ordercountTotal2?.CJrate].join(' vs '), + dataIndex: 'CJrate', + render: (text, r) => , + }, + ], + }, + { + title: '成交毛利(预计)', + children: [ + { + title: ( + + ), + titleX: [result.ordercountTotal1?.YJLY, result.ordercountTotal2?.YJLY].join(' vs '), + dataIndex: 'YJLY', + render: (text, r) => , + }, + ], + }, + + { + title: '单个订单价值', + children: [ + { + title: ( + + ), + titleX: [result.ordercountTotal1?.Ordervalue, result.ordercountTotal2?.Ordervalue].join(' vs '), + dataIndex: 'Ordervalue', + render: (text, r) => , + }, + ], + }, + ], + size: 'small', + pagination: false, + scroll: { x: 100 * 7 }, + loading, + }; + + return ( + <> +
+ + + { + setSearchValues(obj, form); + getToBOrderCount(obj); + onTabChange(activeTab); + }} + /> + + + + + + + + + + + + + + onTabChange(active_key)} + items={[ + { + key: 'customer_types', + label: ( + + + 分销客户 + + ), + }, + ].map((ele) => { + return { + ...ele, + children: ( + <> + + + + + + ), + }; + })} + /> + + + +
+

各项占比

+ {/* setIsShowEmpty(e.target.checked)} + > + 包含空值 + */} +
+ + + + + + + + {showDiff && + + + } + + + + + ); +}); +export default ToBOrder; diff --git a/src/views/toB/ToBOrderSub.jsx b/src/views/toB/ToBOrderSub.jsx new file mode 100644 index 0000000..5a53590 --- /dev/null +++ b/src/views/toB/ToBOrderSub.jsx @@ -0,0 +1,280 @@ +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(/&/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: 'COLI_Success', + key: 'COLI_Success', + render: (text, record) => {text == 1 ? '是' : '否'}, + sorter: (a, b) => b.COLI_Success - a.COLI_Success, + }, + // { + // title: "人数(成/童/婴)", + // dataIndex: "COLI_PersonNum", + // key: "COLI_PersonNum", + // render: (text, record) => ( + // + // {record.COLI_PersonNum}/{record.COLI_ChildNum}/{record.COLI_BabyNum} + // + // ), + // }, + { + title: '预计利润', + dataIndex: 'CGI_YJLY', + key: 'CGI_YJLY', + }, + { + title: '预定时间', + dataIndex: 'COLI_ApplyDate', + key: 'COLI_ApplyDate', + }, + { + 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 ( +
+ +
+
{caption}
+ +
+
+
record.key} + expandable={{ + expandedRowRender: (record) => ( +
+              {/* 
+                客户需求
+              
+              {record.COLI_CustomerRequest} */}
+              
+                订单内容
+              
+              {record.COLI_OrderDetailText}
+            
+ ), + }} + /> + + ); +}; + +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: ( + + + 订单内容 + + ), + title: '订单内容', + children: ( + <> + + + + + + ), + }, + ]; + return ( +
+ +
+ 返回 + + + { + setSearchValues(obj, form); + getToBOrderCount(obj, ordertype, ordertype_sub); + getToBOrderDetailByType(obj, ordertype, ordertype_sub); + }} + /> + + + + + + + + + + + + + + + + + + + ); +}); +export default ToBOrderSub; diff --git a/src/zustand/ToBOrder.js b/src/zustand/ToBOrder.js new file mode 100644 index 0000000..0d8230b --- /dev/null +++ b/src/zustand/ToBOrder.js @@ -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;