diff --git a/package-lock.json b/package-lock.json index 99e66d5..ab23224 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@ant-design/pro-components": "^2.6.16", "antd": "^4.22.6", "dingtalk-jsapi": "^3.0.9", + "immer": "^10.2.0", "insert-css": "^2.0.0", "mobx": "^6.6.1", "mobx-react": "^7.5.2", @@ -20,7 +21,8 @@ "react-router-dom": "^6.3.0", "react-scripts": "^5.0.1", "web-vitals": "^2.1.4", - "xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz" + "xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz", + "zustand": "^5.0.8" }, "devDependencies": { "@types/react": "^18.2.20", @@ -1521,6 +1523,16 @@ "react-dom": "^16.8.0 || ^17.0.0" } }, + "node_modules/@antv/xflow-core/node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@antv/xflow-extension": { "version": "1.0.51", "resolved": "https://registry.npmmirror.com/@antv/xflow-extension/-/xflow-extension-1.0.51.tgz", @@ -5477,7 +5489,7 @@ "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "devOptional": true }, "node_modules/@types/q": { "version": "1.5.5", @@ -5498,7 +5510,7 @@ "version": "18.2.20", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.20.tgz", "integrity": "sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==", - "dev": true, + "devOptional": true, "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -5536,7 +5548,7 @@ "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true + "devOptional": true }, "node_modules/@types/semver": { "version": "7.3.13", @@ -11457,9 +11469,14 @@ } }, "node_modules/immer": { - "version": "9.0.15", - "resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.15.tgz", - "integrity": "sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==" + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } }, "node_modules/import-fresh": { "version": "3.3.0", @@ -17826,6 +17843,16 @@ "node": ">=8" } }, + "node_modules/react-dev-utils/node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/react-dev-utils/node_modules/loader-utils": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", @@ -21314,6 +21341,35 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } }, "dependencies": { @@ -22652,6 +22708,13 @@ "mana-syringe": "^0.2.2", "reflect-metadata": "^0.1.13", "rxjs": "^6.6.7" + }, + "dependencies": { + "immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==" + } } }, "@antv/xflow-extension": { @@ -25409,7 +25472,7 @@ "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", - "dev": true + "devOptional": true }, "@types/q": { "version": "1.5.5", @@ -25430,7 +25493,7 @@ "version": "18.2.20", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.20.tgz", "integrity": "sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==", - "dev": true, + "devOptional": true, "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -25468,7 +25531,7 @@ "version": "0.16.3", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", - "dev": true + "devOptional": true }, "@types/semver": { "version": "7.3.13", @@ -29901,9 +29964,9 @@ "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==" }, "immer": { - "version": "9.0.15", - "resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.15.tgz", - "integrity": "sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==" + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==" }, "import-fresh": { "version": "3.3.0", @@ -34399,6 +34462,11 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, + "immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==" + }, "loader-utils": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", @@ -37049,6 +37117,12 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "requires": {} } } } diff --git a/package.json b/package.json index 2e46f5a..82c8302 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@ant-design/pro-components": "^2.6.16", "antd": "^4.22.6", "dingtalk-jsapi": "^3.0.9", + "immer": "^10.2.0", "insert-css": "^2.0.0", "mobx": "^6.6.1", "mobx-react": "^7.5.2", @@ -15,7 +16,8 @@ "react-router-dom": "^6.3.0", "react-scripts": "^5.0.1", "web-vitals": "^2.1.4", - "xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz" + "xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz", + "zustand": "^5.0.8" }, "scripts": { "start": "react-scripts start", diff --git a/src/App.jsx b/src/App.jsx index d6c2687..fd78c86 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,28 +1,26 @@ import './App.css'; -import React, { useContext, useState } from 'react'; -import Icon, { - HomeOutlined, - TeamOutlined, +import { useContext, useState } from 'react'; +import { DashboardOutlined, - FileProtectOutlined, - CustomerServiceTwoTone, SnippetsTwoTone, DollarOutlined, AreaChartOutlined, - WechatOutlined, UserOutlined, FlagOutlined, - PieChartOutlined, - BarChartOutlined, CoffeeOutlined, DesktopOutlined, - WhatsAppOutlined + WhatsAppOutlined, + ShoppingCartOutlined, + GiftOutlined, + RocketOutlined } from '@ant-design/icons'; -import { Layout, Menu, Image, Badge, Button } from 'antd'; +import { Layout, Menu, Button } from 'antd'; import { BrowserRouter, Route, Routes, NavLink } from 'react-router-dom'; import Home from './views/Home'; import Dashboard from './views/Dashboard'; import Orders from './views/Orders'; import Orders_sub from './views/Orders_sub'; +import BizOrder from './views/biz/BizOrder'; +import BizOrderSub from './views/biz/BizOrderSub'; import ProtectedRoute from './views/ProtectedRoute'; import Customer_care_inchina from './charts/Customer_care_inchina'; import Customer_care_potential from './charts/Customer_care_potential'; @@ -58,6 +56,7 @@ import OPRisk from './views/sales-crm/Risk'; import Cruise from './views/Cruise'; import Hotel from './views/Hotel'; import HostCaseCount from './views/HostCaseCount'; +import TrainsUpsell from './views/biz/reports/TrainsUpsell'; const App = () => { const { Content, Footer, Sider, } = Layout; @@ -81,7 +80,7 @@ const App = () => { key: 'meeting-2024-GH', label: GH区域数据, // GH例会数据-2024 }, - ] + ], }, { key: 2, @@ -104,6 +103,13 @@ const App = () => { }, ], }, + { + key: 20, label: '商务市场', icon: , + children: [ + { key: 201, label: 订单数据 }, + { key: 202, label: TrainsUpsell }, + ] + }, { key: 5, label: '销售', @@ -118,7 +124,7 @@ const App = () => { { key: 3, label: '客运', - icon: , + icon: , children: [ { key: 31, @@ -151,7 +157,7 @@ const App = () => { { key: 6, label: '客服', - icon: , + icon: , children: [ { key: 61, @@ -253,6 +259,9 @@ const App = () => { } /> } /> } /> + } /> + } /> + } /> }> } /> diff --git a/src/components/DateGroupRadio/date.js b/src/components/DateGroupRadio/date.js index 9bd9d15..be91eab 100644 --- a/src/components/DateGroupRadio/date.js +++ b/src/components/DateGroupRadio/date.js @@ -161,5 +161,5 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper, // '\ndata1KeyMappedStr', data1KeyMappedStr, // '\ndata2KeyMappedStr', data2KeyMappedStr // ); - cb(dateGroup, retData, avg1, parse2.avgVal); + return cb(dateGroup, retData, avg1, parse2.avgVal); }; diff --git a/src/components/search/SearchForm.jsx b/src/components/search/SearchForm.jsx index b6ee01a..3f94fcc 100644 --- a/src/components/search/SearchForm.jsx +++ b/src/components/search/SearchForm.jsx @@ -92,6 +92,11 @@ export default observer((props) => { transform: (value) => value?.key || '', default: '', }, + 'IncludeInternal': { + key: 'IncludeInternal', + transform: (value) => value?.key || '', + default: '', + }, 'operator': { key: 'operator', // transform: (value) => value?.key || '', @@ -379,6 +384,21 @@ function getFields(props) { , 3 ), + item( + 'IncludeInternal', + 99, + + + , + 3 + ), item( 'countryArea', 99, diff --git a/src/libs/ht.js b/src/libs/ht.js index bca157c..d584d37 100644 --- a/src/libs/ht.js +++ b/src/libs/ht.js @@ -39,7 +39,7 @@ export const deptUnits = [ export const groups = [ { value: '1,2,28,7,33', key: '1,2,28,7,33', label: 'GH事业部', code: 'GH', children: [1, 2, 28, 7, 33] }, { value: '8,9,11,12,20,21', key: '8,9,11,12,20,21', label: '国际事业部', code: 'INT', children: [8, 9, 11, 12, 20, 21] }, - { value: '10,18,16,30', key: '10,18,16,30', label: '孵化学院', code: '', children: [10, 18, 16, 30] }, + { value: '10,18,16,30', key: '10,18,16,30', label: '孵化学院', code: 'CTX', children: [10, 18, 16, 30] }, { value: '1', key: '1', label: 'CH直销', code: '', children: [] }, { value: '2', key: '2', label: 'CH大客户', code: '', children: [] }, { value: '28', key: '28', label: 'HT项目组(前AH)', code: 'HT', children: [] }, @@ -61,6 +61,11 @@ export const groupsMappedByCode = groups.reduce((a, c) => ({ ...a, [String(c.cod export const groupsMappedByKey = groups.reduce((a, c) => ({ ...a, [String(c.key)]: c }), {}); export const leafGroup = groups.slice(3); export const overviewGroup = groups.slice(0, 3); // todo: 花梨鹰 APP Trippest +export const groupsCTplus = [ + // { value: '10,18', key: '10,18', label: 'CT商旅', code: 'CT+', children: [10, 18,] }; + { value: '18', key: '18', label: 'CT', code: 'CT', children: [] }, + { value: '10', key: '10', label: '商旅', code: '', children: [] }, +]; /** * 来源 */ diff --git a/src/views/Orders.jsx b/src/views/Orders.jsx index 39c799e..703a470 100644 --- a/src/views/Orders.jsx +++ b/src/views/Orders.jsx @@ -327,8 +327,8 @@ class Orders extends Component { colorField: "OrderType", radius: 0.8, label: { - type: "outer", - content: "{name} {value} \n {percentage}", + type: "spider", + content: "{name} {value} \t {percentage}", }, legend: false, // 不显示图例 interactions: [ diff --git a/src/views/biz/BizOrder.jsx b/src/views/biz/BizOrder.jsx new file mode 100644 index 0000000..f3b7078 --- /dev/null +++ b/src/views/biz/BizOrder.jsx @@ -0,0 +1,442 @@ +import { useContext } from 'react'; +import { Row, Col, Tabs, Table, Divider, Spin, Checkbox, Space } from 'antd'; +import { ContainerOutlined, BlockOutlined, SmileOutlined, MobileOutlined, CustomerServiceOutlined } from '@ant-design/icons'; +import { Line, Pie } from '@ant-design/charts'; +import { NavLink } from 'react-router-dom'; +import * as comm from '../../utils/commons'; +import DateGroupRadio from '../../components/DateGroupRadio'; +import SearchForm from '../../components/search/SearchForm'; +import { TableExportBtn } from '../../components/Data'; + +import { observer } from 'mobx-react'; +import { toJS } from 'mobx'; +import { stores_Context } from '../../config'; +import { useShallow } from 'zustand/shallow'; +import useBizOrderStore, { orderCountDataMapper, orderCountDataFieldMapper } from '../../zustand/BizOrder'; + +const BizOrder = observer(() => { + const { date_picker_store: searchFormStore } = useContext(stores_Context); + + const [searchValues, setSearchValues] = useBizOrderStore(useShallow((state) => [state.searchValues, state.setSearchValues])); + const [activeTab, setActiveTab] = useBizOrderStore(useShallow((state) => [state.activeTab, state.setActiveTab])); + const [loading, onTabChange] = useBizOrderStore(useShallow((state) => [state.loading, state.onTabChange])); + + const orderCountDataRaw = useBizOrderStore((state) => state.orderCountDataRaw); + const [orderCountDataLines, avgLineValue] = useBizOrderStore(useShallow((state) => [state.orderCountDataLines, state.avgLineValue])); + const [onChangeDateGroup, activeDateGroupRadio] = useBizOrderStore(useShallow((state) => [state.onChangeDateGroup, state.activeDateGroupRadio])); + + const orderCountDataByType = useBizOrderStore((state) => state.orderCountDataByType); + const result = orderCountDataByType[activeTab] || {}; + + const getBizOrderCount = useBizOrderStore((state) => state.getBizOrderCount); + + 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} \n {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: !showDiff + ? result.ordercountTotal1?.OrderCount + : comm.show_vs_tag(result.ordercountTotal1?.OrderCount_vs, result.ordercountTotal1?.OrderCount_diff, result.ordercountTotal1?.OrderCount, result.ordercountTotal2?.OrderCount), + titleX: [result.ordercountTotal1?.OrderCount, result.ordercountTotal2?.OrderCount].join(' vs '), + dataIndex: 'OrderCount', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.OrderCount_vs, r.OrderCount_diff, r.OrderCount, r.diff?.OrderCount)), + }, + ], + }, + { + title: '成交数', + children: [ + { + title: !showDiff + ? result.ordercountTotal1?.CJCount + : comm.show_vs_tag(result.ordercountTotal1?.CJCount_vs, result.ordercountTotal1?.CJCount_diff, result.ordercountTotal1?.CJCount, result.ordercountTotal2?.CJCount), + titleX: [result.ordercountTotal1?.CJCount, result.ordercountTotal2?.CJCount].join(' vs '), + dataIndex: 'CJCount', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.CJCount_vs, r.CJCount_diff, r.CJCount, r.diff?.CJCount)), + }, + ], + }, + { + title: '成交人数', + children: [ + { + title: !showDiff + ? result.ordercountTotal1?.CJPersonNum + : comm.show_vs_tag(result.ordercountTotal1?.CJPersonNum_vs, result.ordercountTotal1?.CJPersonNum_diff, result.ordercountTotal1?.CJPersonNum, result.ordercountTotal2?.CJPersonNum), + titleX: [result.ordercountTotal1?.CJPersonNum, result.ordercountTotal2?.CJPersonNum].join(' vs '), + dataIndex: 'CJPersonNum', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.CJPersonNum_vs, r.CJPersonNum_diff, r.CJPersonNum, r.diff?.CJPersonNum)), + }, + ], + }, + { + title: '成交率', + children: [ + { + title: !showDiff + ? result.ordercountTotal1?.CJrate + : comm.show_vs_tag(result.ordercountTotal1?.CJrate_vs, result.ordercountTotal1?.CJrate_diff, result.ordercountTotal1?.CJrate, result.ordercountTotal2?.CJrate), + titleX: [result.ordercountTotal1?.CJrate, result.ordercountTotal2?.CJrate].join(' vs '), + dataIndex: 'CJrate', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.CJrate_vs, r.CJrate_diff, r.CJrate, r.diff?.CJrate)), + }, + ], + }, + { + title: '成交毛利(预计)', + children: [ + { + title: !showDiff + ? result.ordercountTotal1?.YJLY + : comm.show_vs_tag(result.ordercountTotal1?.YJLY_vs, result.ordercountTotal1?.YJLY_diff, result.ordercountTotal1?.YJLY, result.ordercountTotal2?.YJLY), + titleX: [result.ordercountTotal1?.YJLY, result.ordercountTotal2?.YJLY].join(' vs '), + dataIndex: 'YJLY', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.YJLY_vs, r.YJLY_diff, r.YJLY, r.diff?.YJLY)), + }, + ], + }, + + { + title: '单个订单价值', + children: [ + { + title: !showDiff + ? result.ordercountTotal1?.Ordervalue + : comm.show_vs_tag(result.ordercountTotal1?.Ordervalue_vs, result.ordercountTotal1?.Ordervalue_diff, result.ordercountTotal1?.Ordervalue, result.ordercountTotal2?.Ordervalue), + titleX: [result.ordercountTotal1?.Ordervalue, result.ordercountTotal2?.Ordervalue].join(' vs '), + dataIndex: 'Ordervalue', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.Ordervalue_vs, r.Ordervalue_diff, r.Ordervalue, r.diff?.Ordervalue)), + }, + ], + }, + ], + size: 'small', + pagination: false, + scroll: { x: 100 * 7 }, + loading, + }; + + return ( + <> +
+ + + { + setSearchValues(obj, form); + getBizOrderCount(obj); + onTabChange(activeTab); + }} + /> + + + + + + + + + + + + + + onTabChange(active_key)} + items={[ + { + key: 'servicetype', + label: ( + + + 服务类型 + + ), + }, + { + key: 'Form', + label: ( + + + 来源类型 + + ), + }, + // { + // key: 'Product', + // label: ( + // + // + // 产品类型 + // + // ), + // }, + { + key: 'Country', + label: ( + + + 国籍 + + ), + }, + // { + // key: 'line', + // label: ( + // + // + // 线路 + // + // ), + // }, + // { + // key: 'city', + // label: ( + // + // + // 目的地 + // + // ), + // }, + { + key: 'LineClass', + label: ( + + + 页面类型 + + ), + }, + { + key: 'ordersource', + label: ( + + + 来源设备 + + ), + }, + // { + // key: 'GuestGroupType', + // label: ( + // + // + // 客群类别 + // + // ), + // }, + // { + // key: 'TravelMotivation', + // label: ( + // + // + // 出行动机 + // + // ), + // }, + // { + // key: 'ToB', + // label: ( + // + // + // 客运类别 + // + // ), + // }, + // { + // key: 'FoodRequirement', + // label: ( + // + // + // 饮食要求 + // + // ), + // }, + // { + // key: 'hobbies', + // label: ( + // + // + // 兴趣爱好 + // + // ), + // }, + // { + // key: 'ages', + // label: ( + // + // + // 年龄段 + // + // ), + // }, + ].map((ele) => { + return { + ...ele, + children: ( + <> + + + + + + ), + }; + })} + /> + + + +
+

各项占比

+ {/* setIsShowEmpty(e.target.checked)} + > + 包含空值 + */} +
+ + + + + + + + + + + + + ); +}); +export default BizOrder; diff --git a/src/views/biz/BizOrderSub.jsx b/src/views/biz/BizOrderSub.jsx new file mode 100644 index 0000000..40df6ea --- /dev/null +++ b/src/views/biz/BizOrderSub.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 * as comm from '../../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 useBizOrderStore, { orderCountDataMapper, orderCountDataFieldMapper } from '../../zustand/BizOrder'; + +// 在逗号和分号处自动换行的函数 +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 BizOrderSub = observer(({ ...props }) => { + const { ordertype, ordertype_sub, ordertype_title } = useParams(); + const { date_picker_store: searchFormStore } = useContext(stores_Context); + + const [searchValues, setSearchValues] = useBizOrderStore(useShallow((state) => [state.searchValues, state.setSearchValues])); + const [searchValuesToSub] = useBizOrderStore(useShallow((state) => [state.searchValuesToSub])); + + const [loading, typeLoading] = useBizOrderStore(useShallow((state) => [state.loading, state.typeLoading])); + const orderCountDataRawSub = useBizOrderStore((state) => state.orderCountDataRawSub); + const [orderCountDataLinesSub, avgLineValueSub] = useBizOrderStore(useShallow((state) => [state.orderCountDataLinesSub, state.avgLineValueSub])); + const [onChangeDateGroupSub, activeDateGroupRadioSub] = useBizOrderStore(useShallow((state) => [state.onChangeDateGroupSub, state.activeDateGroupRadioSub])); + + const orderDetails = useBizOrderStore((state) => state.orderDetails); + + const getBizOrderCount = useBizOrderStore((state) => state.getBizOrderCount); + const getBizOrderDetailByType = useBizOrderStore((state) => state.getBizOrderDetailByType); + + useEffect(() => { + getBizOrderCount(searchValuesToSub, ordertype, ordertype_sub); + getBizOrderDetailByType(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} ${comm.getWeek(datum.xField)}`; // 显示周几 + break; + + default: + break; + } + return ret; + }, + }, + smooth: true, + }; + + const tab_items = [ + { + key: 'detail', + label: ( + + + 订单内容 + + ), + title: '订单内容', + children: ( + <> + + + + + + ), + }, + ]; + return ( +
+ +
+ 返回 + + + { + setSearchValues(obj, form); + getBizOrderCount(obj, ordertype, ordertype_sub); + getBizOrderDetailByType(obj, ordertype, ordertype_sub); + }} + /> + + + + + + + + + + + + + + + + + + + ); +}); +export default BizOrderSub; diff --git a/src/views/biz/reports/TrainsUpsell.jsx b/src/views/biz/reports/TrainsUpsell.jsx new file mode 100644 index 0000000..47d61a3 --- /dev/null +++ b/src/views/biz/reports/TrainsUpsell.jsx @@ -0,0 +1,238 @@ +import { useContext } from 'react'; +import { Row, Col, Table, Spin, Space, Divider } from 'antd'; +import { Funnel, Pie } from '@ant-design/charts'; + +import * as comm from '../../../utils/commons'; +import SearchForm from '../../../components/search/SearchForm'; + +import { observer } from 'mobx-react'; +import { toJS } from 'mobx'; +import { stores_Context } from '../../../config'; +import { useShallow } from 'zustand/shallow'; +import useTrainsStore from '../../../zustand/Trains'; + +const buildFunnelData = (data1, data2) => { + // const key = 'OrderCount'; + const data01 = [ + { stage: '火车票服务', number: data1?.data?.[0]?.OrderCount, dateRange: data1?.dateRangeStr }, + { stage: '成行出票', number: data1?.data?.[0]?.CJCount, dateRange: data1?.dateRangeStr }, + { stage: '火车票Upsell', number: (data1?.data?.[1]?.OrderCount || 0) + 0, dateRange: data1?.dateRangeStr }, + { stage: 'Upsell成行', number: data1?.data?.[1]?.CJCount, dateRange: data1?.dateRangeStr }, + ]; + const data02 = !comm.isEmpty(data2) + ? [ + { stage: '火车票服务', number: data2?.data?.[0]?.OrderCount, dateRange: data2?.dateRangeStr }, + { stage: '成行出票', number: data2?.data?.[0]?.CJCount, dateRange: data2?.dateRangeStr }, + { stage: '火车票Upsell', number: data2?.data?.[1]?.OrderCount, dateRange: data2?.dateRangeStr }, + { stage: 'Upsell成行', number: data2?.data?.[1]?.CJCount, dateRange: data2?.dateRangeStr }, + ] + : []; + return data01.concat(data02); + // return data02.concat(data01); +}; + +const buildPieData = (data1, data2) => { + const data01 = !comm.isEmpty(data1) + ? [ + { stage: '火车票出票', number: Number((data1?.data?.[0]?.YJLY || '0').replaceAll(',', '')), dateRange: data1?.dateRangeStr }, + { stage: 'Upsell成行', number: Number((data1?.data?.[1]?.YJLY || '0').replaceAll(',', '')), dateRange: data1?.dateRangeStr }, + ] + : []; + const data02 = !comm.isEmpty(data2) + ? [ + { stage: '火车票出票', number: Number((data2?.data?.[0]?.YJLY || '0').replaceAll(',', '')), dateRange: data2?.dateRangeStr }, + { stage: 'Upsell成行', number: Number((data2?.data?.[1]?.YJLY || '0').replaceAll(',', '')), dateRange: data2?.dateRangeStr }, + ] + : []; + return data01.concat(data02); +}; + +const TrainsUpsell = observer(({ ...props }) => { + const { date_picker_store: searchFormStore } = useContext(stores_Context); + const [searchValues, setSearchValues] = useTrainsStore(useShallow((state) => [state.searchValues, state.setSearchValues])); + + const [loading] = useTrainsStore(useShallow((state) => [state.loading])); + const [trainsOrdersSummary, compareData] = useTrainsStore((state) => state.trainsOrdersSummary); + // const showDiff = !comm.isEmpty(searchValuesToSub.DateDiff2); + const showDiff = !comm.isEmpty(compareData); + + const getTrainsWithUpsell = useTrainsStore((state) => state.getTrainsWithUpsell); + + const data0 = buildFunnelData(trainsOrdersSummary, compareData); + const chartConfig = { + data: data0, + xField: 'stage', + yField: 'number', + compareField: showDiff ? 'dateRange' : false, + // isTransposed: true, + legend: false, + // tooltip: { + // shared: true, + // showMarkers: false, + // showTitle: false, + // }, + label: { + position: 'right', + // offsetX: 10, + style: { + fill: '#002c45', + }, + }, + shape: 'pyramid', + theme: { + colors10: [ + // '#b5d8f3', + '#c4e1ff', + // '#739ec1', + '#7ebdff', + // '#3d759b', + '#5b90f9', + // '#1f5373', + '#526bd1', + '#002c45', + ], + }, + }; + + const pieData = buildPieData(trainsOrdersSummary); + const pieData2 = buildPieData(compareData); + // console.log('🟠', pieData, pieData2); + const pieConfig = { + appendPadding: 0, + // data: pieData, + angleField: 'number', + colorField: 'stage', + radius: 0.8, + innerRadius: 0.6, + // startAngle: Math.PI , + // endAngle: Math.PI * 1.5, + label: { + type: 'spider', + // content: '{name} {value} \n {percentage}', + content: '{name}\n{percentage}', + }, + statistic: false, + legend: false, // 不显示图例 + interactions: [ + { + type: 'element-selected', + }, + { + type: 'element-active', + }, + ], + }; + + const tableProps = { + title: () => `${trainsOrdersSummary?.dateRangeStr || ''}` + (showDiff ? ` vs ${compareData?.dateRangeStr}` : ''), + dataSource: trainsOrdersSummary?.data || [], + columns: [ + { + title: '#', + fixed: 'left', + dataIndex: 'OrderType', + }, + { + title: '数量', + dataIndex: 'OrderCount', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.OrderCount_vs, r.OrderCount_diff, r.OrderCount, r.diff?.OrderCount)), + }, + { + title: '成交数', + dataIndex: 'CJCount', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.CJCount_vs, r.CJCount_diff, r.CJCount, r.diff?.CJCount)), + }, + { + title: '成交人数', + dataIndex: 'CJPersonNum', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.CJPersonNum_vs, r.CJPersonNum_diff, r.CJPersonNum, r.diff?.CJPersonNum)), + }, + { + title: '成交率', + dataIndex: 'CJrate', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.CJrate_vs, r.CJrate_diff, r.CJrate, r.diff?.CJrate)), + }, + { + title: '成交毛利(预计)', + dataIndex: 'YJLY', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.YJLY_vs, r.YJLY_diff, r.YJLY, r.diff?.YJLY)), + }, + + { + title: '单个订单价值', + dataIndex: 'Ordervalue', + render: (text, r) => (!showDiff ? text : comm.show_vs_tag(r.Ordervalue_vs, r.Ordervalue_diff, r.Ordervalue, r.diff?.Ordervalue)), + }, + ], + size: 'small', + pagination: false, + scroll: { x: 100 * 7 }, + loading, + }; + return ( + <> +
+ +
+ { + setSearchValues(obj, form); + getTrainsWithUpsell(obj); + }} + /> + + + +
+ + + + +

订单数量

+ + + +

预计利润

+
+ + {showDiff && ( + + )} +
+ + + + + + + ); +}); +export default TrainsUpsell; diff --git a/src/zustand/BizOrder.js b/src/zustand/BizOrder.js new file mode 100644 index 0000000..433a593 --- /dev/null +++ b/src/zustand/BizOrder.js @@ -0,0 +1,234 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { immer } from 'zustand/middleware/immer'; +import { groupsMappedByCode } from '../libs/ht'; +import { fetchJSON } from '../utils/request'; +import { HT_HOST } from '../config'; +import { resultDataCb } from '../components/DateGroupRadio/date'; +import { isEmpty } from '../utils/commons'; + +const defaultParams = { WebCode: 'all', IncludeTickets: 1, IncludeInternal: 1 }; + +export const fetchBizOrderCount = async (params, type = '', typeVal = '') => { + const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCount_biz', { + ...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 fetchBizOrderCountByType = async (type, params) => { + const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType_biz', { + ...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 }), {}); + const rows2Map = res.ordercount2.reduce((a, row2) => ({ ...a, [row2.OrderTypeSN]: row2 }), {}); + + const mixRow1 = res.ordercount1.map((row1) => ({ ...row1, 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 }; +}; + +const _detailRes = { ordercount1: [], ordercount2: [] }; +export const fetchBizOrderDetailByType = async (params, type = '', typeVal = '', orderContent = 'detail') => { + const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType_Sub_biz', { + ...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': 'WebCode', _f: 'sum' }; + +/** + * -------------------------------------------------------------------------------------------------------- + */ +const initialState = { + loading: false, + typeLoading: false, + searchValues: { + DateType: { key: 'applyDate', label: '提交日期' }, + WebCode: { key: 'all', label: '所有来源' }, + IncludeTickets: { key: '1', label: '含门票' }, + DepartmentList: groupsMappedByCode.CTX, // { key: 'All', label: '所有来源' }, // + }, + searchValuesToSub: { + DateType: 'applyDate', + WebCode: 'all', + IncludeTickets: '1', + DepartmentList: -1, // -1: All + }, + + activeTab: 'servicetype', + activeDateGroupRadio: 'day', + + orderCountDataRaw: {}, + orderCountDataLines: [], + avgLineValue: 0, + + orderCountDataByType: {}, + + // 二级页面 + orderCountDataRawSub: {}, + orderCountDataLinesSub: [], + avgLineValueSub: 0, + activeDateGroupRadioSub: 'day', + orderDetails: [], +}; + +const useBizOrderStore = 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 + getBizOrderCount: async (params, type, typeVal) => { + const { setLoading } = get(); + setLoading(true); + try { + const res = await fetchBizOrderCount(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) { + } finally { + setLoading(false); + } + }, + + getBizOrderCount_type: async (type) => { + const { setTypeLoading, searchValuesToSub, setOrderCountDataByType } = get(); + setTypeLoading(true); + try { + const res = await fetchBizOrderCountByType(type, searchValuesToSub); + setOrderCountDataByType(type, res); + } catch (error) { + } finally { + setTypeLoading(false); + } + }, + + onTabChange: async (tab) => { + const { setActiveTab, getBizOrderCount_type } = get(); + setActiveTab(tab); + await getBizOrderCount_type(tab); + }, + + // sub + getBizOrderDetailByType: async (params, type, typeVal) => { + const { setTypeLoading } = get(); + try { + setTypeLoading(true); + const res = await fetchBizOrderDetailByType(params, type, typeVal, 'detail'); + set({ orderDetails: res }); + } catch (error) { + } finally { + setTypeLoading(false); + } + }, + })), + { name: 'bizOrder' } + ) +); +export default useBizOrderStore; diff --git a/src/zustand/Trains.js b/src/zustand/Trains.js new file mode 100644 index 0000000..9c05fa1 --- /dev/null +++ b/src/zustand/Trains.js @@ -0,0 +1,178 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { immer } from 'zustand/middleware/immer'; +import { groupsCTplus, groupsMappedByCode } from '../libs/ht'; +import { fetchJSON } from '../utils/request'; +import { HT_HOST } from '../config'; +import { resultDataCb } from '../components/DateGroupRadio/date'; +import { groupBy, isEmpty } from '../utils/commons'; + +const SERVICETYPE_TRAINSBOOKING = '2'; // 火车票服务 +const FORM_TRAINSBOOKING = 32024; // 火车票预定 +const FORM_TRAINSUPSELL = 32214; // 火车票Upsell + +const defaultParams = { WebCode: 'all', IncludeTickets: 1, IncludeInternal: 1 }; +const _res = { ordercount1: [], ordercount2: [] }; + +export const fetchBizTrainsOrderSummaryByType = async (params, type = 'Form', typeVal = '') => { + const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType_biz', { + ...defaultParams, + ...params, + COLI_ApplyDate1: params.Date1, + COLI_ApplyDate2: params.Date2, + COLI_ApplyDateOld1: params.DateDiff1 || '', + COLI_ApplyDateOld2: params.DateDiff2 || '', + OrderType: type, + }); + const res = errcode !== 0 ? _res : result || _res; + const dateStr = [params.Date1, params.Date2].map((d) => d.substring(0, 10)).join('~'); + const ret = [ + { dateRangeStr: dateStr, data: res.ordercount1.find((row) => row.OrderTypeSN === typeVal) }, + // { dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === typeVal) }, + ]; + const dateDiffStr = isEmpty(params.DateDiff1) ? '' : [params.DateDiff1, params.DateDiff2].map((d) => d.substring(0, 10)).join('~'); + if (!isEmpty(dateDiffStr)) { + ret.push({ dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === typeVal) }); + } + return ret; +}; + +/** + * 从传统订单中获取upsell数据 + */ +export const fetchTrainsUpsellTSummaryByType = async (params, type = 'Form', typeVal = '') => { + const { errcode, errmsg, ...result } = await fetchJSON(HT_HOST + '/service-web/QueryData/GetOrderCountByType', { + ...defaultParams, + ...params, + COLI_ApplyDate1: params.Date1, + COLI_ApplyDate2: params.Date2, + COLI_ApplyDateOld1: params.DateDiff1 || '', + COLI_ApplyDateOld2: params.DateDiff2 || '', + OrderType: type, + }); + const res = errcode !== 0 ? _res : result || _res; + const dateStr = [params.Date1, params.Date2].map((d) => d.substring(0, 10)).join('~'); + const ret = [ + { dateRangeStr: dateStr, data: res.ordercount1.find((row) => row.OrderTypeSN === typeVal) }, + // { dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === typeVal) }, + ]; + const dateDiffStr = isEmpty(params.DateDiff1) ? '' : [params.DateDiff1, params.DateDiff2].map((d) => d.substring(0, 10)).join('~'); + if (!isEmpty(dateDiffStr)) { + ret.push({ dateRangeStr: dateDiffStr, data: res.ordercount2.find((row) => row.OrderTypeSN === typeVal) }); + } + return ret; +}; + +export const fetchTrainsWithUpsellSummary = async (params) => { + const [trains, upsell] = await Promise.all([ + // fetchBizTrainsOrderSummaryByType(params, 'Form', FORM_TRAINSBOOKING), // todo: 换成servicetype=2 + fetchBizTrainsOrderSummaryByType(params, 'servicetype', SERVICETYPE_TRAINSBOOKING), + fetchTrainsUpsellTSummaryByType(params, 'Form', FORM_TRAINSUPSELL), + ]); + if (!isEmpty(trains[1])) { + trains[0].data.diff = trains[1].data; + upsell[0].data.diff = upsell[1].data; + } + const mergedMap = [...trains, ...upsell].reduce((acc, currentItem, currentIndex) => { + const dateRange = currentItem.dateRangeStr; + + if (acc[dateRange]) { + acc[dateRange].data.push(currentItem.data); + + for (const key in currentItem) { + if (key !== 'dateRange' && key !== 'data') { + acc[dateRange][key] = currentItem[key]; + } + } + } else { + acc[dateRange] = { + ...currentItem, + data: [currentItem.data], + }; + } + + return acc; + }, {}); + return Object.values(mergedMap); + // return groupBy([...trains, ...upsell], 'dateRangeStr'); +}; + +/** + * -------------------------------------------------------------------------------------------------------- + */ +const initialState = { + loading: false, + typeLoading: false, + searchValues: { + DateType: { key: 'applyDate', label: '提交日期' }, + WebCode: { key: 'all', label: '所有来源' }, + IncludeTickets: { key: '1', label: '含门票' }, + DepartmentList: groupsCTplus, // groupsMappedByCode.CTX, // { key: 'All', label: '所有来源' }, // + }, + searchValuesToSub: { + DateType: 'applyDate', + WebCode: 'all', + IncludeTickets: '1', + DepartmentList: -1, // -1: All + }, + + trainsOrdersSummary: [], + + activeTab: 'Form', + activeDateGroupRadio: 'day', + + orderCountDataRaw: {}, + orderCountDataLines: [], + avgLineValue: 0, + + orderCountDataByType: {}, + + // 二级页面 + orderCountDataRawSub: {}, + orderCountDataLinesSub: [], + avgLineValueSub: 0, + activeDateGroupRadioSub: 'day', + orderDetails: [], +}; + +const useTrainsStore = 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 ---- + + // site effects + getTrainsWithUpsell: async (params, type, typeVal) => { + const { setLoading } = get(); + setLoading(true); + set({ trainsOrdersSummary: [] }); + try { + const res = await fetchTrainsWithUpsellSummary(params); + set({ trainsOrdersSummary: res }); + } catch (error) { + } finally { + setLoading(false); + } + }, + + // sub + })), + { name: 'trains' } + ) +); +export default useTrainsStore;