Compare commits

...

173 Commits

Author SHA1 Message Date
Lei OT 021fb429ce perf: 销售进度: 表格优化 4 days ago
ZJYHX a9f45e334d perf:客运-老客户:订单数占比、毛利占比值保留小数 1 month ago
ZJYHX 488b33ae50 perf:客运-老客户:增加订单数占比、毛利占比 1 month ago
Lei OT 6dbf9cfb56 2.11.6 1 month ago
ZJYHX 108becd2cd perf:客服-目的地:增加国籍统计 1 month ago
Lei OT 73e1dcdb4c Revert "perf: 数据透视: + 预定的出发日期"
This reverts commit 16b08311fa.
1 month ago
Lei OT baae505831 perf: 老客户-分析: 移动端显示 1 month ago
Lei OT 16b08311fa perf: 数据透视: + 预定的出发日期 1 month ago
Lei OT e0e7c3c84f 2.11.5 2 months ago
ZJYHX 87f24c40fa fix:东道主项目:加载动画 2 months ago
ZJYHX bed6b81753 feat:东道主项目:单团明细 2 months ago
ZJYHX 171dd7514a fix:错误id命名 2 months ago
Lei OT a0ea6507a3 feat: 老客户-分析 2 months ago
Lei OT 40cd81f9a7 2.11.4 2 months ago
Lei OT 876216458e perf: 数据透视: 增加日期 2 months ago
Lei OT 8a0f919a42 fix: 2 months ago
Lei OT 020c9fc3e9 Merge remote-tracking branch 'origin/main' 2 months ago
Lei OT b2e539a4d2 perf: 导出表格: render 显示不正确; 酒店/三峡导出 2 months ago
ZJYHX fee1185406 feat:东道主项目页 2 months ago
Lei OT e297d36256 perf: 老客户: 默认参数 2 months ago
Lei OT 667574ec34 perf: 销售-老客户: 默认参数 2 months ago
Lei OT 185db1ec80 2.11.3 2 months ago
Lei OT 6dc82a0a01 perf: 销售-老客户: + 对比 2 months ago
Lei OT 4724b13a2d fix: 客服-目的地接团: 明细 2 months ago
Lei OT f532f138ff perf: 三峡,酒店: 合计 2 months ago
Lei OT 3b51ae8d08 2.11.2 2 months ago
Lei OT a6f7884e6e perf: 三峡: 常用的地接社列表; fix: 地接社 参数 2 months ago
Lei OT 928e27913d perf: 三峡: 人数 2 months ago
Lei OT 94363df0fc 2.11.1 2 months ago
Lei OT 621d3a2446 Merge remote-tracking branch 'origin/main' 2 months ago
ZJYHX 8f12fc8747 fix:老客户:修复数据量大时加载状态不提前消失 2 months ago
Lei OT 397c886aea perf: 三峡: 预订类型 2 months ago
ZJYHX 459db55453 fix:老客户:修复数据量大时加载状态不提前消失 2 months ago
ZJYHX 6317c24a75 perf:老客户:添加总计对比折线;修改折线颜色 2 months ago
Lei OT c9e13385dc 2.11.0 2 months ago
Lei OT a02fa37f17 fear: CRM统计: 个人看板; 违规明细 2 months ago
Lei OT ccbd0be1cd Merge remote-tracking branch 'origin/main' 2 months ago
Lei OT c7d3cf5746 fix: 三峡游船, 酒店 参数 2 months ago
ZJYHX 23b1d2c81b fix:老客户:切换页面折线颜色变换 2 months ago
Lei OT 7c08e8bfe6 Merge branch 'feature/hotel-cruise' 2 months ago
Lei OT 1e46cf6e63 feat: 客服: 三峡游船: 人数 2 months ago
Lei OT 17cedf14d9 feat: 客服: 酒店 2 months ago
ZJYHX 2fae92fa2a perf:老客户:增加折线虚实颜色 2 months ago
Lei OT 08aa01e33c feat: 客服: 三峡游船 2 months ago
ZJYHX 17a85122b1 fix:老客户:折线显示 2 months ago
ZJYHX 1b86ab1192 perf:老客户:增加对比折线 2 months ago
ZJYHX be31c3983d perf:老客户:添加数据对比、对比折线 2 months ago
ZJYHX 5551b2e362 perf:客服:添加目的地接数据对比 3 months ago
Lei OT 7cfd79a0d2 Merge branch 'main' into feature/hotel-cruise 3 months ago
Lei OT dcb185be5a Merge remote-tracking branch 'origin/main' 3 months ago
ZJYHX c3547538fd perf:客服:增加对比 3 months ago
Lei OT cb598ecba1 perf: CRM统计: tips 3 months ago
Lei OT a32943fffb Merge branch 'main' into feature/hotel-cruise
# Conflicts:
#	src/stores/CustomerServices.js
3 months ago
Lei OT a7b24d206d perf: 过程指标: tips 3 months ago
Lei OT f8eef38944 站点来源: + JH 3 months ago
Lei OT 89d151f388 2.10.7 3 months ago
Lei OT 6ea43a58e2 perf: 老客户: 订单走势 3 months ago
Lei OT 12e99bb816 2.10.6 3 months ago
Lei OT d29dab5824 feat: 数据透视: 增加 人天消费; 酒店星级; perf: 毛利范围 3 months ago
Lei OT f1e0c1200b 2.10.5 3 months ago
Lei OT 6f640bac4d perf: 客服: 增加按抵达日期统计; 3 months ago
Lei OT e822b9bb3a feat: 统计: 饮食要求、兴趣爱好、年龄段 3 months ago
Lei OT ac11babdab perf: 客服: 增加按抵达日期统计; 汇总按整团统计 3 months ago
Lei OT 04431635b5 2.10.4 4 months ago
Lei OT 80d4050527 perf: 老客户统计增加2列: 上次走团, 上次小组 4 months ago
Lei OT 6d6a7d98ee fix: CRM-统计: 过程指标 4 months ago
Lei OT b583bfcd2f 2.10.3 4 months ago
Lei OT 539f57e16d perf: CRM-统计: 结果指标: 增加日期选择; 修改样式 4 months ago
Lei OT e5ff51ae63 feat: CRM-统计: 过程指标 4 months ago
Lei OT cc3d0c19ad 2.10.2 4 months ago
Lei OT 00e22a6f08 conf: 来源配置: GH站外渠道 (中国), GH站外渠道 (海外) 4 months ago
Lei OT 02c09c9821 perf: 统计分布 4 months ago
Lei OT bd67f6ca4e 2.10.1 4 months ago
Lei OT 04d1cc2b39 perf: GH例会数据, 2025模板: 销售: 年走团毛利 4 months ago
Lei OT d1ee551a4e todo: 三峡, 酒店 4 months ago
Lei OT 4531ce1469 perf: CRM-统计: 导航 4 months ago
Lei OT d2696d8b01 2.10.0 4 months ago
Lei OT 557e4ac104 Merge branch 'feature/crm'
# Conflicts:
#	src/stores/Index.js
4 months ago
Lei OT 9a4c693c9e perf: GH例会数据, 2025模板: 销售: 年走团毛利 4 months ago
Lei OT 2be734a591 feat: CRM-统计数据 4 months ago
Lei OT 88e09d0bc9 feat: GH例会数据, 2025模板: 客服: 不显示好评个数, 因数据获取不准确 5 months ago
Lei OT ff4852475c 2.9.0 5 months ago
Lei OT a0b71dba79 feat: GH例会数据, 2025模板 5 months ago
Lei OT b2ce88e8aa 2.8.4 5 months ago
Lei OT 20e237f75d perf: 修改GH例会模板的文字 5 months ago
Lei OT b4a2b02709 perf: 数据透视: 增加查看订单明细, 导出 5 months ago
Lei OT 3890c4700f 2.8.3 7 months ago
Lei OT cb211ec207 perf: 客运-老客户: 增加条件`门票` 7 months ago
Lei OT 83e570bb49 feat: 数据透视: 增加毛利范围 1, 1.5, 2, 3, 4 8 months ago
Lei OT 545ad25d05 2.8.2 8 months ago
Lei OT 496fbc1ebd feat: 数据透视: 毛利范围[2W] 8 months ago
Lei OT e53f241d10 perf: 老客户: 提示同时勾选的数据, 存在重复计数 9 months ago
Lei OT 93766a28b6 perf: 客服: 小组多选 11 months ago
Lei OT b5115a8d1e 2.8.1 11 months ago
Lei OT 322ce021e6 perf: 搜索组件: 站点来源: +GH 国际各个站点 11 months ago
Lei OT 55bd0576a8 2.8.0 12 months ago
Lei OT 4792291025 perf: 导出; 解决汇总行空; 表头文字 12 months ago
Lei OT f2a937cc81 feat: 订单统计: 增加`站外渠道` 12 months ago
Lei OT d05d15ae09 feat: 订单统计: 客运标记 12 months ago
Lei OT 4743a66408 2.7.9 1 year ago
Lei OT 49b1657046 perf: 老客户/推荐-销售业绩: 导出, 可选总表和展开表 1 year ago
Lei OT 71b0cce66a 2.7.8 1 year ago
Lei OT 9e3bb33043 perf: 老客户/推荐-销售业绩: 合并账户 1 year ago
Lei OT c2317d40a7 导出的老客户推荐的值 1 year ago
Lei OT 046b7e2988 显示团天数和人数 1 year ago
Lei OT d18c59bb50 2.7.7 1 year ago
Lei OT ab79cdc289 feat: 老客户/推荐-销售业绩 1 year ago
Lei OT 345a2b88d8 2.7.6 1 year ago
Lei OT 530e558812 perf: 数据透视: +顾问 1 year ago
Lei OT ebdb7d41b4 2.7.5 1 year ago
Lei OT 3c9c9ede8c perf: GH例会: 顾问数据: 年走团 1 year ago
Lei OT 536a8b7f86 perf: GH例会: 顾问数据: 周/年成交 1 year ago
Lei OT 216bd9c0e6 perf: GH例会: 客服数据: 好评总数, AH, GH 1 year ago
Lei OT 55d205aaaa 2.7.4 1 year ago
Lei OT 0e76cc1f08 perf: GH例会: 客服数据: AH, GH 1 year ago
Lei OT 854c4a5ebc perf: GH例会: 顾问数据 1 year ago
Lei OT 5f55529f68 perf: GH例会: 客服数据: 中国 1 year ago
Lei OT 8eb539abb2 conf: 调整pagespy日志文件大小 1 year ago
Lei OT e42f1f1b6c fix: 登录后 init PageSpy 1 year ago
Lei OT f92f3f53bb 2.7.3 1 year ago
Lei OT 1025d321ef perf: GH例会: 顾问成交; 年数据用1.1-12.31; CH顾问成交用总览 1 year ago
Lei OT 00f0c1a7b1 chore: PageSpy 1 year ago
Lei OT a7280a79b9 feat: GH例会: 顾问成交; 客服; todo: 年数据用1.1-12.31; 顾问成交用总览/产品接口 1 year ago
Lei OT 512b4699ba 2.7.2 1 year ago
Lei OT 5dcdcd6345 perf: 销售业绩数据: 筛选顾问 1 year ago
Lei OT ac82432e67 perf: 销售业绩数据: 筛选顾问 1 year ago
Lei OT 35d2731560 perf: 销售业绩数据: 名称排序 1 year ago
Lei OT 513b7c7c1c 2.7.1 1 year ago
Lei OT 401d5720d8 perf: GH例会数据: 规则调整: 印度 1 year ago
Lei OT e4a63f38f6 perf: GH例会数据: 规则调整: 老客户: 仅To C 客户 1 year ago
Lei OT 1a2ee6b637 2.7.0 1 year ago
Lei OT 81ee6594c0 feat: GH 市场例会数据 1 year ago
Jimmy Liow fe29cb3d1c 企业微信接口已经不再续费,无法调用联系人和消息。下线。 1 year ago
Lei OT 8017b9c2c8 fix: 临时订单数量: 查询站点多选 1 year ago
Lei OT 5e98893ef0 2.6.5 1 year ago
Lei OT f8820d0eb5 perf: 数据透视: 增加 老客户; 老客户推荐 1 year ago
Lei OT 1bc85a8b38 perf: 查询日期范围的格式 1 year ago
YCC 16fb5b3dfc 显示出发日期 1 year ago
Lei OT a777eff3bd 2.6.4 1 year ago
Lei OT 766411cf16 perf(数据透视): 汇总表增加: 成团周期, 预定周期, 团天数 1 year ago
Lei OT 956303d9e5 fix: 导出 1 year ago
Lei OT 606c1e9983 common utils 1 year ago
Lei OT 9718c023a0 2.6.3 2 years ago
Lei OT 8fc609c1c0 fix: 数据透视: 目的地国籍 2 years ago
Lei OT d272cf6806 fix: 导出 2 years ago
Lei OT b4f869453a perf: 服务状态和403 区分 2 years ago
Lei OT d0d0cb897f style: 地图: 红色 2 years ago
Lei OT 08b19ee3a9 2.6.2 2 years ago
Lei OT 3f62a59648 conf: 测试地址: 144 2 years ago
Lei OT 8484beba64 feat: 数据透视: +人均天计算 2 years ago
Lei OT 528a950b22 perf: 看板: 增加目的地国籍地图 2 years ago
Lei OT 3ba2b44daa perf: 文旅局服务人数: 解决小数点问题 2 years ago
Lei OT 8caf1a5fb8 2.6.1 2 years ago
Lei OT 18dcfe3759 perf: 文旅局服务人数 2 years ago
Lei OT d80ecf398b perf: 文旅局服务人数 2 years ago
YCC 43780b5fc2 删除虚拟控制台,防止页面卡死 2 years ago
Lei OT 805804ee88 文旅局服务人数 2 years ago
Lei OT 151dcff1a8 feat: 文旅局服务人数 2 years ago
Lei OT ea7048e41b Merge branch 'main' into feature/person-num
# Conflicts:
#	src/App.jsx
2 years ago
Lei OT 9695335b30 数据透视: 目的地 2 years ago
Lei OT 5dafa48ab3 perf: 数据透视: +目的地国籍. 途径的计算会重复 2 years ago
Lei OT e59c1e11bd 2.4.5 2 years ago
Lei OT 95e799cdae perf: 客运订单: +页面类型; 不显示在华 2 years ago
Lei OT d141e0fa7d perf: 优化导航 2 years ago
Lei OT 609b6761b3 perf: 统计分布: 目的地/国籍等, 只要前30 2 years ago
Lei OT a48bbf35f3 perf: 移动端 2 years ago
Lei OT 6eea280a1d 2.4.4 2 years ago
Lei OT cfb4a5f5f7 perf: 移动端折叠导航 2 years ago
Lei OT ad91ce73a0 2.4.3 2 years ago
Lei OT f39ed273c3 perf: 销售业绩: 合并相同名字的账户 2 years ago
Lei OT c8e66c12d8 perf: 统计分布: 目的地/国籍等, 分开请求; 显示预定团数 2 years ago
Lei OT 95ec1fa822 fix: 年度对比: 折线中断 2 years ago
Lei OT 7af50f0e9e feat: 财务>文旅局服务人数 2 years ago

16
package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "haina-dashboard",
"version": "2.4.2",
"version": "2.11.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "haina-dashboard",
"version": "2.4.2",
"version": "2.11.6",
"dependencies": {
"@ant-design/charts": "^1.4.2",
"@ant-design/pro-components": "^2.6.16",
@ -19363,9 +19363,9 @@
}
},
"node_modules/stylis": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.1.1.tgz",
"integrity": "sha512-lVrM/bNdhVX2OgBFNa2YJ9Lxj7kPzylieHd3TNjuGE0Re9JB7joL5VUKOVH1kdNNJTgGPpT8hmwIAPLaSyEVFQ=="
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
},
"node_modules/supercluster": {
"version": "7.1.5",
@ -35568,9 +35568,9 @@
}
},
"stylis": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.1.1.tgz",
"integrity": "sha512-lVrM/bNdhVX2OgBFNa2YJ9Lxj7kPzylieHd3TNjuGE0Re9JB7joL5VUKOVH1kdNNJTgGPpT8hmwIAPLaSyEVFQ=="
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
},
"supercluster": {
"version": "7.1.5",

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

@ -25,11 +25,20 @@
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Hainatravel Dashboard</title>
<script crossorigin="anonymous" src="https://page-spy.mycht.cn/page-spy/index.min.js"></script>
<script crossorigin="anonymous" src="https://page-spy.mycht.cn/plugin/data-harbor/index.min.js"></script>
<script crossorigin="anonymous" src="https://page-spy.mycht.cn/plugin/rrweb/index.min.js"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<script src="https://unpkg.com/vconsole@latest/dist/vconsole.min.js"></script>
<div id="root"></div>
<script defer>
window.initPageSpy = function () {
PageSpy.registerPlugin(new DataHarborPlugin({ maximum: 2 * 1024 * 1024, }));
window.$pageSpy = new PageSpy({ api: 'page-spy.mycht.cn', project: 'Dashboard', title: window.__spytitle, autoRender: false, });
// console.log(window.$pageSpy.address.substring(0, 4)); // device ID
}
</script>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.

@ -1,6 +1,6 @@
import './App.css';
import React, { useContext } from 'react';
import {
import React, { useContext, useState } from 'react';
import Icon, {
HomeOutlined,
TeamOutlined,
DashboardOutlined,
@ -14,9 +14,10 @@ import {
FlagOutlined,
PieChartOutlined,
BarChartOutlined,
CoffeeOutlined,
CoffeeOutlined, DesktopOutlined,
WhatsAppOutlined
} from '@ant-design/icons';
import { Layout, Menu, Image, Badge, } from 'antd';
import { Layout, Menu, Image, Badge, Button } from 'antd';
import { BrowserRouter, Route, Routes, NavLink } from 'react-router-dom';
import Home from './views/Home';
import Dashboard from './views/Dashboard';
@ -36,24 +37,52 @@ import Credit_card_bill from './views/Credit_card_bill';
import Sale from './views/Sale';
import Sale_sub from './views/Sale_sub';
import Sale_KPI from './views/Sale_KPI';
import Logo from './logo.png';
// import Logo from './logo.png';
import { observer } from 'mobx-react';
import ExchangeRate from './charts/ExchangeRate';
import KPI from './views/KPI';
import Distribution from './views/Distribution';
import Detail from './views/Detail';
import ServicePersonNum from './views/ServicePersonNum';
import DataPivot from './views/DataPivot';
import Welcome from './views/Welcome';
import Meeting2024GH from './views/Meeting2024-GH';
import Meeting2025GH from './views/Meeting2025-GH';
import SalesCustomerCareRegular from './views/SalesCustomerCareRegular';
import { stores_Context, APP_VERSION } from './config';
import { WaterMark } from '@ant-design/pro-components';
import CooperationIcon from './components/icons/CooperationIcon';
import OPDashboard from './views/sales-crm/Dashboard';
import OPProcess from './views/sales-crm/Process';
import OPRisk from './views/sales-crm/Risk';
import Cruise from './views/Cruise';
import Hotel from './views/Hotel';
import HostCaseCount from './views/HostCaseCount';
const App = () => {
const { Content, Footer, Sider } = Layout;
const { auth_store } = useContext(stores_Context);
const { Content, Footer, Sider, } = Layout;
const { auth_store, date_picker_store } = useContext(stores_Context);
const [collapsed, setCollapsed] = useState(false);
const menu_items = [
{ key: 1, label: <NavLink to="/">欢迎</NavLink>, icon: <CoffeeOutlined /> },
{ key: 'annual', label: <NavLink to="/annual">综合看板</NavLink>, icon: <DashboardOutlined /> },
{ key: 'annual', label: <NavLink to="/annual">综合年度</NavLink>, icon: <DashboardOutlined /> },
{
key: 'mixed',
label: '报告',
icon: <DesktopOutlined />,
children: [
{
key: 'meeting-2025-GH',
label: <NavLink to="/orders/meeting-2025-GH">GH例会数据-2025</NavLink>, // GH-2024
},
{
key: 'meeting-2024-GH',
label: <NavLink to="/orders/meeting-2024-GH">GH区域数据</NavLink>, // GH-2024
},
]
},
{
key: 2,
label: '市场',
@ -72,7 +101,6 @@ const App = () => {
{
key: 'orders-pivot',
label: <NavLink to="/orders/pivot">数据透视</NavLink>,
// icon: <DashboardOutlined />,
},
],
},
@ -100,18 +128,11 @@ const App = () => {
key: 32,
label: <NavLink to="/customer_care_regular">老客户</NavLink>,
},
{ key: 'customer_care_regular_sales', label: <NavLink to="/customer_care_regular_sales">老客户-分析</NavLink> },
{
key: 33,
label: <NavLink to="/customer_care_inchina">在华客户</NavLink>,
},
{
key: 34,
label: <NavLink to="/wechat_session">微信会话存档</NavLink>,
},
{
key: 35,
label: <NavLink to="/whatsapp_session">WhatsApp会话存档</NavLink>,
},
],
},
{
@ -124,12 +145,13 @@ const App = () => {
label: <NavLink to="/credit_card_bill">信用卡账单</NavLink>,
},
{ key: 42, label: <NavLink to="/exchange_rate">汇率</NavLink> },
{ key: 'service_person_num', label: <NavLink to="/service_person_num">服务人数</NavLink> },
],
},
{
key: 6,
label: '客服',
icon: <WechatOutlined />,
icon: <CooperationIcon/>,
children: [
{
key: 61,
@ -139,6 +161,20 @@ const App = () => {
key: 62,
label: <NavLink to="/destination/group/count">目的地接团</NavLink>,
},
{ key: 'cruise', label: <NavLink to="/cruise">三峡游船</NavLink> },
{ key: 'hotel', label: <NavLink to="/hotel">酒店</NavLink> },
{ key: 'hostcase', label: <NavLink to="/hostcase/count">东道主项目</NavLink> },
],
},
{
key: 'crm',
label: '销售平台',
icon: <WhatsAppOutlined />,
children: [
// { key: 'xx', type: 'divider' },
{ key: 'op_dashboard', label: <NavLink to="/op_dashboard">结果</NavLink> },
{ key: 'op_process', label: <NavLink to="/op_process">过程</NavLink> },
// { key: 'op_risk', label: <NavLink to="/op_risk"></NavLink> },
],
},
{ key: 'kpi', label: <NavLink to="/kpi">目标配置</NavLink>, icon: <FlagOutlined /> },
@ -154,8 +190,11 @@ const App = () => {
];
const callDebug = () => {
const vConsole = new window.VConsole({ theme: 'dark' });
auth_store.get_auth();
// const vConsole = new window.VConsole({ theme: 'dark' });
// auth_store.get_auth();
// window.$pageSpy.render();
window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
window.$pageSpy.triggerPlugins('onOfflineLog', 'download');
};
return (
<BrowserRouter>
@ -170,17 +209,27 @@ const App = () => {
<Sider
collapsible={true}
breakpoint="lg"
collapsedWidth="0"
collapsed={collapsed}
style={{
overflow: 'auto',
// overflow: 'auto',
height: '100vh',
position: 'sticky',
left: 0,
top: 0,
bottom: 0,
zIndex: 999,
}}
zeroWidthTriggerStyle={collapsed ? { zIndex: 90, bottom: '64px', top: '3px', left: 'unset', right: '-34px' } : { bottom: '64px', top: '3px', left: 'unset', right: 0 }}
onBreakpoint={(broken) => {
date_picker_store.setSiderBroken(broken);
}}
onCollapse={(collapsed, type) => {
setCollapsed(collapsed);
}}
>
<Image src={Logo} preview={false} />
<Menu theme="dark" defaultSelectedKeys={['1']} defaultOpenKeys={['2', '5']} mode="inline" items={menu_items} />
{/* <Image src={Logo} preview={false} /> */}
<Menu theme="dark" defaultSelectedKeys={['1']} defaultOpenKeys={[]} mode="inline" items={menu_items} onClick={() => date_picker_store.siderBroken ? setCollapsed(!collapsed) : false} />
</Sider>
<Layout className="site-layout">
@ -202,10 +251,13 @@ const App = () => {
<Route path="/orders_sub/:ordertype/:ordertype_sub/:ordertype_title" element={<Orders_sub />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/:page/pivot" element={<DataPivot />} />
<Route path="/orders/meeting-2024-GH" element={<Meeting2024GH />} />
<Route path="/orders/meeting-2025-GH" element={<Meeting2025GH />} />
</Route>
<Route element={<ProtectedRoute auth={['admin', 'director_bu', 'customer_care']} />}>
<Route path="/customer_care_inchina" element={<Customer_care_inchina />} />
<Route path="/customer_care_regular" element={<Customer_care_regular />} />
<Route path="/customer_care_regular_sales" element={<SalesCustomerCareRegular />} />
<Route path="/customer_care_potential" element={<Customer_care_potential />} />
<Route path="/whatsapp_session" element={<WhatsApp_session />} />
<Route path="/wechat_session" element={<Wechat_session />} />
@ -213,10 +265,14 @@ const App = () => {
<Route path="/destination/group/count" element={<DestinationGroupCount />} />
<Route path="/agent/:agentId/group/list" element={<AgentGroupList />} />
<Route path="/destination/:destinationId/group/list" element={<DestinationGroupList />} />
</Route>
<Route path="/cruise" element={<Cruise />} />
<Route path="/hotel" element={<Hotel />} />
<Route path="/hostcase/count" element={<HostCaseCount />} />
</Route>
<Route element={<ProtectedRoute auth={['admin', 'director_bu', 'financial']} />}>
<Route path="/credit_card_bill" element={<Credit_card_bill />} />
<Route path="/exchange_rate" element={<ExchangeRate />} />
<Route path="/service_person_num" element={<ServicePersonNum />} />
</Route>
<Route element={<ProtectedRoute auth={['admin', 'director_bu', 'sale']} />}>
<Route path="/sale" element={<Sale />} />
@ -224,6 +280,10 @@ const App = () => {
<Route path="/sale_kpi" element={<Sale_KPI />} />
<Route path="/distribution" element={<Distribution />} />
<Route path="/:page/pivot" element={<DataPivot />} />
<Route path="/op_dashboard" element={<OPDashboard />} />
<Route path="/op_process" element={<OPProcess />} />
<Route path="/op_risk" element={<OPRisk />} />
<Route path="/op_risk/sales/:opisn" element={<OPRisk />} />
</Route>
</Routes>
</Content>
@ -238,7 +298,8 @@ const App = () => {
<span>
v<span>{APP_VERSION}</span>{' '}
</span>{' '}
©2022 <span onClick={callDebug}> Created by IT</span>
©2022 <span > Created by IT</span>
{window.$pageSpy && <Button type='link' onClick={callDebug}>上传/下载debug日志({window.$pageSpy.address.substring(0, 4)})</Button>}
</Footer>
</Layout>
</Layout>

@ -17,7 +17,7 @@ const Customer_care_inchina = () => {
return (
<div>
<Row gutter={16} className='sticky-top'>
<Row gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{

@ -16,9 +16,9 @@ const Customer_care_potential = () => {
return (
<div>
<Row gutter={16} className='sticky-top'>
<Row gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"}>
<Col className="gutter-row" span={24}>
<SearchForm className='sticky-top'
<SearchForm className={date_picker_store.siderBroken ? "" : "sticky-top"}
defaultValue={{
initialValue: {
...date_picker_store.formValues,
@ -153,9 +153,9 @@ const Customer_care_potential = () => {
key: 'SourceType',
},
{
title: '在华',
dataIndex: 'ZH',
key: 'ZH',
title: '页面类型',
dataIndex: 'COLI_LineClass',
key: 'COLI_LineClass',
},
]
} size="small"

@ -1,19 +1,22 @@
import React, { useContext, useEffect } from 'react';
import { Row, Col, Divider, Table } from 'antd';
import React, { useContext, useState } from 'react';
import { Row, Col, Divider, Table, Tooltip } from 'antd';
import { InfoCircleOutlined } from '@ant-design/icons';
import { utils, writeFileXLSX } from 'xlsx';
import { stores_Context } from '../config';
import { observer } from 'mobx-react';
import SearchForm from './../components/search/SearchForm';
import LineWithAvg from '../components/LineWithAvg';
import { flow } from 'mobx';
const Customer_care_regular = () => {
const { orders_store, date_picker_store, customer_store } = useContext(stores_Context);
const regular_data = customer_store.regular_data;
useEffect(() => {}, []);
// useEffect(() => {}, []);
return (
<div>
<Row gutter={16} className="sticky-top">
<Row gutter={16} className={date_picker_store.siderBroken ? '' : 'sticky-top'}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
@ -21,17 +24,30 @@ const Customer_care_regular = () => {
...date_picker_store.formValues,
...regular_data.searchValues,
},
shows: ['DateType', 'DepartmentList', 'WebCode', 'dates'],
shows: ['DateType', 'DepartmentList', 'WebCode', 'dates', 'IncludeTickets'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
dates: { hide_vs: true },
years: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
onSubmit={async (_err, obj, form, str) => {
customer_store.setSearchValues(obj, form, 'regular_data');
customer_store.regular_customer_order();
customer_store.regular_customer_order(true);
regular_data.data_compare=[];
if (obj.DateDiff1 && obj.DateDiff2){
regular_data.isCompareLine=true;
regular_data.showCompareSum=true;
await customer_store.regular_customer_order();
customer_store.regular_customer_order(false,true);
customer_store.regular_customer_order(true,false,true);
customer_store.regular_customer_order(true,true,true);
}
else{
regular_data.isCompareLine=false;
regular_data.showCompareSum=false;
customer_store.regular_customer_order();
customer_store.regular_customer_order(true);
}
}}
/>
</Col>
@ -51,9 +67,30 @@ const Customer_care_regular = () => {
key: 'ItemName',
},
{
title: '订单数',
title: () => (
<>
订单数{' '}
<Tooltip key='total_data_tips_title' title="总订单: 当同时勾选老客户和推荐时, 将重复计数">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'OrderNum',
key: 'OrderNum',
render: (text, record, index) => (
<>
<span>{text}</span>&nbsp;&nbsp;
{<Tooltip key='total_data_tips' title={regular_data.total_data_tips}>
{index === 0 && regular_data.total_data_tips!=='' && <InfoCircleOutlined className='ant-tag-gold' />}
</Tooltip>}
</>
),
},
{
title: '订单数占比',
dataIndex: 'OrderRate',
key: 'OrderRate',
render: (text) => typeof text === 'number'?<span>{parseFloat((text * 100).toFixed(2))}%</span>:text,
},
{
title: '成行数',
@ -64,13 +101,19 @@ const Customer_care_regular = () => {
title: '成行率',
dataIndex: 'SUCRate',
key: 'SUCRate',
render: (text, record) => <span>{Math.round(text * 100)}%</span>,
render: (text) => typeof text === 'number'?<span>{Math.round(text * 100)}%</span>:text,
},
{
title: '毛利',
dataIndex: 'ML',
key: 'ML',
},
{
title: '毛利占比',
dataIndex: 'OrderMLRate',
key: 'OrderMLRate',
render: (text) => typeof text === 'number'?<span>{parseFloat((text * 100).toFixed(2))}%</span>:text,
},
{
title: '人数(含成人+儿童)',
dataIndex: 'PersonNum',
@ -82,6 +125,13 @@ const Customer_care_regular = () => {
rowKey={(record) => record.ItemName}
/>
</Col>
<Col span={24}>
<LineWithAvg dataSource={regular_data.pivotData} loading={regular_data.detail_loading} xField={regular_data.pivotX} yField={regular_data.pivotY}
seriesField='_ylabel' showCompareSum={regular_data.showCompareSum} solidLineTime={regular_data.solidLineTime} solidLineCompareTime={regular_data.solidLineCompareTime}
solidLineDash={regular_data.solidLineDash} isCompareLine={regular_data.isCompareLine}/>
</Col>
<Col span={24}>
<Divider orientation="right" plain>
<a
@ -96,7 +146,7 @@ const Customer_care_regular = () => {
<Table
id="table_to_xlsx"
pagination={false}
loading={regular_data.loading}
loading={regular_data.detail_loading}
dataSource={regular_data.data_detail}
scroll={{ x: 1200 }}
columns={[
@ -111,7 +161,7 @@ const Customer_care_regular = () => {
key: 'COLI_ApplyDate',
},
{
title: '订单状态',
title: '订单状态',width: '4rem',
dataIndex: 'OrderState',
key: 'OrderState',
render: (text, record) => <span>{text == 1 ? '成行' : '未成行'}</span>,
@ -168,10 +218,25 @@ const Customer_care_regular = () => {
key: 'SourceType',
},
{
title: '在华',
dataIndex: 'ZH',
key: 'ZH',
},
title: '页面类型',
dataIndex: 'COLI_LineClass',
key: 'COLI_LineClass',
},
{ title: '上次 订单号', dataIndex: 'coli_id_Last', key: 'coli_id_Last', width: '4em',
render: (_, r) => ({
props: { style: { backgroundColor: '#5B8FF9'+'1A' } },
children: _,
}), },
{ title: '上次 走团日期', dataIndex: 'COLI_OrderStartDate_Last', key: 'COLI_OrderStartDate_Last',width: '4em',
render: (_, r) => ({
props: { style: { backgroundColor: '#5B8FF9'+'1A' } },
children: _,
}), },
{ title: '上次 小组', dataIndex: 'Department_Last', key: 'Department_Last',width: '4em',
render: (_, r) => ({
props: { style: { backgroundColor: '#5B8FF9'+'1A' } },
children: _,
}), },
]}
size="small"
rowKey={(record) => record.COLI_ID}

@ -70,7 +70,7 @@ class OrdersTempTable extends Component {
<div>
<h2>临时订单数量</h2>
<SiteSelect store={ordersTemp_data}/>
<SiteSelect store={ordersTemp_data} mode={'multiple'} />
<Space><DatePickerCharts hide_vs={true}/>
<Button type="primary" icon={<SearchOutlined/>} loading={ordersTemp_data.loading}
onClick={() => {

@ -35,6 +35,12 @@ export default observer((props) => {
// xField: 'value',
// yField: 'year',
// seriesField: 'type',
xAxis: {
label: {
autoHide: false,
autoRotate: true,
},
},
label: {
// label
position: 'middle',
@ -62,5 +68,6 @@ export default observer((props) => {
},
annotations: [...annotationsLine],
}, extProps);
// console.log(config);
return <Column {...config} data={dataSource} />;
});

@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import React, { useState, useEffect } from "react";
import { Tag, Button, message } from 'antd';
import { CaretUpOutlined, CaretDownOutlined, DownloadOutlined } from '@ant-design/icons';
import { utils, writeFile } from "xlsx";
@ -30,48 +30,70 @@ export const VSTag = (props) => {
/**
* 导出表格数据存为xlsx
* @property label 文件名字
* @property columns 表格列
* @property dataSource 表格数据
* @property btnTxt 按钮文字
*/
export const TableExportBtn = (props) => {
const output_name = `${props.label}`;
export const TableExportBtn = ({label, columns, dataSource, btnTxt, ...props}) => {
const output_name = `${label}`;
const [columnsMap, setColumnsMap] = useState([]);
const [summaryRow, setSummaryRow] = useState({});
useEffect(() => {
const flatCols = props.columns.flatMap((v, k) => (v.children ? v.children.map((vc) => ({ ...vc, title: `${v?.titleX || v.title}-${vc.title || ''}` })) : v)).filter(c => c.title);
// console.log('flatCols', flatCols);
const r1 = columns.reduce((r, v) => ({
...r,
...(v.children ? v.children.reduce((rc, vc, ci) => ({
...rc,
...(vc?.titleX ? {[`${v?.titleX || v.title},${vc.titleX}`]: vc.titleX } : {[(v?.titleX || v.title) + (vc.key || ci || '')]: `${vc?.titleX || vc?.title || ''}`}),
}), {}) : {})
}), {});
const flatCols = columns.flatMap((v, k) =>
v.children ? v.children.map((vc, ci) => ({ ...vc, title: `${v?.titleX || v.title}` + (vc?.titleX ? `,${vc.titleX}` : (vc.key || ci || '')) })) : {...v, title: `${v?.titleX || v.title}`}
);
// .filter((c) => c.dataIndex)
// !['string', 'number'].includes(typeof vc.title) ? `${v?.titleX || v.title}` : `${v?.titleX || v.title}-${vc.title || ''}`
;
setColumnsMap(flatCols);
// console.log('flatCols', flatCols);
setSummaryRow(r1);
// console.log('summaryRow', r1);
return () => {};
}, [props.columns]);
}, [columns]);
const onExport = () => {
if (isEmpty(props.dataSource)) {
if (isEmpty(dataSource)) {
message.warning('无结果.');
return false;
}
const data = props.dataSource.map((item) => {
const data = dataSource.map((item) => {
const itemMapped = columnsMap.reduce((sv, kset) => {
const export_val = typeof kset?.dataExport === 'function' ? kset.dataExport('', item) : null;
const render_val = typeof kset?.render === 'function' ? kset.render('', item) : null;
const data_val = kset?.dataIndex ? (Array.isArray(kset.dataIndex) ? getNestedValue(item, kset.dataIndex) : item[kset.dataIndex]) : undefined;
const v = { [kset.title]: data_val || render_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 };
return { ...sv, ...v };
}, {});
return itemMapped;
});
// console.log('data', data);
const ws = utils.json_to_sheet(data, { header: columnsMap.filter((r) => r.dataIndex).map((r) => r.title) });
const ws = utils.json_to_sheet([].concat(isEmpty(summaryRow) ? [] : [summaryRow], data), { header: columnsMap.filter((r) => r.dataIndex).map((r) => r.title) });
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, 'sheet');
writeFile(wb, `${output_name}.xlsx`);
};
return (
<Button
<Button {...props}
type="link"
icon={<DownloadOutlined />}
size="small"
disabled={false}
onClick={onExport}
>
导出excel
{btnTxt || '导出excel'}
</Button>
);
};

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

@ -13,7 +13,7 @@ export default observer((props) => {
r.push({...v, [config.yField]: v[kpiKey], [config.seriesField]: `${v[config.seriesField]} ${dataFieldAlias[kpiKey].label}`, extraLine: true,});
}
return r;
}, []).sort(sortBy(config.xField)) : dataSource;
}, []).sort(sortBy(config.xField)) : (dataSource.slice()).sort(sortBy(config.xField));
const dataColors = [
"#5D7092","#F6BD16","#6F5EF9","#6DC8EC","#945FB9","#FF9845","#1E9493",
"#FF99C3","#FF6B3B","#626681","#FFC100","#9FB40F","#76523B","#DAD5B5",

@ -5,22 +5,22 @@ import { dataFieldAlias } from '../libs/ht';
import { cloneDeep } from '../utils/commons';
export default observer((props) => {
const { dataSource, sourceField, valueField, ...extConfig } = props;
const { dataSource, sourceField, valueField, containerNode, ...extConfig } = props;
const [mdataSource, setMdataSource] = useState([]);
useEffect(() => {
const dataMapped = (cloneDeep(dataSource) || []).reduce((r, v) => ({...r,
[(v.groupsLabel || '_').replace('(待删除)', '')]: v
[(v[sourceField] || '_').replace('(待删除)', '')]: v
}), {});
if (dataMapped?.['中国']) {
dataMapped['中国'].groupsLabel = '中华人民共和国';
dataMapped['中国'][sourceField] = '中华人民共和国';
}
setMdataSource(Object.values(dataMapped));
return () => {};
}, [dataSource, valueField]);
const config = {
container: '#topC',
container: containerNode || '#topC',
map: {
// type: 'amap',
type: 'mapbox',
@ -38,7 +38,7 @@ export default observer((props) => {
// type: 'topojson',
// },
source: {
data: mdataSource.filter((ele) => ele[sourceField]),
data: mdataSource.filter((ele) => ele[sourceField] && ele[valueField] !== 0 ),
joinBy: {
geoField: 'name',
sourceField: sourceField || 'name',
@ -48,19 +48,46 @@ export default observer((props) => {
color: {
field: valueField || 'value',
value: [
'#001D70',
'#0047A5',
'#1A4397',
'#2555B7',
'#3165D1',
'#3D76DD',
'#467BE8',
'#6296FE',
'#7EA6F9',
'#98B7F7',
'#BDD0F8',
'#DDE6F7',
'#F2F5FC'].reverse(),
'#820C1B',
'#a31022',
'#ac2738',
'#b53f4e',
'#be5764',
'#c76f7a',
'#d18790',
'#da9fa6',
'#e3b7bc',
'#eccfd2',
'#f5e7e8',
'#fde7ea',
// '#8a1313',
// '#ad1818',
// '#b52f2f',
// '#bd4646',
// '#c55d5d',
// '#cd7474',
// '#d68b8b',
// '#dea2a2',
// '#e6b9b9',
// '#eed0d0',
// '#f6e7e7',
// '#fde7ea',
// '#001D70',
// '#0047A5',
// '#1A4397',
// '#2555B7',
// '#3165D1',
// '#3D76DD',
// '#467BE8',
// '#6296FE',
// '#7EA6F9',
// '#98B7F7',
// '#BDD0F8',
// '#DDE6F7',
// '#F2F5FC'
].reverse(),
scale: { type: 'quantile' },
},
viewLevel: {

@ -0,0 +1,265 @@
import { observer } from 'mobx-react';
import { message } from 'antd';
import { Mix, getCanvasPattern, } from '@ant-design/plots';
import { merge, isEmpty, cloneDeep } from '../utils/commons';
import { dataFieldAlias } from '../libs/ht';
const COLOR_SETS = [
"#FF6B3B",
"#9FB40F",
"#76523B",
"#DAD5B5",
"#E19348",
"#F383A2",
];
const COLOR_SETS2 = [
"#5B8FF9",
"#61DDAA",
"#65789B",
];
/**
* 订单数, 团数: 柱形图
* 成交率: 折线图
*/
export default observer((props) => {
const { dataSource, summaryData: areaData, ...config } = props;
const { xField, yFields, colFields, lineFields, seriesField, tooltip, ...extConfig } = config;
const diffData0 = dataSource.reduce((r, row) => {
r.push({ ...row, yField: row[colFields[0]], yGroup: dataFieldAlias[colFields[0]].alias });
r.push({ ...row, yField: row[colFields[1]], yGroup: dataFieldAlias[colFields[1]].alias });
return r;
}, []);
const diffData1 = dataSource.reduce((r, row) => {
r.push({ ...row, yField: row[lineFields[1]], yGroup: dataFieldAlias[lineFields[1]].alias });
return r;
}, []);
const calcAxis = isEmpty(diffData0) ? 300 : (Math.max(...diffData0.map(ele => ele.yField))) * 3;
// const calcAxisC = isEmpty(diffData0) ? 300 : (Math.max(...diffDataPercent.map(ele => ele.yField))) * 3;
const diffLine = [
// {
// type: 'text',
// position: ['start', 0],
// content: `, `,
// offsetX: -15,
// style: {
// fill: COLOR_SETS[0],
// textBaseline: 'bottom',
// },
// },
// {
// type: 'line',
// start: [-10, 0],
// end: ['max', 0],
// style: {
// stroke: COLOR_SETS[0],
// // lineDash: [2, 2],
// lineWidth: 0.5,
// },
// },
];
const pattern = (datum, color) => {
return getCanvasPattern({
type: String(datum.yGroup).includes(' ') ? 'line' : '',
cfg: {
backgroundColor: color,
},
});
};
const MixConfig = {
appendPadding: 15,
height: 400,
syncViewPadding: true,
tooltip: {
shared: true,
// customItems: (originalItems) => {
// // process originalItems,
// const items = originalItems.map((ele) => ({ ...ele, name: ele.data?.extraLine ? ele.name : `${ele.name} ${dataFieldAlias[yField]?.alias || yField}` }));
// return items;
// },
},
legend: {
position: 'top',
layout: 'horizontal',
custom: true,
items: [
...['团数', '订单数'].map((ele, ei) => ({
name: `${ele}`,
value: `${ele}`,
marker: {
symbol: 'square',
style: {
fill: COLOR_SETS2[ei],
r: 5,
},
},
})),
...['', '成行率'].map((ele, ei) => ({ // '',
name: `${ele}`,
value: `${ele}`,
marker: {
symbol: 'hyphen',
style: {
stroke: COLOR_SETS[ei],
r: 5,
lineWidth: 2
},
},
})),
],
},
// event: (chart, e) => {
// console.log('mix', chart, e);
// if (e.type === 'click') {
// props.itemClick(e);
// }
// },
onReady: (plot) => {
plot.on('plot:click', (...args) => {
// message.info(', ');
});
plot.on('element:click', (e) => {
const {
data: { data },
} = e;
// console.log('plot element', data);
props.itemClick(data);
});
// axis-label
plot.on('axis-label:click', (e, ...args) => {
const { text } = e.target.attrs;
// console.log(text);
props.itemClick({ [xField]: text });
});
},
plots: [
{
type: 'column',
options: {
data: diffData0,
isGroup: true,
xField,
yField: 'yField',
seriesField: 'yGroup',
// xAxis: false,
meta: merge({
...cloneDeep(dataFieldAlias),
}),
// color: '#b32b19',
// color: '#f58269',
legend: false, // {},
// smooth: true,
yAxis: {
type: 'linear',
tickCount: 4,
min: 0,
max: calcAxis,
title: { text: '团数', autoRotate: false, position: 'end' },
},
xAxis: {
label: {
autoHide: false,
autoRotate: true,
},
},
label: false,
color: COLOR_SETS2,
pattern,
},
},
{
type: 'line',
options: {
data: diffData1,
isGroup: true,
xField,
yField: 'yField',
seriesField: 'yGroup',
xAxis: false,
legend: false, // {},
meta: merge(
{
...cloneDeep(dataFieldAlias),
},
{ yField: dataFieldAlias[lineFields[1]] }
),
// color: '#1AAF8B',
color: COLOR_SETS[1],
// smooth: true,
point: {
size: 4,
shape: 'cicle',
},
yAxis: {
type: 'linear',
// tickCount: 4,
min: 0,
position: 'right',
line: null,
grid: null,
title: { text: dataFieldAlias[lineFields[1]].label, autoRotate: false, position: 'end' },
},
label: {
style: {
fontWeight: 700,
stroke: '#fff',
lineWidth: 1,
},
},
lineStyle: (datum) => {
if (String(datum.yGroup).includes(' ')) {
return {
lineDash: [4, 4],
opacity: 0.75,
};
}
return {
opacity: 1,
};
},
},
},
// {
// type: 'column',
// options: {
// data: diffData2,
// xField,
// yField: 'yField',
// seriesField: 'yGroup',
// columnWidthRatio: 0.28,
// meta: {
// // yField: {
// // formatter: (v) => `${v}%`,
// // },
// },
// isGroup: true,
// xAxis: false,
// yAxis: {
// line: null,
// grid: null,
// label: false,
// position: 'left',
// // min: -calcAxisC,
// // max: calcAxisC/4,
// min: -3000,
// max: 250,
// tickCount: 4,
// },
// legend: false, // {},
// color: COLOR_SETS,
// // annotations: diffLine,
// minColumnWidth: 5,
// maxColumnWidth: 5,
// // ()
// dodgePadding: 1,
// // ()
// // intervalPadding: 20,
// },
// },
],
};
return <Mix {...MixConfig} />;
});

@ -0,0 +1,9 @@
import Icon, {} from '@ant-design/icons';
const CooperationSvg = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11.8611 2.39057C12.8495 1.73163 14.1336 1.71797 15.1358 2.35573L19.291 4.99994H20.9998C21.5521 4.99994 21.9998 5.44766 21.9998 5.99994V14.9999C21.9998 15.5522 21.5521 15.9999 20.9998 15.9999H19.4801C19.5396 16.9472 19.0933 17.9102 18.1955 18.4489L13.1021 21.505C12.4591 21.8907 11.6609 21.8817 11.0314 21.4974C10.3311 22.1167 9.2531 22.1849 8.47104 21.5704L3.33028 17.5312C2.56387 16.9291 2.37006 15.9003 2.76579 15.0847C2.28248 14.7057 2 14.1254 2 13.5109V6C2 5.44772 2.44772 5 3 5H7.94693L11.8611 2.39057ZM4.17264 13.6452L4.86467 13.0397C6.09488 11.9632 7.96042 12.0698 9.06001 13.2794L11.7622 16.2518C12.6317 17.2083 12.7903 18.6135 12.1579 19.739L17.1665 16.7339C17.4479 16.5651 17.5497 16.2276 17.4448 15.9433L13.0177 9.74551C12.769 9.39736 12.3264 9.24598 11.9166 9.36892L9.43135 10.1145C8.37425 10.4316 7.22838 10.1427 6.44799 9.36235L6.15522 9.06958C5.58721 8.50157 5.44032 7.69318 5.67935 7H4V13.5109L4.17264 13.6452ZM14.0621 4.04306C13.728 3.83047 13.3 3.83502 12.9705 4.05467L7.56943 7.65537L7.8622 7.94814C8.12233 8.20827 8.50429 8.30456 8.85666 8.19885L11.3419 7.45327C12.5713 7.08445 13.8992 7.53859 14.6452 8.58303L18.5144 13.9999H19.9998V6.99994H19.291C18.9106 6.99994 18.5381 6.89148 18.2172 6.68727L14.0621 4.04306ZM6.18168 14.5448L4.56593 15.9586L9.70669 19.9978L10.4106 18.7659C10.6256 18.3897 10.5738 17.9178 10.2823 17.5971L7.58013 14.6247C7.2136 14.2215 6.59175 14.186 6.18168 14.5448Z"></path></svg>
);
const CooperationIcon = (props) => <Icon component={CooperationSvg} {...props} />;
export default CooperationIcon;

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

@ -128,6 +128,8 @@ class SearchInput extends React.Component {
notFoundContent={null}
allowClear={true}
onClear={this.handleClear}
maxTagCount={1}
maxTagPlaceholder={(omittedValues) => `+${omittedValues.length}`}
>
{options}
</Select>

@ -3,7 +3,7 @@ import { toJS } from 'mobx';
import { observer } from 'mobx-react';
import { DATE_FORMAT, SMALL_DATETIME_FORMAT, stores_Context } from './../../config';
import { SearchOutlined } from '@ant-design/icons';
import { Form, Row, Col, Select, Button, Space, DatePicker } from 'antd';
import { Form, Row, Col, Select, Button, Space, DatePicker, Input, InputNumber } from 'antd';
import moment from 'moment';
// import locale from 'antd/es/date-picker/locale/zh_CN';
import BusinessSelect from './BusinessSelect';
@ -15,8 +15,10 @@ import DatePickerCharts from './DatePickerCharts';
import YearPickerCharts from './YearPickerCharts';
import SearchInput from './Input';
import { objectMapper, at, empty, isEmpty } from './../../utils/commons';
import { departureDateTypes } from './../../libs/ht';
import './search.css';
import HotelStarSelect from './HotelStarSelect';
const EditableContext = createContext();
const Option = Select.Option;
@ -52,6 +54,11 @@ export default observer((props) => {
transform: (value) => value?.key || '',
default: '',
},
'departureDateType': {
key: 'DateType',
transform: (value) => value?.key || '',
default: '',
},
'HTBusinessUnits': {
key: 'HTBusinessUnits',
transform: (value) => {
@ -87,7 +94,13 @@ export default observer((props) => {
},
'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 : '') : '',
default: '',
},
'date': {
key: 'date',
transform: (value) => (value ? moment(value).format(SMALL_DATETIME_FORMAT) : undefined),
default: '',
},
'applyDate': [
@ -168,9 +181,34 @@ export default observer((props) => {
transform: (value) => value?.value || value?.key || '',
default: '',
},
'cruiseDirection': {
key: 'cruiseDirection',
transform: (value) => value?.value || '',
default: '',
},
'cruiseBookType': {
key: 'cruiseBookType',
transform: (value) => value?.value || '',
default: '',
},
'hotelBookType': {
key: 'hotelBookType',
transform: (value) => value?.value || '',
default: '',
},
'hotelRecommandRate': {
key: 'hotelRecommandRate',
transform: (value) => value?.value || '',
default: '',
},
'hotelStar': {
key: 'hotelStar',
transform: (value) => value?.value || '',
default: '',
},
};
let dest = {};
const { applyDate, applyDate2, year, yearDiff, dates, months, ...omittedValue } = values;
const { departureDateType, applyDate, applyDate2, year, yearDiff, dates, months, date, ...omittedValue } = values;
dest = { ...omittedValue, ...objectMapper(values, destinationObject) };
for (const key in dest) {
if (Object.prototype.hasOwnProperty.call(dest, key)) {
@ -207,11 +245,11 @@ export default observer((props) => {
};
const onValuesChange = (...args) => {
const [changedValues, allValues] = args;
// console.log('form onValuesChange', Object.keys(changedValues), args);
const dest = formValuesMapper(allValues);
searchFormStore.setFormValues(allValues);
searchFormStore.setFormValuesToSub(dest);
// console.log('form onValuesChange', Object.keys(changedValues), args);
};
return (
@ -262,23 +300,25 @@ function getFields(props) {
};
let baseChildren = [];
baseChildren = [
item(
'keyword', // {...fieldComProps.keyword}
99,
<Form.Item name="keyword" {...fieldProps.keyword}>
<Input allowClear {...fieldProps.keyword} />
</Form.Item>,
fieldProps?.keyword?.col || 6
),
item(
'agency',
99,
<Form.Item name={'agency'} >
<SearchInput
autoGet
url="/service-web/QueryData/GetVEIName"
map={{ 'CAV_VEI_SN': 'key', 'VEI2_CompanyBN': 'label' }}
resultkey={'result1'}
placeholder="所有地接社"
/>
<Form.Item name={'agency'}>
<SearchInput autoGet url="/service-web/QueryData/GetVEIName" map={{ 'CAV_VEI_SN': 'key', 'VEI2_CompanyBN': 'label' }} resultkey={'result1'} placeholder="所有地接社" {...fieldProps.agency} />
</Form.Item>
),
item(
'billtype',
99,
<Form.Item name={'billtype'} >
<Form.Item name={'billtype'}>
<SearchInput
autoGet
url="/service-web/QueryData/GetCreditCardBillType"
@ -342,11 +382,13 @@ function getFields(props) {
item(
'countryArea',
99,
<Form.Item name={`countryArea`} initialValue={at(props, 'initialValue.countryArea')[0] || (fieldProps?.countryArea?.show_all ? { key: 'all', label: '所有国家' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="所有国家" labelInValue>
{fieldProps?.countryArea?.show_all && <Option key="all" value="">
所有国家
</Option>}
<Form.Item name={`countryArea`} initialValue={at(props, 'initialValue.countryArea')[0] || (fieldProps?.countryArea?.show_all ? { key: 'all', label: '国内外' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="国内外" labelInValue allowClear={fieldProps?.countryArea?.show_all || false}>
{fieldProps?.countryArea?.show_all && (
<Option key="all" value="" disabled>
国内外
</Option>
)}
<Option key="china" value="china">
国内
</Option>
@ -360,16 +402,18 @@ function getFields(props) {
item(
'orderStatus',
99,
<Form.Item name={`orderStatus`} initialValue={at(props, 'initialValue.orderStatus')[0] || (fieldProps?.orderStatus?.show_all ? { key: '-1', label: '所有' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="所有状态" labelInValue>
{fieldProps?.orderStatus?.show_all && <Select.Option key="所有" value="-1">
所有
</Select.Option> }
<Form.Item name={`orderStatus`} initialValue={at(props, 'initialValue.orderStatus')[0] || (fieldProps?.orderStatus?.show_all ? { key: '-1', label: '成行状态' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="成行状态" labelInValue allowClear>
{fieldProps?.orderStatus?.show_all && (
<Select.Option key="-1" value="-1" disabled>
成行状态
</Select.Option>
)}
<Select.Option key="已成行" value="1">
已成行
已成行
</Select.Option>
<Select.Option key="未成行" value="0">
未成行
未成行
</Select.Option>
</Select>
</Form.Item>,
@ -384,6 +428,20 @@ function getFields(props) {
</Form.Item>,
fieldProps?.DateType?.col || 3
),
item(
'departureDateType',
99,
<Form.Item name={`departureDateType`} initialValue={at(props, 'initialValue.departureDateType')[0] || { key: 'departureDate', label: '抵达日期' }}>
<Select labelInValue={true} style={{ width: '100%' }} placeholder="选择日期类型">
{departureDateTypes.map((ele) => (
<Select.Option key={ele.key} value={ele.key} disabled={fieldProps?.departureDateType?.disabledKeys.includes(ele.key)}>
{ele.label}
</Select.Option>
))}
</Select>
</Form.Item>,
fieldProps?.departureDateType?.col || 3
),
item(
'years',
99,
@ -409,17 +467,38 @@ function getFields(props) {
</Form.Item>,
fieldProps?.dates?.col || midCol
),
item(
'date',
99,
<Form.Item name={`date`} initialValue={at(props, 'initialValue.date')[0]}>
<DatePicker picker="date" placeholder="日期" />
</Form.Item>,
fieldProps?.date?.col || midCol
),
item(
'operator',
99,
<Form.Item name={'operator'} dependencies={['DepartmentList']}>
<SearchInput
{...fieldProps.operator}
autoGet
url="/service-Analyse2/GetOperatorInfo"
map={{ 'op_id': 'key', 'cn_name': 'label' }}
resultkey={'result'}
placeholder="输入搜索顾问: 中/英名字"
dependenciesFun={() => ({ dept_id: (form.getFieldValue('DepartmentList')?.value || '').replace('ALL', ''), ...(fieldProps?.operator?.param || {}) })}
dependenciesFun={() => {
let dependenciesValue = '';
if (Array.isArray(form.getFieldValue('DepartmentList'))) {
dependenciesValue = form
.getFieldValue('DepartmentList')
.map((e) => e.value)
.join(',')
.replace('ALL', '');
} else {
dependenciesValue = (form.getFieldValue('DepartmentList')?.value || '').replace('ALL', '');
}
return { dept_id: dependenciesValue, ...(fieldProps?.operator?.param || {}) };
}}
/>
</Form.Item>
),
@ -444,6 +523,128 @@ function getFields(props) {
<SearchInput autoGet url="/service-Analyse2/GetGlobalDestinationInfo" map={{ 'c_id': 'key', 'cn_name': 'label' }} resultkey={'result'} placeholder="输入搜索城市: 中/英名字" />
</Form.Item>
),
item(
'cruiseDirection',
99,
<Form.Item name={`cruiseDirection`} initialValue={at(props, 'initialValue.cruiseDirection')[0] || undefined}>
<Select style={{ width: '100%' }} placeholder="上下水" labelInValue allowClear>
{fieldProps?.cruiseDirection?.show_all && (
<Option key="all" value="" disabled>
上下水
</Option>
)}
<Option key="1" value="1">
上水
</Option>
<Option key="2" value="2">
下水
</Option>
{/* <Option key="long" value="long">
长线
</Option> */}
</Select>
</Form.Item>,
3
),
item(
'cruiseBookType',
99,
<Form.Item name={`cruiseBookType`} initialValue={at(props, 'initialValue.cruiseBookType')[0] || (fieldProps?.cruiseBookType?.show_all ? { key: '-1', label: '预定类型' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="预定类型" labelInValue allowClear>
{fieldProps?.cruiseBookType?.show_all && (
<Option key="-1" value="-1" disabled>
预定类型
</Option>
)}
<Option key="1" value="1">
单订三峡
</Option>
<Option key="0" value="0">
含行程
</Option>
</Select>
</Form.Item>,
3
),
item(
'roomsRange',
99,
<Form.Item noStyle>
<Input.Group compact>
<Form.Item name={'RoomNumStart'} noStyle>
<InputNumber style={{ width: 'calc(50% - 15px)', textAlign: 'center' }} placeholder="房间数" />
</Form.Item>
<Input style={{ width: 30, borderLeft: 0, borderRight: 0, pointerEvents: 'none', }} placeholder="~" disabled />
<Form.Item name={'RoomNumEnd'} noStyle>
<InputNumber style={{ width: 'calc(50% - 15px)', textAlign: 'center', borderLeft: 0 }} placeholder="房间数" />
</Form.Item>
</Input.Group>
</Form.Item>,
fieldProps?.roomsRange?.col || 4
),
item(
'personRange',
99,
<Form.Item noStyle>
<Input.Group compact>
<Form.Item name={'PersonNumStart'} noStyle>
<InputNumber style={{ width: 'calc(50% - 15px)', textAlign: 'center' }} placeholder="人数" />
</Form.Item>
<Input style={{ width: 30, borderLeft: 0, borderRight: 0, pointerEvents: 'none', }} placeholder="~" disabled />
<Form.Item name={'PersonNumEnd'} noStyle>
<InputNumber style={{ width: 'calc(50% - 15px)', textAlign: 'center', borderLeft: 0 }} placeholder="人数" />
</Form.Item>
</Input.Group>
</Form.Item>,
fieldProps?.personRange?.col || 4
),
item(
'hotelBookType',
99,
<Form.Item name={`hotelBookType`} initialValue={at(props, 'initialValue.hotelBookType')[0] || (fieldProps?.hotelBookType?.show_all ? { key: 'all', label: '预定类型' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="预定类型" labelInValue allowClear={fieldProps?.hotelBookType?.show_all || false}>
{fieldProps?.hotelBookType?.show_all && (
<Option key="all" value="" disabled>
预定类型
</Option>
)}
<Option key="1" value="1">
代订
</Option>
<Option key="0" value="0">
自订
</Option>
</Select>
</Form.Item>,
3
),
item(
'hotelRecommandRate',
99,
<Form.Item name={`hotelRecommandRate`} initialValue={at(props, 'initialValue.hotelRecommandRate')[0] || (fieldProps?.hotelRecommandRate?.show_all ? { key: 'all', label: '推荐等级' } : undefined)}>
<Select style={{ width: '100%' }} placeholder="推荐等级" labelInValue allowClear={fieldProps?.hotelRecommandRate?.show_all || false}>
{fieldProps?.hotelRecommandRate?.show_all && (
<Option key="all" value="" disabled>
推荐等级
</Option>
)}
<Option key="1" value="1">
主推
</Option>
<Option key="0" value="0">
非主推
</Option>
</Select>
</Form.Item>,
3
),
item(
'hotelStar',
99,
<Form.Item name={`hotelStar`} initialValue={at(props, 'initialValue.hotelStar')[0] || undefined}>
<HotelStarSelect {...fieldProps.hotelStar} labelInValue={true} />
</Form.Item>
),
];
baseChildren = baseChildren
.map((x) => {

@ -34,6 +34,8 @@ class SiteSelect extends Component {
maxTagCount={1}
maxTagPlaceholder={(omittedValues) => ` +${omittedValues.length}...`}
allowClear={_mode != null}
dropdownStyle={{height: '400px'}}
listHeight={400}
{...extProps}
>
{_show_all===true ? <Select.Option key="ALL" value="ALL">所有来源</Select.Option> : ''}

@ -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.100:890";
export const HT_HOST = process.env.NODE_ENV === "production" ? "https://p9axztuwd7x8a7.mycht.cn" : "http://202.103.68.144:890";

@ -1,4 +1,5 @@
import { fixTo4Decimals, fixTo1Decimals, fixToInt, groupBy, sortBy, cloneDeep, pick, unique, flush, fixTo2Decimals } from '../utils/commons';
import moment from 'moment';
import { fixTo4Decimals, fixTo1Decimals, fixToInt, groupBy, sortBy, cloneDeep, pick, unique, flush, fixTo2Decimals, isEmpty } from '../utils/commons';
/**
* 事业部
@ -66,24 +67,34 @@ export const overviewGroup = groups.slice(0, 3); // todo: 花梨鹰 APP Trippest
export const sites = [
{ value: '2', key: '2', label: 'CHT', code: 'CHT' },
{ value: '8', key: '8', label: 'AH', code: 'AH' },
{ value: '186', key: '186', label: 'JH', code: 'JH' },
{ value: '163', key: '163', label: 'GH', code: 'GH' },
{ value: '184', key: '184', label: 'GH站外渠道 (中国)', code: 'ZWQD' },
{ value: '185', key: '185', label: 'GH站外渠道 (海外)', code: 'GH_ZWQD_HW' },
{ value: '28', key: '28', label: '客运中国', code: 'GHKYZG' },
{ value: '7', key: '7', label: '客运海外', code: 'GHKYHW' },
{ value: '172', key: '172', label: 'GHToB 海外', code: 'GHTOBHW' },
{ value: '176', key: '176', label: 'GHToB 中国', code: 'GHTOBZG' },
{ value: '11,12,20,21,10,18', key: '11,12,20,21,10,18', label: '国际(入境)', code: 'JP,VAC,IT,GM,RU,VC' },
{ value: '122,200,211,100,188', key: '122,200,211,100,188', label: '国际(海外)', code: 'VACHW,ITHW,GMHW,RUHW,VCHW' },
{ value: '178,179,180,181,182,183', key: '178,179,180,181,182,183', label: '国际GH', code: 'gh_gm,gh_jp,gh_vc,gh_vac,gh_ru,gh_it'},
{ value: '11', key: '11', label: '日语', code: 'JP' },
{ value: '179', key: '179', label: 'GH-日语', code: 'gh_jp'}, // www.globalhighlights.jp
{ value: '12', key: '12', label: '西语', code: 'VAC' },
{ value: '122', key: '122', label: '西语海外', code: 'VACHW' },
{ value: '181', key: '181', label: 'GH-西语', code: 'gh_vac'}, // www.globalhighlights.es
{ value: '20', key: '20', label: '意大利', code: 'IT' },
{ value: '200', key: '200', label: '意大利海外', code: 'ITHW' },
{ value: '183', key: '183', label: 'GH-意语', code: 'gh_it'}, // www.globalhighlights.it
{ value: '21', key: '21', label: '德语', code: 'GM' },
{ value: '211', key: '211', label: '德语海外', code: 'GMHW' },
{ value: '178', key: '178', label: 'GH-德语', code: 'gh_gm'}, // wwww.globalhighlights.de
{ value: '10', key: '10', label: '俄语', code: 'RU' },
{ value: '100', key: '100', label: '俄语海外', code: 'RUHW' },
{ value: '182', key: '182', label: 'GH-俄语', code: 'gh_ru'}, // www.globalhighlights.ru
{ value: '18', key: '18', label: '法语', code: 'VC' },
{ value: '188', key: '188', label: '法语海外', code: 'VCHW' },
{ value: '180', key: '180', label: 'GH-法语', code: 'gh_vc'}, // www.globalhighlights.fr
{ value: '16', key: '16', label: 'CT', code: 'CT' },
{ value: '30', key: '30', label: 'TP', code: 'trippest' },
{ value: '31', key: '31', label: '花梨鹰', code: 'HLY' },
@ -94,6 +105,10 @@ export const dateTypes = [
{ key: 'confirmDate', value: 'confirmDate', label: '确认日期' },
{ key: 'startDate', value: 'startDate', label: '走团日期' },
];
export const departureDateTypes = [
...dateTypes,
{ key: 'departureDate', value: 'departureDate', label: '抵达日期' },
];
/**
* 结果字段
@ -170,16 +185,166 @@ export const KPISubjects = [
// { key: 'sum_person_num', value: 'sum_person_num', label: '人数' },
];
export const HotelStars = [
{ key: '1', value: '1', label: '五星' },
{ key: '2', value: '2', label: '四星' },
{ key: '3', value: '3', label: '三星' },
{ key: '4', value: '4', label: '二星' },
{ key: '8', value: '8', label: '准五星 ' },
{ key: '9', value: '9', label: '准四星' },
{ key: '10', value: '10', label: '客栈' },
{ key: '11', value: '11', label: '公寓' },
{ key: '12', value: '12', label: '四合院酒店' },
{ key: '13', value: '13', label: '豪华五星' },
];
export const CruiseAgency = [
{ key: '14193', value: '14193', label: '长江海外游轮旅游有限公司' },
{ key: '1067', value: '1067', label: '湖北东方皇家游船公司(待删除)' },
{ key: '70', value: '70', label: '湖北皇家长江旅游船有限公司' },
{ key: '159', value: '159', label: '文嘉船务' },
{ key: '77', value: '77', label: '武汉扬子江' },
{ key: '1337', value: '1337', label: '重庆薇灿' },
{ key: '4378', value: '4378', label: '重庆新世纪国旅' },
{ key: '-1', value: '-1', label: '自订' },
];
/**
* 计算指标值的分段区间
* @param {number} value
* @returns
*/
const calcPPPriceRange = (value) => {
if (value < 0) {
return '--';
}
const step = 30; // step = 30 USD
const start = Math.floor(value / step) * step;
const end = start + step;
if (value >= 301) {
return `≥301`;
}
return `${start === 0 ? start : (start+1)}-${end}`;
};
function calculateRangeScale(data, numScales = 36) {
if (!data || data.length === 0 || numScales <= 0) {
return [];
}
const sortedData = [...data].sort((a, b) => a - b);
const min = sortedData[0];
const max = sortedData[sortedData.length - 1];
if (min === max) {
return [roundToNice(min), roundToNice(min)];
}
const scales = [roundToNice(min)];
const scaleSize = sortedData.length / numScales;
for (let i = 1; i < numScales; i++) {
const index = Math.floor(i * scaleSize);
scales.push(roundToNice(sortedData[Math.min(index, sortedData.length - 1)]));
}
scales.push(roundToNice(max));
return [...new Set(scales)];
}
function roundToNice(value) {
if (value === 0) {
return 0;
}
const magnitude = Math.pow(10, Math.floor(Math.log10(Math.abs(value))));
const normalized = value / magnitude;
let rounded;
if (normalized < 1.5) {
rounded = Math.floor(normalized);
} else if (normalized < 3) {
rounded = Math.floor(normalized * 2) / 2; // round to 0.5
} else if (normalized < 5) {
rounded = Math.floor(normalized/2) * 2; // round to 2
} else if (normalized < 7.5) {
rounded = Math.floor(normalized / 5) * 5;
} else {
// rounded = Math.floor(normalized / 5) * 5;
rounded = Math.floor(normalized / 10) * 10;
}
return rounded * magnitude;
};
const findRange = (value, scale) => {
if (value < scale[0]) {
return `0-${scale[0]}`; // `Value ${value} is below the scale range.`;
}
for (let i = 1; i < scale.length; i++) {
if (value >= scale[i - 1] && value < scale[i]) {
return `${scale[i - 1]}-${scale[i]}`; // `Value ${value} is in the range [${scale[i - 1]}, ${scale[i]})`;
}
}
if (value >= scale[scale.length - 1]) {
return `${scale[scale.length - 1]}`; // `Value ${value} is in the range [${scale[scale.length - 1]}, Infinity)`;
}
};
const SumML_range = [1, 1.5, 2, 3, 4].map(v => v * 10000);
/**
* 数据透视计算
* @param {object[]} data
* @param {any[]} groupbyKeys
* @returns
*/
export const pivotBy = (data, [rows, columns, date]) => {
export const pivotBy = (_data, [rows, columns, date]) => {
console.time('pivot3----');
console.log('pivotBy', [rows, columns, date]);
const groupbyKeys = flush([].concat(rows, columns, [date]));
// if (groupbyKeys.includes('PPPriceRange')) {
// }
// 补充计算的字段
const RTXF_WB_values = cloneDeep(_data).map(ele => ele.RTXF_WB); // 人天消费
// const max_RTXF_WB = Math.max(...RTXF_WB_values);
const RTXF_WB_range = calculateRangeScale(RTXF_WB_values);
let data = cloneDeep(_data).map(ele => {
ele.startYearMonth = ele.startDate ? moment(ele.startDate).format('YYYY-MM') : '';
ele.startMonth = ele.startDate ? moment(ele.startDate).format('MM') : '';
ele.applyYearMonth = ele.applyDate ? moment(ele.applyDate).format('YYYY-MM') : '';
ele.applyMonth = ele.applyDate ? moment(ele.applyDate).format('MM') : '';
ele.PPPrice = (Number(ele.orderState) === 1 && ele.tourdays && ele.personNum) ? fixToInt(ele.quotePrice / ele.tourdays / ele.personNum) : -1; // 报价: 人均天
ele.PPPriceRange = calcPPPriceRange(ele.PPPrice);
ele.RTXF_WB_range = findRange(ele.RTXF_WB, RTXF_WB_range);
ele.IsOld_txt = ele.IsOld === '1' ? '老客户' : '否';
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.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-';
// ele.SumML_ctxt3 = ele.ML > 30000 ? '3W+' : '3W-';
// ele.SumML_ctxt4 = ele.ML > 40000 ? '4W+' : '4W-';
ele.SumML_ctxt = findRange(ele.ML, SumML_range);
return ele;
});
// 数组的字段值, 拆分处理
if (groupbyKeys.includes('destinationCountry_AsJOSN')) {
data = data.reduce((r, v, i) => {
const vjson = isEmpty(v.destinationCountry_AsJOSN) ? [] : v.destinationCountry_AsJOSN;
const xv = (vjson).reduce((rv, cv, vi) => {
rv.push({...v, destinationCountry_AsJOSN: cv, key: vi === 0 ? v.key : `${v.key}@${cv}`});
return rv;
}, []);
r = r.concat(xv);
return r;
}, []);
}
if (groupbyKeys.includes('destinations_AsJOSN')) {
data = data.reduce((r, v, i) => {
const vjson = isEmpty(v.destinations_AsJOSN) ? [] : v.destinations_AsJOSN;
const xv = (vjson).reduce((rv, cv, vi) => {
rv.push({...v, destinations_AsJOSN: cv, key: vi === 0 ? v.key : `${v.key}@${cv}`});
return rv;
}, []);
r = r.concat(xv);
return r;
}, []);
}
const getKeys = (keys) => keys.map((keyField) => [...new Set(data.map((f) => f[keyField]))]);
const [rowsKeys, columnsKeys, dateKeys] = [getKeys(rows), getKeys(columns), [getKeys([date])[0].filter(s => s)]];
// console.log('rowsKeys', rowsKeys, 'columnsKeys', columnsKeys, 'dateKeys', dateKeys);
@ -207,7 +372,12 @@ export const pivotBy = (data, [rows, columns, date]) => {
_label: colKey || '(空)',
key: _rowKey,
_a:dataObj[colKey].map((v) => `${v.PPPrice}: ${v.PPPriceRange}`).join(', '),
_b:dataObj[colKey].map((v) => `${v.orderState}: ${v.quotePrice}/ ${v.tourdays}/ ${v.personNum}`).join(', '),
SumOrder: _len,
ResumeOrder: 0,
ResumeConfirmOrder: 0,
SumPersonNum: 0,
ConfirmPersonNum: 0,
@ -221,29 +391,43 @@ export const pivotBy = (data, [rows, columns, date]) => {
confirmDays: 0,
SingleML: 0,
OrderValue: 0,
PPPrice: 0,
AvgPPPrice: 0,
confirmTourdays: 0,
PPPriceRange: '',
unitPPPriceRange: '',
};
const calculatedData = dataObj[colKey].reduce((r, v) => {
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 += v.quotePrice;
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;
}, initialData);
// Calculations
calculatedData.tourdays = Math.ceil(calculatedData.tourdays / _len);
calculatedData.confirmTourdays = calculatedData.ConfirmOrder > 0 ? Math.ceil(calculatedData.confirmTourdays / calculatedData.ConfirmOrder) : '0';
calculatedData.applyDays = Math.ceil(calculatedData.applyDays / _len);
calculatedData.confirmDays = Math.ceil(calculatedData.confirmDays / _len);
const _rowCalc = {
ConfirmRates: calculatedData.ConfirmOrder ? fixTo4Decimals(calculatedData.ConfirmOrder / calculatedData.SumOrder) : 0,
ConfirmRates: calculatedData.ConfirmOrder ? fixTo4Decimals(calculatedData.ConfirmOrder / calculatedData.SumOrder*100) : 0,
OrderValue: calculatedData.SumOrder ? fixToInt(calculatedData.SumML / calculatedData.SumOrder) : 0,
SingleML: calculatedData.ConfirmOrder ? fixToInt(calculatedData.SumML / calculatedData.ConfirmOrder) : 0,
AvgPPPrice: calculatedData.ConfirmOrder ? fixToInt(calculatedData.PPPrice / calculatedData.ConfirmOrder) : -1,
unitPPPrice:
calculatedData.confirmTourdays && calculatedData.ConfirmPersonNum ? fixToInt(calculatedData.quotePrice / calculatedData.confirmTourdays / calculatedData.ConfirmPersonNum) : -1,
};
// Formatter
calculatedData.transactions = fixTo2Decimals(calculatedData.transactions);
@ -251,12 +435,15 @@ export const pivotBy = (data, [rows, columns, date]) => {
calculatedData.SumML_txt = dataFieldAlias.SumML.formatter(calculatedData.SumML);
calculatedData.quotePrice = fixTo2Decimals(calculatedData.quotePrice);
calculatedData.ConfirmRates_txt = dataFieldAlias.ConfirmRates.formatter(_rowCalc.ConfirmRates);
calculatedData.SingleML = fixTo2Decimals(calculatedData.SingleML);
// calculatedData.SingleML = fixTo2Decimals(calculatedData.SingleML);
calculatedData.PPPrice = fixTo2Decimals(calculatedData.PPPrice);
calculatedData.PPPriceRange = calcPPPriceRange(_rowCalc.AvgPPPrice);
calculatedData.unitPPPriceRange = calcPPPriceRange(_rowCalc.unitPPPrice);
DataGroupByKeys[colKey] = { ...calculatedData, ..._rowCalc };
});
return { groupByKeys: DataGroupByKeys, key: outerKeys.join('_') };
return { groupByKeys: DataGroupByKeys, key: outerKeys.join('_'), keys: outerKeys.join('_').split('_') };
};
const groupData = groupBy(data, (row) => groupbyKeys.map((kk) => `${row[kk]}`).join('=@='));
@ -266,24 +453,46 @@ export const pivotBy = (data, [rows, columns, date]) => {
const transposeData = (keys, dataProp, [dataKey, colKeys]=[]) =>
Object.keys(dataProp)
.map((rowKey) => {
const rowLabel = keys.length === 0 ? '总' : keys.map(ekey => dataProp[rowKey][0][ekey]).join('»');
const _colKey = dataKey || 'dataKey';
const _colData = groupBy(dataProp[rowKey], (crow) => (colKeys || keys).map((kk) => `${crow[kk]}`).join('=@='));
const _columnsObj = calcTradeFields(_colData);
return { ...pick(dataProp[rowKey][0], keys), [_colKey]: _columnsObj.groupByKeys, key: _columnsObj.key };
return { ...pick(dataProp[rowKey][0], keys), [_colKey]: _columnsObj.groupByKeys, key: _columnsObj.key, keys: _columnsObj.keys, rowLabel };
})
.map((everyR) => {
const _colKey = dataKey || 'dataKey';
const allColumns = Object.values(everyR[_colKey]).reduce((r, c) => r.concat([c]), []);
const summaryCalc = ['ConfirmOrder', 'SumOrder', 'SumML', 'transactions', 'SumPersonNum', 'ConfirmPersonNum', 'quotePrice', 'tourdays', 'applyDays', 'confirmDays'].reduce(
(r, skey) => ({ ...r, [skey]: allColumns.reduce((a, c) => Number(c.orderState) === 1 ? a : fixTo2Decimals(a + c[skey]), 0) }),
everyR
);
const summaryCalc = [
'ConfirmOrder',
'SumOrder',
'ResumeOrder', 'ResumeConfirmOrder',
'SumML',
'transactions',
'SumPersonNum',
'ConfirmPersonNum',
'quotePrice',
'tourdays',
'applyDays',
'confirmDays',
'PPPrice', 'AvgPPPrice',
'confirmTourdays',
].reduce((r, skey) => ({ ...r,
[skey]: allColumns.reduce((a, c) => (fixTo2Decimals(a + c[skey])), 0),
[`${skey}_arr`]: allColumns.reduce((a, c) => (a.concat(c[skey])), []),
}),everyR);
summaryCalc.tourdays = Math.ceil(summaryCalc.tourdays / allColumns.length);
summaryCalc.confirmTourdays = summaryCalc.ConfirmOrder > 0 ? Math.ceil(summaryCalc.confirmTourdays / summaryCalc.ConfirmOrder) : '0';
summaryCalc.applyDays = Math.ceil(summaryCalc.applyDays / allColumns.length);
summaryCalc.confirmDays = Math.ceil(summaryCalc.confirmDays / allColumns.length);
summaryCalc.ConfirmRates = summaryCalc.ConfirmOrder ? fixTo2Decimals(summaryCalc.ConfirmOrder / summaryCalc.SumOrder*100) : 0;
summaryCalc.ResumeConfirmRates = summaryCalc.ResumeConfirmOrder ? fixTo2Decimals(summaryCalc.ResumeConfirmOrder / summaryCalc.ResumeOrder*100) : 0;
summaryCalc.OrderValue = summaryCalc.SumOrder ? fixToInt(summaryCalc.SumML / summaryCalc.SumOrder) : 0;
summaryCalc.SingleML = summaryCalc.ConfirmOrder ? fixTo2Decimals(summaryCalc.SumML / summaryCalc.ConfirmOrder) : 0;
summaryCalc.AvgPPPrice = Math.ceil(summaryCalc.AvgPPPrice / allColumns.length);
summaryCalc.PPPriceRange = calcPPPriceRange(summaryCalc.AvgPPPrice);
summaryCalc.unitPPPrice = summaryCalc.confirmTourdays && summaryCalc.ConfirmPersonNum ? fixToInt(summaryCalc.quotePrice / summaryCalc.confirmTourdays / summaryCalc.ConfirmPersonNum) : -1;
summaryCalc.unitPPPriceRange = calcPPPriceRange(summaryCalc.unitPPPrice);
summaryCalc.SumML_txt = dataFieldAlias.SumML.formatter(summaryCalc.SumML);
summaryCalc.ConfirmRates_txt = dataFieldAlias.ConfirmRates.formatter(summaryCalc.ConfirmRates);
@ -297,8 +506,12 @@ export const pivotBy = (data, [rows, columns, date]) => {
const columnsData = groupBy(data, (row) => columns.map((kk) => `${row[kk]}`).join('=@='));
const summaryColumns = transposeData(columns, columnsData, ['rows', rows]);
const rowsMixcolumns = flush([].concat(rows, columns));
const rowsMixcolumnsData = groupBy(data, (row) => rowsMixcolumns.map((kk) => `${row[kk]}`).join('=@='));
const summaryMix = transposeData(rowsMixcolumns, rowsMixcolumnsData);
console.timeEnd('pivot3----');
return { data: pivotResult, columnValues: [rowsKeys, columnsKeys, dateKeys], summaryRows, summaryColumns };
return { data: pivotResult, columnValues: [rowsKeys, columnsKeys, dateKeys], summaryRows, summaryColumns, pivotKeys: groupbyKeys, summaryMix };
};
// todo: 优化 pivotBy 速度

@ -0,0 +1,27 @@
{
"get|/inbound_person_num/test": {
"errcode": 0,
"errmsg": "",
"data": null,
"loading": null,
"resultTotal": {
"orgz": "@integer(10,99)",
"orgzPDays": "@integer(10,99)",
"hosts": "@integer(10,99)",
"hostsPDays": "@integer(10,99)",
"IndividualService": "@integer(10,99)",
"groupsKey": "0",
"groupsLabel": "总"
},
"result|10": [
{
"orgz": "@integer(10,99)",
"orgzPDays": "@integer(10,99)",
"hosts": "@integer(10,99)",
"hostsPDays": "@integer(10,99)",
"groupsKey": "@id",
"groupsLabel": "@region"
}
]
}
}

@ -43,6 +43,8 @@ class AuthStore {
runInAction(() => {
_this.user = json.result;
_this.auth = json.result.authlist;
window.__spytitle = json.result.name;
window.initPageSpy();
});
})
.catch((error) => {

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

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

@ -34,7 +34,7 @@ class DashboardStore {
this.orders_data.loading = true;
const date_picker_store = this.rootStore.date_picker_store;
let url = "/service-web/QueryData/GetOrderCount";
url += "?WebCode=all&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.DATE_FORMAT) + "%2023:59:59";
url += "?WebCode=all&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
fetch(config.HT_HOST + url)
.then(response => response.json())
@ -79,7 +79,7 @@ class DashboardStore {
const date_picker_store = this.rootStore.date_picker_store;
let url = "/service-baseinfo/QueryWebData?type=orders_temp&db=1";
const website_code = "'" + this.ordersTemp_data.webcode.join("','") + "'";
url += "&WebSite=" + website_code + "&ApplyDateStart=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&ApplyDateEnd=" + date_picker_store.end_date.format(config.DATE_FORMAT) + "%2023:59:59";
url += "&WebSite=" + website_code + "&ApplyDateStart=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&ApplyDateEnd=" + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
fetch(config.HT_HOST + url)
.then(response => response.json())
.then(json => {
@ -100,7 +100,7 @@ class DashboardStore {
const date_picker_store = this.rootStore.date_picker_store;
let url = "/service-baseinfo/QueryWebData?type=orders_temp_detail&db=1";
const website_code = "'" + this.ordersTemp_data.webcode.join("','") + "'";
url += "&WebSite=" + website_code + "&ApplyDateStart=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&ApplyDateEnd=" + date_picker_store.end_date.format(config.DATE_FORMAT) + "%2023:59:59";
url += "&WebSite=" + website_code + "&ApplyDateStart=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&ApplyDateEnd=" + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
fetch(config.HT_HOST + url)
.then(response => response.json())
.then(json => {
@ -139,8 +139,8 @@ class DashboardStore {
} else {
url += "&ApplydateCheck=0&OrderStartdateCheck=1";
}
url += "&ApplydateStart=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&ApplydateEnd=" + date_picker_store.end_date.format(config.DATE_FORMAT) + "%2023:59:59";
url += "&OrderStartdateStart=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&OrderStartdateEnd=" + date_picker_store.end_date.format(config.DATE_FORMAT) + "%2023:59:59";
url += "&ApplydateStart=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&ApplydateEnd=" + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
url += "&OrderStartdateStart=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&OrderStartdateEnd=" + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
fetch(config.HT_HOST + url)
.then(response => response.json())
.then(json => {
@ -222,7 +222,7 @@ class DashboardStore {
const date_picker_store = this.rootStore.date_picker_store;
this.exchangeRate_data.loading = true;
let url = "/service-web/QueryData/GetCurrency?Currency=ALL";
url += "&Currdate1=" + Currdate1_start + "&Currdate2=" + Currdate1_end + "%2023:59:59";
url += "&Currdate1=" + Currdate1_start + "&Currdate2=" + Currdate1_end + "%2023:59:00";
fetch(config.HT_HOST + url)
.then(response => response.json())
.then(json => {

@ -1,5 +1,6 @@
import {makeAutoObservable} from "mobx";
import moment from "moment";
import { DATE_FORMAT, DATETIME_FORMAT, SMALL_DATETIME_FORMAT } from '../config';
/**
* 管理搜索组件的状态
*/
@ -56,8 +57,8 @@ class DatePickerStore {
WebCode: 'ALL',
IncludeTickets: '1',
DateType: 'confirmDate',
Date1: this.start_date.format('YYYY-MM-DD'),
Date2: this.end_date.format('YYYY-MM-DD 23:59:59'),
Date1: this.start_date.format(DATE_FORMAT),
Date2: this.end_date.format(SMALL_DATETIME_FORMAT),
};
setFormValues(data){
@ -67,6 +68,11 @@ class DatePickerStore {
setFormValuesToSub(data){
this.formValuesToSub = data;
}
siderBroken = false;
setSiderBroken(broken){
this.siderBroken = broken;
}
}
export default DatePickerStore;

@ -1,19 +1,19 @@
import { makeAutoObservable, runInAction, toJS } from 'mobx';
import * as req from '../utils/request';
import { DATE_FORMAT } from './../config';
import { DATE_FORMAT, SMALL_DATETIME_FORMAT } from './../config';
import moment from 'moment';
import { isEmpty, pick, sortBy, fixTo2Decimals, cloneDeep, unique } from '../utils/commons';
const modelMapper = {
'tourDays': { url: '/service-Analyse2/GetTradeApartByTourDays', keySort: true },
'PML': { url: '/service-Analyse2/GetTradeApartByPML', keySort: true },
'ConfirmDays': { url: '/service-Analyse2/GetTradeApartByConfirmDays', keySort: true },
'ApplyDays': { url: '/service-Analyse2/GetTradeApartByApplyDays', keySort: true },
'PersonNum': { url: '/service-Analyse2/GetTradeApartByPersonNum', keySort: true },
'destination': { url: '/service-Analyse2/GetTradeApartByDestination', keySort: false },
'GlobalDestination': { url: '/service-Analyse2/GetTradeApartByGlobalDestination', keySort: false },
'destinationCountry': { url: '/service-Analyse2/GetTradeApartByDestinationCountry', keySort: false },
'guestCountry': { url: '/service-Analyse2/GetTradeApartByGuestCountry', keySort: false },
'tourDays': { url: '/service-Analyse2/GetTradeApartByTourDays', keySort: true, dynamicsX: false },
'PML': { url: '/service-Analyse2/GetTradeApartByPML', keySort: true, dynamicsX: false },
'ConfirmDays': { url: '/service-Analyse2/GetTradeApartByConfirmDays', keySort: true, dynamicsX: false },
'ApplyDays': { url: '/service-Analyse2/GetTradeApartByApplyDays', keySort: true, dynamicsX: false },
'PersonNum': { url: '/service-Analyse2/GetTradeApartByPersonNum', keySort: true, dynamicsX: false },
'destination': { url: '/service-Analyse2/GetTradeApartByDestination', keySort: false, dynamicsX: true, },
'GlobalDestination': { url: '/service-Analyse2/GetTradeApartByGlobalDestination', keySort: false, dynamicsX: true, },
'destinationCountry': { url: '/service-Analyse2/GetTradeApartByDestinationCountry', keySort: false, dynamicsX: true, },
'guestCountry': { url: '/service-Analyse2/GetTradeApartByGuestCountry', keySort: false, dynamicsX: true, },
};
class Distribution {
constructor(appStore) {
@ -24,7 +24,7 @@ class Distribution {
/**
* 各个类型的分布
*/
getApartData = async (param) => {
getApartData = async (param, getDiff = undefined) => {
this.pageLoading = true;
const mkey = this.curTab;
this[mkey] = { loading: true, dataSource: [] };
@ -36,25 +36,41 @@ class Distribution {
const [DateToQ1, DateToQ2] = [moment(param.Date1).subtract(moment(param.Date2).diff(param.Date1, 'days') + 1, 'days'), moment(param.Date1).subtract(1, 'days')];
// 同比的参数: 去年同期
const [DateToY1, DateToY2] = [moment(param.Date1).subtract(1, 'year'), moment(param.Date2).subtract(1, 'year')];
param.DateToY1 = DateToY1.format(DATE_FORMAT);
param.DateToY2 = DateToY2.format(`${DATE_FORMAT} 23:59:59`);
param.DateToQ1 = DateToQ1.format(DATE_FORMAT);
param.DateToQ2 = DateToQ2.format(`${DATE_FORMAT} 23:59:59`);
const json = await req.fetchJSON(modelMapper[mkey].url, param);
const dynamicsX = getDiff ?? modelMapper[mkey].dynamicsX;
if (getDiff === undefined) {
param.DateToY1 = DateToY1.format(DATE_FORMAT);
param.DateToY2 = DateToY2.format(SMALL_DATETIME_FORMAT);
param.DateToQ1 = DateToQ1.format(DATE_FORMAT);
param.DateToQ2 = DateToQ2.format(SMALL_DATETIME_FORMAT);
}
const json = dynamicsX === false ? await req.fetchJSON(modelMapper[mkey].url, param) : await this.getApartDataStep(param);
if (json.errcode === 0) {
const dataLength = json.result.length;
const pickResult = dataLength > 20 ? json.result.slice(0, 30) : json.result;
const dataSource = calcDiff({ result: pickResult, resultToY: json.resultToY, resultToQ: json.resultToQ }, modelMapper[mkey].keySort);
const dataSource = calcDiff({ result: json.result, resultToY: json.resultToY, resultToQ: json.resultToQ }, modelMapper[mkey].keySort);
runInAction(() => {
this[mkey].loading = false;
this[mkey].originData = json.result;
this[mkey].dataSource = dataSource;
this[mkey].dataSource = dataLength > 20 ? dataSource.slice(0,30) : dataSource;
this.pageLoading = false;
});
}
return this[mkey];
};
getApartDataStep = async (param) => {
const mkey = this.curTab;
this[mkey] = { loading: true, dataSource: [] };
const xParam = cloneDeep(param);
delete xParam.DateToY1;
delete xParam.DateToY2;
delete xParam.DateToQ1;
delete xParam.DateToQ2;
const { result, ...jsonY } = await req.fetchJSON(modelMapper[mkey].url, { ...xParam });
const { result: resultToY } = await req.fetchJSON(modelMapper[mkey].url, { ...xParam, Date1: param.DateToY1, Date2: param.DateToY2 });
const { result: resultToQ } = await req.fetchJSON(modelMapper[mkey].url, { ...xParam, Date1: param.DateToQ1, Date2: param.DateToQ2 });
return { ...jsonY, result, resultToY, resultToQ };
};
/**
* 明细
*/
@ -153,6 +169,17 @@ const calcDiff = ({ result, resultToY, resultToQ }, keySort) => {
: 0,
ConfirmOrderDiffY: resultMapped[row.key].ConfirmOrder - resultToYMapped[row.key].ConfirmOrder,
ConfirmOrderDiffQ: resultMapped[row.key].ConfirmOrder - resultToQMapped[row.key].ConfirmOrder,
SumOrderY: resultToYMapped?.[row.key]?.SumOrder || 0,
SumOrderToY: resultToYMapped?.[row.key]?.SumOrder
? fixTo2Decimals(((resultMapped[row.key].SumOrder - resultToYMapped[row.key].SumOrder) / resultToYMapped[row.key].SumOrder) * 100)
: 0,
SumOrderQ: resultToQMapped?.[row.key]?.SumOrder || 0,
SumOrderToQ: resultToQMapped?.[row.key]?.SumOrder
? fixTo2Decimals(((resultMapped[row.key].SumOrder - resultToQMapped[row.key].SumOrder) / resultToQMapped[row.key].SumOrder) * 100)
: 0,
SumOrderDiffY: resultMapped[row.key].SumOrder - resultToYMapped[row.key].SumOrder,
SumOrderDiffQ: resultMapped[row.key].SumOrder - resultToQMapped[row.key].SumOrder,
};
return { ...resultMapped[row.key], ...diff, resultToY: resultToYMapped[row.key], resultToQ: resultToQMapped[row.key] };
});

@ -1,7 +1,7 @@
import {makeAutoObservable, runInAction} from "mobx";
import * as dd from 'dingtalk-jsapi';
import * as config from "../config";
import * as comm from '../utils/commons';
import { fetchJSON } from '../utils/request';
// 财务管理
@ -96,9 +96,9 @@ class FinancialStore {
const date_picker_store = this.rootStore.date_picker_store;
let url = '/service-web/QueryData/GetCreditCardBills';
url += `?business_unit=${this.credit_card_data.business_units}&groups=${this.credit_card_data.groups.toString()}&billtype=${this.bill_type_data.bill_types}`;
url += '&billdate1=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&billdate2=' + date_picker_store.end_date.format(config.DATE_FORMAT) + '%2023:59:59';
url += '&billdate1=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&billdate2=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) {
url += '&billdateOld1=' + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + '&billdateOld2=' + date_picker_store.end_date_cp.format(config.DATE_FORMAT) + '%2023:59:59';
url += '&billdateOld1=' + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + '&billdateOld2=' + date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT);
}
fetch(config.HT_HOST + url)
.then((response) => response.json())
@ -117,9 +117,9 @@ class FinancialStore {
const date_picker_store = this.rootStore.date_picker_store;
let url = '/service-web/QueryData/GetCreditCardBillsByType';
url += `?business_unit=${this.credit_card_data.business_units}&groups=${this.credit_card_data.groups.toString()}`;
url += '&billdate1=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&billdate2=' + date_picker_store.end_date.format(config.DATE_FORMAT) + '%2023:59:59';
url += '&billdate1=' + date_picker_store.start_date.format(config.DATE_FORMAT) + '&billdate2=' + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) {
url += '&billdateOld1=' + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + '&billdateOld2=' + date_picker_store.end_date_cp.format(config.DATE_FORMAT) + '%2023:59:59';
url += '&billdateOld1=' + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + '&billdateOld2=' + date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT);
}
fetch(config.HT_HOST + url)
.then((response) => response.json())
@ -132,6 +132,107 @@ class FinancialStore {
console.log('fetch data failed', error);
});
}
/**
* 服务人数页面 ----
*/
serviceModelMapper = {
'inbound': { url: '/service-Analyse2/inbound_person_num', keySort: true, dynamicsX: false },
'outbound': { url: '/service-Analyse2/outbound_person_num', keySort: true, dynamicsX: false },
'domestic': { url: '/service-Analyse2/domestic_person_num', keySort: true, dynamicsX: false },
};
servicePersonNum = { curTab: 'inbound', loading: false,
'inbound': { loading: false, dataSource: [], rawData: [], NoEmptyData: [], },
'outbound': { loading: false, dataSource: [], rawData: [], NoEmptyData: [], },
'domestic': { loading: false, dataSource: [], rawData: [], NoEmptyData: [], },
};
setCurTab(v) {
this.servicePersonNum.curTab = v;
}
resetPersonNumData = () => {
this.servicePersonNum.inbound = { loading: false, dataSource: [], rawData: [], NoEmptyData: [], };
this.servicePersonNum.outbound = { loading: false, dataSource: [], rawData: [], NoEmptyData: [], };
this.servicePersonNum.domestic = { loading: false, dataSource: [], rawData: [], NoEmptyData: [], };
};
setPersonNumTableDataSource = (notnull) => {
const mkey = this.servicePersonNum.curTab;
if (comm.isEmpty(this.servicePersonNum[mkey].dataSource)) {
return false;
}
this.servicePersonNum[mkey].dataSource = (notnull === false ? this.servicePersonNum[mkey].rawData : this.servicePersonNum[mkey].NoEmptyData);
};
handleRows = (json) => {
const mkey = this.servicePersonNum.curTab;
const sumFun = (_data, key='sum') => ['orgz', 'orgzPDays', 'hosts', 'hostsPDays'].reduce((r, skey) => ({ ...r, [skey]: _data.reduce((a, c) => a + (c[skey] || 0), 0) }), {
groupsKey: key, // `sumResult`,
groupsLabel: '合计',
});
const percentRow = (row, sumResult) => {
return ['orgz', 'orgzPDays', 'hosts', 'hostsPDays'].reduce((r, skey) => ({ ...r, [`${skey}Percent`]: sumResult[skey] ? comm.fixTo4Decimals(row[skey] / sumResult[skey]) : 0 }), row);
};
const sumResult = sumFun(json.result, 'sumResult');
const emptyRows = json.result.filter(ele => ele.groupsLabel === '' || ele.groupsLabel === '未知');
const sumEmptyResult = sumFun(emptyRows, 'sumEmptyResult');
const NotEmptyRows = json.result.filter(ele => ele.groupsLabel !== '' && ele.groupsLabel !== '未知');
const sumNotEmptyRows = sumFun(NotEmptyRows, 'sumNotEmptyRows');
const result = NotEmptyRows.map((row) => ({...row, ...percentRow(row, sumNotEmptyRows)})).sort(comm.sortBy('orgz')).reverse().map((row) => {
const newData = ['orgz', 'orgzPDays', 'hosts', 'hostsPDays'].reduce(
(r, skey) => ({ ...r, [skey]: row[`${skey}Percent`] ? row[skey] + comm.fixToInt(sumEmptyResult[skey] * row[`${skey}Percent`]) : row[skey] }),
row
);
return newData;
});
const sumAfter = sumFun(result, 'sumAfter');
const diffSum = ['orgz', 'orgzPDays', 'hosts', 'hostsPDays'].reduce((r, skey) => ({ ...r, [skey]: sumResult[skey]-sumAfter[skey]}), {});
const result0 = ['orgz', 'orgzPDays', 'hosts', 'hostsPDays'].reduce((r, skey) => ({...r, [skey]: (result?.[0]?.[skey] || 0)+(diffSum?.[skey] || 0)}), {});
result[0] = { ...result[0], ...result0 };
const totalRow = Object.keys(json.resultTotal).length > 1 ? { ...json.resultTotal, groupsKey: `total${mkey}` } : (mkey === 'domestic' ? sumResult : { groupsKey: 'empty'});
const IndividualServiceRow = {orgz: json.resultTotal.IndividualService, groupsKey: `individualService${mkey}`, groupsLabel: '单项服务人数', };
return {
sumResult,
result,
rawData: [].concat([IndividualServiceRow, totalRow], json.result.sort(comm.sortBy('orgz')).reverse()),
NoEmptyData: [].concat([IndividualServiceRow, totalRow], result),
};
};
/**
* 获取服务人数
*/
async getPersonNum(queryData) {
const mkey = this.servicePersonNum.curTab;
const url = this.serviceModelMapper[mkey].url;
this.servicePersonNum.loading = true;
this.servicePersonNum[this.servicePersonNum.curTab].loading = true;
const json = await fetchJSON(url, {...queryData, DateType: 'startDate'});
const { rawData, NoEmptyData } = this.handleRows(json);
if (json.errcode === 0) {
runInAction(() => {
this.servicePersonNum.loading = false;
this.servicePersonNum[this.servicePersonNum.curTab].loading = false;
this.servicePersonNum[this.servicePersonNum.curTab].dataSource = rawData;
this.servicePersonNum[this.servicePersonNum.curTab].rawData = rawData;
this.servicePersonNum[this.servicePersonNum.curTab].NoEmptyData = NoEmptyData;
});
}
return json;
}
/**
* ---- end 服务人数页面
*/
}

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

@ -15,6 +15,10 @@ import KPI from "./KPI";
import DictData from "./DictData";
import Distribution from "./Distribution";
import DataPivot from './DataPivot';
import MeetingData from './MeetingData2024';
import MeetingData2025 from './MeetingData2025';
import SalesCRMData from './SalesCRMData';
import HotelCruise from './HotelCruise';
class Index {
constructor() {
this.dashboard_store = new DashboardStore(this);
@ -33,6 +37,10 @@ class Index {
this.DictDataStore = new DictData(this);
this.DistributionStore = new Distribution(this);
this.DataPivotStore = new DataPivot(this);
this.MeetingDataStore = new MeetingData(this);
this.MeetingData2025Store = new MeetingData2025(this);
this.SalesCRMDataStore = new SalesCRMData(this);
this.HotelCruiseStore = new HotelCruise(this);
makeAutoObservable(this);
}

@ -0,0 +1,355 @@
import { makeAutoObservable, runInAction } from 'mobx';
import { fetchJSON } from '../utils/request';
import { objectMapper, pick, price_to_number, } from '../utils/commons';
import { pivotBy } from '../libs/ht';
import moment from "moment";
import { DATE_FORMAT, DATETIME_FORMAT, SMALL_DATETIME_FORMAT } from '../config';
/**
* 用于透视的数据
*/
const getDetailData = async (param) => {
const json = await fetchJSON('/service-Analyse2/GetTradeApartDetail', param);
return json.errcode === 0 ? json.result : [];
};
/**
*
*/
const getOrderCountByType = async (param) => {
const paramBody = objectMapper(param, {
WebCode: 'WebCode',
OrderType: 'OrderType',
IncludeTickets: 'IncludeTickets',
DateType: 'DateType',
DepartmentList: 'DepartmentList', // { key: 'DepartmentList', transform: (v) => v.join(',') },
Date1: 'COLI_ApplyDate1',
Date2: 'COLI_ApplyDate2',
});
const url = '/service-web/QueryData/GetOrderCountByType';
const json = await fetchJSON(url, paramBody);
return json.errcode === 0 ? json : {};
};
const getAgentGroupInfoALL = async (param) => {
const paramBody = objectMapper(param, {
DateType: 'DateType',
DepartmentList: 'DepList', // { key: 'DepartmentList', transform: (v) => v.join(',') },
Date1: 'OldDate1',
Date2: 'OldDate2',
});
const url = '/service-web/QueryData/GetAgentGroupInfoALL';
const json = await fetchJSON(url, paramBody);
return json.errcode === 0 ? json : {};
};
const getDepartmentOrderMLByType = async (param) => {
const paramBody = objectMapper(param, {
DateType: 'DateType',
DepartmentList: 'DepartmentList', // { key: 'DepartmentList', transform: (v) => v.join(',') },
OrderType: 'OrderType', // 总览, 产品类型
});
const url = '/service-web/QueryData/GetDepartmentOrderMLByType';
const json = await fetchJSON(url, paramBody);
const { result1 } = json.errcode === 0 ? json : { result1: [] };
const total1 = ['COLI_CJCount', 'COLI_ML2',].reduce(
(r, col) => ({
...r,
[col]: result1.reduce((rr, row) => rr + row[col], 0),
}),
{}
);
return { total1, result1 };
};
const GHproductTypeListSetting = {
ja: ['日本', '东亚跨国'],
se: ['东南亚跨国', '泰国', '越南', '印度尼西亚', '水灯节', '柬埔寨', '老挝'],
in: ['印度', '印度次大陆跨国', '尼泊尔', '不丹', '斯里兰卡'],
};
const GHCountryListSetting = {
ja: ['日本', ],
se: ['泰国', '越南', '印度尼西亚', '水灯节', '柬埔寨', '老挝', '新加坡', '马来西亚', '菲律宾'],
in: ['印度', '印度次大陆跨国', '尼泊尔', '不丹', '斯里兰卡'],
};
const rowItem = (filterData) => {
const { data: dataByLineClass, summaryMix: summaryByLineClass } = pivotBy(filterData, [['COLI_LineClass'], [], []]);
const LineClass_Origin = dataByLineClass.filter((ele) => ele.COLI_LineClass.toLocaleLowerCase().indexOf('网前自然订单') !== -1).reduce((r, c) => r + c.SumOrder, 0);
const LineClass_PPC = dataByLineClass.filter((ele) => ele.COLI_LineClass.toLocaleLowerCase().indexOf('ppc') !== -1).reduce((r, c) => r + c.SumOrder, 0);
const { data: dataByWebCode, summaryMix: summaryByWebCode } = pivotBy(filterData, [['WebCode'], [], []]);
const toB = dataByWebCode.filter((ele) => ele.WebCode.toLocaleLowerCase().indexOf('to b') !== -1).reduce((r, c) => r + c.SumOrder, 0);
const external = dataByWebCode.filter((ele) => ele.WebCode.toLocaleLowerCase().indexOf("站外渠道") !== -1).reduce((r, c) => r + c.SumOrder, 0);
const filterIsOldC = filterData.filter((ele) => ele.WebCode.toLocaleLowerCase().indexOf('to b') === -1);
const { data: dataByIsOld, summaryMix: summaryByIsOld } = pivotBy(filterIsOldC, [['IsOld', 'isCusCommend'], [], []]);
const isOld1 = dataByIsOld.filter((ele) => ele.rowLabel.indexOf('1') !== -1).reduce((r, c) => r + c.SumOrder, 0);
const total = LineClass_Origin + LineClass_PPC + toB + isOld1 + external;
return { LineClass_Origin, LineClass_PPC, toB, external, isOld1, total };
};
// 日本+: 日本+东亚跨国
const dataJA = (rawData, yearData) => {
const productTypeList = GHproductTypeListSetting.ja;
const filterData = rawData.filter((ele) => productTypeList.some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1));
const filterDataYear = yearData.filter((ele) => productTypeList.some((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) !== -1));
const rowYear = filterDataYear.reduce((r, c) => r + c.OrderCount, 0);
return { ...rowItem(filterData), rowYear };
};
// 东南亚+: 东南亚跨国+泰国+越南+印尼+水灯节线路
const dataSE = (rawData, yearData) => {
const productTypeList = GHproductTypeListSetting.se;
const filterData = rawData.filter((ele) => productTypeList.some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1));
const filterDataYear = yearData.filter((ele) => productTypeList.some((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) !== -1));
const rowYear = filterDataYear.reduce((r, c) => r + c.OrderCount, 0);
return { ...rowItem(filterData), rowYear };
};
// 印度+: 印度+次大陆跨国+尼泊尔+不丹+斯里兰卡
const dataIN = (rawData, yearData) => {
const productTypeList = GHproductTypeListSetting.in;
const exceptProduct = ['印度尼西亚'];
const filterData = rawData
.filter((ele) => productTypeList.some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1))
.filter((ele) => exceptProduct.every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1));
const filterDataYear = yearData
.filter((ele) => productTypeList.some((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) !== -1))
.filter((ele) => exceptProduct.every((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) === -1));
const rowYear = filterDataYear.reduce((r, c) => r + c.OrderCount, 0);
return { ...rowItem(filterData), rowYear };
};
// 其他GH
const dataGHOther = (rawData, yearData) => {
const exceptProduct = Object.values(GHproductTypeListSetting).reduce((r, c) => r.concat(c), []);
const filterData = rawData.filter((ele) => exceptProduct.every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1));
const filterDataYear = yearData.filter((ele) => exceptProduct.every((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) === -1));
const rowYear = filterDataYear.reduce((r, c) => r + c.OrderCount, 0);
return { ...rowItem(filterData), rowYear };
};
const dataSales = (tKey, rawData, yearData, yearData2) => {
const targetList = GHCountryListSetting[tKey];
const tIndex = Object.keys(GHCountryListSetting).indexOf(tKey);
const exceptTargetList = Object.keys(GHCountryListSetting).reduce((r, c, i) => r.concat(i < tIndex ? GHCountryListSetting[c] : []), []);
// console.log(tIndex, tKey, 'exceptTargetList', exceptTargetList, 'targetList', targetList);
const filterRaw1 = rawData.filter((ele) => exceptTargetList.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
// console.log(tKey, 'filterRaw1', filterRaw1);
const filterDataC = filterRaw1.filter((ele) => targetList.some((item) => ele.destinationCountry_AsJOSN.includes(item)));
const filterDataT = tKey === 'se' ? filterRaw1.filter((ele) => ['泰国水灯节'].some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1)) : [];
const filterData = filterDataC.concat(filterDataT);
const CJCount = filterData.length; // filterData.reduce((r, c) => r + c.CJCount, 0);
const YJLY = filterData.reduce((r, c) => r + price_to_number(c.ML), 0);
const filterRaw2 = yearData.filter((ele) => exceptTargetList.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
const filterDataYearC = filterRaw2.filter((ele) => targetList.some((item) => ele.destinationCountry_AsJOSN.includes(item)));
const filterDataYearT = tKey === 'se' ? filterRaw2.filter((ele) => ['泰国水灯节'].some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1)) : [];
const filterDataYear = filterDataYearC.concat(filterDataYearT);
const rowYearData = { CJCount: filterDataYear.length, YJLY: filterDataYear.reduce((r, c) => r + price_to_number(c.ML), 0) };
// console.log(tKey, filterDataYear.map(ee => ee.destinationCountry_AsJOSN), filterDataYear.map(ee => ee.productType), filterDataYear);
const filterDataYearRaw2 = yearData2.filter((ele) => exceptTargetList.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
const filterDataYear2C = filterDataYearRaw2.filter((ele) => targetList.some((item) => ele.destinationCountry_AsJOSN.includes(item)));
const filterDataYear2T = tKey === 'se' ? filterDataYearRaw2.filter((ele) => ['泰国水灯节'].some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1)) : [];
const filterDataYear2 = filterDataYear2C.concat(filterDataYear2T);
const rowYearData2 = { CJCount: filterDataYear2.length, YJLY: filterDataYear2.reduce((r, c) => r + price_to_number(c.ML), 0) };
const rowYear = {
YJLY: price_to_number(rowYearData.YJLY), CJCount: rowYearData.CJCount, GroupCount: rowYearData.CJCount,
YJLY2: price_to_number(rowYearData2.YJLY), CJCount2: rowYearData2.CJCount, GroupCount2: rowYearData2.CJCount,
};
const cols = ['YJLY', 'CJCount'].reduce((r, key) => ({ ...r, [key]: filterData.reduce((a, c) => a + price_to_number(c[key]), 0) }), {});
// console.log(tKey, filterData, filterDataYear, filterDataYear2);
return { ...cols, GroupCount:CJCount, CJCount, YJLY, rowYear, rawData: filterData, rawYearData: filterDataYear, rawYearData2: filterDataYear2 };
};
const dataSalesGHOther = (rawData, yearData, yearData2) => {
const exceptContry = Object.values(GHCountryListSetting).reduce((r, c) => r.concat(c), []);
// console.log('exceptContry', exceptContry);
// console.log('OOoo rawData', rawData.map(e => e.destinationCountry_AsJOSN));
const filterData = rawData
.filter((ele) => ['泰国水灯节'].every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1))
.filter((ele) => exceptContry.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
// console.log('OOoo', filterData.map(e => e.destinationCountry_AsJOSN), filterData.map(e => e.productType));
const CJCount = filterData.length; // filterData.reduce((r, c) => r + c.CJCount, 0);
const YJLY = filterData.reduce((r, c) => r + price_to_number(c.ML), 0);
const filterDataYear = yearData
.filter((ele) => ['泰国水灯节'].every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1))
.filter((ele) => exceptContry.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
const rowYearData = { CJCount: filterDataYear.length, YJLY: filterDataYear.reduce((r, c) => r + price_to_number(c.ML), 0) };
// console.log('OOoo year', filterDataYear.map(e => e.destinationCountry_AsJOSN), filterDataYear.map(e => e.productType));
const filterDataYear2 = yearData2
.filter((ele) => ['泰国水灯节'].every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1))
.filter((ele) => exceptContry.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
const rowYearData2 = { CJCount: filterDataYear2.length, YJLY: filterDataYear2.reduce((r, c) => r + price_to_number(c.ML), 0) };
// console.log('Oo', filterDataYear2.map(e => e.destinationCountry_AsJOSN), filterDataYear2);
// console.log('Oo row', rowYearData2);
const rowYear = {
YJLY: price_to_number(rowYearData.YJLY), CJCount: rowYearData.CJCount, GroupCount: rowYearData.CJCount,
YJLY2: price_to_number(rowYearData2.YJLY), CJCount2: rowYearData2.CJCount, GroupCount2: rowYearData2.CJCount,
};
return { GroupCount:CJCount, CJCount, YJLY, rowYear, rawData: filterData, rawYearData: filterDataYear, rawYearData2: filterDataYear2 };
};
class MeetingData {
constructor(rootStore) {
this.rootStore = rootStore;
makeAutoObservable(this);
}
searchValues = {
DateType: { key: 'applyDate', value: 'applyDate', label: '提交日期' },
};
setSearchValues(body) {
this.searchValues = body;
}
GHTableData = [];
GHTableLoading = false;
/**
* 获取市场订单数据 ---------------------------------------------------------------------------------------------------
*/
dataGHOrder = async (param) => {
// console.log('dataGH', param);
this.GHTableLoading = true;
const defaultParam = { DateType: 'applyDate' };
// 本周
const CHData = await getDetailData({ ...param, ...defaultParam, 'DepartmentList': '1', 'WebCode': 'All' });
const exceptCHData = await getDetailData({ ...param, ...defaultParam, 'DepartmentList': '28,33', 'WebCode': 'All' });
const yearStart = moment().startOf("year").format(DATE_FORMAT);
/** 截至今年 - 行 */
const { ordercountTotal1: CHDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1', 'WebCode': 'All', OrderType: 'LineClass' });
const { ordercount1: exceptCHDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '28,33', 'WebCode': 'All', OrderType: 'Product' });
/** 截至今年 - 列 */
const { ordercount1: ColLineClassDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'All', OrderType: 'LineClass' });
const { ordercountTotal1: ColToBDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'GHTOBHW,GHTOBZG', OrderType: 'LineClass' });
const { ordercountTotal1: ColExternalDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'ZWQD', OrderType: 'LineClass' });
// 老客户
const yearDetail = await getDetailData({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'All' });
const { isOld1: isOld1Year } = rowItem(yearDetail);
const colYearRow = {
LineClass_Origin: ColLineClassDataYear.filter((ele) => ele.OrderType.toLocaleLowerCase().indexOf('网前自然订单') !== -1).reduce((r, c) => r + c.OrderCount, 0),
LineClass_PPC: ColLineClassDataYear.filter((ele) => ele.OrderType.toLocaleLowerCase().indexOf('ppc') !== -1).reduce((r, c) => r + c.OrderCount, 0),
toB: ColToBDataYear.OrderCount,
isOld1: isOld1Year,
external: ColExternalDataYear.OrderCount,
};
const rows = [
{ key: 'ch', label: '中国', ...rowItem(CHData), rowYear: CHDataYear.OrderCount },
{ key: 'ja', label: '日本+', ...dataJA(exceptCHData, exceptCHDataYear) },
{ key: 'se', label: '东南亚+', ...dataSE(exceptCHData, exceptCHDataYear) },
{ key: 'in', label: '印度+', ...dataIN(exceptCHData, exceptCHDataYear) },
{ key: 'other', label: '其他GH', ...dataGHOther(exceptCHData, exceptCHDataYear) },
];
const columnsSum = ['LineClass_Origin', 'LineClass_PPC', 'toB', 'external', 'isOld1', 'total', 'rowYear'].reduce(
(r, col) => ({
...r,
[col]: rows.reduce((rr, row) => rr + row[col], 0),
}),
{}
);
rows.push({ key: 'columnSum', label: '合计', ...columnsSum });
rows.push({ key: 'colYearRow', label: '截至', ...colYearRow });
runInAction(() => {
this.GHTableData = rows;
this.GHTableLoading = false;
});
};
GHSalesTableData = [];
GHSalesLoading = false;
/**
* 获取GH销售数据 ---------------------------------------------------------------------------------------------------
*/
dataGHSales = async (param) => {
this.GHSalesLoading = true;
const salesParam = { ...param, DateType: 'confirmDate', 'WebCode': 'All', };
// console.log(param);
const { total1: CHSalesDataWeek } = await getDepartmentOrderMLByType({...salesParam, DepartmentList: '1', OrderType:'ALL'});
const exceptCHSalesDataWeek = await getDetailData({ ...salesParam, 'DepartmentList': '28,33' });
const GHDataWeekConfirm = exceptCHSalesDataWeek.filter((ele) => Number(ele.orderState) === 1); // 成交的
const yearStart = moment().startOf("year").format(DATE_FORMAT);
const yearEnd = moment().endOf("year").format(SMALL_DATETIME_FORMAT);
/** 截至今年 - 成交 */
const { total1: CHDataYear } = await getDepartmentOrderMLByType({...salesParam, Date1: yearStart, DepartmentList: '1', OrderType:'ALL' });
const GHDataYear = await getDetailData({ ...salesParam, Date1: yearStart, 'DepartmentList': '28,33' });
const GHDataYearConfirm = GHDataYear.filter((ele) => Number(ele.orderState) === 1); // 成交的
/** 截至今年 - 走团 */
const { total1: CHStartDataYear } = await getDepartmentOrderMLByType({...salesParam, Date1: yearStart,Date2:yearEnd,DepartmentList: '1', OrderType:'ALL',DateType: 'startDate' });
const GHStartDataYear0 = await getDetailData({ ...salesParam, DateType: 'startDate', 'DepartmentList': '28,33', Date1: yearStart,Date2:yearEnd, });
const GHStartDataYear = GHStartDataYear0.filter((ele) => Number(ele.orderState) === 1); // 成交的
const rows = [
{
key: 'ch',
label: '中国',
YJLY: price_to_number(CHSalesDataWeek.COLI_ML2),
CJCount: (CHSalesDataWeek.COLI_CJCount),
rowYear: { YJLY: price_to_number(CHDataYear.COLI_ML2), CJCount: CHDataYear.COLI_CJCount, YJLY2: price_to_number(CHStartDataYear.COLI_ML2) },
},
{ key: 'ja', label: '日本+', ...dataSales('ja', GHDataWeekConfirm, GHDataYearConfirm, GHStartDataYear) },
{ key: 'se', label: '东南亚+', ...dataSales('se', GHDataWeekConfirm, GHDataYearConfirm, GHStartDataYear) },
{ key: 'in', label: '印度+', ...dataSales('in', GHDataWeekConfirm, GHDataYearConfirm, GHStartDataYear) },
{ key: 'other', label: '其他GH', ...dataSalesGHOther(GHDataWeekConfirm, GHDataYearConfirm, GHStartDataYear) },
];
const columnsSum = ['CJCount', 'YJLY'].reduce((r, col) => ({ ...r, [col]: rows.reduce((rr, row) => rr + row[col], 0) }), {});
const allYearData = rows.map(row => row.rowYear);
const rowYear = ['CJCount', 'YJLY', 'CJCount2', 'YJLY2', ].reduce((r, col) => ({ ...r, [col]: allYearData.reduce((rr, row) => rr + (row[col] || 0), 0) }), {});
rows.push({ key: 'columnSum', label: '合计', ...columnsSum, rowYear });
// console.log(rows, allYearData, rowYear);
runInAction(() => {
this.GHSalesTableData = rows;
this.GHSalesLoading = false;
});
};
GHServiceTableData = [];
GHServiceLoading = false;
/**
* 获取GH服务数据 ---------------------------------------------------------------------------------------------------
*/
dataGHService = async (param) => {
this.GHServiceLoading = true;
const serviceParam = { ...param, DateType: 'startDate', 'WebCode': 'All' };
// 走团数
const { ordercountTotal1: { OrderCount: GroupCount } } = await getOrderCountByType({ ...serviceParam, 'DepartmentList': '1', OrderType: 'Form' });
const exceptCHDataWeek = await getDetailData({ ...serviceParam, 'DepartmentList': '28,33' });
// 走团数 - 年
const yearStart = moment().startOf("year").format(DATE_FORMAT);
const { ordercountTotal1: { OrderCount: GroupCountYear } } = await getOrderCountByType({ ...serviceParam, 'DepartmentList': '1', OrderType: 'Form', Date1: yearStart, });
const exceptCHDataYear = await getDetailData({ ...serviceParam, Date1: yearStart, 'DepartmentList': '28,33' });
// 好评数
const { total1: { GoodCount } } = await getAgentGroupInfoALL({ ...serviceParam, 'DepartmentList': '1', });
const { total1: { GoodCount: GoodCountYear } } = await getAgentGroupInfoALL({ ...serviceParam, Date1: yearStart, 'DepartmentList': '1', });
const { total1: { GoodCount: GHGoodCountWeek } } = await getAgentGroupInfoALL({ ...serviceParam, 'DepartmentList': '28,33', });
const { total1: { GoodCount: GHGoodCountYear } } = await getAgentGroupInfoALL({ ...serviceParam, Date1: yearStart, 'DepartmentList': '28,33', });
const rows = [
{ key: 'ch', label: '中国', ...{GoodCount, GroupCount}, rowYear: { GroupCount: GroupCountYear, GoodCount: GoodCountYear } },
{ key: 'ja', label: '日本+', ...dataSales('ja', exceptCHDataWeek, exceptCHDataYear, []) },
{ key: 'se', label: '东南亚+', ...dataSales('se', exceptCHDataWeek, exceptCHDataYear, []) },
{ key: 'in', label: '印度+', ...dataSales('in', exceptCHDataWeek, exceptCHDataYear, []) },
{ key: 'other', label: '其他GH', ...dataSalesGHOther(exceptCHDataWeek, exceptCHDataYear, []) },
];
const GHRowWeek = { GoodCount: GHGoodCountWeek, GroupCount: 0 };
const columnsSum = ['GoodCount', 'GroupCount'].reduce((r, col) => ({ ...r, [col]: [...rows, GHRowWeek].reduce((rr, row) => rr + (row[col] || 0), 0) }), {});
const allYearData = rows.map((row) => row.rowYear).concat([{ GoodCount: GHGoodCountYear, GroupCount: 0 }]);
// console.log(allYearData);
const rowYear = ['GoodCount', 'GroupCount', ].reduce((r, col) => ({ ...r, [col]: allYearData.reduce((rr, row) => rr + (row[col] || 0), 0) }), {});
rows.push({ key: 'columnSum', label: '合计', ...columnsSum, rowYear });
// console.log(rows);
runInAction(() => {
this.GHServiceTableData = rows;
this.GHServiceLoading = false;
});
};
}
export default MeetingData;

@ -0,0 +1,479 @@
import { makeAutoObservable, runInAction } from 'mobx';
import { fetchJSON } from '../utils/request';
import { objectMapper, pick, price_to_number, } from '../utils/commons';
import { pivotBy } from './../libs/ht';
import moment from "moment";
import { DATE_FORMAT, DATETIME_FORMAT, SMALL_DATETIME_FORMAT } from '../config';
/**
* 用于透视的数据
*/
const getDetailData = async (param) => {
const json = await fetchJSON('/service-Analyse2/GetTradeApartDetail', param);
return json.errcode === 0 ? json.result : [];
};
/**
*
*/
const getOrderCountByType = async (param) => {
const paramBody = objectMapper(param, {
WebCode: 'WebCode',
OrderType: 'OrderType',
IncludeTickets: 'IncludeTickets',
DateType: 'DateType',
DepartmentList: 'DepartmentList', // { key: 'DepartmentList', transform: (v) => v.join(',') },
Date1: 'COLI_ApplyDate1',
Date2: 'COLI_ApplyDate2',
});
const url = '/service-web/QueryData/GetOrderCountByType';
const json = await fetchJSON(url, paramBody);
return json.errcode === 0 ? json : {};
};
const getAgentGroupInfoALL = async (param) => {
const paramBody = objectMapper(param, {
DateType: 'DateType',
DepartmentList: 'DepList', // { key: 'DepartmentList', transform: (v) => v.join(',') },
Date1: 'OldDate1',
Date2: 'OldDate2',
});
const url = '/service-web/QueryData/GetAgentGroupInfoALL';
const json = await fetchJSON(url, paramBody);
return json.errcode === 0 ? json : {};
};
const getDepartmentOrderMLByType = async (param) => {
const paramBody = objectMapper(param, {
DateType: 'DateType',
DepartmentList: 'DepartmentList', // { key: 'DepartmentList', transform: (v) => v.join(',') },
OrderType: 'OrderType', // 总览, 产品类型
});
const url = '/service-web/QueryData/GetDepartmentOrderMLByType';
const json = await fetchJSON(url, paramBody);
const { result1 } = json.errcode === 0 ? json : { result1: [] };
const total1 = ['COLI_CJCount', 'COLI_ML2',].reduce(
(r, col) => ({
...r,
[col]: result1.reduce((rr, row) => rr + row[col], 0),
}),
{}
);
return { total1, result1 };
};
const GHproductTypeListSetting = {
ja: ['日本', '东亚跨国'],
se: ['东南亚跨国', '泰国', '越南', '印度尼西亚', '水灯节', '柬埔寨', '老挝'],
in: ['印度', '印度次大陆跨国', '尼泊尔', '不丹', '斯里兰卡'],
};
const GHCountryListSetting = {
ja: ['日本', ],
se: ['泰国', '越南', '印度尼西亚', '水灯节', '柬埔寨', '老挝', '新加坡', '马来西亚', '菲律宾'],
in: ['印度', '印度次大陆跨国', '尼泊尔', '不丹', '斯里兰卡'],
};
const rowItem = (filterData) => {
const { data: dataByLineClass, summaryMix: summaryByLineClass } = pivotBy(filterData, [['COLI_LineClass'], [], []]);
const LineClass_Origin = dataByLineClass.filter((ele) => ele.COLI_LineClass.toLocaleLowerCase().indexOf('网前自然订单') !== -1).reduce((r, c) => r + c.SumOrder, 0);
const LineClass_PPC = dataByLineClass.filter((ele) => ele.COLI_LineClass.toLocaleLowerCase().indexOf('ppc') !== -1).reduce((r, c) => r + c.SumOrder, 0);
const { data: dataByWebCode, summaryMix: summaryByWebCode } = pivotBy(filterData, [['WebCode'], [], []]);
const toB = dataByWebCode.filter((ele) => ele.WebCode.toLocaleLowerCase().indexOf('to b') !== -1).reduce((r, c) => r + c.SumOrder, 0);
const external = dataByWebCode.filter((ele) => ele.WebCode.toLocaleLowerCase().indexOf("站外渠道") !== -1).reduce((r, c) => r + c.SumOrder, 0);
const filterIsOldC = filterData.filter((ele) => ele.WebCode.toLocaleLowerCase().indexOf('to b') === -1);
const { data: dataByIsOld, summaryMix: summaryByIsOld } = pivotBy(filterIsOldC, [['IsOld', 'isCusCommend'], [], []]);
const isOld1 = dataByIsOld.filter((ele) => ele.rowLabel.indexOf('1') !== -1).reduce((r, c) => r + c.SumOrder, 0);
const total = LineClass_Origin + LineClass_PPC + toB + isOld1 + external;
return { LineClass_Origin, LineClass_PPC, toB, external, isOld1, total };
};
// 日本+: 日本+东亚跨国
const dataJA = (rawData, yearData) => {
const productTypeList = GHproductTypeListSetting.ja;
const filterData = rawData.filter((ele) => productTypeList.some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1));
const filterDataYear = yearData.filter((ele) => productTypeList.some((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) !== -1));
const rowYear = filterDataYear.reduce((r, c) => r + c.OrderCount, 0);
return { ...rowItem(filterData), rowYear };
};
// 东南亚+: 东南亚跨国+泰国+越南+印尼+水灯节线路
const dataSE = (rawData, yearData) => {
const productTypeList = GHproductTypeListSetting.se;
const filterData = rawData.filter((ele) => productTypeList.some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1));
const filterDataYear = yearData.filter((ele) => productTypeList.some((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) !== -1));
const rowYear = filterDataYear.reduce((r, c) => r + c.OrderCount, 0);
return { ...rowItem(filterData), rowYear };
};
// 印度+: 印度+次大陆跨国+尼泊尔+不丹+斯里兰卡
const dataIN = (rawData, yearData) => {
const productTypeList = GHproductTypeListSetting.in;
const exceptProduct = ['印度尼西亚'];
const filterData = rawData
.filter((ele) => productTypeList.some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1))
.filter((ele) => exceptProduct.every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1));
const filterDataYear = yearData
.filter((ele) => productTypeList.some((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) !== -1))
.filter((ele) => exceptProduct.every((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) === -1));
const rowYear = filterDataYear.reduce((r, c) => r + c.OrderCount, 0);
return { ...rowItem(filterData), rowYear };
};
// 其他GH
const dataGHOther = (rawData, yearData) => {
const exceptProduct = Object.values(GHproductTypeListSetting).reduce((r, c) => r.concat(c), []);
const filterData = rawData.filter((ele) => exceptProduct.every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1));
const filterDataYear = yearData.filter((ele) => exceptProduct.every((item) => ele.OrderType.toLocaleLowerCase().indexOf(item) === -1));
const rowYear = filterDataYear.reduce((r, c) => r + c.OrderCount, 0);
return { ...rowItem(filterData), rowYear };
};
const dataSales = (tKey, rawData, yearData, yearData2) => {
const targetList = GHCountryListSetting[tKey];
const tIndex = Object.keys(GHCountryListSetting).indexOf(tKey);
const exceptTargetList = Object.keys(GHCountryListSetting).reduce((r, c, i) => r.concat(i < tIndex ? GHCountryListSetting[c] : []), []);
// console.log(tIndex, tKey, 'exceptTargetList', exceptTargetList, 'targetList', targetList);
const filterRaw1 = rawData.filter((ele) => exceptTargetList.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
// console.log(tKey, 'filterRaw1', filterRaw1);
const filterDataC = filterRaw1.filter((ele) => targetList.some((item) => ele.destinationCountry_AsJOSN.includes(item)));
const filterDataT = tKey === 'se' ? filterRaw1.filter((ele) => ['泰国水灯节'].some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1)) : [];
const filterData = filterDataC.concat(filterDataT);
const CJCount = filterData.length; // filterData.reduce((r, c) => r + c.CJCount, 0);
const YJLY = filterData.reduce((r, c) => r + price_to_number(c.ML), 0);
const filterRaw2 = yearData.filter((ele) => exceptTargetList.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
const filterDataYearC = filterRaw2.filter((ele) => targetList.some((item) => ele.destinationCountry_AsJOSN.includes(item)));
const filterDataYearT = tKey === 'se' ? filterRaw2.filter((ele) => ['泰国水灯节'].some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1)) : [];
const filterDataYear = filterDataYearC.concat(filterDataYearT);
const rowYearData = { CJCount: filterDataYear.length, YJLY: filterDataYear.reduce((r, c) => r + price_to_number(c.ML), 0) };
// console.log(tKey, filterDataYear.map(ee => ee.destinationCountry_AsJOSN), filterDataYear.map(ee => ee.productType), filterDataYear);
const filterDataYearRaw2 = yearData2.filter((ele) => exceptTargetList.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
const filterDataYear2C = filterDataYearRaw2.filter((ele) => targetList.some((item) => ele.destinationCountry_AsJOSN.includes(item)));
const filterDataYear2T = tKey === 'se' ? filterDataYearRaw2.filter((ele) => ['泰国水灯节'].some((item) => ele.productType.toLocaleLowerCase().indexOf(item) !== -1)) : [];
const filterDataYear2 = filterDataYear2C.concat(filterDataYear2T);
const rowYearData2 = { CJCount: filterDataYear2.length, YJLY: filterDataYear2.reduce((r, c) => r + price_to_number(c.ML), 0) };
const rowYear = {
YJLY: price_to_number(rowYearData.YJLY), CJCount: rowYearData.CJCount, GroupCount: rowYearData.CJCount,
YJLY2: price_to_number(rowYearData2.YJLY), CJCount2: rowYearData2.CJCount, GroupCount2: rowYearData2.CJCount,
};
const cols = ['YJLY', 'CJCount'].reduce((r, key) => ({ ...r, [key]: filterData.reduce((a, c) => a + price_to_number(c[key]), 0) }), {});
// console.log(tKey, filterData, filterDataYear, filterDataYear2);
return { ...cols, GroupCount:CJCount, CJCount, YJLY, rowYear, rawData: filterData, rawYearData: filterDataYear, rawYearData2: filterDataYear2 };
};
const dataSalesGHOther = (rawData, yearData, yearData2) => {
const exceptContry = Object.values(GHCountryListSetting).reduce((r, c) => r.concat(c), []);
// console.log('exceptContry', exceptContry);
// console.log('OOoo rawData', rawData.map(e => e.destinationCountry_AsJOSN));
const filterData = rawData
.filter((ele) => ['泰国水灯节'].every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1))
.filter((ele) => exceptContry.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
// console.log('OOoo', filterData.map(e => e.destinationCountry_AsJOSN), filterData.map(e => e.productType));
const CJCount = filterData.length; // filterData.reduce((r, c) => r + c.CJCount, 0);
const YJLY = filterData.reduce((r, c) => r + price_to_number(c.ML), 0);
const filterDataYear = yearData
.filter((ele) => ['泰国水灯节'].every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1))
.filter((ele) => exceptContry.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
const rowYearData = { CJCount: filterDataYear.length, YJLY: filterDataYear.reduce((r, c) => r + price_to_number(c.ML), 0) };
// console.log('OOoo year', filterDataYear.map(e => e.destinationCountry_AsJOSN), filterDataYear.map(e => e.productType));
const filterDataYear2 = yearData2
.filter((ele) => ['泰国水灯节'].every((item) => ele.productType.toLocaleLowerCase().indexOf(item) === -1))
.filter((ele) => exceptContry.every((item) => !ele.destinationCountry_AsJOSN.includes(item)));
const rowYearData2 = { CJCount: filterDataYear2.length, YJLY: filterDataYear2.reduce((r, c) => r + price_to_number(c.ML), 0) };
// console.log('Oo', filterDataYear2.map(e => e.destinationCountry_AsJOSN), filterDataYear2);
// console.log('Oo row', rowYearData2);
const rowYear = {
YJLY: price_to_number(rowYearData.YJLY), CJCount: rowYearData.CJCount, GroupCount: rowYearData.CJCount,
YJLY2: price_to_number(rowYearData2.YJLY), CJCount2: rowYearData2.CJCount, GroupCount2: rowYearData2.CJCount,
};
return { GroupCount:CJCount, CJCount, YJLY, rowYear, rawData: filterData, rawYearData: filterDataYear, rawYearData2: filterDataYear2 };
};
class MeetingData {
constructor(rootStore) {
this.rootStore = rootStore;
makeAutoObservable(this);
}
searchValues = {
DateType: { key: 'applyDate', value: 'applyDate', label: '提交日期' },
};
setSearchValues(body) {
this.searchValues = body;
}
GHTableData = [];
GHTableLoading = false;
/**
* 获取市场订单数据 ---------------------------------------------------------------------------------------------------
*/
dataGHOrder = async (param) => {
// console.log('dataGH', param);
this.GHTableLoading = true;
const defaultParam = { DateType: 'applyDate' };
// 本周
const CHData = await getDetailData({ ...param, ...defaultParam, 'DepartmentList': '1', 'WebCode': 'All' });
const exceptCHData = await getDetailData({ ...param, ...defaultParam, 'DepartmentList': '28,33', 'WebCode': 'All' });
const yearStart = moment().startOf("year").format(DATE_FORMAT);
/** 截至今年 - 行 */
const [
{ ordercountTotal1: CHDataYear },
{ ordercount1: exceptCHDataYear },
] = await Promise.all([
getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1', 'WebCode': 'All', OrderType: 'LineClass' }),
getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '28,33', 'WebCode': 'All', OrderType: 'Product' }),
]);
// const { ordercountTotal1: CHDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1', 'WebCode': 'All', OrderType: 'LineClass' });
// const { ordercount1: exceptCHDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '28,33', 'WebCode': 'All', OrderType: 'Product' });
/** 截至今年 - 列 */
const [
{ ordercount1: ColLineClassDataYear },
{ ordercountTotal1: ColToBDataYear },
{ ordercountTotal1: ColExternalDataYear },
] = await Promise.all([
getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'All', OrderType: 'LineClass' }),
getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'GHTOBHW,GHTOBZG', OrderType: 'LineClass'}),
getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'ZWQD', OrderType: 'LineClass'}),
]);
// const { ordercount1: ColLineClassDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'All', OrderType: 'LineClass' });
// const { ordercountTotal1: ColToBDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'GHTOBHW,GHTOBZG', OrderType: 'LineClass' });
// const { ordercountTotal1: ColExternalDataYear } = await getOrderCountByType({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'ZWQD', OrderType: 'LineClass' });
// 老客户
const yearDetail = await getDetailData({ ...param, ...defaultParam, Date1: yearStart, 'DepartmentList': '1,2,28,7,33', 'WebCode': 'All' });
const { isOld1: isOld1Year } = rowItem(yearDetail);
const colYearRow = {
LineClass_Origin: ColLineClassDataYear.filter((ele) => ele.OrderType.toLocaleLowerCase().indexOf('网前自然订单') !== -1).reduce((r, c) => r + c.OrderCount, 0),
LineClass_PPC: ColLineClassDataYear.filter((ele) => ele.OrderType.toLocaleLowerCase().indexOf('ppc') !== -1).reduce((r, c) => r + c.OrderCount, 0),
toB: ColToBDataYear.OrderCount,
isOld1: isOld1Year,
external: ColExternalDataYear.OrderCount,
};
const rows = [
{ key: 'ch', label: '中国', ...rowItem(CHData), rowYear: CHDataYear.OrderCount },
{ key: 'ja', label: '日本+', ...dataJA(exceptCHData, exceptCHDataYear) },
{ key: 'se', label: '东南亚+', ...dataSE(exceptCHData, exceptCHDataYear) },
{ key: 'in', label: '印度+', ...dataIN(exceptCHData, exceptCHDataYear) },
{ key: 'other', label: '其他GH', ...dataGHOther(exceptCHData, exceptCHDataYear) },
];
const columnsSum = ['LineClass_Origin', 'LineClass_PPC', 'toB', 'external', 'isOld1', 'total', 'rowYear'].reduce(
(r, col) => ({
...r,
[col]: rows.reduce((rr, row) => rr + row[col], 0),
}),
{}
);
rows.push({ key: 'columnSum', label: '合计', ...columnsSum });
rows.push({ key: 'colYearRow', label: '截至', ...colYearRow });
runInAction(() => {
this.GHTableData = rows;
this.GHTableLoading = false;
});
};
GHSalesTableData = [];
GHSalesLoading = false;
/**
* 获取GH销售数据 ---------------------------------------------------------------------------------------------------
*/
dataGHSales = async (param) => {
this.GHSalesLoading = true;
const salesParam = { ...param, DateType: 'confirmDate', WebCode: 'CHT,AH,GH,GHKYZG,GHKYHW,ZWQD', OrderType:'ALL', }; // WebCode: 不含分销
const partnerParam = { WebCode: 'GHTOBHW,GHTOBZG' };
const [
{ total1: CHSalesDataTotal },
{ total1: CHPartnerSalesData },
{ total1: AHSalesDataTotal },
{ total1: AHpartnerSalesData },
{ total1: GHSalesDataTotal },
{ total1: GHpartnerSalesData },
] = await Promise.all([
getDepartmentOrderMLByType({...salesParam, DepartmentList: '1,2', WebCode: 'ALL', }),
getDepartmentOrderMLByType({...salesParam, DepartmentList: '1,2', ...partnerParam }),
getDepartmentOrderMLByType({...salesParam, DepartmentList: '28', WebCode: 'ALL', }),
getDepartmentOrderMLByType({...salesParam, DepartmentList: '28', ...partnerParam }),
getDepartmentOrderMLByType({...salesParam, DepartmentList: '33', WebCode: 'ALL', }),
getDepartmentOrderMLByType({...salesParam, DepartmentList: '33', ...partnerParam }),
]);
// 不含分销 = 总额 - 分销
const CHSalesData = {'COLI_CJCount': CHSalesDataTotal.COLI_CJCount-CHPartnerSalesData.COLI_CJCount, 'COLI_ML2': CHSalesDataTotal.COLI_ML2-CHPartnerSalesData.COLI_ML2};
const AHSalesData = {'COLI_CJCount': AHSalesDataTotal.COLI_CJCount-AHpartnerSalesData.COLI_CJCount, 'COLI_ML2': AHSalesDataTotal.COLI_ML2-AHpartnerSalesData.COLI_ML2};
const GHSalesData = {'COLI_CJCount': GHSalesDataTotal.COLI_CJCount-GHpartnerSalesData.COLI_CJCount, 'COLI_ML2': GHSalesDataTotal.COLI_ML2-GHpartnerSalesData.COLI_ML2};
const partnerSalesData = {'COLI_CJCount': CHPartnerSalesData.COLI_CJCount+AHpartnerSalesData.COLI_CJCount, 'COLI_ML2': CHPartnerSalesData.COLI_ML2+AHpartnerSalesData.COLI_ML2};
const totalSalesData = {
'COLI_CJCount': CHSalesDataTotal.COLI_CJCount + AHSalesDataTotal.COLI_CJCount + GHSalesDataTotal.COLI_CJCount,
'COLI_ML2': CHSalesDataTotal.COLI_ML2 + AHSalesDataTotal.COLI_ML2 + GHSalesDataTotal.COLI_ML2,
};
const yearStart = moment().startOf("year").format(DATE_FORMAT);
const yearEnd = moment().endOf("year").format(SMALL_DATETIME_FORMAT);
/** 截至今年 - 成交 */
const [
{ total1: CHSalesYearTotal },
{ total1: CHPartnerSalesYear },
{ total1: AHSalesYearTotal },
{ total1: AHpartnerSalesYear },
{ total1: GHSalesYearTotal },
{ total1: GHpartnerSalesYear },
] = await Promise.all([
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, DepartmentList: '1,2', WebCode: 'ALL', }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, DepartmentList: '1,2', ...partnerParam }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, DepartmentList: '28', WebCode: 'ALL', }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, DepartmentList: '28', ...partnerParam }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, DepartmentList: '33', WebCode: 'ALL', }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, DepartmentList: '33', ...partnerParam }),
]);
const CHDataYear = {'COLI_CJCount': CHSalesYearTotal.COLI_CJCount-CHPartnerSalesYear.COLI_CJCount, 'COLI_ML2': CHSalesYearTotal.COLI_ML2-CHPartnerSalesYear.COLI_ML2};
const AHDataYear = {'COLI_CJCount': AHSalesYearTotal.COLI_CJCount-AHpartnerSalesYear.COLI_CJCount, 'COLI_ML2': AHSalesYearTotal.COLI_ML2-AHpartnerSalesYear.COLI_ML2};
const GHDataYear = {'COLI_CJCount': GHSalesYearTotal.COLI_CJCount-GHpartnerSalesYear.COLI_CJCount, 'COLI_ML2': GHSalesYearTotal.COLI_ML2-GHpartnerSalesYear.COLI_ML2};
const partnerDataYear = {'COLI_CJCount': CHPartnerSalesYear.COLI_CJCount+AHpartnerSalesYear.COLI_CJCount, 'COLI_ML2': CHPartnerSalesYear.COLI_ML2+AHpartnerSalesYear.COLI_ML2};
const totalDataYear = {
'COLI_CJCount': CHSalesYearTotal.COLI_CJCount + AHSalesYearTotal.COLI_CJCount + GHSalesYearTotal.COLI_CJCount,
'COLI_ML2': CHSalesYearTotal.COLI_ML2 + AHSalesYearTotal.COLI_ML2 + GHSalesYearTotal.COLI_ML2,
};
/** 截至今年 - 走团 */
const [
{ total1: CHStartDataYearTotal },
{ total1: CHPartnerStartDataYear },
{ total1: AHStartDataYearTotal },
{ total1: AHpartnerStartDataYear },
{ total1: GHStartDataYearTotal },
{ total1: GHpartnerStartDataYear },
] = await Promise.all([
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, Date2:yearEnd, DateType: 'startDate', DepartmentList: '1,2', WebCode: 'ALL', }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, Date2:yearEnd, DateType: 'startDate', DepartmentList: '1,2', ...partnerParam }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, Date2:yearEnd, DateType: 'startDate', DepartmentList: '28', WebCode: 'ALL', }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, Date2:yearEnd, DateType: 'startDate', DepartmentList: '28', ...partnerParam }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, Date2:yearEnd, DateType: 'startDate', DepartmentList: '33', WebCode: 'ALL', }),
getDepartmentOrderMLByType({...salesParam, Date1: yearStart, Date2:yearEnd, DateType: 'startDate', DepartmentList: '33', ...partnerParam }),
]);
const CHStartDataYear = {'COLI_CJCount': CHStartDataYearTotal.COLI_CJCount-CHPartnerStartDataYear.COLI_CJCount, 'COLI_ML2': CHStartDataYearTotal.COLI_ML2-CHPartnerStartDataYear.COLI_ML2};
const AHStartDataYear = {'COLI_CJCount': AHStartDataYearTotal.COLI_CJCount-AHpartnerStartDataYear.COLI_CJCount, 'COLI_ML2': AHStartDataYearTotal.COLI_ML2-AHpartnerStartDataYear.COLI_ML2};
const GHStartDataYear = {'COLI_CJCount': GHStartDataYearTotal.COLI_CJCount-GHpartnerStartDataYear.COLI_CJCount, 'COLI_ML2': GHStartDataYearTotal.COLI_ML2-GHpartnerStartDataYear.COLI_ML2};
const partnerStartDataYear = {'COLI_CJCount': CHPartnerStartDataYear.COLI_CJCount+AHpartnerStartDataYear.COLI_CJCount, 'COLI_ML2': CHPartnerStartDataYear.COLI_ML2+AHpartnerStartDataYear.COLI_ML2};
const totalStartDataYear = {
'COLI_CJCount': CHStartDataYearTotal.COLI_CJCount + AHStartDataYearTotal.COLI_CJCount + GHStartDataYearTotal.COLI_CJCount,
'COLI_ML2': CHStartDataYearTotal.COLI_ML2 + AHStartDataYearTotal.COLI_ML2 + GHStartDataYearTotal.COLI_ML2,
};
const rows = [
{
key: 'ch',
label: 'CH(不含分销)',
YJLY: price_to_number(CHSalesData.COLI_ML2),
CJCount: (CHSalesData.COLI_CJCount),
rowYear: { YJLY: price_to_number(CHDataYear.COLI_ML2), CJCount: CHDataYear.COLI_CJCount, YJLY2: price_to_number(CHStartDataYear.COLI_ML2) },
},
{
key: 'ah',
label: 'AH(不含分销)',
YJLY: price_to_number(AHSalesData.COLI_ML2),
CJCount: (AHSalesData.COLI_CJCount),
rowYear: { YJLY: price_to_number(AHDataYear.COLI_ML2), CJCount: AHDataYear.COLI_CJCount, YJLY2: price_to_number(AHStartDataYear.COLI_ML2) },
},
{
key: 'partner',
label: '分销',
YJLY: price_to_number(partnerSalesData.COLI_ML2),
CJCount: (partnerSalesData.COLI_CJCount),
rowYear: { YJLY: price_to_number(partnerDataYear.COLI_ML2), CJCount: partnerDataYear.COLI_CJCount, YJLY2: price_to_number(partnerStartDataYear.COLI_ML2) },
},
{
key: 'gh',
label: 'GH(不含分销)',
YJLY: price_to_number(GHSalesData.COLI_ML2),
CJCount: (GHSalesData.COLI_CJCount),
rowYear: { YJLY: price_to_number(GHDataYear.COLI_ML2), CJCount: GHDataYear.COLI_CJCount, YJLY2: price_to_number(GHStartDataYear.COLI_ML2) },
},
{
key: 'total',
label: '合计',
YJLY: price_to_number(totalSalesData.COLI_ML2),
CJCount: (totalSalesData.COLI_CJCount),
rowYear: { YJLY: price_to_number(totalDataYear.COLI_ML2), CJCount: totalDataYear.COLI_CJCount, YJLY2: price_to_number(totalStartDataYear.COLI_ML2) },
},
];
runInAction(() => {
this.GHSalesTableData = rows;
this.GHSalesLoading = false;
});
};
GHServiceTableData = [];
GHServiceLoading = false;
/**
* 获取GH服务数据 ---------------------------------------------------------------------------------------------------
*/
dataGHService = async (param) => {
this.GHServiceLoading = true;
const serviceParam = { ...param, DateType: 'startDate', 'WebCode': 'All' };
// 走团数
const [
{ ordercountTotal1: { OrderCount: CHGroupCount } },
{ ordercountTotal1: { OrderCount: AHGroupCount } },
{ ordercountTotal1: { OrderCount: GHGroupCount } },
] = await Promise.all([
getOrderCountByType({ ...serviceParam, 'DepartmentList': '1,2', OrderType: 'Form'}),
getOrderCountByType({ ...serviceParam, 'DepartmentList': '28', OrderType: 'Form'}),
getOrderCountByType({ ...serviceParam, 'DepartmentList': '33', OrderType: 'Form'}),
]);
// 走团数 - 年
// * 走团: 整团数, 而非各地接社/目的地的总和
const yearStart = moment().startOf("year").format(DATE_FORMAT);
const [
{ ordercountTotal1: { OrderCount: CHGroupCountYear } },
{ ordercountTotal1: { OrderCount: AHGroupCountYear } },
{ ordercountTotal1: { OrderCount: GHGroupCountYear } },
] = await Promise.all([
getOrderCountByType({ ...serviceParam, 'DepartmentList': '1,2', OrderType: 'Form', Date1: yearStart, }),
getOrderCountByType({ ...serviceParam, 'DepartmentList': '28', OrderType: 'Form', Date1: yearStart, }),
getOrderCountByType({ ...serviceParam, 'DepartmentList': '33', OrderType: 'Form', Date1: yearStart, }),
]);
// 好评数
const [
{ total1: { GoodCount: CHGoodCount } },
{ total1: { GoodCount: AHGoodCount } },
{ total1: { GoodCount: GHGoodCount } },
] = await Promise.all([
getAgentGroupInfoALL({ ...serviceParam, 'DepartmentList': '1,2', }),
getAgentGroupInfoALL({ ...serviceParam, 'DepartmentList': '28', }),
getAgentGroupInfoALL({ ...serviceParam, 'DepartmentList': '33', }),
]);
// 好评数 - 年
const [
{ total1: { GoodCount: CHGoodCountYear, } },
{ total1: { GoodCount: AHGoodCountYear, } },
{ total1: { GoodCount: GHGoodCountYear, } },
] = await Promise.all([
getAgentGroupInfoALL({ ...serviceParam, Date1: yearStart, 'DepartmentList': '1,2', }),
getAgentGroupInfoALL({ ...serviceParam, Date1: yearStart, 'DepartmentList': '28', }),
getAgentGroupInfoALL({ ...serviceParam, Date1: yearStart, 'DepartmentList': '33', }),
]);
const rows = [
{ key: 'ch', label: 'CH', GoodCount: CHGoodCount, GroupCount: CHGroupCount, rowYear: { GroupCount: CHGroupCountYear, GoodCount: CHGoodCountYear } },
{ key: 'ah', label: 'AH', GoodCount: AHGoodCount, GroupCount: AHGroupCount, rowYear: { GroupCount: AHGroupCountYear, GoodCount: AHGoodCountYear } },
{ key: 'Gh', label: 'GH', GoodCount: GHGoodCount, GroupCount: GHGroupCount, rowYear: { GroupCount: GHGroupCountYear, GoodCount: GHGoodCountYear } },
];
// const GHRowWeek = { GoodCount: GHGoodCountWeek, GroupCount: 0 };
// const columnsSum = ['GoodCount', 'GroupCount'].reduce((r, col) => ({ ...r, [col]: [...rows, GHRowWeek].reduce((rr, row) => rr + (row[col] || 0), 0) }), {});
// const allYearData = rows.map((row) => row.rowYear).concat([{ GoodCount: GHGoodCountYear, GroupCount: 0 }]);
// // console.log(allYearData);
// const rowYear = ['GoodCount', 'GroupCount', ].reduce((r, col) => ({ ...r, [col]: allYearData.reduce((rr, row) => rr + (row[col] || 0), 0) }), {});
// rows.push({ key: 'columnSum', label: '合计', ...columnsSum, rowYear });
// console.log(rows);
runInAction(() => {
this.GHServiceTableData = rows;
this.GHServiceLoading = false;
});
};
}
export default MeetingData;

@ -227,9 +227,9 @@ class OrdersStore {
let url = "/service-web/QueryData/GetOrderCount";
url += `?OrderType=${ordertype}&OrderType_val=${ordertype_sub}&IncludeTickets=${this.include_tickets}`;
url += `&DepartmentList=${this.groups.toString()}&DateType=${this.date_type}`;
url += "&WebCode=" + this.webcode + "&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.DATE_FORMAT) + "%2023:59:59";
url += "&WebCode=" + this.webcode + "&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) {
url += "&COLI_ApplyDateold1=" + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "&COLI_ApplyDateold2=" + date_picker_store.end_date_cp.format(config.DATE_FORMAT) + "%2023:59:59";
url += "&COLI_ApplyDateold1=" + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "&COLI_ApplyDateold2=" + date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT);
}
fetch(config.HT_HOST + url)
.then(response => response.json())
@ -256,10 +256,10 @@ class OrdersStore {
const date_picker_store = this.rootStore.date_picker_store;
let url = "/service-web/QueryData/GetOrderCount"; // ?WebCode=cht&COLI_ApplyDate1=2022-08-01&COLI_ApplyDate2=2022-08-31&COLI_ApplyDateold1=2021-08-01&COLI_ApplyDateold2=2021-08-31';
url += "?WebCode=" + this.webcode + "&IncludeTickets=" + this.include_tickets;
url += "&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.DATE_FORMAT) + "%2023:59:59";
url += "&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT) ;
url += `&DepartmentList=${this.groups.toString()}&DateType=${this.date_type}`;
if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) {
url += "&COLI_ApplyDateold1=" + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "&COLI_ApplyDateold2=" + date_picker_store.end_date_cp.format(config.DATE_FORMAT) + "%2023:59:59";
url += "&COLI_ApplyDateold1=" + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "&COLI_ApplyDateold2=" + date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT) ;
}
fetch(config.HT_HOST + url)
.then(response => response.json())
@ -284,9 +284,9 @@ class OrdersStore {
let url = "/service-web/QueryData/GetOrderCountByType";
url += "?WebCode=" + this.webcode + "&OrderType=" + order_type + "&IncludeTickets=" + this.include_tickets;
url += `&DepartmentList=${this.groups.toString()}&DateType=${this.date_type}`;
url += "&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.DATE_FORMAT) + "%2023:59:59";
url += "&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) {
url += "&COLI_ApplyDateold1=" + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "&COLI_ApplyDateold2=" + date_picker_store.end_date_cp.format(config.DATE_FORMAT) + "%2023:59:59";
url += "&COLI_ApplyDateold1=" + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "&COLI_ApplyDateold2=" + date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT);
}
fetch(config.HT_HOST + url)
.then(response => response.json())
@ -311,9 +311,9 @@ class OrdersStore {
}
url += `?WebCode=${this.webcode}&OrderType=${ordertype}&OrderType_val=${ordertype_sub}&SubOrderType=${sub_type}` + "&IncludeTickets=" + this.include_tickets;
url += `&DepartmentList=${this.groups.toString()}&DateType=${this.date_type}`;
url += "&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.DATE_FORMAT) + "%2023:59:59";
url += "&COLI_ApplyDate1=" + date_picker_store.start_date.format(config.DATE_FORMAT) + "&COLI_ApplyDate2=" + date_picker_store.end_date.format(config.SMALL_DATETIME_FORMAT);
if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) {
url += "&COLI_ApplyDateold1=" + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "&COLI_ApplyDateold2=" + date_picker_store.end_date_cp.format(config.DATE_FORMAT) + "%2023:59:59";
url += "&COLI_ApplyDateold1=" + date_picker_store.start_date_cp.format(config.DATE_FORMAT) + "&COLI_ApplyDateold2=" + date_picker_store.end_date_cp.format(config.SMALL_DATETIME_FORMAT);
}
fetch(config.HT_HOST + url)
.then(response => response.json())

@ -37,6 +37,7 @@ class SaleStore {
WebCode: { key: 'All', label: '所有来源'},
IncludeTickets: { key: '1', label: '含门票'},
DepartmentList: [groupsMappedByCode.GH],
operator: '',
};
setSearchValues(obj, values) {
@ -44,10 +45,11 @@ class SaleStore {
this.webcode = obj.WebCode;
this.include_tickets = obj.IncludeTickets;
this.date_type = obj.DateType;
this.searchValues.operator = obj.operator;
}
salesTrade = {
groupType: 'dept', loading: false,
groupType: 'dept', loading: false, tableDataSource: [],
operator: [], dept: [], overview: [],
operatorMapped: {}, pickSales: [], pickSalesData: [],
};
@ -82,6 +84,7 @@ class SaleStore {
// 切换标签页
onChange_Tabs(active_key) {
this.active_tab_key = active_key;
this.type_data = {};
}
// 下单日期或者出发日期
@ -110,9 +113,9 @@ class SaleStore {
this.date_title = `${date1_start}~${date1_end}`;
let url = '/service-web/QueryData/GetDepartmentOrderML';
url += `?DepartmentList=${this.groups.toString()}&DateType=${this.date_type}&WebCode=${this.webcode}&IncludeTickets=${this.include_tickets}`;
url += `&Date1=${date1_start}&Date2=${date1_end}%2023:59:59`;
url += `&Date1=${date1_start}&Date2=${date1_end}%2023:59:00`;
if (date2_start && date2_end) {
url += `&OldDate1=${date2_start}&OldDate2=${date2_end}%2023:59:59`;
url += `&OldDate1=${date2_start}&OldDate2=${date2_end}%2023:59:00`;
this.date_title += ` ${date2_start}~${date2_end}`;
}
fetch(config.HT_HOST + url)
@ -152,9 +155,9 @@ class SaleStore {
this.date_title = `${date1_start}~${date1_end}`;
let url = '/service-web/QueryData/GetDepartmentOrderMLByType';
url += `?DepartmentList=${this.groups.toString()}&DateType=${this.date_type}&OrderType=${this.active_tab_key}&WebCode=${this.webcode}&IncludeTickets=${this.include_tickets}`;
url += `&Date1=${date1_start}&Date2=${date1_end}%2023:59:59`;
url += `&Date1=${date1_start}&Date2=${date1_end}%2023:59:00`;
if (date2_start && date2_end) {
url += `&OldDate1=${date2_start}&OldDate2=${date2_end}%2023:59:59`;
url += `&OldDate1=${date2_start}&OldDate2=${date2_end}%2023:59:00`;
this.date_title += ` ${date2_start}~${date2_end}`;
}
fetch(config.HT_HOST + url)
@ -164,35 +167,42 @@ class SaleStore {
// eslint-disable-next-line no-constant-condition
if (false) {
} else {
const result1 = comm.isEmpty(this.searchValues.operator) ? json.result1 : json.result1.filter(row => (this.searchValues.operator.split(',')).includes(String(row.OPI_SN)));
const result2 = comm.isEmpty(this.searchValues.operator) ? json.result2 : json.result2.filter(row => (this.searchValues.operator.split(',')).includes(String(row.OPI_SN)));
const allOPI1 = comm.uniqWith(result1.map(rr => ({ text: rr.OPI_Name, value: rr.OPI_SN })), (a, b) => JSON.stringify(a) === JSON.stringify(b)).sort((a, b) => a.text.localeCompare(b.text));
if (this.active_tab_key === 'All') {
result.columns = [
{
title: '',
title: '账户名',
children: [
{
title: '',
dataIndex: 'OPI_Name',
},
],
sorter: (a, b) => (a?.OPI_Name || '').localeCompare(b.OPI_Name, 'zh-CN'),
filters: allOPI1,
onFilter: (value, record) => record.OPI_SN === value,
filterSearch: true,
},
{
title: '毛利',
children: [{ title: json.result1.reduce((a, b) => a + comm.price_to_number(b.COLI_ML), 0), dataIndex: 'COLI_ML' }],
children: [{ title: result1.reduce((a, b) => a + comm.price_to_number(b.COLI_ML), 0), dataIndex: 'COLI_ML' }],
sorter: (a, b) => comm.price_to_number(b.COLI_ML) - comm.price_to_number(a.COLI_ML),
},
{
title: '成行率',
children: [{ title: comm.formatPercent(json.result1.reduce((a, b) => a + b.COLI_CJCount, 0) / json.result1.reduce((a, b) => a + b.COLI_OrderCount, 0)), dataIndex: 'COLI_CJrate' }],
children: [{ title: comm.formatPercent(result1.reduce((a, b) => a + b.COLI_CJCount, 0) / result1.reduce((a, b) => a + b.COLI_OrderCount, 0)), dataIndex: 'COLI_CJrate' }],
sorter: (a, b) => parseInt(b.COLI_CJrate) - parseInt(a.COLI_CJrate),
},
{
title: '成团数',
children: [{ title: json.result1.reduce((a, b) => a + b.COLI_CJCount, 0), dataIndex: 'COLI_CJCount' }],
children: [{ title: result1.reduce((a, b) => a + b.COLI_CJCount, 0), dataIndex: 'COLI_CJCount' }],
sorter: (a, b) => b.COLI_CJCount - a.COLI_CJCount,
},
{
title: '订单数',
children: [{ title: json.result1.reduce((a, b) => a + b.COLI_OrderCount, 0), dataIndex: 'COLI_OrderCount' }],
children: [{ title: result1.reduce((a, b) => a + b.COLI_OrderCount, 0), dataIndex: 'COLI_OrderCount' }],
sorter: (a, b) => b.COLI_OrderCount - a.COLI_OrderCount,
},
{
@ -212,7 +222,7 @@ class SaleStore {
},
];
result.dataSource = json.result1;
result.dataSource = result1;
} else if (this.active_tab_key === 'ResponseRateByWL') {
result.columns = [
{
@ -223,49 +233,55 @@ class SaleStore {
dataIndex: 'OPI_Name',
},
],
sorter: (a, b) => (a?.OPI_Name || '').localeCompare(b.OPI_Name, 'zh-CN'),
filters: allOPI1,
onFilter: (value, record) => record.OPI_SN === value,
filterSearch: true,
},
{
title: '报价次数',
children: [{ title: json.result1.reduce((a, b) => a + b.PriceTime, 0), dataIndex: 'PriceTime' }],
children: [{ title: result1.reduce((a, b) => a + b.PriceTime, 0), dataIndex: 'PriceTime' }],
sorter: (a, b) => b.PriceTime - a.PriceTime,
},
{
title: '邮件发送次数',
children: [{ title: json.result1.reduce((a, b) => a + b.mailSendTime, 0), dataIndex: 'mailSendTime' }],
children: [{ title: result1.reduce((a, b) => a + b.mailSendTime, 0), dataIndex: 'mailSendTime' }],
sorter: (a, b) => b.mailSendTime - a.mailSendTime,
},
{
title: 'WhatsApp客人会话次数',
children: [{ title: json.result1.reduce((a, b) => a + b.WhatsAppGuestChatCount, 0), dataIndex: 'WhatsAppGuestChatCount' }],
children: [{ title: result1.reduce((a, b) => a + b.WhatsAppGuestChatCount, 0), dataIndex: 'WhatsAppGuestChatCount' }],
sorter: (a, b) => b.WhatsAppGuestChatCount - a.WhatsAppGuestChatCount,
},
{
title: 'WhatsApp外联会话次数',
children: [{ title: json.result1.reduce((a, b) => a + b.WhatsAppWLChatCount, 0), dataIndex: 'WhatsAppWLChatCount' }],
children: [{ title: result1.reduce((a, b) => a + b.WhatsAppWLChatCount, 0), dataIndex: 'WhatsAppWLChatCount' }],
sorter: (a, b) => b.WhatsAppWLChatCount - a.WhatsAppWLChatCount,
},
{
title: 'WhatsApp新增客户数',
children: [{ title: json.result1.reduce((a, b) => a + b.WhatsAppNewGuestCount, 0), dataIndex: 'WhatsAppNewGuestCount' }],
children: [{ title: result1.reduce((a, b) => a + b.WhatsAppNewGuestCount, 0), dataIndex: 'WhatsAppNewGuestCount' }],
sorter: (a, b) => b.WhatsAppNewGuestCount - a.WhatsAppNewGuestCount,
},
{
title: '微信客人会话次数',
children: [{ title: json.result1.reduce((a, b) => a + b.WXGuestChatCount, 0), dataIndex: 'WXGuestChatCount' }],
children: [{ title: result1.reduce((a, b) => a + b.WXGuestChatCount, 0), dataIndex: 'WXGuestChatCount' }],
sorter: (a, b) => b.WXGuestChatCount - a.WXGuestChatCount,
},
{
title: '微信外联会话次数',
children: [{ title: json.result1.reduce((a, b) => a + b.WXWLChatCount, 0), dataIndex: 'WXWLChatCount' }],
children: [{ title: result1.reduce((a, b) => a + b.WXWLChatCount, 0), dataIndex: 'WXWLChatCount' }],
sorter: (a, b) => b.WXWLChatCount - a.WXWLChatCount,
},
{
title: '微信新增客户数',
children: [{ title: json.result1.reduce((a, b) => a + b.WXNewGuestCount, 0), dataIndex: 'WXNewGuestCount' }],
children: [{ title: result1.reduce((a, b) => a + b.WXNewGuestCount, 0), dataIndex: 'WXNewGuestCount' }],
sorter: (a, b) => b.WXNewGuestCount - a.WXNewGuestCount,
},
];
result.dataSource = json.result1.sort(comm.sortBy('PriceTime')).reverse();
result.dataSource = result1
.sort(comm.sortBy('PriceTime'))
.reverse();
} else if (this.active_tab_key === 'ResponseRateWhatsApp') {
result.columns = [
{
@ -276,6 +292,10 @@ class SaleStore {
dataIndex: 'OPI_Name',
},
],
sorter: (a, b) => (a?.OPI_Name || '').localeCompare(b.OPI_Name, 'zh-CN'),
filters: allOPI1,
onFilter: (value, record) => record.OPI_SN === value,
filterSearch: true,
},
{
title: '首次回复率',
@ -341,11 +361,13 @@ class SaleStore {
sorter: (a, b) => b.COLI_ConfirmTimeAVG - a.COLI_ConfirmTimeAVG,
},
];
result.dataSource = json.result1.sort(comm.sortBy('COLI_ConfirmTimeAVG')).reverse();
result.dataSource = result1
.sort(comm.sortBy('COLI_ConfirmTimeAVG'))
.reverse();
} else {
const diffDateFlagYes = !comm.isEmpty(date_moment.start_date_cp);
// if (this.active_tab_key == "Country")
const mergeDiffData = calcDiff({result1: json.result1, result2: json.result2});
const mergeDiffData = calcDiff({result1, result2});
// 获取类型的项目,去掉重复,作为列名
const type_name_arr = [];
mergeDiffData.map((item) => {
@ -362,7 +384,7 @@ class SaleStore {
const total_data_value = items.length ? items.reduce((a, b) => a + b.COLI_YJLY, 0) : ''; // 记录累加
const total_data_value_diff = items.length ? items.reduce((a, b) => a + b.COLI_YJLY2, 0) : ''; // 记录累加
if (comm.empty(type_data[op_sn])) {
type_data[op_sn] = [{ key: item.OPI_SN }, { T_name: item.OPI_Name }, { T_total: total_data_value }, { V_total: total_data_value_diff }];
type_data[op_sn] = [{ key: item.OPI_SN }, { T_name: item.OPI_Name }, { T_OPI: item.OPI_SN }, { T_total: total_data_value }, { V_total: total_data_value_diff }];
}
const _diff = comm.objectMapper(comm.pick(item, ['COLI_YJLY_diff', 'COLI_YJLY_vs', 'COLI_YJLY2']), {
COLI_YJLY2: { key: `v_${item.SubTypeSN}` },
@ -394,7 +416,12 @@ class SaleStore {
totalDiff.diff = (totalDiff.val-totalDiff.diffVal);
totalDiff.vs = comm.fixTo2Decimals(((totalDiff.val-totalDiff.diffVal)/totalDiff.diffVal)*100)+'%';
result.columns.push(
{ title: '顾问', children: [{ title: '', dataIndex: 'T_name', render: (text, record) => <NavLink to={`/sale_sub/${this.active_tab_key}`}>{text}</NavLink> }] },
{ title: '顾问', children: [{ title: '', dataIndex: 'T_name', render: (text, record) => <NavLink to={`/sale_sub/${this.active_tab_key}`}>{text}</NavLink> }],
sorter: (a, b) => (a?.T_name || '').localeCompare(b.T_name, 'zh-CN'),
filters: allOPI1,
onFilter: (value, record) => record.T_OPI === value,
filterSearch: true,
},
{
title: '合计',
children: [
@ -453,9 +480,9 @@ class SaleStore {
const date2_end = comm.empty(date_moment.end_date_cp) ? '' : date_moment.end_date_cp.format(config.DATE_FORMAT);
let url = '/service-web/QueryData/GetDepartmentOrderMLByType_sub';
url += `?DepartmentList=${this.groups.toString()}&DateType=${this.date_type}&subType=${type_sub}&subTypeVal=-1&WebCode=${this.webcode}&IncludeTickets=${this.include_tickets}`;
url += `&Date1=${date1_start}&Date2=${date1_end}%2023:59:59`;
url += `&Date1=${date1_start}&Date2=${date1_end}%2023:59:00`;
if (date2_start && date2_end) {
url += `&OldDate1=${date2_start}&OldDate2=${date2_end}%2023:59:59`;
url += `&OldDate1=${date2_start}&OldDate2=${date2_end}%2023:59:00`;
this.date_title += ` ${date2_start}~${date2_end}`;
}
fetch(config.HT_HOST + url)
@ -469,6 +496,7 @@ class SaleStore {
{
title: '',
dataIndex: 'OPI_Name',
sorter: (a, b) => (a?.OPI_Name || '').localeCompare(b.OPI_Name, 'zh-CN'),
},
{
title: '毛利',
@ -529,15 +557,36 @@ class SaleStore {
});
}
setTableDataSource = (merge = false) => {
if (comm.isEmpty(this.salesTrade[this.salesTrade.groupType]) || comm.isEmpty(this.salesTrade.operator)) {
return false;
}
this.salesTrade.tableDataSource = [].concat(this.salesTrade[this.salesTrade.groupType], merge === false ? this.salesTrade.operator : this.salesTrade.operatorAccount);
};
async fetchOperatorTradeData(groupType, queryData) {
this.salesTrade.loading = true;
const param1 = Object.assign({}, queryData, {groupType, groupDateType: 'year' });
const yearData = await this.fetchTradeDataAll(param1);
const { mergeRows: yearMergeRows } = yearData.result1;
const yData = parseSaleData(yearMergeRows, ['groupsKey', 'groupDateType']);
const param2 = Object.assign({}, queryData, {groupType, groupDateType: 'month' });
const monthData = await this.fetchTradeDataAll(param2);
const { mergeRows: monthMergeRows } = monthData.result1;
const { salesTradeDataMapped, accountMergeYearMonth } = await this.handleYearMonthData(groupType, queryData, yearData, monthData);
runInAction(() => {
this.salesTrade.loading = false;
this.salesTrade[groupType] = Object.values(salesTradeDataMapped).sort(comm.sortBy('yearML')).reverse();
this.salesTrade[`${groupType}Account`] = accountMergeYearMonth.sort(comm.sortBy('yearML')).reverse();
this.salesTrade[`${groupType}Mapped`] = Object.values(salesTradeDataMapped).reduce((r, v) => ({...r, [v.groupsKey]: Object.values(v.mData)}), {});
});
}
handleYearMonthData = async (groupType, queryData, yearData, monthData) => {
const { mergeRows: yearMergeRows, mergeLabelsRows: yearMergeLabelsRows } = yearData.result1;
const { mergeRows: monthMergeRows, mergeLabelsRows: monthMergeLabelsRows } = monthData.result1;
const yData = parseSaleData(yearMergeRows, ['groupsKey', 'groupDateType']);
const mData = parseSaleData(monthMergeRows, ['groupsKey', 'groupDateType']);
const mergeYearMonth = Object.keys(yData).map(ykey => ({
...yData[ykey],
@ -547,6 +596,17 @@ class SaleStore {
yearML: Object.values(yData[ykey].data)[0]?.SumML || 0, // 整理排序用
}));
const accountYData = parseSaleData(yearMergeLabelsRows, ['groupsKey', 'groupDateType']);
const accountMData = parseSaleData(monthMergeLabelsRows, ['groupsKey', 'groupDateType']);
const accountMergeYearMonth = Object.keys(accountYData).map(ykey => ({
...accountYData[ykey],
mData: accountMData[ykey].data,
yData: Object.values(accountYData[ykey].data)[0],
data: undefined,
yearML: Object.values(accountYData[ykey].data)[0]?.SumML || 0, // 整理排序用
}));
const accountSalesTradeDataMapped = accountMergeYearMonth.reduce((ro, vo) => ({...ro, [vo.groupsKey]: vo}), {});
const kpiObjects = mergeYearMonth.map(v => ({ key: v.groupsKey, value: v.groupsKey, label: v.groupsLabel }));
const kpiData = await this.getKPISettings(groupType, queryData, kpiObjects);
const salesTradeDataMapped = mergeYearMonth.reduce((ro, vo) => ({...ro, [vo.groupsKey]: vo}), {});
@ -560,18 +620,16 @@ class SaleStore {
if (comm.isEmpty(salesTradeDataMapped[ele.object_id].mData[`month_${padM}`])) {
salesTradeDataMapped[ele.object_id].mData[`month_${padM}`] = {};
}
// set
Object.assign(salesTradeDataMapped[ele.object_id].mData[`month_${padM}`], _monthMLKPI);
return km;
});
return ele;
});
runInAction(() => {
this.salesTrade.loading = false;
this.salesTrade[groupType] = Object.values(salesTradeDataMapped).sort(comm.sortBy('yearML')).reverse();
this.salesTrade[`${groupType}Mapped`] = Object.values(salesTradeDataMapped).reduce((r, v) => ({...r, [v.groupsKey]: Object.values(v.mData)}), {});
});
}
return { salesTradeDataMapped, accountSalesTradeDataMapped, accountMergeYearMonth };
};
async getKPISettings(curObject, queryData, objects) {
const getkpiParam = comm.objectMapper(queryData, {

@ -0,0 +1,210 @@
import { makeAutoObservable, runInAction } from 'mobx';
import { fetchJSON } from '../utils/request';
import { isEmpty, sortDescBy, groupBy, pick, unique } from '../utils/commons';
import { groupsMappedByCode } from './../libs/ht';
import { DATE_FORMAT, SMALL_DATETIME_FORMAT } from './../config';
import moment from 'moment';
const fetchResultsData = async (param) => {
const defaultParam = {
WebCode: 'All',
DepartmentList: '',
opisn: -1,
Date1: '',
Date2: '',
groupType: '',
groupDateType: '',
};
const json = await fetchJSON('/service-Analyse2/sales_crm_results', { ...defaultParam, ...param });
return json.errcode === 0 ? json.result : [];
};
const fetchProcessData = async (param) => {
const defaultParam = {
WebCode: 'All',
DepartmentList: '',
opisn: -1,
Date1: '',
Date2: '',
groupType: '',
groupDateType: '',
};
const json = await fetchJSON('/service-Analyse2/sales_crm_process', { ...defaultParam, ...param });
return json.errcode === 0 ? json.result : [];
};
const fetchRiskDetailData = async (param) => {
const defaultParam = {
opisn: -1,
DateType: '',
WebCode: 'All',
DepartmentList: '',
Date1: '',
Date2: '',
IncludeTickets: '1',
};
const json = await fetchJSON('/service-Analyse2/sales_crm_process_detail', {...defaultParam, ...param});
return json.errcode === 0 ? json.result : [];
};
class SalesCRMData {
constructor(appStore) {
this.appStore = appStore;
makeAutoObservable(this);
}
async get90n180Data(param = {}) {
const retProps = param?.retLabel || '';
let retKey = param.groupDateType === '' ? (param.groupType === 'overview' ? 'dataSource' : 'details') : 'byDate';
retKey = param.opisn ? `operator_${param.opisn}`: retKey;
if (param.opisn ) {
if (!isEmpty(this.results.details)) {
const _this_opi_row = this.results.details.filter(ele => ele.groupsKey === param.opisn);
this.results[retKey] = _this_opi_row;
return;
}
if (!isEmpty(this.results[retKey])) {
return;
}
}
this.results.loading = true;
const date90=this.searchValues.date90;
const date180=this.searchValues.date180;
const [result90, result180] = await Promise.all([
fetchResultsData({ ...this.searchValuesToSub, ...date90, ...param }),
fetchResultsData({ ...this.searchValuesToSub, ...date180, ...param }),
]);
const _90O = groupBy(result90, 'groupsKey');
const _180O = groupBy(result180, 'groupsKey');
const result2 = unique(Object.keys(_90O).concat(Object.keys(_180O))).map((key) => {
return {
...pick(_90O[key]?.[0] || _180O[key][0], ['groupsKey', 'groupsLabel', 'groupType']),
...(retProps && retKey === 'dataSource' ? { groupsLabel: retProps, retProps } : { retProps }),
key: `${param.groupType}-${key}`,
result90: _90O[key]?.[0] || {},
result180: _180O[key]?.[0] || {},
};
});
// console.log(result2, '+++++ +++', retKey);
// console.log(this.results[retKey]?.length);
runInAction(() => {
this.results.loading = false;
this.results[retKey] = [].concat((this.results[retKey] || []), result2);
});
return this.results;
}
async getResultData(param = {}) {
let retKey = param.groupDateType === '' ? 'byOperator' : 'byDate';
retKey = param.opisn ? `operator_byDate_${param.opisn}`: retKey;
if (!isEmpty(this.results[retKey])) {
return;
}
this.results.loading = true;
this.results[retKey] = [];
const res = await fetchResultsData({ ...this.searchValuesToSub, ...param });
runInAction(() => {
this.results.loading = false;
this.results[retKey] = retKey === 'byOperator' ? res.filter(ele => ele.SumML > 0).sort(sortDescBy('SumML')) : res;
});
return this.results;
};
async getProcessData(param = {}) {
// const retKey = param.groupDateType === '' ? 'byOperator' : 'byDate';
let retKey = param.groupDateType === '' ? (param.groupType !== 'operator' ? 'dataSource' : 'details') : 'byDate';
retKey = param.opisn ? `operator_${param.opisn}`: retKey;
if (param.opisn) {
if (!isEmpty(this.process.details)) {
const _this_opi_row = this.process.details.filter(ele => ele.groupsKey === param.opisn);
this.process[retKey] = _this_opi_row;
return;
}
if (!isEmpty(this.process[retKey])) {
return;
}
}
this.process.loading = true;
this.process[retKey] = [];
const res = await fetchProcessData({ ...this.searchValuesToSub, ...param });
runInAction(() => {
this.process.loading = false;
this.process[retKey] = [].concat(this.process[retKey], res);
});
}
async getRiskDetailData(param = {}) {
this.risk.loading = true;
this.risk.dataSource = [];
const res = await fetchRiskDetailData({ ...this.searchValuesToSub, ...param });
runInAction(() => {
this.risk.loading = false;
this.risk.dataSource = res;
this.risk.byLostType = groupBy(res, 'lost_type');
});
}
searchValues = {
date: moment(),
Date1: moment().startOf("week").subtract(7, "days"),
Date2: moment().endOf("week").subtract(7, "days"),
DateType: { key: 'applyDate', label: '提交日期'},
WebCode: { key: 'All', label: '所有来源' },
// IncludeTickets: { key: '1', label: '含门票'},
DepartmentList: [groupsMappedByCode.GH],
operator: '-1',
opisn: '-1',
date90: {
Date1: moment().subtract(90, 'days').format(DATE_FORMAT),
Date2: moment().subtract(30, 'days').format(SMALL_DATETIME_FORMAT),
},
date180: {
Date1: moment().subtract(180, 'days').format(DATE_FORMAT),
Date2: moment().subtract(50, 'days').format(SMALL_DATETIME_FORMAT),
}
};
searchValuesToSub = {
date: moment().format(DATE_FORMAT),
Date1: moment().startOf("week").subtract(7, "days").format(DATE_FORMAT),
Date2: moment().endOf("week").subtract(7, "days").format(SMALL_DATETIME_FORMAT),
DateType: 'applyDate',
DepartmentList: groupsMappedByCode.GH.value,
WebCode: 'All',
operator: '-1',
opisn: '-1',
};
setSearchValues(obj, values) {
this.searchValues = { ...this.searchValues, ...values };
if (values.date) {
this.searchValues.date90 = {
Date1: (values.date.clone()).subtract(90, 'days').format(DATE_FORMAT),
Date2: (values.date.clone()).subtract(30, 'days').format(SMALL_DATETIME_FORMAT),
};
this.searchValues.date180 = {
Date1: (values.date.clone()).subtract(180, 'days').format(DATE_FORMAT),
Date2: (values.date.clone()).subtract(50, 'days').format(SMALL_DATETIME_FORMAT),
};
}
this.searchValuesToSub = {...this.searchValuesToSub, ...obj};
}
results = { loading: false, dataSource: [], details: [], byDate: [], byOperator: [] };
process = { loading: false, dataSource: [], details: [], byDate: [], byOperator: [] };
risk = { loading: false, dataSource: [], byLostType: {}, };
resetData = (rootKey = '') => {
if (rootKey === '') {
return false;
}
this[rootKey].loading = false;
for (const key of Object.keys(this[rootKey])) {
if (key !== 'loading') {
this[rootKey][key] = [];
}
}
};
}
export default SalesCRMData;

@ -392,7 +392,8 @@ export const parseMergeItem = ({traditional, biz}) => {
const mergeRes = [].concat(traditional[resKey], biz[resKey]);
const groupsData = mergeRes.reduce((r, v) => {
if (v.groupsKey ) {
(r[v.groupsKey] || (r[v.groupsKey] = [])).push(v);
const groupsKeyLower = v.groupsKey.toLowerCase();
(r[groupsKeyLower] || (r[groupsKeyLower] = [])).push(v);
}
return r;
}, {});
@ -415,8 +416,8 @@ export const parseMergeItem = ({traditional, biz}) => {
// ConfirmRatesKPIrates: 0, // todo:
}));
// 按对象汇总
const TMapped = traditional[resKey].reduce((r, v) => ({...r, [v.groupsKey]: v}), {});
const BMapped = biz[resKey].reduce((r, v) => ({...r, [v.groupsKey]: v}), {});
const TMapped = traditional[resKey].reduce((r, v) => ({...r, [(v.groupsKey.toLowerCase())]: v}), {});
const BMapped = biz[resKey].reduce((r, v) => ({...r, [(v.groupsKey.toLowerCase())]: v}), {});
const summary = Object.keys(groupsData).map(groupsKey => {
const _groupsKey = groupsKey; // groupsData[groupsKey]?.[0]?.groupsKey || '';
return ['ConfirmOrder', 'SumOrder', 'SumML', 'transactions', 'SumPersonNum'].reduce(
@ -435,9 +436,9 @@ export const parseMergeItem = ({traditional, biz}) => {
}));
// 按每一行
// const ByDate = sortKeys(groupBy(mergeRes, ele => ele.groupDateVal));
const allRowsKeysData = groupBy(mergeRes, ele => `${ele.groupsKey}@${ele.groupDateVal}`);
const allRowsKeysData = groupBy(mergeRes, ele => `${ele.groupsKey.toLowerCase()}@${ele.groupDateVal}`);
const mergeRows = Object.keys(allRowsKeysData).map(rkey => {
const _groupsKey = allRowsKeysData[rkey]?.[0]?.groupsKey || '';
const _groupsKey = (allRowsKeysData[rkey]?.[0]?.groupsKey || '').toLowerCase();
const sumFields = ['ConfirmOrder', 'SumOrder', 'SumML', 'transactions', 'SumPersonNum'].reduce(
(r, skey) => ({ ...r,
[skey]: allRowsKeysData[rkey].reduce((a, c) => a + c[skey], 0),
@ -456,7 +457,32 @@ export const parseMergeItem = ({traditional, biz}) => {
OrderKPIrates: row.OrderKPIvalue ? fixTo2Decimals((row.SumOrder/row.OrderKPIvalue)*100) : 0,
// ConfirmRatesKPIrates: 0, // todo:
}));
return Object.assign(res, { [resKey]: Object.assign({}, mergeItem, { mergeRows, summary, summaryRows }) });
// 合并账户
const allRowsLabelsData = groupBy(mergeRes, ele => `${ele.groupsLabel.replace(/\([^)]*\)/gi, '').toLowerCase()}@${ele.groupDateVal}`);
const mergeLabelsRows = Object.keys(allRowsLabelsData).map(rkey => {
const _groupsLabel = rkey.split('@')[0].toLowerCase();
const _groupsKey = (allRowsLabelsData[rkey]?.[0]?.groupsKey || '').toLowerCase();
const sumFields = [
'ConfirmOrder', 'SumOrder', 'SumML', 'transactions', 'SumPersonNum',
'ConfirmOrderKPIvalue', 'OrderKPIvalue', 'MLKPIvalue',
].reduce(
(r, skey) => ({ ...r,
[skey]: allRowsLabelsData[rkey].reduce((a, c) => a + c[skey], 0),
// [`${skey}_arr`]: [ _groupsKey ? TMapped?.[_groupsKey]?.[skey] || 0 : 0, _groupsKey ? BMapped?.[_groupsKey]?.[skey] || 0 : 0,],
}),
allRowsLabelsData[rkey]?.[0] || {}
);
const pickFields = pick(allRowsLabelsData[rkey], Object.keys(allRowsLabelsData[rkey]).filter(_k => _k.endsWith('KPIvalue') ));
return {...pickFields, ...sumFields, groupsLabel: _groupsLabel, groupsKey: _groupsLabel };
}, {}).map(row => ({...row,
ConfirmRates: row.SumOrder ? fixTo2Decimals((row.ConfirmOrder/row.SumOrder)*100) : 0,
MLKPIrates: row.MLKPIvalue ? fixTo2Decimals((row.SumML/row.MLKPIvalue)*100) : 0,
OrderValue: row.SumOrder ? fixToInt((row.SumML/row.SumOrder)) : 0,
ConfirmOrderKPIrates: row.ConfirmOrderKPIvalue ? fixTo2Decimals((row.ConfirmOrder/row.ConfirmOrderKPIvalue)*100) : 0,
OrderKPIrates: row.OrderKPIvalue ? fixTo2Decimals((row.SumOrder/row.OrderKPIvalue)*100) : 0,
// ConfirmRatesKPIrates: 0, // todo:
}));
return Object.assign(res, { [resKey]: Object.assign({}, mergeItem, { mergeRows, summary, summaryRows, mergeLabelsRows }) });
}, {});
};

@ -72,6 +72,14 @@ export function formatPercent(number) {
return Math.round(number * 100) + "%";
}
export function formatPercentToFloat(number) {
return parseFloat((number * 100).toFixed(2)) + "%";
}
export function percentToDecimal(number) {
return parseFloat(number) / 100;
}
export function formatDate(date) {
if (isEmpty(date)) {
return "NaN";
@ -256,7 +264,7 @@ export function unique(arr) {
const x = new Set(arr);
return [...x];
}
export const uniqWith = (arr, fn) => arr.filter((element, index) => arr.findIndex((step) => fn(element, step)) === index);
export function getWeek(date) {
// 参数时间戳
const week = moment(date).day();
@ -290,17 +298,36 @@ export function set_array_index(result) {
}
/**
* 排序
* 数组排序
*/
export const sortBy = (key) => {
return (a, b) => (a[key] > b[key]) ? 1 : ((b[key] > a[key]) ? -1 : 0);
};
export const sortDescBy = (key) => {
return (a, b) => (a[key] < b[key]) ? 1 : ((b[key] < a[key]) ? -1 : 0);
};
/**
* Object排序keys
*/
export const sortKeys = (obj) =>
Object.keys(obj)
.sort()
.reduce((a, k2) => ({...a, [k2]: obj[k2]}), {});
/**
* 数组排序, 给定排序数组
* @param {array} items 需要排序的数组
* @param {array} keyName 排序的key
* @param {array} keyOrder 给定排序
* @returns
*/
export const sortArrayByOrder = (items, keyName, keyOrder) => {
return items.sort((a, b) => {
return keyOrder.indexOf(a[keyName]) - keyOrder.indexOf(b[keyName]);
});
};
/**
* 合并Object, 递归地
*/
@ -341,7 +368,7 @@ export function merge(...objects) {
*/
export function groupBy(array, callback) {
return array.reduce((groups, item) => {
const key = callback(item);
const key = typeof callback === 'function' ? callback(item) : item[callback];
if (!groups[key]) {
groups[key] = [];
@ -466,6 +493,7 @@ export function objectMapper(input, keyMap) {
if (map) {
let value = input[key];
if (map.transform) value = map.transform(value);
if (typeof map === 'string') mappedObj[map] = value;
mappedObj[map.key || key] = value;
}
}
@ -541,9 +569,16 @@ export const numberFormatter = (number) => {
return new Intl.NumberFormat().format(number);
};
/**
* @example
* const obj = { a: { b: 'c' } };
* const keyArr = ['a', 'b'];
* getNestedValue(obj, keyArr); // Returns: 'c'
*/
export const getNestedValue = (obj, keyArr) => {
return keyArr.reduce((acc, curr) => {
return acc && acc[curr];
return acc && acc.hasOwnProperty(curr) ? acc[curr] : undefined;
// return acc && acc[curr];
}, obj);
};

@ -20,7 +20,7 @@ const AgentGroupCount = () => {
return (
<>
<Space direction="vertical" style={{ width: '100%' }}>
<Row gutter={16} className='sticky-top' >
<Row gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"} >
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
@ -28,12 +28,12 @@ const AgentGroupCount = () => {
...date_picker_store.formValues,
...customerServicesStore.searchValues,
},
shows: ['agency', 'DateType', 'DepartmentList', 'countryArea', 'dates'],
shows: ['agency', 'departureDateType', 'DepartmentList', 'countryArea', 'dates'],
fieldProps: {
DepartmentList: { show_all: true },
DepartmentList: { show_all: true, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
dates: { hide_vs: true },
DateType: { disabledKeys: ['applyDate'] },
years: { hide_vs: true },
departureDateType: { disabledKeys: ['applyDate'] },
},
}}
onSubmit={(_err, obj, form) => {
@ -46,6 +46,9 @@ const AgentGroupCount = () => {
<Row>
<Col span={24}>
<Typography.Title level={3}>地接社团信息</Typography.Title>
<Divider orientation="right" plain>
<TableExportBtn label={'地接社团信息'} {...{ columns: agentGroupListColumns, dataSource: agentGroupList }} />
</Divider>
<Table
sticky
id="agentGroupList"
@ -57,9 +60,6 @@ const AgentGroupCount = () => {
pagination={false}
scroll={{ x: 1000 }}
/>
<Divider orientation="right" plain>
<TableExportBtn label={'地接社团信息'} {...{ columns: agentGroupListColumns, dataSource: agentGroupList }} />
</Divider>
</Col>
</Row>
</Space>

@ -32,12 +32,12 @@ const AgentGroupList = () => {
...date_picker_store.formValues,
...customerServicesStore.searchValues,
},
shows: ['DateType', 'DepartmentList', 'dates'],
shows: ['departureDateType', 'DepartmentList', 'dates'],
fieldProps: {
DepartmentList: { show_all: true },
WebCode: { show_all: false, mode: 'multiple' },
dates: { hide_vs: true },
DateType: { disabledKeys: ['applyDate'] },
departureDateType: { disabledKeys: ['applyDate'] },
},
}}
onSubmit={(_err, obj, form, str) => {

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

@ -2,7 +2,7 @@ import { useContext, useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { useParams, useLocation } from 'react-router-dom';
import { stores_Context } from '../config';
import { Row, Col, Spin, Table, Select, Typography, Card, Button, Space, Divider } from 'antd';
import { Row, Col, Spin, Table, Select, Typography, Card, Button, Space, Divider, Alert } from 'antd';
import { cloneDeep, groupBy, isEmpty, omit, pick, sortBy, unique, cartesianProductArray } from '../utils/commons';
import { dataFieldAlias, pivotBy } from '../libs/ht';
import SearchForm from '../components/search/SearchForm';
@ -21,17 +21,36 @@ const filterFields = [
{ key: 'COLI_LineClass', label: '页面渠道' },
{ key: 'guestGroupType', label: '客群类别' },
{ key: 'travelMotivation', label: '出行目的' },
// { key: 'operatorName', label: '' },
// { key: 'WebCode', label: '' },
// todo: , ,
{ key: 'startMonth', label: '出行日期-月份' },
{ key: 'startYearMonth', label: '出行日期-年月' },
{ key: 'applyMonth', label: '预订日期-月份' },
{ key: 'applyYearMonth', label: '预订日期-年月' },
{ key: 'operatorName', label: '顾问' },
{ key: 'WebCode', label: '来源站点' },
{ key: 'IsOld_txt', label: '是否老客户' },
{ key: 'isCusCommend_txt', label: '是否老客户推荐' },
{ key: 'hasOld_txt', label: '老客户(推荐)' },
{ key: 'HotelStar', label: '酒店星级' },
{ key: 'destinationCountry_AsJOSN', label: '目的地国籍' },
{ key: 'destinations_AsJOSN', label: '目的地城市' },
{ key: 'RTXF_WB_range', label: '人天消费(外币)' },
{ key: 'PPPriceRange', label: '人均天/单(外币)' },
// { key: 'unitPPPriceRange', label: '()' },
// { key: 'SumML_ctxt1', label: '[1W]' },
// { key: 'SumML_ctxt1_5', label: '[1.5W]' },
// { key: 'SumML_ctxt2', label: '[2W]' },
// { key: 'SumML_ctxt3', label: '[3W]' },
// { key: 'SumML_ctxt4', label: '[4W]' },
{ key: 'SumML_ctxt', label: '毛利' },
];
const filterFieldsMapped = filterFields.reduce((r, v) => ({ ...r, [v.key]: v }), {});
/** 预设的选项, 只有行 */
const quickOptions = [
// { label: '×', fields: [['country'], ['productType']] },
{ label: ' 来源站点 ', fields: [['WebCode'], []] },
{ label: '[ 产品×客群 ]', fields: [['productType', 'guestGroupType'], []] },
{ label: '[ 国籍×客群 ]', fields: [['country', 'guestGroupType'], []] },
{ label: '[ 客群×目的地国籍 ]', fields: [['guestGroupType', 'destinationCountry_AsJOSN'], []] },
// { label: '[ × ]×[ ]', fields: [['country', 'guestGroupType'], []] },
];
@ -56,6 +75,12 @@ const pageSetting = {
childrenColumns: [
{ key: 'ConfirmOrder', title: '成交数', dataIndex: 'ConfirmOrder', width: '5em' },
{ key: 'ConfirmRates', title: '成交率', dataIndex: 'ConfirmRates_txt', width: '5em' },
{ key: 'SumML', title: '毛利', dataIndex: 'SumML', width: '5em' }, // SumML_txt
{ key: 'SingleML', title: '单团毛利', dataIndex: 'SingleML', width: '5em' },
{ key: 'tourdays', title: '团天数', dataIndex: 'tourdays', width: '5em' },
{ key: 'confirmTourdays', title: '✅团天数', dataIndex: 'confirmTourdays', width: '5em' },
{ key: 'SumPersonNum', title: '人数', dataIndex: 'SumPersonNum', width: '5em' },
{ key: 'ConfirmPersonNum', title: '✅人数', dataIndex: 'ConfirmPersonNum', width: '5em' },
],
searchInitial: { DateType: { key: 'applyDate', value: 'applyDate', label: '提交日期' } },
},
@ -70,8 +95,16 @@ const pageSetting = {
{ key: 'SumOrder', title: '订单数', dataIndex: 'SumOrder', width: '5em' },
{ key: 'ConfirmOrder', title: '成交数', dataIndex: 'ConfirmOrder', width: '5em' },
{ key: 'ConfirmRates', title: '成交率', dataIndex: 'ConfirmRates_txt', width: '5em' },
// { key: 'ResumeOrder', title: '', dataIndex: 'ResumeOrder', width: '5em', render: (_, r) => `${r.ResumeConfirmOrder}/${r.ResumeOrder}` },
{ key: 'SingleML', title: '单团毛利', dataIndex: 'SingleML', width: '5em' },
{ key: 'OrderValue', title: '单个订单价值', dataIndex: 'OrderValue', width: '5em' },
{ key: 'PPPriceRange', title: '人均天(外币)', dataIndex: 'PPPriceRange', width: '5em' },
{ key: 'AvgPPPrice', title: '人均天/单(外币)', dataIndex: 'AvgPPPrice', width: '5em' },
// { key: 'unitPPPrice', title: '()', dataIndex: 'unitPPPrice', width: '5em' },
// { key: 'unitPPPriceRange', title: '()', dataIndex: 'unitPPPriceRange', width: '5em' },
{ key: 'confirmDays', title: '成团周期(天)', dataIndex: 'confirmDays', width: '5em' },
{ key: 'applyDays', title: '预定周期(天)', dataIndex: 'applyDays', width: '5em' },
{ key: 'tourdays', title: '团天数', dataIndex: 'tourdays', width: '5em' },
],
searchInitial: { DateType: { key: 'confirmDate', value: 'confirmDate', label: '确认日期' } },
},
@ -81,7 +114,7 @@ export default observer((props) => {
const { page } = useParams();
const { pathname } = useLocation();
const { date_picker_store: searchFormStore, orders_store, DataPivotStore } = useContext(stores_Context);
const { formValues, formValuesToSub } = searchFormStore;
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const { originData } = DataPivotStore.detailData[page];
const { xField: defaultDateType, yField: defaultValKey, yFieldAlias, tableColumns, childrenColumns, searchInitial } = pageSetting[page];
@ -93,13 +126,18 @@ export default observer((props) => {
const [dataBeforeXChange, setDataBeforeXChange] = useState([]);
const [dataSource, setDataSource] = useState([]);
// const [dataSourceMapped, setDataSourceMapped] = useState({});
const [pivotRow, setPivotRow] = useState({});
const [pivotRowDataSource, setPivotRowDataSource] = useState([]);
const [pivotDataSource, setPivotDataSource] = useState([]);
const [pivotTableDataSource, setPivotTableDataSource] = useState([]);
const [pivotTableColumnSummary, setPivotTableColumnSummary] = useState({}); // ,
const [pivotDateColumns, setPivotDateColumns] = useState([[], []]);
const [pivotDateColumnsValues, setPivotDateColumnsValues] = useState([[], []]);
const [showPassCountryTips, setShowPassCountryTips] = useState(false);
useEffect(() => {
calcDataByDate();
resetX();
@ -143,16 +181,28 @@ export default observer((props) => {
*/
const calcDataByDate = (_rawData) => {
// console.log(';;;;;', pivotDateColumns);
const { data, columnValues, summaryRows, summaryColumns } = pivotBy(_rawData || rawData, [].concat(pivotDateColumns, [curXfield]));
// console.log('data====', data, '\ncolumnValues', columnValues, '\nsummaryRows', summaryRows, '\nsummaryColumns', summaryColumns);
const { data, columnValues, summaryRows, summaryColumns, pivotKeys, summaryMix } = pivotBy(_rawData || rawData, [].concat(pivotDateColumns, [curXfield]));
// console.log('data====', data, '\ncolumnValues', columnValues, '\nsummaryRows', summaryRows, '\nsummaryColumns', summaryColumns, '\nsummaryMix', summaryMix);
setShowPassCountryTips(pivotKeys.includes('destinationCountry_AsJOSN'));
setDataBeforePick(data.sort(sortBy(curXfield)));
// 线,
// const sortMixData = cloneDeep(summaryMix).sort(sortBy(defaultValKey)).reverse();
// setPivotDataSource(sortMixData);
// const sortRank = sortMixData.map(ele => ele.rowLabel);
// 线
setDataSource(data.sort(sortBy(curXfield)));
//
// .map(ele => ({...ele, rowLabel: `${sortRank.indexOf(ele.rowLabel)+1}).${ele.rowLabel}` }))
//
const sortRowData = cloneDeep(summaryRows).sort(sortBy(defaultValKey)).reverse();
setPivotTableDataSource(sortRowData);
setPivotRow({});
setPivotRowDataSource([]);
//
const sortColData = summaryColumns.sort(sortBy(defaultValKey)).reverse();
const _col1 = pivotDateColumns[1][0] || '';
const _sortByDateOrVal = (_col1.includes('Month') || _col1.includes('Date')) ? _col1 : defaultValKey;
let sortColData = summaryColumns.sort(sortBy(_sortByDateOrVal));
sortColData = _sortByDateOrVal === defaultValKey ? sortColData.reverse() : sortColData;
const colDataMapped = isEmpty(pivotDateColumns[1]) ? sortColData[0] : sortColData.reduce((r, v) => ({...r, [v[pivotDateColumns[1][0]]]: v}), {});
setPivotTableColumnSummary(colDataMapped);
//
@ -187,6 +237,11 @@ export default observer((props) => {
// },
itemMarginBottom: 12, //
},
tooltip: {
customItems: (originalItems) => {
return originalItems.map(ele => ({...ele, valueR: ele.data[defaultValKey]})).sort(sortBy('valueR')).reverse();
},
},
};
const [lineConfig, setLineConfig] = useState(cloneDeep(line_config));
@ -195,7 +250,7 @@ export default observer((props) => {
const [rightFields, setRightFields] = useState(filterFields);
const [rowFields, setRowFields] = useState([]);
const [columnFields, setColumnFields] = useState([]);
const [rowSelection, setRowSelection] = useState();
const [rowSelection, setRowSelection] = useState([]);
const [columnSelection, setColumnSelection] = useState();
//
const quickOpt = (i) => {
@ -353,9 +408,75 @@ export default observer((props) => {
],
};
const detailsTableProps = {
loading: false,
// sticky: true,
scroll: { x: 1000, y: 400 },
pagination: false,
columns: [
{
title: '订单号',
dataIndex: 'o_id',
key: 'o_id',
},
{
title: '来源站点',
dataIndex: 'WebCode',
key: 'WebCode',
},
{
title: '页面渠道',
dataIndex: 'COLI_LineClass',
key: 'COLI_LineClass',
},
{
title: '来源类型',
dataIndex: 'SourceType',
key: 'SourceType',
},
{
title: '客群类别',
dataIndex: 'guestGroupType',
key: 'guestGroupType',
},
{
title: '成行',
dataIndex: 'orderState',
key: 'orderState',
render: (text, record) => <span>{text === '1' ? '是' : '否'}</span>,
sorter: (a, b) => b.orderState - a.orderState,
},
// {
// title: "(//)",
// dataIndex: "COLI_PersonNum",
// key: "COLI_PersonNum",
// render: (text, record) => (
// <span>
// {record.COLI_PersonNum}/{record.COLI_ChildNum}/{record.COLI_BabyNum}
// </span>
// ),
// },
{
title: '预计利润',
dataIndex: 'ML',
key: 'ML',
},
{
title: '预定时间',
dataIndex: 'applyDate',
key: 'applyDate',
},
{
title: '出发日期',
dataIndex: 'startDate',
key: 'startDate',
},
],
};
return (
<div key={pathname}>
<Row gutter={16} className="sticky-top">
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
@ -383,17 +504,23 @@ export default observer((props) => {
size={'small'}
title={'透视选项'}
extra={
<Space>
<Text type={'secondary'}>预设:</Text>
<Row gutter={3}>
<Col flex={'0 0 auto'}>
<Text type={'secondary'}>预设:</Text>
</Col>
{quickOptions.map((ele, elei) => (
<Button key={ele.label} onClick={() => quickOpt(elei)} type={'primary'} ghost size={'small'}>
{ele.label}
</Button>
<Col key={ele.label}>
<Button key={ele.label} onClick={() => quickOpt(elei)} type={'primary'} ghost size={'small'}>
{ele.label}
</Button>
</Col>
))}
<Button key={'reset-quick'} onClick={resetFields} type={'primary'} ghost size={'small'}>
重置
</Button>
</Space>
<Col>
<Button key={'reset-quick'} onClick={resetFields} type={'primary'} ghost size={'small'}>
重置
</Button>
</Col>
</Row>
}
>
<Row gutter={16}>
@ -407,7 +534,8 @@ export default observer((props) => {
))}
</div> */}
</Col>
<Col span={12}>
{/* 行: */}
<Col md={12} sm={24} xs={24}>
<Row gutter={8} align={'middle'}>
<Col flex={'4em'} align={'end'}>
<Text strong>: </Text>
@ -467,9 +595,10 @@ export default observer((props) => {
</Col>
</Row>
</Col>
<Col span={12} style={{ borderLeft: '1px solid #d9d9d9' }}>
{/* 列: */}
<Col md={12} sm={24} xs={24} style={{ borderLeft: '1px solid #d9d9d9' }}>
<Row gutter={8} align={'middle'}>
<Col flex={'6em'} align={'end'}>
<Col flex={'4em'} align={'end'}>
<Text strong>: </Text>
</Col>
<Col flex={'auto'}>
@ -492,6 +621,9 @@ export default observer((props) => {
</Select>
</Col>
<Col span={24}>
<Row gutter={16} align={'bottom'} className="mt-1">
<Col span={24}>{showPassCountryTips && <Alert message="途径的国家将会重复计算" type="warning" showIcon />}</Col>
</Row>
{/* {columnFields.length > 0
? cloneDeep(pivotDateColumnsValues)
.slice(rowFields.length)
@ -538,7 +670,7 @@ export default observer((props) => {
走势: <span style={{ fontSize: 'smaller' }}>{dataFieldAlias[lineConfig.yField].label}</span>
</h3>
</Col>
<Col flex={'1 0 50%'} style={{ textAlign: 'right' }} align={'end'}>
<Col style={{ textAlign: 'right' }} align={'end'}>
<DateGroupRadio
visible={true}
dataRaw={{ data1: dataBeforeXChange }}
@ -560,9 +692,43 @@ export default observer((props) => {
透视汇总表: <span style={{ fontSize: 'smaller' }}>{dataFieldAlias[lineConfig.yField].label}</span>
</h3>
<Divider orientation="right">
<TableExportBtn label={'pivot'} {...{ columns: targetTableProps.columns, dataSource: pivotTableDataSource }} />
<TableExportBtn
label={'pivot-' + pivotDateColumns[0].map((ele, ri) => `${filterFieldsMapped[ele].label}`).join('×')}
{...{ columns: targetTableProps.columns, dataSource: pivotTableDataSource }}
/>
</Divider>
<Table
{...targetTableProps}
dataSource={pivotTableDataSource}
components={{ body: { cell: TdCell } }}
onRow={(record) => {
return {
//
onClick: (event) => {
setPivotRow(record);
const thisDetail = rawData.filter((ele) => {
return record.keys.includes(String(ele.key));
});
setPivotRowDataSource(thisDetail);
},
};
}}
/>
</Spin>
</section>
<section>
<Spin spinning={loading}>
<h3>
点击上方表格行查看单行数据的订单明细:{' '}
<span style={{ fontSize: 'smaller' }}>{pivotDateColumns[0].map((ele, ri) => `${filterFieldsMapped[ele].label}: ${pivotRow[pivotDateColumns[0][ri]] || ''}`).join('; ')}</span>
</h3>
<Divider orientation="right">
<TableExportBtn
label={'pivot-' + pivotDateColumns[0].map((ele, ri) => `${pivotRow[pivotDateColumns[0][ri]] || ''}`).join('-')}
{...{ columns: detailsTableProps.columns, dataSource: pivotRowDataSource }}
/>
</Divider>
<Table {...targetTableProps} dataSource={pivotTableDataSource} components={{ body: { cell: TdCell } }} />
<Table {...detailsTableProps} dataSource={pivotRowDataSource} components={{ body: { cell: TdCell } }} />
</Spin>
</section>
</div>

@ -16,7 +16,7 @@ const DestinationGroupCount = () => {
return (
<>
<Space direction="vertical" style={{ width: '100%' }}>
<Row gutter={16} className='sticky-top'>
<Row gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
@ -25,13 +25,13 @@ const DestinationGroupCount = () => {
countryArea: { key: 'china', label: '国内' },
...customerServicesStore.searchValues,
},
shows: ['DateType', 'DepartmentList', 'countryArea', 'orderStatus', 'dates'],
shows: ['departureDateType', 'DepartmentList', 'countryArea', 'orderStatus', 'dates'],
fieldProps: {
DepartmentList: { show_all: true },
DepartmentList: { show_all: true, mode: 'multiple' },
orderStatus: { show_all: true },
countryArea: { show_all: false },
dates: { hide_vs: true },
DateType: { disabledKeys: ['applyDate'] },
years: { hide_vs: true },
departureDateType: { disabledKeys: ['applyDate'] },
},
}}
onSubmit={(_err, obj, form) => {
@ -44,6 +44,9 @@ const DestinationGroupCount = () => {
<Row>
<Col span={24}>
<Typography.Title level={3}>目的地团信息</Typography.Title>
<Divider orientation="right" plain>
<TableExportBtn label={'目的地团信息'} {...{ columns: destinationGroupCountColumns, dataSource: destinationGroupCount }} />
</Divider>
<Table
sticky
id="destinationGroupCount"
@ -55,9 +58,6 @@ const DestinationGroupCount = () => {
pagination={false}
scroll={{ x: 1000 }}
/>
<Divider orientation="right" plain>
<TableExportBtn label={'目的地团信息'} {...{ columns: destinationGroupCountColumns, dataSource: destinationGroupCount }} />
</Divider>
</Col>
</Row>
</Space>

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

@ -19,15 +19,22 @@ const apartOptions = [
{ key: 'ConfirmDays', value: 'ConfirmDays', label: '成团周期' },
{ key: 'ApplyDays', value: 'ApplyDays', label: '预定周期' },
{ key: 'PersonNum', value: 'PersonNum', label: '人等' },
{ key: 'destination', value: 'destination', label: '国内目的地' },
{ key: 'GlobalDestination', value: 'GlobalDestination', label: '海外目的地' },
{ key: 'destinationCountry', value: 'destinationCountry', label: '目的地国籍' },
{ key: 'guestCountry', value: 'guestCountry', label: '客人国籍' },
{ key: 'destination', value: 'destination', label: '国内目的地', },
{ key: 'GlobalDestination', value: 'GlobalDestination', label: '海外目的地', },
{ key: 'destinationCountry', value: 'destinationCountry', label: '目的地国籍', },
{ key: 'guestCountry', value: 'guestCountry', label: '客人国籍', },
];
// TdCellDataTable
const TdCell = (tdprops) => {
// onMouseEnter, onMouseLeave
const { onMouseEnter, onMouseLeave, ...restProps } = tdprops;
return <td {...restProps} />;
};
export default observer(() => {
const { date_picker_store: searchFormStore, DistributionStore } = useContext(stores_Context);
const { formValues, formValuesToSub } = searchFormStore;
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const { curTab, dateStringQ, dateStringY } = DistributionStore;
const pageRefresh = (obj) => {
@ -36,12 +43,6 @@ export default observer(() => {
});
};
useEffect(() => {
if (empty(DistributionStore[curTab].dataSource)) {
pageRefresh();
}
}, [curTab]);
useEffect(() => {
DistributionStore.setFormDates(formValuesToSub);
DistributionStore.resetData();
@ -50,6 +51,9 @@ export default observer(() => {
const onTabsChange = (tab) => {
DistributionStore.setCurTab(tab);
if (empty(DistributionStore[tab].dataSource)) {
pageRefresh();
}
};
const RingProgressConfig = {
height: 60,
@ -74,26 +78,37 @@ export default observer(() => {
};
const columns = [
{ title: '#', dataIndex: 'label' },
{
title: '预定',
dataIndex: 'SumOrder',
render: (v, r) => (
<>
<Space direction={'vertical'}>
<Text strong>{v}</Text>
<span>
<span>同比: </span> <VSTag diffPercent={r.SumOrderToY} diffData={r.SumOrderDiffY} />
</span>
<span>
<span>环比: </span> <VSTag diffPercent={r.SumOrderToQ} diffData={r.SumOrderDiffQ} />
</span>
</Space>
</>
),
},
{
title: '团数',
dataIndex: 'ConfirmOrder',
render: (v, r) => (
<>
<Row align={'middle'}>
<Col flex={"100px"}>
<Text strong>{v}</Text>
</Col>
<Col flex={'auto'}>
<Space direction={'vertical'}>
<span>
<Text strong>{v}</Text>
<span>
<span>同比: </span> <VSTag diffPercent={r.ConfirmOrderToY} diffData={r.ConfirmOrderDiffY} />
</span>
<span>
<span>环比: </span> <VSTag diffPercent={r.ConfirmOrderToQ} diffData={r.ConfirmOrderDiffQ} />
</span>
</Space>
</Col>
</Row>
</>
),
},
@ -102,21 +117,15 @@ export default observer(() => {
dataIndex: 'SumML',
render: (v, r) => (
<>
<Row align={'middle'}>
<Col flex={"150px"}>
<Text strong>{dataFieldAlias.SumML.formatter(v)}</Text>
</Col>
<Col flex={'auto'}>
<Space direction={'vertical'}>
<span>
<Text strong>{dataFieldAlias.SumML.formatter(v)}</Text>
<span>
<span>同比: </span> <VSTag diffPercent={r.SumMLToY} diffData={dataFieldAlias.SumML.formatter(r.SumMLDiffY)} />
</span>
<span>
<span>环比: </span> <VSTag diffPercent={r.SumMLToQ} diffData={dataFieldAlias.SumML.formatter(r.SumMLDiffQ)} />
</span>
</Space>
</Col>
</Row>
</>
),
},
@ -137,12 +146,14 @@ export default observer(() => {
children: [
{
title: '团数占比',
titleX: '团数占比',
width: 90,
dataIndex: ['resultToY', 'ConfirmOrderPercent'], // 'ConfirmOrderPercent',
render: (v, r) => r.resultToY.ConfirmOrderPercent ? <RingProgress {...RingProgressConfigY} percent={r.resultToY.ConfirmOrderPercent / 100} /> : '-',
},
{
title: '业绩占比',
titleX: '业绩占比',
width: 90,
dataIndex: ['resultToY', 'SumMLPercent'], // 'SumMLPercent',
render: (v, r) => r.resultToY.SumMLPercent ? <RingProgress {...RingProgressConfigY} percent={r.resultToY.SumMLPercent / 100} /> : '-',
@ -156,12 +167,14 @@ export default observer(() => {
children: [
{
title: '团数占比',
titleX: '团数占比',
width: 90,
dataIndex: ['resultToQ', 'ConfirmOrderPercent'], // 'ConfirmOrderPercent',
render: (v, r) => r.resultToQ.ConfirmOrderPercent ? <RingProgress {...RingProgressConfigQ} percent={r.resultToQ.ConfirmOrderPercent / 100} /> : '-',
},
{
title: '业绩占比',
titleX: '业绩占比',
width: 90,
dataIndex: ['resultToQ', 'SumMLPercent'], // 'SumMLPercent',
render: (v, r) => r.resultToQ.SumMLPercent ? <RingProgress {...RingProgressConfigQ} percent={r.resultToQ.SumMLPercent / 100} /> : '-',
@ -176,7 +189,7 @@ export default observer(() => {
};
return (
<>
<Row gutter={16} className='sticky-top' >
<Row gutter={16} className={siderBroken ? "" : "sticky-top"} >
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
@ -212,6 +225,7 @@ export default observer(() => {
</Divider>
<Table
id="table_to_xlsx_sale"
components={{ body: { cell: TdCell } }}
dataSource={DistributionStore[curTab].dataSource}
columns={columns}
size="small"

@ -1,6 +1,6 @@
import { useContext, useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { Row, Col, Spin, Space, Radio, Table, Button } from 'antd';
import { Row, Col, Spin, Space, Radio, Table, Button, } from 'antd';
import { CheckCircleTwoTone, MoneyCollectTwoTone, FlagTwoTone, SmileTwoTone } from '@ant-design/icons';
import { stores_Context } from '../config';
import StatisticCard2 from '../components/StatisticCard2';
@ -25,6 +25,7 @@ const topSeries = [
{ key: 'country', value: 'country', label: '国籍', graphVisible: true },
{ key: 'webcode', value: 'webcode', label: '站点', graphVisible: false },
{ key: 'bizarea', value: 'bizarea', label: '国境', graphVisible: false },
{ key: 'destinationcountry', value: 'destinationcountry', label: '目的地国籍', graphVisible: true },
];
const allGroupTypes = [
@ -37,17 +38,27 @@ const iconSets = [CheckCircleTwoTone, MoneyCollectTwoTone, FlagTwoTone, SmileTwo
export default observer(() => {
// const navigate = useNavigate();
const { TradeStore, date_picker_store: searchFormStore } = useContext(stores_Context);
const { TradeStore, date_picker_store: searchFormStore, DistributionStore } = useContext(stores_Context);
const { searchValues, sideData, summaryData, BuData, topData, timeData, timeLineKey, targetTableProps, timeDiffData, groupKey } = TradeStore;
const { formValues } = searchFormStore;
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const { curTab, dateStringQ, dateStringY } = DistributionStore;
useEffect(() => {
if (empty(summaryData.dataSource)) {
// pageRefresh();
}
DistributionStore.setCurTab('destinationCountry');
// if (empty(summaryData.dataSource)) {
// // pageRefresh();
// }
return () => {};
}, []);
const getDestinationCountry = (obj) => {
DistributionStore.setCurTab('destinationCountry');
DistributionStore.getApartData({
...(obj || formValuesToSub),
}, false);
};
const [topSeriesSet, setTopSeriesSet] = useState(topSeries);
const [overviewFlag, setOverviewFlag] = useState(true);
const [groupTypeVal, setGroupTypeVal] = useState('overview');
@ -182,7 +193,7 @@ export default observer(() => {
const [showDiff, setShowDiff] = useState(false);
return (
<>
<Row gutter={16} className="sticky-top">
<Row gutter={16} className={siderBroken ? "" : "sticky-top"}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
@ -200,6 +211,7 @@ export default observer(() => {
onSubmit={(_err, obj, form, str) => {
TradeStore.setSearch(obj, form);
pageRefresh(obj);
getDestinationCountry(obj);
}}
/>
</Col>
@ -226,26 +238,26 @@ export default observer(() => {
</Spin>
</section>
<section>
<Space gutter={16} size={'small'}>
<h3>{showDiff === false ? '走势' : '对比'}</h3>
<DataFieldRadio value={timeDataField} onChange={handleChangetimeDataField} />
<Radio.Group options={datePartOptions} optionType="button" onChange={handleChangeDateType} value={dateField} />
{searchValues.yearDiff && (
<Button type="link" size={'small'} onClick={() => setShowDiff(!showDiff)}>
<Row gutter={16}>
<Col flex={'4em'}><h3>{showDiff === false ? '走势' : '对比'}</h3></Col>
<Col ><DataFieldRadio value={timeDataField} onChange={handleChangetimeDataField} /></Col>
<Col ><Radio.Group options={datePartOptions} optionType="button" onChange={handleChangeDateType} value={dateField} /></Col>
<Col >{searchValues.yearDiff && (
<Button type="primary" ghost size={'small'} onClick={() => setShowDiff(!showDiff)}>
{showDiff === false ? '显示对比' : '返回本期走势'}
</Button>
)}
</Space>
)}</Col>
</Row>
{showDiff === false ? (
<Spin spinning={timeData.loading}>
<MixTBWithKPI dataSource={timeData.dataSource} summaryData={timeData.origin?.summaryRows || []} {...lineConfig} />
</Spin>
) : (
<Spin spinning={timeDiffData.loading}>
<Space gutter={16} size={'small'}>
<h3>分类对比</h3>
<Radio.Group options={allGroupTypes} optionType="button" onChange={handleChangeDiffType} value={diffGroupKey} />
</Space>
<Row gutter={16}>
<Col flex={'6em'}><h3>分类对比</h3></Col>
<Col ><Radio.Group options={allGroupTypes} optionType="button" onChange={handleChangeDiffType} value={diffGroupKey} /></Col>
</Row>
<LineWithKPI
dataSource={timeDiffData.dataSource}
showKPI={false}
@ -293,12 +305,10 @@ export default observer(() => {
</h3>
</section>
<section>
<Space>
<h3>TOP</h3>
<div>
<DataFieldRadio value={valueKey} onChange={handleChangeValueKey} />
</div>
</Space>
<Row gutter={16}>
<Col flex={'4em'}><h3>TOP</h3></Col>
<Col ><DataFieldRadio value={valueKey} onChange={handleChangeValueKey} /></Col>
</Row>
<Row gutter={layoutProps3.gutter}>
{topSeriesSet.map((item) =>
item.graphVisible ? (
@ -310,13 +320,31 @@ export default observer(() => {
</Col>
) : null
)}
<Col key={'mapG'} flex={'1 0 600px'} >
<Col key={'mapG'} span={22}>
<hr />
<h3>来源国籍分布</h3>
<Spin spinning={topData?.country?.loading || false}>
<div id="topC" style={{ height: '700px' }}>
<MapCountry sourceField={'groupsLabel'} valueField={BUConfig.measureField} dataSource={topData?.country?.dataSource || []} />
<MapCountry sourceField={'groupsLabel'} valueField={BUConfig.measureField} dataSource={topData?.country?.dataSource || []} containerNode='#topC' />
</div>
</Spin>
</Col>
<Col key={'mapG-r'} span={1}></Col>
<Col key={'mapDestinationCountry'} span={22}>
<hr />
<h3>目的地国籍分布</h3>
{/* <Spin spinning={DistributionStore.pageLoading || false}>
<div id="mapDestinationCountry" style={{ height: '700px' }}>
<MapCountry sourceField={'label'} valueField={BUConfig.measureField} dataSource={DistributionStore.destinationCountry.originData || []} containerNode='#mapDestinationCountry' />
</div>
</Spin> */}
<Spin spinning={topData?.destinationcountry?.loading || false}>
<div id="mapDestinationCountry" style={{ height: '700px' }}>
<MapCountry sourceField={'groupsLabel'} valueField={BUConfig.measureField} dataSource={topData?.destinationcountry?.dataSource || []} containerNode='#mapDestinationCountry' />
</div>
</Spin>
</Col>
<Col key={'mapGc-r'} span={1}></Col>
</Row>
</section>
</>

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

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

@ -0,0 +1,133 @@
import { useContext } from 'react';
import { observer } from 'mobx-react';
import { stores_Context } from '../config';
import { Table, Row, Col, Divider, Tooltip } from 'antd';
import { InfoCircleOutlined } from '@ant-design/icons';
import SearchForm from '../components/search/SearchForm';
import { TableExportBtn } from './../components/Data';
import { fixTo2Decimals } from './../utils/commons';
const numberConvert10K = (number, scale = 10) => {
return fixTo2Decimals(number / (1000 * scale)) + '';
};
// TdCellDataTable
const TdCell = (tdprops) => {
// onMouseEnter, onMouseLeave
const { onMouseEnter, onMouseLeave, ...restProps } = tdprops;
return <td {...restProps} />;
};
export default observer((props) => {
const { date_picker_store: searchFormStore, MeetingDataStore } = useContext(stores_Context);
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const dataRefresh = async (obj) => {
MeetingDataStore.dataGHOrder({ ...(obj || formValuesToSub) });
MeetingDataStore.dataGHSales({ ...(obj || formValuesToSub) });
MeetingDataStore.dataGHService({ ...(obj || formValuesToSub) });
};
const targetTableProps = {
loading: MeetingDataStore.GHLoading,
// sticky: true,
scroll: { x: 1000, y: 400 },
pagination: false,
components: { body: { cell: TdCell } },
orderColumns: [
{ key: 'label', title: '市场:', dataIndex: 'label', width: 150 },
{
key: 'LineClass_Origin',
titleX: '网站',
title: () => (
<>
网站{' '}
<Tooltip title="网前自然订单">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'LineClass_Origin',
},
{
key: 'external',
titleX: '站外渠道',
title: () => (
<>
站外渠道{' '}
<Tooltip title="Facebook, Pinterest, Youtube, Instagram">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'external',
},
{ key: 'LineClass_PPC', title: 'PPC', dataIndex: 'LineClass_PPC' },
{ key: 'toB', title: 'To B', dataIndex: 'toB' },
{ key: 'isOld1', title: 'C老客户', dataIndex: 'isOld1' },
{ key: 'total', title: '合计', dataIndex: 'total' },
{ key: 'rowYear', title: '截至年订单数', dataIndex: 'rowYear' },
// { key: 'groupsLabel2', title: '', dataIndex: 'groupsLabel2' },
// { key: 'groupsLabel2', title: '', dataIndex: 'groupsLabel2' },
],
salesColumns: [
{ key: 'label', title: '顾问:', dataIndex: 'label', width: 150 },
{ key: 'CJCount', title: '成交个数', dataIndex: 'CJCount' },
{ key: 'YJLY', title: '成交毛利(万)', dataIndex: 'YJLY', render: (text) => numberConvert10K(text) },
{ key: 'CJCount1', title: '年成交个数', dataIndex: ['rowYear', 'CJCount'] },
{ key: 'YJLY1', title: '年成交毛利(万)', dataIndex: ['rowYear', 'YJLY'], render: (text) => numberConvert10K(text) },
{ key: 'YJLY2', title: '年走团毛利(万)', dataIndex: ['rowYear', 'YJLY2'], render: (text) => numberConvert10K(text) },
],
serviceColumns: [
{ key: 'label', title: '客服:', dataIndex: 'label', width: 150 },
{ key: 'GroupCount', title: '走团个数', dataIndex: 'GroupCount' },
{ key: 'GoodCount', title: '好评个数', dataIndex: 'GoodCount' },
{ key: 'GroupCount1', title: '年走团个数', dataIndex: ['rowYear', 'GroupCount'] },
{ key: 'GoodCount2', title: '年好评个数', dataIndex: ['rowYear', 'GoodCount'] },
],
};
return (
<>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
...MeetingDataStore.searchValues,
// ...searchInitial,
},
shows: ['IncludeTickets', 'dates'], // 'country'
fieldProps: {
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
MeetingDataStore.setSearchValues(form);
dataRefresh(obj);
}}
/>
</Col>
</Row>
<Divider orientation="right" plain>
<span>GH: 市场</span>
<TableExportBtn label={`${formValuesToSub.Date1}-GH: 市场`} {...{ columns: targetTableProps.orderColumns, dataSource: MeetingDataStore.GHTableData }} />
</Divider>
<Table {...targetTableProps} key={'GHTable'} dataSource={MeetingDataStore.GHTableData} columns={targetTableProps.orderColumns} loading={MeetingDataStore.GHTableLoading} />
<Divider orientation="right" plain>
<span>GH: 顾问成交</span>
<TableExportBtn label={`${formValuesToSub.Date1}-GH: 顾问成交`} {...{ columns: targetTableProps.salesColumns, dataSource: MeetingDataStore.GHSalesTableData }} />
</Divider>
<Table {...targetTableProps} key={'GHSales'} dataSource={MeetingDataStore.GHSalesTableData} columns={targetTableProps.salesColumns} loading={MeetingDataStore.GHSalesLoading} />
<Divider orientation="right" plain>
<span>GH: 客服</span>
<TableExportBtn label={`${formValuesToSub.Date1}-GH: 客服`} {...{ columns: targetTableProps.serviceColumns, dataSource: MeetingDataStore.GHServiceTableData }} />
</Divider>
<Table {...targetTableProps} key={'GHService'} dataSource={MeetingDataStore.GHServiceTableData} columns={targetTableProps.serviceColumns} loading={MeetingDataStore.GHServiceLoading} />
</>
);
});

@ -0,0 +1,134 @@
import { useContext } from 'react';
import { observer } from 'mobx-react';
import { stores_Context } from '../config';
import { Table, Row, Col, Divider, Tooltip } from 'antd';
import { InfoCircleOutlined } from '@ant-design/icons';
import SearchForm from '../components/search/SearchForm';
import { TableExportBtn } from './../components/Data';
import { fixTo2Decimals } from './../utils/commons';
const numberConvert10K = (number, scale = 10) => {
return fixTo2Decimals(number / (1000 * scale)) + '';
};
// TdCellDataTable
const TdCell = (tdprops) => {
// onMouseEnter, onMouseLeave
const { onMouseEnter, onMouseLeave, ...restProps } = tdprops;
return <td {...restProps} />;
};
export default observer((props) => {
const { date_picker_store: searchFormStore, MeetingData2025Store: MeetingDataStore } = useContext(stores_Context);
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const dataRefresh = async (obj) => {
MeetingDataStore.dataGHOrder({ ...(obj || formValuesToSub) });
MeetingDataStore.dataGHSales({ ...(obj || formValuesToSub) });
MeetingDataStore.dataGHService({ ...(obj || formValuesToSub) });
};
const targetTableProps = {
loading: MeetingDataStore.GHLoading,
// sticky: true,
scroll: { x: 1000, y: 400 },
pagination: false,
components: { body: { cell: TdCell } },
orderColumns: [
{ key: 'label', title: '市场:', dataIndex: 'label', width: 150 },
{
key: 'LineClass_Origin',
titleX: '网站',
title: () => (
<>
网站{' '}
<Tooltip title="网前自然订单">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'LineClass_Origin',
},
{
key: 'external',
titleX: '站外渠道',
title: () => (
<>
站外渠道{' '}
<Tooltip title="Facebook, Pinterest, Youtube, Instagram">
<InfoCircleOutlined />
</Tooltip>
</>
),
dataIndex: 'external',
},
{ key: 'LineClass_PPC', title: 'PPC', dataIndex: 'LineClass_PPC' },
{ key: 'toB', title: 'To B', dataIndex: 'toB' },
{ key: 'isOld1', title: 'C老客户', dataIndex: 'isOld1' },
{ key: 'total', title: '合计', dataIndex: 'total' },
{ key: 'rowYear', title: '截至年订单数', dataIndex: 'rowYear' },
// { key: 'groupsLabel2', title: '', dataIndex: 'groupsLabel2' },
// { key: 'groupsLabel2', title: '', dataIndex: 'groupsLabel2' },
],
salesColumns: [
{ key: 'label', title: '顾问:', dataIndex: 'label', width: 150 },
{ key: 'CJCount', title: '成交个数', dataIndex: 'CJCount' },
{ key: 'YJLY', title: '成交毛利(万)', dataIndex: 'YJLY', render: (text) => numberConvert10K(text) },
{ key: 'CJCount1', title: '年成交个数', dataIndex: ['rowYear', 'CJCount'] },
{ key: 'YJLY1', title: '年成交毛利(万)', dataIndex: ['rowYear', 'YJLY'], render: (text) => numberConvert10K(text) },
{ key: 'YJLY2', title: '年走团毛利(万)', dataIndex: ['rowYear', 'YJLY2'], render: (text) => numberConvert10K(text) },
],
serviceColumns: [
{ key: 'label', title: '客服:', dataIndex: 'label', width: 150 },
{ key: 'GroupCount', title: '走团个数', dataIndex: 'GroupCount' },
// { key: 'GoodCount', title: '', dataIndex: 'GoodCount' },
{ key: 'GroupCount1', title: '年走团个数', dataIndex: ['rowYear', 'GroupCount'] },
// { key: 'GoodCount2', title: '', dataIndex: ['rowYear', 'GoodCount'] },
],
};
return (
<>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
...MeetingDataStore.searchValues,
// ...searchInitial,
},
shows: ['IncludeTickets', 'dates'], // 'country'
fieldProps: {
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
MeetingDataStore.setSearchValues(form);
dataRefresh(obj);
}}
/>
</Col>
</Row>
<h2>GH例会数据 2025</h2>
<Divider orientation="left" plain>
<span>GH: 市场</span>
<TableExportBtn label={`${formValuesToSub.Date1}-GH: 市场`} {...{ columns: targetTableProps.orderColumns, dataSource: MeetingDataStore.GHTableData }} style={{ marginLeft: 'auto' }} />
</Divider>
<Table {...targetTableProps} key={'GHTable'} dataSource={MeetingDataStore.GHTableData} columns={targetTableProps.orderColumns} loading={MeetingDataStore.GHTableLoading} />
<Divider orientation="left" plain>
<span>GH: 顾问成交</span>
<TableExportBtn label={`${formValuesToSub.Date1}-GH: 顾问成交`} {...{ columns: targetTableProps.salesColumns, dataSource: MeetingDataStore.GHSalesTableData }} />
</Divider>
<Table {...targetTableProps} key={'GHSales'} dataSource={MeetingDataStore.GHSalesTableData} columns={targetTableProps.salesColumns} loading={MeetingDataStore.GHSalesLoading} />
<Divider orientation="left" plain>
<span>GH: 客服</span>
<TableExportBtn label={`${formValuesToSub.Date1}-GH: 客服`} {...{ columns: targetTableProps.serviceColumns, dataSource: MeetingDataStore.GHServiceTableData }} />
</Divider>
<Table {...targetTableProps} key={'GHService'} dataSource={MeetingDataStore.GHServiceTableData} columns={targetTableProps.serviceColumns} loading={MeetingDataStore.GHServiceLoading} />
</>
);
});

@ -1,6 +1,6 @@
import React, { Component } from "react";
import { Row, Col, Tabs, Table, Divider, Spin } from "antd";
import { ContainerOutlined, BlockOutlined, SmileOutlined, TagsOutlined, GlobalOutlined, FullscreenOutlined, DingtalkOutlined, CarryOutOutlined } from "@ant-design/icons";
import { ContainerOutlined, BlockOutlined, SmileOutlined, TagsOutlined, GlobalOutlined, FullscreenOutlined, DingtalkOutlined, CarryOutOutlined, CoffeeOutlined, ClockCircleOutlined, HeartOutlined, IdcardOutlined, ContactsOutlined } from "@ant-design/icons";
import { stores_Context } from "../config";
import { Line, Pie } from "@ant-design/charts";
import { observer } from "mobx-react";
@ -28,83 +28,92 @@ class Orders extends Component {
if (date_picker_store.start_date_cp && date_picker_store.end_date_cp) {
//
result.columns = [
{
title: "",
{
title: '#',
fixed: 'left',
children: [
{
title: (
<span>
<div>
{date_picker_store.start_date.format(config.DATE_FORMAT)}~{date_picker_store.end_date.format(config.DATE_FORMAT)}
</div>
<div>
{date_picker_store.start_date_cp.format(config.DATE_FORMAT)}~{date_picker_store.end_date_cp.format(config.DATE_FORMAT)}
</div>
</span>
),
dataIndex: "OrderType",
children: [
{
title: (
<span>
<div>
{date_picker_store.start_date.format(config.DATE_FORMAT)}~{date_picker_store.end_date.format(config.DATE_FORMAT)}
</div>
<div>
{date_picker_store.start_date_cp.format(config.DATE_FORMAT)}~{date_picker_store.end_date_cp.format(config.DATE_FORMAT)}
</div>
</span>
),
titleX: `${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)}`,
dataIndex: 'OrderType',
fixed: 'left',
render: (text, record) => <NavLink to={`/orders_sub/${orders_store.active_tab_key}/${record.OrderTypeSN}/${record.OrderType}`}>{text}</NavLink>,
},
],
},
{
title: "数量",
children: [
{
title: comm.show_vs_tag(ordercountTotal1.OrderCount_vs, ordercountTotal1.OrderCount_diff, ordercountTotal1.OrderCount, ordercountTotal2.OrderCount),
dataIndex: "OrderCount",
},
],
},
{
title: "成交数",
children: [
{
title: comm.show_vs_tag(ordercountTotal1.CJCount_vs, ordercountTotal1.CJCount_diff, ordercountTotal1.CJCount, ordercountTotal2.CJCount),
dataIndex: "CJCount",
},
],
},
{
title: "成交人数",
children: [
{
title: comm.show_vs_tag(ordercountTotal1.CJPersonNum_vs, ordercountTotal1.CJPersonNum_diff, ordercountTotal1.CJPersonNum, ordercountTotal2.CJPersonNum),
dataIndex: "CJPersonNum",
},
],
},
{
title: "成交率",
children: [
{
title: comm.show_vs_tag(ordercountTotal1.CJrate_vs, ordercountTotal1.CJrate_diff, ordercountTotal1.CJrate, ordercountTotal2.CJrate),
dataIndex: "CJrate",
},
],
},
{
title: "成交毛利(预计)",
children: [
{
title: comm.show_vs_tag(ordercountTotal1.YJLY_vs, ordercountTotal1.YJLY_diff, ordercountTotal1.YJLY, ordercountTotal2.YJLY),
dataIndex: "YJLY",
},
],
},
render: (text, record) => <NavLink to={`/orders_sub/${orders_store.active_tab_key}/${record.OrderTypeSN}/${record.OrderType}`}>{text}</NavLink>,
},
],
},
{
title: '数量',
children: [
{
title: comm.show_vs_tag(ordercountTotal1.OrderCount_vs, ordercountTotal1.OrderCount_diff, ordercountTotal1.OrderCount, ordercountTotal2.OrderCount),
titleX: [ordercountTotal1.OrderCount, ordercountTotal2.OrderCount].join(' vs '),
dataIndex: 'OrderCount',
},
],
},
{
title: '成交数',
children: [
{
title: comm.show_vs_tag(ordercountTotal1.CJCount_vs, ordercountTotal1.CJCount_diff, ordercountTotal1.CJCount, ordercountTotal2.CJCount),
titleX: [ordercountTotal1.CJCount, ordercountTotal2.CJCount].join(' vs '),
dataIndex: 'CJCount',
},
],
},
{
title: '成交人数',
children: [
{
title: comm.show_vs_tag(ordercountTotal1.CJPersonNum_vs, ordercountTotal1.CJPersonNum_diff, ordercountTotal1.CJPersonNum, ordercountTotal2.CJPersonNum),
titleX: [ordercountTotal1.CJPersonNum, ordercountTotal2.CJPersonNum].join(' vs '),
dataIndex: 'CJPersonNum',
},
],
},
{
title: '成交率',
children: [
{
title: comm.show_vs_tag(ordercountTotal1.CJrate_vs, ordercountTotal1.CJrate_diff, ordercountTotal1.CJrate, ordercountTotal2.CJrate),
titleX: [ordercountTotal1.CJrate, ordercountTotal2.CJrate].join(' vs '),
dataIndex: 'CJrate',
},
],
},
{
title: '成交毛利(预计)',
children: [
{
title: comm.show_vs_tag(ordercountTotal1.YJLY_vs, ordercountTotal1.YJLY_diff, ordercountTotal1.YJLY, ordercountTotal2.YJLY),
titleX: [ordercountTotal1.YJLY, ordercountTotal2.YJLY].join(' vs '),
dataIndex: 'YJLY',
},
],
},
{
title: "单个订单价值",
children: [
{
title: comm.show_vs_tag(ordercountTotal1.Ordervalue_vs, ordercountTotal1.Ordervalue_diff, ordercountTotal1.Ordervalue, ordercountTotal2.Ordervalue),
dataIndex: "Ordervalue",
},
],
},
];
{
title: '单个订单价值',
children: [
{
title: comm.show_vs_tag(ordercountTotal1.Ordervalue_vs, ordercountTotal1.Ordervalue_diff, ordercountTotal1.Ordervalue, ordercountTotal2.Ordervalue),
titleX: [ordercountTotal1.Ordervalue, ordercountTotal2.Ordervalue].join(' vs '),
dataIndex: 'Ordervalue',
},
],
},
];
// 1.OrderType 2.OrderType
let has_same_type = false; // 12
for (const item of data.ordercount1) {
@ -118,11 +127,17 @@ class Orders extends Component {
OrderType: item.OrderType,
OrderTypeSN: item.OrderTypeSN,
OrderCount: comm.show_vs_tag(item.OrderCount_vs, item.OrderCount_diff, item.OrderCount, item2.OrderCount),
OrderCount_X: ([item.OrderCount, item2.OrderCount].join(' vs ')),
CJCount: comm.show_vs_tag(item.CJCount_vs, item.CJCount_diff, item.CJCount, item2.CJCount),
CJCount_X: ([item.CJCount, item2.CJCount].join(' vs ')),
CJPersonNum: comm.show_vs_tag(item.CJPersonNum_vs, item.CJPersonNum_diff, item.CJPersonNum, item2.CJPersonNum),
CJPersonNum_X: ([item.CJPersonNum, item2.CJPersonNum].join(' vs ')),
CJrate: comm.show_vs_tag(item.CJrate_vs, item.CJrate_diff, item.CJrate, item2.CJrate),
CJrate_X: ([item.CJrate, item2.CJrate].join(' vs ')),
YJLY: comm.show_vs_tag(item.YJLY_vs, item.YJLY_diff, item.YJLY, item2.YJLY),
YJLY_X: ([item.YJLY, item2.YJLY].join(' vs ')),
Ordervalue: comm.show_vs_tag(item.Ordervalue_vs, item.Ordervalue_diff, item.Ordervalue, item2.Ordervalue),
Ordervalue_X: ([item.Ordervalue, item2.Ordervalue].join(' vs ')),
});
}
}
@ -133,11 +148,17 @@ class Orders extends Component {
OrderType: item.OrderType,
OrderTypeSN: item.OrderTypeSN,
OrderCount: comm.show_vs_tag(comm.formatPercent(item.OrderCount), item.OrderCount, item.OrderCount, 0),
OrderCount_X: ([item.OrderCount, 0].join(' vs ')),
CJCount: comm.show_vs_tag(comm.formatPercent(item.CJCount), item.CJCount, item.CJCount, 0),
CJCount_X: ([item.CJCount, 0].join(' vs ')),
CJPersonNum: comm.show_vs_tag(comm.formatPercent(item.CJPersonNum), item.CJPersonNum, item.CJPersonNum, 0),
CJPersonNum_X: ([item.CJPersonNum, 0].join(' vs ')),
CJrate: comm.show_vs_tag(item.CJrate, item.CJrate, item.CJrate, 0),
CJrate_X: ([item.CJrate, 0].join(' vs ')),
YJLY: comm.show_vs_tag(comm.formatPercent(item.YJLY), item.YJLY, item.YJLY, 0),
YJLY_X: ([item.YJLY, 0].join(' vs ')),
Ordervalue: comm.show_vs_tag(comm.formatPercent(item.Ordervalue), item.Ordervalue, item.Ordervalue, 0),
Ordervalue_X: ([item.Ordervalue, 0].join(' vs ')),
});
}
}
@ -155,18 +176,24 @@ class Orders extends Component {
OrderType: item2.OrderType,
OrderTypeSN: item2.OrderTypeSN,
OrderCount: comm.show_vs_tag(comm.formatPercent(-item2.OrderCount), -item2.OrderCount, 0, item2.OrderCount),
OrderCount_X: ([ 0, item2.OrderCount].join(' vs ')),
CJCount: comm.show_vs_tag(comm.formatPercent(-item2.CJCount), -item2.CJCount, 0, item2.CJCount),
CJCount_X: ([ 0, item2.CJCount].join(' vs ')),
CJPersonNum: comm.show_vs_tag(comm.formatPercent(-item2.CJPersonNum), -item2.CJPersonNum, 0, item2.CJPersonNum),
CJPersonNum_X: ([0, item2.CJPersonNum].join(' vs ')),
CJrate: comm.show_vs_tag(-item2.CJrate, -item2.CJrate, 0, item2.CJrate),
CJrate_X: ([ 0, item2.CJrate].join(' vs ')),
YJLY: comm.show_vs_tag(comm.formatPercent(-item2.YJLY), -item2.YJLY, 0, item2.YJLY),
YJLY_X: ([0, item2.YJLY].join(' vs ')),
Ordervalue: comm.show_vs_tag(comm.formatPercent(-item2.Ordervalue), -item2.Ordervalue, 0, item2.Ordervalue),
Ordervalue_X: ([ 0, item2.Ordervalue].join(' vs ')),
});
}
}
} else {
result.columns = [
{
title: "",
title: "#",
fixed: 'left',
children: [
{
@ -177,6 +204,7 @@ class Orders extends Component {
</div>
</span>
),
titleX: `${date_picker_store.start_date.format(config.DATE_FORMAT)}~${date_picker_store.end_date.format(config.DATE_FORMAT)}`,
fixed: 'left',
dataIndex: "OrderType",
render: (text, record) => <NavLink to={`/orders_sub/${orders_store.active_tab_key}/${record.OrderTypeSN}/${record.OrderType}`}>{text}</NavLink>,
@ -324,7 +352,7 @@ class Orders extends Component {
return (
<div>
<Row gutter={16} className='sticky-top' >
<Row gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"} >
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
@ -440,6 +468,18 @@ class Orders extends Component {
</span>
),
},
{
key: 'ToB',
label: (
<span>
<ContactsOutlined />
客运类别
</span>
),
},
{key: 'FoodRequirement',label: (<span><CoffeeOutlined />饮食要求</span>),},
{key: 'hobbies',label: (<span><HeartOutlined/>兴趣爱好</span>),},
{key: 'ages',label: (<span><IdcardOutlined/>年龄段</span>),},
].map((ele) => {
return {
...ele,

@ -149,8 +149,8 @@ const Orders_sub = () => {
},
{
title: "出发日期",
dataIndex: "COLI_OrderStartDate",
key: "COLI_OrderStartDate",
dataIndex: "CGI_ArriveDate",
key: "CGI_ArriveDate",
},
{
title: "客人需求",
@ -262,7 +262,7 @@ const Orders_sub = () => {
};
return (
<div>
<Row gutter={{ sm: 16, lg: 32 }} className="sticky-top">
<Row gutter={{ sm: 16, lg: 32 }} className={date_picker_store.siderBroken ? "" : "sticky-top"}>
<Col md={24} lg={12} xxl={14}>
<NavLink to={`/orders`}>返回</NavLink>
</Col>

@ -31,51 +31,54 @@ const ProtectedRoute = ({ auth }) => {
<div>
{/* '试着联系一下技术,所需权限: ' + auth.toString() */}
<Result
status="403"
title="403 权限不足"
status={auth_store.user.name === 'loading' ? '500' : '403'}
title={auth_store.user.name === 'loading' ? '无服务' : '403 权限不足'}
// title="403 "
subTitle={
<>
<div style={{ width: 300, textAlign: 'left', margin: 'auto auto' }}>
<div>
<Text type={'danger'} strong>
申请步骤:
</Text>
</div>
<ol style={{padding: '0 1rem'}}>
<li>
复制以下信息
{/* <Button type="link" onClick={() => copyToClipboard(applyInfo)}>
auth_store.user.name !== 'loading' ? (
<>
<div style={{ width: 300, textAlign: 'left', margin: 'auto auto' }}>
<div>
<Text type={'danger'} strong>
申请步骤:
</Text>
</div>
<ol style={{ padding: '0 1rem' }}>
<li>
复制以下信息
{/* <Button type="link" onClick={() => copyToClipboard(applyInfo)}>
复制
</Button> */}
</li>
{/* <li> */}
</li>
{/* <li> */}
<pre className={'p-s1'} style={{ border: '1px solid rgba(0,0,0,.15)', borderRadius: 4 }}>
{applyInfo}
</pre>
{/* </li> */}
<li>
<Button type="link" href={authApplyLink}>
点击打开 &raquo;OA审批 &raquo;
</Button>
<ul>
<li>输入申请信息(姓名等)</li>
<li>
选择开通权限: <Text type={'warning'}>HT系统分析</Text>
</li>
<li>
填入上述复制的信息到 <Text type={'warning'}>权限内容</Text>
</li>
</ul>
</li>
</ol>
<div>
<Text type={'secondary'} strong>
示例:
</Text>
{/* </li> */}
<li>
<Button type="link" href={authApplyLink}>
点击打开 &raquo;OA审批 &raquo;
</Button>
<ul>
<li>输入申请信息(姓名等)</li>
<li>
选择开通权限: <Text type={'warning'}>HT系统分析</Text>
</li>
<li>
填入上述复制的信息到 <Text type={'warning'}>权限内容</Text>
</li>
</ul>
</li>
</ol>
<div>
<Text type={'secondary'} strong>
示例:
</Text>
</div>
<Image alt="example" src={authExample} preview={false} />
</div>
<Image alt="example" src={authExample} preview={false} />
</div>
</>
</>
) : null
}
/>
</div>

@ -48,6 +48,10 @@ const Sale = () => {
...config_data,
...{
//seriesField: "OPI_Name",//
columnWidthRatio: 0.28,
// dodgePadding: 1,
// minColumnWidth: 5,
// maxColumnWidth: 15,
label: {
position: 'top',
},
@ -206,7 +210,7 @@ const Sale = () => {
return (
<div>
<Row gutter={16} className='sticky-top' >
<Row gutter={16} className={date_picker_store.siderBroken ? "" : "sticky-top"} >
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
@ -219,6 +223,7 @@ const Sale = () => {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
years: { hide_vs: true },
operator: { mode: 'multiple'},
},
}}
onSubmit={(_err, obj, form, str) => {

@ -1,5 +1,5 @@
import React, { useContext } from 'react';
import { Row, Col, Table, Select, Space, Typography, Progress, Spin, Divider } from 'antd';
import React, { useContext, useState } from 'react';
import { Row, Col, Table, Select, Space, Typography, Progress, Spin, Divider, Button, Switch } from 'antd';
import { stores_Context } from '../config';
import { observer } from 'mobx-react';
import * as comm from '../utils/commons';
@ -14,10 +14,9 @@ const overviewGroupKeys = overviewGroup.map(item => item.key);
const Sale_KPI = () => {
const { sale_store, date_picker_store: searchFormStore } = useContext(stores_Context);
const { formValues } = searchFormStore;
const { groupType, loading, operator, } = sale_store.salesTrade;
const { formValues, siderBroken } = searchFormStore;
const { groupType, loading, operator, tableDataSource: dataSource } = sale_store.salesTrade;
const dataSource = [].concat(sale_store.salesTrade[groupType], operator);
const yearData = sale_store.salesTrade[groupType].reduce((r, ele) => r.concat(Object.values(ele.mData)), []);
const operatorObjects = operator.map((v) => ({ key: v.groupsKey, value: v.groupsKey, label: v.groupsLabel }));
const pageRefresh = async (queryData) => {
@ -25,10 +24,15 @@ const Sale_KPI = () => {
|| overviewGroupKeys.includes(queryData.DepartmentList.toLowerCase()); // queryData.DepartmentList.toLowerCase().includes(',');
const _groupType = overviewFlag ? 'overview' : 'dept';
sale_store.setGroupType(_groupType);
sale_store.fetchOperatorTradeData(_groupType, { ...queryData, groupDateType: 'year' });
sale_store.fetchOperatorTradeData('operator', { ...queryData, groupDateType: 'year' });
await sale_store.fetchOperatorTradeData(_groupType, { ...queryData, groupDateType: 'year' });
await sale_store.fetchOperatorTradeData('operator', { ...queryData, groupDateType: 'year' });
sale_store.setPickSales([]);
sale_store.setTableDataSource(false);
setIfmerge(false);
};
const [ifmerge, setIfmerge] = useState(false);
const monthCol = new Array(12).fill(1).map((_, index) => {
return {
title: `${index + 1}`,
@ -154,7 +158,7 @@ const Sale_KPI = () => {
const lineConfig = { appendPadding: 10, xField: 'groupDateVal', yField: 'SumML', seriesField: 'groupsLabel', isGroup: true, smooth: false, meta: comm.cloneDeep(dataFieldAlias), };
return (
<div>
<Row gutter={16} className="sticky-top">
<Row gutter={16} className={siderBroken ? "" : "sticky-top"}>
<Col md={24} lg={24} xxl={24}>
<SearchForm
defaultValue={{
@ -179,39 +183,41 @@ const Sale_KPI = () => {
<Spin spinning={loading}>
<h2 style={{ marginTop: '.5em' }}>年度业绩组成和走势</h2>
<Row>
<Col className="gutter-row" md={8}>
<Col className="gutter-row" md={8} sm={24} xs={24}>
<Donut
{...{ angleField: 'SumML', colorField: 'groupsLabel', label1: { style: { color: '#000000' }, type: 'spider', content: '{name}\n{percentage}' }, legend: false, label2: false }}
title={formValues.DepartmentList?.label}
dataSource={operator.map((row) => ({ ...row, SumML: row.yearML }))}
/>
</Col>
<Col className="gutter-row" md={16}>
<Col className="gutter-row" md={16} sm={24} xs={24}>
<LineWithKPI dataSource={yearData} showKPI={true} {...lineConfig} {...{ legend: false }} />
</Col>
<Col className="gutter-row" md={24}>
<Space gutter={16} size={'large'}>
<h2>顾问业绩走势</h2>
<Select
labelInValue
mode={'multiple'}
style={{ width: '400px' }}
placeholder={'选择顾问'}
onChange={sale_store.setPickSales}
value={sale_store.salesTrade.pickSales}
maxTagCount={1}
maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`}
allowClear={true}
>
{operatorObjects.map((ele) => (
<Select.Option key={ele.key} value={ele.value}>
{ele.label}
</Select.Option>
))}
</Select>
</Space>
<Row gutter={16}>
<Col flex={'12em'}><h2>顾问业绩走势</h2></Col>
<Col flex={'auto'}>
<Select
labelInValue
mode={'multiple'}
style={{ width: '100%' }}
placeholder={'选择顾问'}
onChange={sale_store.setPickSales}
value={sale_store.salesTrade.pickSales}
maxTagCount={1}
maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`}
allowClear={true}
>
{operatorObjects.map((ele) => (
<Select.Option key={ele.key} value={ele.value}>
{ele.label}
</Select.Option>
))}
</Select>
</Col>
</Row>
</Col>
<Col className="gutter-row" md={24}>
<Col className="gutter-row" span={24}>
<LineWithKPI dataSource={sale_store.salesTrade.pickSalesData} showKPI={true} {...lineConfig} />
</Col>
</Row>
@ -219,10 +225,23 @@ const Sale_KPI = () => {
<Row>
<Col className="gutter-row" md={24}>
<Divider orientation="right">
{dataSource.length > 0 && (
<Switch
unCheckedChildren="各账户"
checkedChildren="合并"
key={'openOrMerge'}
checked={ifmerge}
onChange={(e) => {
sale_store.setTableDataSource(e);
setIfmerge(e);
}}
/>
)}
<TableExportBtn label={'sales kpi'} {...{ columns: columnsForExport, dataSource: dataForExport }} />
</Divider>
<Table
sticky
sticky={{ offsetHeader: 64 }}
key={`salesTradeTable`}
loading={loading}
columns={columns}

@ -0,0 +1,202 @@
import { useContext, useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { stores_Context } from '../config';
import { Table, Row, Col, Divider, Switch, Space, Tabs } from 'antd';
import SearchForm from '../components/search/SearchForm';
import { TableExportBtn, VSTag } from './../components/Data';
import { fixTo2Decimals, isEmpty } from './../utils/commons';
// TdCellDataTable
const TdCell = (tdprops) => {
// onMouseEnter, onMouseLeave
const { onMouseEnter, onMouseLeave, ...restProps } = tdprops;
return <td {...restProps} />;
};
const SalesCustomerCareRegular = (props) => {
const { date_picker_store: searchFormStore, customer_store } = useContext(stores_Context);
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const { pivotData: pageData } = customer_store.sales_regular_data;
const pivotOptions = [{key: 'operatorName', label: '顾问'}, {key: 'country', label: '国家'}];
const [pivotRow, setPivotRow] = useState('operatorName');
const onTabsChange = (key) => {
setPivotRow(key);
};
const [dataSource, setDataSource] = useState([]);
const [ifmerge, setIfmerge] = useState(false);
const [dataForExport, setDataForExport] = useState([]);
const [dataForExportS, setDataForExportS] = useState([]);
useEffect(() => {
if ( ! ifmerge) {
setDataSource(pageData[pivotRow].data);
setDataForExport(
pageData[pivotRow].data.reduce(
(r, c) =>
r.concat(
[{ ...c, children: undefined }],
c.children
.reduce((rc, ele) => rc.concat([{ ...ele, [pivotRow]: ele.rowLabel }], [{ ...ele.vsData, [pivotRow]: ele.vsData.rowLabel, vsData: {} }]), [])
.filter((ele) => ele.SumOrder !== undefined)
),
[]
)
);
setDataForExportS(pageData[pivotRow].data.reduce((r, c) => r.concat([{...c, children: undefined}], [{ ...c.vsData, vsData: {} }]), []).filter((ele) => ele.SumOrder !== undefined));
} else {
setDataSource(pageData[pivotRow].mergedData);
setDataForExport(
pageData[pivotRow].mergedData.reduce(
(r, c) =>
r.concat(
[{ ...c, children: undefined }],
c.children.reduce((rc, ele) => rc.concat([{ ...ele, operatorName: ele.rowLabel }], [{ ...ele.vsData, operatorName: ele.vsData.rowLabel, vsData: {} }]), [])
.filter((ele) => ele.SumOrder !== undefined)
),
[]
)
);
setDataForExportS(pageData[pivotRow].mergedData.reduce((r, c) => r.concat([{...c, children: undefined}], [{ ...c.vsData, vsData: {} }]), []).filter((ele) => ele.SumOrder !== undefined));
}
return () => {};
}, [ifmerge, pageData[pivotRow].data]);
const rowColumns = [
{ title: '日期区间', dataIndex: 'seriesKey', key: 'seriesKey' },
{ title: '顾问', dataIndex: 'operatorName', key: 'operatorName' },
{ title: '订单号', dataIndex: 'o_id', key: 'o_id' },
{ title: '预定日期', dataIndex: 'applyDate', key: 'applyDate' },
{ title: '订单状态', key: 'orderState', render: (_, r) => r.orderState === '1' ? '成行' : '', },
{ title: '毛利', dataIndex: 'ML', key: 'ML' },
{ title: '人数', dataIndex: 'personNum', key: 'personNum' },
{ title: '天数', dataIndex: 'tourdays', key: 'tourdays' },
// { title: '', dataIndex: 'CGI_PersonDays', key: 'CGI_PersonDays' },
// { title: '', dataIndex: 'COLI_OrderStartDate', key: 'COLI_OrderStartDate' },
{ title: '小组', dataIndex: 'dept', key: 'dept' },
{ title: '老客户', key: 'IsOld', render: (_, r) => r.IsOld === '1' ? '是' : '' },
{ title: '老客户推荐', key: 'IsCusCommend', render: (_, r) => r.isCusCommend === '1' ? '是' : '' },
{ title: '网站', dataIndex: 'WebCode', key: 'WebCode' },
{ title: '来源', dataIndex: 'SourceType', key: 'SourceType' },
{ title: '页面类型', dataIndex: 'COLI_LineClass', key: 'COLI_LineClass' },
];
const calcDelta = (r, key) => !isEmpty(Number(r.vsData[key])) ? fixTo2Decimals((Number(r[key] || 0) - Number(r.vsData[key]))/Number(r.vsData[key]) *100) : null;
const renderVS = (v, r, key) => {
const delta = calcDelta(r, key);
return <>
<Space direction={'vertical'}>
<span>
{v || 0}
{r.vsData[key] ? <span type="secondary"> VS {r.vsData[key]}</span> : null}
</span>
{delta && <VSTag diffPercent={delta} />}
</Space>
</>;
};
const columns = [
{ key: 'SumOrder', title: '订单数', dataIndex: 'SumOrder', width: '5em', render: (v, r) => renderVS(v, r, 'SumOrder') },
{ key: 'ConfirmOrder', title: '成交数', dataIndex: 'ConfirmOrder', width: '5em', render: (v, r) => renderVS(v, r, 'ConfirmOrder') },
{ key: 'ConfirmPersonNum', title: '✔人数(SUM)', dataIndex: 'ConfirmPersonNum', width: '5em', render: (v, r) => renderVS(v, r, 'ConfirmPersonNum') },
{ key: 'confirmTourdays', title: '✔团天数(AVG)', dataIndex: 'confirmTourdays', width: '5em', render: (v, r) => renderVS(v, r, 'confirmTourdays') },
{ key: 'SumML', title: '预计毛利', dataIndex: 'SumML', width: '5em', render: (v, r) => renderVS(v, r, 'SumML') }, // SumML_txt
{ key: 'ConfirmRates', title: '成交率', dataIndex: 'ConfirmRates_txt', width: '5em', render: (v, r) => renderVS(v, r, 'ConfirmRates') },
{ key: 'SingleML', title: '单团毛利', dataIndex: 'SingleML', width: '5em', render: (v, r) => renderVS(v, r, 'SingleML') },
];
return (
<>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
...customer_store.sales_regular_data.searchValues,
},
shows: ['DateType', 'DepartmentList', 'WebCode', 'dates', 'IncludeTickets'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple' },
WebCode: { show_all: false, mode: 'multiple' },
dates: { hide_vs: false },
},
}}
onSubmit={(_err, obj, form, str) => {
customer_store.setSearchValues(obj, form, 'sales_regular_data');
customer_store.get_sales_regular_data_vs(obj, pivotRow);
}}
/>
</Col>
</Row>
<Tabs
type={'card'}
activeKey={pivotRow}
onChange={onTabsChange}
items={pivotOptions.map((ele) => {
return {
...ele,
children: (
<>
{/* <h2>{ele.label}-老客户, 含推荐</h2> */}
<>
<Divider orientation={'right'} style={{backgroundColor: '#fff', margin: 0, padding: '10px 0'}} >
{pageData[pivotRow].data.length > 0 && pivotRow === 'operatorName' && (
<Switch
unCheckedChildren="各账户"
checkedChildren="合并"
key={'openOrMerge'}
checked={ifmerge}
onChange={(e) => {
setIfmerge(e);
}}
/>
)}
<Divider type={'vertical'} />
<TableExportBtn
btnTxt="导出明细"
label={`${formValuesToSub.Date1}-老客户-明细`}
{...{ columns: [{ title: ele.label, dataIndex: pivotRow, key: pivotRow }, ...rowColumns], dataSource: pageData[pivotRow].rawData }}
/>
<Divider type={'vertical'} />
<TableExportBtn
btnTxt="导出下表-展开"
label={`${formValuesToSub.Date1}-${ele.label}.老客户`}
{...{ columns: [{ title: ele.label, dataIndex: pivotRow, key: pivotRow }, ...columns], dataSource: dataForExport }}
/>
<Divider type={'vertical'} />
<TableExportBtn
btnTxt="导出下表-总"
label={`${formValuesToSub.Date1}-${ele.label}.老客户`}
{...{ columns: [{ title: ele.label, dataIndex: pivotRow, key: pivotRow }, ...columns], dataSource: dataForExportS }}
/>
</Divider>
</>
<Table
sticky
dataSource={dataSource}
loading={pageData[ele.key].loading}
columns={[
{
key: ele.key,
title: ele.label,
dataIndex: ele.key,
width: '6em',
filters: pageData[ele.key].filterColValues,
onFilter: (value, record) => value.includes(record[ele.key]),
filterSearch: true,
},
...columns,
]}
pagination={false}
/>
</>
),
};
})}
/>
</>
);
};
export default observer(SalesCustomerCareRegular);

@ -0,0 +1,123 @@
import { useContext, useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { stores_Context } from '../config';
import { Spin, Table, Row, Col, Tabs, Switch } from 'antd';
import SearchForm from './../components/search/SearchForm';
import { TableExportBtn } from './../components/Data';
import { empty } from '../utils/commons';
import './kpi.css';
const apartOptions = [
{ key: 'inbound', value: 'inbound', label: '入境' },
{ key: 'outbound', value: 'outbound', label: '出境' },
{ key: 'domestic', value: 'domestic', label: '国内' },
];
const apartOptionsMapped = apartOptions.reduce((r, v) => ({...r, [v.value]: v}), {});
export default observer((props) => {
const { financial_store: financialStore, date_picker_store: searchFormStore } = useContext(stores_Context);
const { formValues, formValuesToSub } = searchFormStore;
const { servicePersonNum } = financialStore;
const { curTab } = servicePersonNum;
const [ifNull, setIfNull] = useState(false);
useEffect(() => {
// DistributionStore.setFormDates(formValuesToSub);
financialStore.resetPersonNumData();
return () => {};
}, [formValuesToSub]);
const pageRefresh = (queryData = formValuesToSub) => {
// console.log(queryData, 'qqqq');
financialStore.getPersonNum(queryData);
financialStore.setPersonNumTableDataSource(false);
setIfNull(false);
};
const columns = [
{ title: apartOptionsMapped[curTab].label, dataIndex: 'groupsLabel', children: [{ title: `${formValuesToSub.Date1}~${formValuesToSub.Date2.substring(0, 10)}`, dataIndex: 'groupsLabel' }] },
{
title: '人次数',
children: [{ title: '组织', dataIndex: 'orgz' }, ...(['inbound', 'domestic'].includes(curTab) ? [{ title: '接待', dataIndex: 'hosts' }] : [])],
},
{
title: '人天',
children: [{ title: '组织', dataIndex: 'orgzPDays' }, ...(['inbound', 'domestic'].includes(curTab) ? [{ title: '接待', dataIndex: 'hostsPDays' }] : [])],
},
];
return (
<>
<Row gutter={16} style={{ margin: '-16px -8px', position: 'sticky', top: 0, zIndex: 10 }}>
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
DateType: { key: 'startDate', value: 'startDate', label: '走团日期' },
},
shows: ['dates'],
fieldProps: {
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
pageRefresh(obj);
}}
/>
</Col>
</Row>
<section>
<Tabs
activeKey={curTab}
onChange={(v) => {
financialStore.setCurTab(v);
if (empty(servicePersonNum[v].dataSource)) {
pageRefresh();
}
}}
tabBarExtraContent={{
right: (
<>
<Switch
unCheckedChildren="原数据"
checkedChildren="去除空"
key={'ifNull'}
checked={ifNull}
onChange={(e) => {
financialStore.setPersonNumTableDataSource(e);
setIfNull(e);
}}
/>
<TableExportBtn label={'服务人数_'+apartOptionsMapped[curTab].label} {...{ columns, dataSource: servicePersonNum[curTab].dataSource }} />
</>
),
}}
type="card"
items={apartOptions.map((ele) => {
return {
...ele,
children: (
<Spin spinning={servicePersonNum.loading}>
<Table
id="table_to_xlsx_sale"
dataSource={servicePersonNum[curTab].dataSource}
rowKey="groupsKey"
columns={columns}
size="small"
loading={servicePersonNum[curTab].loading}
pagination={false}
scroll={{ x: '100%' }}
/>
</Spin>
),
};
})}
/>
</section>
</>
);
});

@ -0,0 +1,334 @@
import React, { useContext, useState, useEffect } from 'react';
import { observer } from 'mobx-react';
import { Link } from 'react-router-dom';
import { stores_Context, DATE_FORMAT, SMALL_DATETIME_FORMAT } from '../../config';
import moment from 'moment';
import { Row, Col, Table, Select, Spin, Tag } from 'antd';
import SearchForm from '../../components/search/SearchForm';
import MixFieldsDetail from '../../components/MixFieldsDetail';
import { fixTo2Decimals, isEmpty, pick } from '../../utils/commons';
import Column from '../../components/Column';
import { groupsMappedByKey } from '../../libs/ht';
const COLOR_SETS = [
"#FFFFFF",
// "#5B8FF9",
// "#FF6B3B",
"#9FB40F",
"#76523B",
"#DAD5B5",
"#E19348",
"#F383A2",
];
const transparentHex = '1A';
const numberConvert10K = (number, scale = 10) => {
return fixTo2Decimals((number/(1000*scale)));
};
export default observer((props) => {
const { SalesCRMDataStore, date_picker_store: searchFormStore } = useContext(stores_Context);
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const _formValuesToSub = pick(formValuesToSub, ['DepartmentList', 'WebCode']);
const { searchValues, resetData, results } = SalesCRMDataStore;
const operatorObjects = results.details.map((v) => ({ key: v.groupsKey, value: v.groupsKey, label: v.groupsLabel, text: v.groupsLabel }));
// console.log(operatorObjects);
const pageRefresh = async (obj) => {
resetData('results');
const deptList = obj.DepartmentList.split(',');
const includeCH = ['1', '2', '7'].some(ele => deptList.includes(ele));
const includeAH = ['28'].every(ele => deptList.includes(ele));
const includeGH = ['33'].every(ele => deptList.includes(ele));
const otherDept = deptList.filter(ele => !['1', '2', '7', '28', '33'].includes(ele));
const separateParam = [];
if (includeCH) {
const inCH = deptList.filter(k => ['1', '2', '7'].includes(k)).join(',');
separateParam.push({ DepartmentList: inCH, retLabel: 'CH'});
}
if (includeAH) {
separateParam.push({ DepartmentList: '28', retLabel: 'AH'});
}
if (includeGH) {
separateParam.push({ DepartmentList: '33', retLabel: 'GH'});
}
if (!isEmpty(otherDept) && (!isEmpty(includeAH) || !isEmpty(includeCH) || !isEmpty(includeGH))) {
separateParam.push({ DepartmentList: otherDept.join(','), retLabel: otherDept.map(k => groupsMappedByKey[k].label).join(', ') }); // ''
}
if (!includeAH && !includeCH && !includeGH) {
separateParam.push({ DepartmentList: obj.DepartmentList });
}
// console.log('separateParam', separateParam, otherDept);
// console.log('formValuesToSub --- pageRefresh', formValuesToSub.DepartmentList);
// return;
for await (const subParam of separateParam) {
// console.log(subParam);
await SalesCRMDataStore.get90n180Data({
...(obj || _formValuesToSub),
...subParam,
groupType: 'overview',
// groupType: 'dept', // todo:
groupDateType: '',
});
await SalesCRMDataStore.get90n180Data({
...(obj || _formValuesToSub),
...subParam,
groupType: 'operator',
groupDateType: '',
});
}
};
const getFullYearDiagramData = async (obj) => {
// console.log('invoke --- getFullYearDiagramData');
// console.log('formValuesToSub --- getFullYearDiagramData', formValuesToSub.DepartmentList);
await SalesCRMDataStore.getResultData({
..._formValuesToSub,
Date1: moment(obj.date).startOf('year').format(DATE_FORMAT),
Date2: moment(obj.date).endOf('year').format(SMALL_DATETIME_FORMAT),
groupType: 'overview',
// groupType: 'operator',
groupDateType: 'month',
...(obj),
});
};
const getDiagramData = async (obj) => {
// console.log('invoke --- getDiagramData');
// console.log(_formValuesToSub, SalesCRMDataStore.searchValuesToSub);
await SalesCRMDataStore.getResultData({
..._formValuesToSub,
...SalesCRMDataStore.searchValuesToSub,
// Date1: moment().startOf('year').format(DATE_FORMAT),
// Date2: moment().endOf('year').format(SMALL_DATETIME_FORMAT),
// groupType: 'overview',
groupType: 'operator',
groupDateType: '',
...(obj),
});
};
const retPropsMinRatesSet = { 'CH': 12, 'AH': 8, 'default': 10 }; //
/**
* 业绩数据列
* ! 成行率: CH个人成行率<12%, AH<8%
*/
const dataFields = (suffix, colRootIndex) => [
{
key: 'ConfirmRates' + suffix,
title: '成行率',
dataIndex: [suffix, 'ConfirmRates'],
width: '5em',
// CH<12%, AH<8%
render: (val, r) => ({
props: { style: { color: val < (retPropsMinRatesSet?.[r?.retProps || 'default'] || retPropsMinRatesSet.default) ? 'red' : 'green', backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: val ? `${val}%` : '',
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].ConfirmRates - b[suffix].ConfirmRates),
},
{
key: 'SumML' + suffix,
title: '业绩/万',
dataIndex: [suffix, 'SumML'],
width: '5em',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: numberConvert10K(_),
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].SumML - b[suffix].SumML),
},
{
key: 'ConfirmOrder' + suffix,
title: '团数',
dataIndex: [suffix, 'ConfirmOrder'],
width: '5em',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: _,
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].ConfirmOrder - b[suffix].ConfirmOrder),
},
{
key: 'SumOrder' + suffix,
title: '订单数',
dataIndex: [suffix, 'SumOrder'],
width: '5em',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: _,
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].SumOrder - b[suffix].SumOrder),
},
{
key: 'ResumeOrder' + suffix,
title: '老客户团数',
dataIndex: [suffix, 'ResumeOrder'],
width: '5em',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: _,
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].ResumeOrder - b[suffix].ResumeOrder),
},
{
key: 'ResumeRates' + suffix,
title: '老客户成行率',
dataIndex: [suffix, 'ResumeRates'],
width: '5em',
render: (val, r) => ({
props: { style: { backgroundColor: COLOR_SETS[colRootIndex]+transparentHex } },
children: val ? `${val}%` : ''
}),
sorter: (a, b) => (a.groupType === 'overview' ? -1 : b.groupType === 'overview' ? 0 : a[suffix].ResumeRates - b[suffix].ResumeRates),
},
];
const dashboardTableProps = {
pagination: false,
size: 'small',
showSorterTooltip: false,
columns: [
{
title: '',
dataIndex: 'groupsLabel',
key: 'name',
width: '5em',
filterSearch: true,
filters: operatorObjects.sort((a, b) => a.text.localeCompare(b.text)),
onFilter: (value, record) => record.groupsKey === value || record.groupType === 'overview',
render: (text, record) => (record.groupType !== 'operator' ? text : <Link to={`/op_risk/sales/${record.groupsKey}`}>{text}</Link>),
},
{
title: () => (<>前90 -30<br/>{searchValues.date90.Date1} {searchValues.date90.Date2}</>),
key: 'date',
children: dataFields('result90', 0),
},
{
title: () => (<>前180 -50<br/>{searchValues.date180.Date1} {searchValues.date180.Date2}</>),
key: 'department',
children: dataFields('result180', 1),
},
],
rowClassName: (record, rowIndex) => {
return record.groupType === 'overview' ? 'ant-tag-blue' : '';
},
};
const columnConfig = {
xField: 'groupsLabel',
yField: 'SumML',
label: { position: 'top' },
};
const [clickColumn, setClickColumn] = useState({});
const [clickColumnTitle, setClickColumnTitle] = useState('');
const onChartItemClick = (colData) => {
// console.log('onChartItemClick', colData);
if (colData.groupType === 'operator') {
// test: 0
return false; // ,
}
setClickColumn(colData);
setClickColumnTitle(moment(colData.groupDateVal).format('YYYY-MM'));
};
const chartsConfig = {
colFields: ['ConfirmOrder', 'SumOrder'],
lineFields: ['SumML', 'ConfirmRates'],
seriesField: null,
xField: 'groupDateVal',
itemClick: onChartItemClick,
};
useEffect(() => {
if (isEmpty(clickColumnTitle)) {
return () => {};
}
getDiagramData({
Date1: moment(clickColumn.groupDateVal).startOf('month').format(DATE_FORMAT),
Date2: moment(clickColumn.groupDateVal).endOf('month').format(SMALL_DATETIME_FORMAT),
groupType: 'operator', // test: overview
groupDateType: '',
});
return () => {};
}, [clickColumnTitle]);
return (
<>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col md={24} lg={24} xxl={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
...SalesCRMDataStore.searchValues,
},
shows: ['DepartmentList', 'WebCode', 'date'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple', col: 5 },
WebCode: { show_all: false, mode: 'multiple', col: 5 },
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
SalesCRMDataStore.setSearchValues(obj, form);
pageRefresh(obj);
getFullYearDiagramData({ groupType: 'overview', ...obj });
}}
/>
</Col>
</Row>
<section>
<Table {...dashboardTableProps} bordered dataSource={[...results.dataSource, ...results.details]} loading={results.loading} sticky />
</section>
<section>
<Row gutter={16}>
<Col flex={'12em'}><h3>每月数据</h3></Col>
<Col flex={'auto'}>
<Select
labelInValue
// mode={'multiple'}
style={{ width: '100%' }}
placeholder={'选择顾问'}
// onChange={sale_store.setPickSales}
// onSelect={}
onChange={(labelInValue) => labelInValue ? getFullYearDiagramData({ groupType: 'operator', opisn: labelInValue.value, }) : false}
onClear={() => getFullYearDiagramData({ groupType: 'overview', opisn: -1, })}
// value={sale_store.salesTrade.pickSales}
// maxTagCount={1}
// maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} ...`}
allowClear={true}
options={operatorObjects}
/>
{/* {operatorObjects.map((ele) => (
<Select.Option key={ele.key} value={ele.value}>
{ele.label}
</Select.Option>
))}
</Select> */}
</Col>
</Row>
<Spin spinning={results.loading}>
{/* 小组每月; x轴: 日期; y轴: [订单数, ...] */}
<MixFieldsDetail {...chartsConfig} dataSource={results.byDate} />
</Spin>
<h3>
点击上方图表的柱状图, 查看当月 <Tag color='orange'>业绩</Tag>数据: <Tag color='orange'>{clickColumnTitle}</Tag>
</h3>
{/* 显示小组的详情: 所有顾问? */}
<Spin spinning={results.loading}>
<Column {...columnConfig} dataSource={results.byOperator} />
</Spin>
{/* <Table columns={[{ title: '', dataIndex: 'date', key: 'date', width: '5em' }, ...dataFields]} bordered size='small' /> */}
</section>
<section>
{/* 月份×小组的详情; x轴: 顾问; y轴: [订单数, ...] */}
{/* <MixFieldsDetail {...chartsConfig} xField={'label'} dataSource={[]} /> */}
</section>
</>
);
});

@ -0,0 +1,258 @@
import React, { useContext } from 'react';
import { Link } from 'react-router-dom';
import { observer } from 'mobx-react';
import { stores_Context } from '../../config';
import { InfoCircleOutlined } from '@ant-design/icons';
import { Row, Col, Table, Tooltip } from 'antd';
import SearchForm from '../../components/search/SearchForm';
import { pick } from '../../utils/commons';
const COLOR_SETS = [
"#FFFFFF",
// "#5B8FF9",
// "#FF6B3B",
"#9FB40F",
"#76523B",
"#DAD5B5",
"#E19348",
"#F383A2",
];
const transparentHex = '1A';
export default observer((props) => {
const { SalesCRMDataStore, date_picker_store: searchFormStore } = useContext(stores_Context);
// const { formValues, siderBroken } = searchFormStore;
const { formValues, formValuesToSub, siderBroken } = searchFormStore;
const _formValuesToSub = pick(formValuesToSub, ['DepartmentList', 'WebCode']);
const { resetData, process } = SalesCRMDataStore;
const operatorObjects = process.details.map((v) => ({ key: v.groupsKey, value: v.groupsKey, label: v.groupsLabel, text: v.groupsLabel }));
const pageRefresh = async (obj) => {
resetData('process');
Promise.allSettled([
SalesCRMDataStore.getProcessData({
...(obj || _formValuesToSub),
// ...subParam,
// groupType: 'overview',
groupType: 'dept',
groupDateType: '',
}),
SalesCRMDataStore.getProcessData({
...(obj || _formValuesToSub),
// ...subParam,
groupType: 'operator',
groupDateType: '',
}),
]);
};
const tableSorter = (a, b, colName) => (a.groupType !== 'operator' ? -1 : b.groupType !== 'operator' ? 0 : a[colName] - b[colName]);
const percentageRender = val => val ? `${val}%` : '';
const activityTableProps = {
rowKey: 'groupsKey',
pagination: { pageSize: 10, showSizeChanger: false },
size: 'small',
rowClassName: (record, rowIndex) => {
return record.groupType === 'operator' ? '': 'ant-tag-blue';
},
showSorterTooltip: false,
columns: [
{ title: '', dataIndex: 'groupsLabel', key: 'groupsLabel', width: '6rem',
filterSearch: true,
filters: operatorObjects.sort((a, b) => a.text.localeCompare(b.text)),
onFilter: (value, record) => record.groupsKey === value || record.groupType !== 'operator',
render: (text, record) => (record.groupType !== 'operator' ? text : <Link to={`/op_risk/sales/${record.groupsKey}`}>{text}</Link>),
}, // :
{
title: '顾问动作',
key: 'date',
children: [
{ title: () => (
<>
首次响应率24H{' '}
<Tooltip title="达到24H内回复的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'firstTouch24', key: 'firstTouch24', render: percentageRender,
sorter: (a, b) => tableSorter(a, b, 'firstTouch24'),
},
{ title: () => (
<>
48H内报价率{' '}
<Tooltip title="达到48H内报价的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'firstQuote48', key: 'firstQuote48',render: percentageRender ,
sorter: (a, b) => tableSorter(a, b, 'firstQuote48'), },
{ title: () => (
<>
一次报价率{' '}
<Tooltip title="首次报价的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'quote1', key: 'quote1',render: percentageRender ,
sorter: (a, b) => tableSorter(a, b, 'quote1'), },
{ title: () => (
<>
二次报价率{' '}
<Tooltip title="二次报价的订单数/一次报价后回复数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'quote2', key: 'quote2',render: percentageRender ,
sorter: (a, b) => tableSorter(a, b, 'quote2'), },
{ title: () => (
<>
&gt;50条会话{' '}
<Tooltip title=">50条会话的订单数/总订单">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'turnsGT50', key: 'turnsGT50', render: percentageRender ,
sorter: (a, b) => tableSorter(a, b, 'turnsGT50'), },
{ title: () => (
<>
违规数{' '}
<Tooltip title="未遵循24H回复的订单数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'violations', key: 'violations',
sorter: (a, b) => tableSorter(a, b, 'violations'), },
],
},
{
title: '客人回复',
key: 'department',
children: [
{
title: () => (
<>
首次回复率24H{' '}
<Tooltip title="达到24H内回复的订单数/总订单数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'firstReply24', key: 'firstReply24',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1]+transparentHex } },
children: percentageRender(_),
}),
sorter: (a, b) => tableSorter(a, b, 'firstReply24'), },
{
title: () => (
<>
48H内报价回复率{' '}
<Tooltip title="48H内报价后的回复数/报价的订单数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'replyQuote48', key: 'replyQuote48',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1]+transparentHex } },
children: percentageRender(_),
}),
sorter: (a, b) => tableSorter(a, b, 'replyQuote48'), },
{
title: () => (
<>
一次报价回复率{' '}
<Tooltip title="一次报价后回复率/订单总数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'replyQuote1', key: 'replyQuote1',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1]+transparentHex } },
children: percentageRender(_),
}),
sorter: (a, b) => tableSorter(a, b, 'replyQuote1'), },
{
title: () => (
<>
二次报价回复率{' '}
<Tooltip title="二次报价后回复数/一次报价后回复数">
<InfoCircleOutlined />
</Tooltip>
</>
), dataIndex: 'replyQuote2', key: 'replyQuote2',
render: (_, r) => ({
props: { style: { backgroundColor: COLOR_SETS[1]+transparentHex } },
children: percentageRender(_),
}),
sorter: (a, b) => tableSorter(a, b, 'replyQuote2'), },
],
},
],
};
const riskTableProps = {
rowKey: 'groupsKey',
pagination: { pageSize: 10, showSizeChanger: false },
size: 'small',
rowClassName: (record, rowIndex) => {
return record.groupType === 'operator' ? '': 'ant-tag-blue';
},
showSorterTooltip: false,
columns: [
{ title: '', dataIndex: 'groupsLabel', key: 'groupsLabel', width: '6rem',
filterSearch: true,
filters: operatorObjects.sort((a, b) => a.text.localeCompare(b.text)),
onFilter: (value, record) => record.groupsKey === value || record.groupType !== 'operator',
render: (text, record) => (record.groupType !== 'operator' ? text : <Link to={`/op_risk/sales/${record.groupsKey}`}>{text}</Link>),
}, // :
{ title: '>24H回复', dataIndex: 'lostTouch24', key: 'lostTouch24',
sorter: (a, b) => tableSorter(a, b, 'lostTouch24'),
},
{ title: '首次报价周期>48h', dataIndex: 'lostQuote48', key: 'lostQuote48',
sorter: (a, b) => tableSorter(a, b, 'lostQuote48'),
},
{ title: '报价次数<1次', dataIndex: 'lostQuote1', key: 'lostQuote1',
sorter: (a, b) => tableSorter(a, b, 'lostQuote1'),
},
{ title: '报价次数<2次', dataIndex: 'lostQuote2', key: 'lostQuote2',
sorter: (a, b) => tableSorter(a, b, 'lostQuote2'),
},
],
};
return (
<>
<Row gutter={16} className={siderBroken ? '' : 'sticky-top'}>
<Col md={24} lg={24} xxl={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
...SalesCRMDataStore.searchValues,
},
shows: ['DepartmentList', 'WebCode', 'DateType', 'dates'],
fieldProps: {
DepartmentList: { show_all: false, mode: 'multiple', col: 5 },
WebCode: { show_all: false, mode: 'multiple', col: 5 },
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
SalesCRMDataStore.setSearchValues(obj, form);
pageRefresh(obj);
}}
/>
</Col>
</Row>
<section>
<Table {...activityTableProps} bordered dataSource={[...process.dataSource, ...process.details]} loading={process.loading} sticky />
</section>
<section>
<h3>未成行订单 数量</h3>
<Table {...riskTableProps} bordered dataSource={[...process.dataSource, ...process.details]} loading={process.loading} sticky />
</section>
</>
);
});

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