Merge branch 'feature/2.0' of github.com:hainatravel/dashboard into feature/2.0

feature/2.0-sales-trade
YCC 2 years ago
commit fac532d83a

@ -10,9 +10,9 @@ import {
DollarOutlined,
AreaChartOutlined,
WechatOutlined,
UserOutlined, FlagOutlined, PieChartOutlined
UserOutlined, FlagOutlined, PieChartOutlined, BarChartOutlined
} from '@ant-design/icons';
import { Layout, Menu, Image, Spin } from 'antd';
import { Layout, Menu, Image, Badge } from 'antd';
import { BrowserRouter, Route, Routes, NavLink } from 'react-router-dom';
import Home from './views/Home';
import Dashboard from './views/Dashboard';
@ -37,6 +37,7 @@ 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';
const App = () => {
const { Content, Footer, Sider } = Layout;
@ -123,6 +124,15 @@ const App = () => {
},
{ key: 'kpi', label: <NavLink to="/kpi">目标</NavLink>, icon: <FlagOutlined /> },
{ key: 'distribution', label: <NavLink to="/distribution">统计分布</NavLink>, icon: <PieChartOutlined /> },
{
key: 'detail',
label: (
<NavLink to="/detail">
<Badge.Ribbon text="Beta">统计分析</Badge.Ribbon>
</NavLink>
),
icon: <BarChartOutlined />,
},
];
return (
@ -165,6 +175,7 @@ const App = () => {
<Route path="/" element={<Home />} />
<Route path="/kpi" element={<KPI />} />
<Route path="/distribution" element={<Distribution />} />
<Route path="/detail" element={<Detail />} />
<Route element={<ProtectedRoute auth={['admin', 'director_bu', 'marketing']} />}>
<Route path="/orders" element={<Orders />} />
<Route path="/orders_sub/:ordertype/:ordertype_sub/:ordertype_title" element={<Orders_sub />} />

@ -0,0 +1,53 @@
import { observer } from 'mobx-react';
import { Line } from '@ant-design/plots';
import { merge, isEmpty } from '../utils/commons';
import { dataFieldAlias } from '../libs/ht';
const uniqueByKey = (array, key, pickLast) => {
const seen = new Map();
const isPickLast = pickLast === true;
return array.filter(item => {
const k = item[key];
const storedItem = seen.get(k);
if(storedItem) {
if(isPickLast) {
seen.set(k, item); // update with last item
}
return false;
}
seen.set(k, item);
return true;
});
};
export default observer((props) => {
const { config, dataSource, ...extProps } = props;
const kpiKey = dataFieldAlias[config.yField]?.nestkey?.v;
const _data = dataSource.reduce((r, v) => {
r.push(v);
if ( ! isEmpty(v[kpiKey])) { // 线 : #F4664A
r.push({...v, [config.yField]: v[kpiKey], [config.seriesField]: dataFieldAlias[kpiKey].label});
}
return r;
}, []);
const mergeLineConfig = merge({
color: ['#598cf3', '#F4664A', '#FAAD14'],
lineStyle: (data) => {
console.log(data);
if (data[config.seriesField].includes('目标')) {
return {
lineDash: [4, 4],
opacity: 0.5,
};
}
return {
opacity: 1,
};
},
}, config);
return <Line {...mergeLineConfig} data={_data} />;
});

@ -0,0 +1,42 @@
import { useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { sortBy, merge } from '../utils/commons';
import { dataFieldAlias } from '../libs/ht';
import { Mix, Scatter } from '@ant-design/plots';
export default observer((props) => {
const { dataSource, ...extProps } = props;
const config = merge(
{
appendPadding: 10,
// xField: 'Revenue (Millions)',
// yField: 'Rating',
shape: 'circle',
// colorField: 'Genre',
size: 4,
yAxis: {
nice: true,
line: {
style: {
stroke: '#aaa',
},
},
},
xAxis: {
min: -100,
grid: {
line: {
style: {
stroke: '#eee',
},
},
},
line: {
style: {
stroke: '#aaa',
},
},
},
}, extProps);
return <Scatter {...config} data={dataSource} />;
});

@ -5,7 +5,7 @@ import { merge } from '../utils/commons';
export default observer((props) => {
const { dataSource, line, title, ...extProps } = props;
const yMax = (Math.max(line?.value || 0, ...dataSource.map((ele) => ele[extProps.yField])))*1;
const yMax = (Math.max(line?.value || 0, (dataSource.reduce((r, ele) => r+ele[extProps.yField], 0))))*1;
const annotationsLine = line
? [
{

@ -6,6 +6,7 @@ import SearchForm from './../search/SearchForm';
import { bu, KPIObjects, KPISubjects } from './../../libs/ht';
import { isEmpty, fixTo2Decimals, fixTo4Decimals, cloneDeep, numberFormatter, fixToInt, merge } from './../../utils/commons';
import ProfitTable from './SubjectTable/Profit';
import { toJS } from 'mobx';
const Todo = (props) => {
return <h2>TODO</h2>;
@ -28,7 +29,7 @@ export default observer((props) => {
const { KPIStore, DictDataStore, date_picker_store: searchFormStore } = useContext(stores_Context);
const { sort, initialValue, hides, shows, fieldProps: _fieldProps } = {
sort: '',
initialValue: searchFormStore.formValues,
initialValue: '', // searchFormStore.formValues,
fieldProps: {},
hides: [],
shows: ['DateType', 'years'],
@ -43,15 +44,15 @@ export default observer((props) => {
<Col className="gutter-row mb-n1 p-none" span={24}>
<SearchForm
defaultValue={{
initialValue,
sort, initialValue,
shows,
fieldProps,
}}
confirmText="查询"
onSubmit={(_err, obj, form, str) => {
console.log('invoke kpi setting search');
// console.log('invoke kpi setting search');
if (typeof onSearchSubmit === 'function') {
onSearchSubmit(obj);
onSearchSubmit(obj, form);
}
}}
/>
@ -63,6 +64,7 @@ export default observer((props) => {
tabPosition={'left'}
onChange={(sub) => {
KPIStore.setSettingSubject(sub);
// onSearchSubmit(searchFormStore.formValuesToSub);
}}
items={KPISubjects.map((ele, i) => {
const SubjectTableComponent = subjectComponents[ele.key];

@ -1,23 +1,22 @@
import { useContext } from 'react';
import { observer } from 'mobx-react';
// import { stores_Context } from '../config';
import { Table } from 'antd';
import KPISettings from './KPISettings';
import { bu, KPISubjects } from '../../libs/ht';
const sort = { DateType: 10, years: 11 };
const yearInitial = {};
const searchFormItemSet = {
'bu': { shows: ['DateType', 'years', 'HTBusinessUnits'] },
'dept': { shows: ['DateType', 'years', 'DepartmentList'], fieldProps: { DepartmentList: { allowClear: true } } },
'operator': { shows: ['DateType', 'years', 'DepartmentList'] }, // , 'operator'
'destination': { shows: ['DateType', 'years', 'destination'] },
'country': { shows: ['DateType', 'years', 'country'] },
'bu': { shows: ['DateType', 'years', 'HTBusinessUnits'], sort },
'dept': { shows: ['DateType', 'years', 'DepartmentList'], sort, fieldProps: { DepartmentList: { allowClear: true,isLeaf: true, show_all: false } }, },
'operator': { shows: ['DateType', 'years', 'DepartmentList', 'operator'], fieldProps: { DepartmentList: { allowClear: true, isLeaf: true }, operator: { param: { is_assign: 1 } } }, sort },
'destination': { shows: ['DateType', 'years', 'destination'], sort },
'country': { shows: ['DateType', 'years', 'country'], sort },
};
export default observer((props) => {
const searchProps = searchFormItemSet?.[props.curObject] || {};
return (
<>
<KPISettings {...{ searchProps, objects: bu, KPISubjects }} {...props} />
<KPISettings {...{ searchProps, KPISubjects }} {...props} />
</>
);
});

@ -20,12 +20,12 @@ export default observer((props) => {
const { curObject } = props;
const [dataSource, setDataSource] = useState([]);
useEffect(() => {
onSearchSubmit({
object: curObject,
date_type: 'applyDate',
start_date: searchFormStore.start_date.startOf('year').format('YYYY-MM-DD'),
end_date: searchFormStore.end_date.endOf('year').format('YYYY-MM-DD 23:59'),
});
// onSearchSubmit({
// object: curObject,
// date_type: 'applyDate',
// start_date: searchFormStore.start_date.startOf('year').format('YYYY-MM-DD'),
// end_date: searchFormStore.end_date.endOf('year').format('YYYY-MM-DD 23:59'),
// });
return () => {};
}, []);
@ -129,6 +129,7 @@ export default observer((props) => {
value: mVal,
kpi_id: curObj.kpiDataMapped?.[`M${mIndex}`]?.kpi_id || undefined,
key: undefined,
group_date_type: 'month',
};
});
return r.concat(allMonth);
@ -143,6 +144,7 @@ export default observer((props) => {
start_date: moment([KPIStore.settingYear, 0, 1]).format('YYYY-MM-DD'),
end_date: moment([KPIStore.settingYear, 11, 1]).endOf('M').format('YYYY-MM-DD HH:mm'),
kpi_id: kpiYear?.kpi_id || undefined,
group_date_type: 'year',
}))(dataSource?.[0] || {});
tableData.unshift(yearRow);
console.log('sub', tableData, delKpiIds);

@ -24,11 +24,11 @@ export default observer((props) => {
const [editableRowsKeys, setEditableRowKeys] = useState([]);
useEffect(() => {
setDataSource(KPIStore.pageData);
setEditableRowKeys([]);
setEditOpen(false);
return () => {};
}, [KPIStore.pageData]);
const PercentInput = useMemo(
() =>
// eslint-disable-next-line react/display-name
@ -84,7 +84,7 @@ export default observer((props) => {
title: '年度目标',
dataIndex: 'yearValue',
valueType: 'digit',
fieldProps: { style: { width: '100%' }, step: 10000*100 },
fieldProps: { style: { width: '100%' }, step: 10000 * 100 },
formItemProps: {
style: { width: '100%' },
},
@ -138,22 +138,23 @@ export default observer((props) => {
value: mVal,
kpi_id: curObj.kpiDataMapped?.[`M${mIndex}`]?.kpi_id || undefined,
key: undefined,
group_date_type: 'month',
};
});
return r.concat(allMonth);
const yearRow = (({ object, object_name, object_id, subject, date_type, yearValue, kpiYear }) => ({
object,
object_name,
object_id,
subject,
date_type,
value: yearValue,
start_date: moment([KPIStore.settingYear, 0, 1]).format('YYYY-MM-DD'),
end_date: moment([KPIStore.settingYear, 11, 1]).endOf('M').format('YYYY-MM-DD HH:mm'),
kpi_id: kpiYear?.kpi_id || undefined,
group_date_type: 'year',
}))(curObj);
return r.concat(allMonth, yearRow);
}, []);
const yearRow = (({ object, object_name, object_id, subject, date_type, yearValue, kpiYear }) => ({
object,
object_name,
object_id,
subject,
date_type,
value: yearValue,
start_date: moment([KPIStore.settingYear, 0, 1]).format('YYYY-MM-DD'),
end_date: moment([KPIStore.settingYear, 11, 1]).endOf('M').format('YYYY-MM-DD HH:mm'),
kpi_id: kpiYear?.kpi_id || undefined,
}))(dataSource?.[0] || {});
tableData.unshift(yearRow);
console.log('sub', tableData, 'del:', delKpiIds);
// return false; // debug:
KPIStore.onSubmit(tableData, { delQueue: delKpiIds }).then((res) => {
@ -180,28 +181,37 @@ export default observer((props) => {
date_type: searchFormStore.formValuesToSub.DateType,
kpiDataMapped: {},
key: Date.now().toString(32),
group_date_type: 'month',
}),
{}
); // v.formItemProps.initialValue
const makeInitialTable = (e) => {
setEditOpen(e);
// todo: ,
// test: ,
const _initialRow = Object.assign({}, initialRow, initialPercentKey);
const _objects = isEmpty(objects) ? curObjectItem?.data || [] : objects;
// console.log('ooo', objects, isEmpty(objects), curObjectItem?.data || []);
const _initialTable = _objects.map((obj) => ({
...cloneDeep(_initialRow),
object_name: obj.label,
object_id: obj.value,
key: Date.now().toString(32) + obj.value,
}));
// console.log(_initialRow, 'iiiii');
const mergePageData = Object.values(
Object.assign(
{},
_initialTable.reduce((r, v) => ({ ...r, [v.object_name]: v }), {}),
dataSource.reduce((r, v) => ({ ...r, [v.object_name]: v }), {})
)
);
if (e && isEmpty(dataSource)) {
const _initialRow = Object.assign({}, initialRow, initialPercentKey);
const _objects = isEmpty(objects) ? (curObjectItem?.data || []) : objects;
console.log('ooo', objects, isEmpty(objects), curObjectItem?.data || []);
const _initialTable = _objects.map((obj) => ({
...cloneDeep(_initialRow),
object_name: obj.label,
object_id: obj.value,
key: Date.now().toString(32) + obj.value,
}));
console.log(_initialRow, 'iiiii');
setDataSource(_initialTable);
setEditableRowKeys(_initialTable.map((ele) => ele.key));
return false;
}
setEditableRowKeys(e ? dataSource.map((ele) => ele.key) : []);
setDataSource(mergePageData);
setEditableRowKeys(e ? mergePageData.map((ele) => ele.key) : []);
};
const [delKpiIds, setDelKpiIds] = useState([]);
return (
@ -226,13 +236,11 @@ export default observer((props) => {
return [defaultDoms.delete];
},
onDelete: (_key, _row) => {
// console.log('del', _key, _row);
const rowKpiIds = (_row?.kpiData || []).map((ele) => ele.kpi_id);
rowKpiIds.push(_row?.kpiYear?.kpi_id);
setDelKpiIds(rowKpiIds);
},
onValuesChange: (record, recordList) => {
// console.log('on edit, onValuesChange',record, recordList);
onTableChange(recordList);
},
onChange: (editableKeys, editableRows) => {
@ -242,9 +250,14 @@ export default observer((props) => {
/>
</Col>
<Col className="gutter-row mb-n1 p-none mt-1 align_center" span={24}>
<Button className="mt-1 mb-1 align_center" disabled={!editOpen} type="primary" key="save" onClick={onTableSubmit}>
保存数据
</Button>
<Space size={'large'}>
<Button className="mt-1 mb-1 align_center" disabled={!editOpen} type="primary" key="save" onClick={onTableSubmit}>
保存数据
</Button>
{!editOpen && <Button className="mt-1 mb-1 align_center" disabled={false} type={'ghost'} key="initTable" onClick={() => {makeInitialTable(true);}}>
编辑设置
</Button>}
</Space>
</Col>
</Row>
</>

@ -2,8 +2,8 @@ import { useContext } from 'react';
import { observer } from 'mobx-react';
// import { stores_Context } from '../config';
import { Button, Table, Switch, Input, Space, Typography, Row, Col, Spin, Radio, Tabs } from 'antd';
import SearchForm from './../search/SearchForm';
import { bu, KPIObjects } from './../../libs/ht';
import SearchForm from '../../search/SearchForm';
import { bu, KPIObjects } from '../../../libs/ht';
export default observer((props) => {
// const { } = useContext(stores_Context);

@ -20,7 +20,7 @@ const Business_unit = (props) => {
}
store?.bu_handleChange(value);
}}
labelInValue={true}
labelInValue={false}
{...extProps}
>
{props.show_all ? <Select.Option key="-1" value="ALL">ALL 事业部</Select.Option> : ''}

@ -21,7 +21,7 @@ const Business_unit = (props) => {
}
// store?.bu_handleChange(value);
}}
labelInValue={true}
labelInValue={false}
{...extProps}
>
{_show_all ? <Select.Option key="-1" value="ALL">ALL 事业部</Select.Option> : ''}

@ -23,15 +23,16 @@ class DataTypeSelect extends Component {
};
render() {
const store = this.props.store;
const { store, ...extProps } = this.props;
return (
<Select labelInValue={true}
<Select labelInValue={false}
// value={store.date_type}
value={this.props?.value || store?.date_type}
style={{ width: '100%' }}
placeholder="选择日期类型"
// onChange={(value) => store.onChange_datetype(value)}
onChange={this.handleChange}
{...extProps}
>
{dateTypes.map((ele) => (
<Select.Option key={ele.key} value={ele.key}>

@ -1,7 +1,7 @@
import React, { Component } from 'react';
import { Select } from 'antd';
import { observer } from 'mobx-react';
import { groups } from '../../libs/ht';
import { groups, leafGroup } from '../../libs/ht';
class GroupSelect extends Component {
constructor(props) {
@ -9,9 +9,10 @@ class GroupSelect extends Component {
}
render() {
const { store, mode, value, onChange, show_all, ...extProps } = this.props;
const { store, mode, value, onChange, show_all, isLeaf, ...extProps } = this.props;
const _mode = mode || store?.group_select_mode || null;
const _show_all = ['tags', 'multiple'].includes(_mode) ? false : show_all;
const options = isLeaf===true ? leafGroup : groups;
return (
<div>
<Select
@ -25,7 +26,7 @@ class GroupSelect extends Component {
}
store?.group_handleChange(value);
}}
labelInValue={true}
labelInValue={false}
maxTagCount={1}
maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`}
allowClear={_mode != null}
@ -38,7 +39,7 @@ class GroupSelect extends Component {
) : (
''
)}
{groups.map((ele) => (
{options.map((ele) => (
<Select.Option key={ele.key} value={ele.key}>
{ele.label}
</Select.Option>

@ -4,7 +4,7 @@ import querystring from 'querystring';
// import * as oMapper from 'object-mapper';
import { fetchJSON } from './../../utils/request';
import { observer } from 'mobx-react';
import { objectMapper } from './../../utils/commons';
import { merge, objectMapper } from './../../utils/commons';
const { Option } = Select;
@ -24,10 +24,7 @@ function curl(opts, callback) {
const _p = [{ 'key': '0', 'label': '空' }];
return callback(_p);
}
const param = {
code: 'utf-8',
q: opts.value,
};
const param = merge({ code: 'utf-8', q: opts.value }, opts.param);
// const str = new URLSearchParams({
// code: 'utf-8',
// q: opts.value,
@ -66,9 +63,9 @@ class SearchInput extends React.Component {
componentDidMount() {
if (this.props.autoGet === true) {
const { map, resultkey } = this.props;
const { map, resultkey, param } = this.props;
const mapKey = Object.keys(map).reduce((r, v) => ({ ...r, [v]: { key: map[v] } }), {});
curl({ value: '', url: this.props.url || '', map: mapKey, resultkey }, (data) =>
curl({ value: '', url: this.props.url || '', map: mapKey, resultkey, param }, (data) =>
this.setState({ data, autoData: data }, () => (typeof this.props.onSearchAfter === 'function' ? this.props.onSearchAfter(data, this.state.value) : ''))
);
}
@ -90,10 +87,10 @@ class SearchInput extends React.Component {
this.setState({ data: f || [] });
return false;
}
const { map, resultkey } = this.props;
const { map, resultkey, param } = this.props;
const mapKey = Object.keys(map).reduce((r, v) => ({ ...r, [v]: { key: map[v] } }), {});
if (value) {
curl({ value, url: this.props.url || '', map: mapKey, resultkey }, (data) =>
curl({ value, url: this.props.url || '', map: mapKey, resultkey, param }, (data) =>
this.setState({ data }, () => (typeof this.props.onSearchAfter === 'function' ? this.props.onSearchAfter(data, this.state.value) : ''))
);
} else {
@ -102,6 +99,7 @@ class SearchInput extends React.Component {
};
handleChange = (value, option) => {
console.log('invoke denpendencies change', value);
this.setState({ value }, () => this.props.onChange(value, option));
};

@ -45,7 +45,6 @@ export default observer((props) => {
const { onSubmit, confirmText } = props;
const formValuesMapper = (values) => {
console.log('Received values of form, origin form value: ', values);
const destinationObject = {
'DateType': {
key: 'DateType',
@ -55,28 +54,28 @@ export default observer((props) => {
'HTBusinessUnits': {
key: 'HTBusinessUnits',
transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? (!isNaN(parseInt(value.key), 10) ? value.key : '') : '-1';
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? (!isNaN(parseInt(value.key), 10) ? value.key : '') : '';
},
default: '',
},
'businessUnits': {
key: 'businessUnits',
transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? (!isNaN(parseInt(value.key), 10) ? value.key : '') : '-1';
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? (!isNaN(parseInt(value.key), 10) ? value.key : '') : '';
},
default: '',
},
'DepartmentList': {
key: 'DepartmentList',
transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.key : 'ALL';
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.key : '';
},
default: '',
},
'WebCode': {
key: 'WebCode',
transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.key : 'ALL';
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.key : '';
},
default: '',
},
@ -154,12 +153,13 @@ export default observer((props) => {
}
// omit empty
Object.keys(dest).forEach((key) => (dest[key] == null || dest[key] === '' || dest[key].length === 0) && delete dest[key]);
console.log('form value send to onSubmit:', dest);
return dest;
};
const onFinish = (values) => {
console.log('Received values of form, origin form value: ', values);
const dest = formValuesMapper(values);
console.log('form value send to onSubmit:', dest);
const str = new URLSearchParams(dest).toString();
searchFormStore.setFormValues(values);
searchFormStore.setFormValuesToSub(dest);
@ -175,8 +175,11 @@ export default observer((props) => {
};
const onValuesChange = (...args) => {
const [changedValues, allValues] = args;
console.log('form onValuesChange', args);
// console.log('form onValuesChange', Object.keys(changedValues), args);
const dest = formValuesMapper(allValues);
searchFormStore.setFormValues(allValues);
searchFormStore.setFormValuesToSub(dest);
};
return (
@ -229,28 +232,28 @@ function getFields(props) {
'HTBusinessUnits',
99,
<Form.Item name={`HTBusinessUnits`} initialValue={at(props, 'initialValue.HTBusinessUnits')[0] || undefined}>
<BusinessUnitSelect {...fieldProps.HTBusinessUnits} />
<BusinessUnitSelect {...fieldProps.HTBusinessUnits} labelInValue={true} />
</Form.Item>
),
item(
'businessUnits',
99,
<Form.Item name={`businessUnits`} initialValue={at(props, 'initialValue.businessUnits')[0] || undefined}>
<BusinessSelect {...fieldProps.businessUnits} />
<BusinessSelect {...fieldProps.businessUnits} labelInValue={true} />
</Form.Item>
),
item(
'DepartmentList',
99,
<Form.Item name={`DepartmentList`} initialValue={at(props, 'initialValue.DepartmentList')[0] || (fieldProps?.DepartmentList?.show_all ? { key: 'ALL', label: '所有小组' } : undefined)}>
<GroupSelect {...fieldProps.DepartmentList} />
<GroupSelect {...fieldProps.DepartmentList} labelInValue={true} />
</Form.Item>
),
item(
'WebCode',
99,
<Form.Item name={`WebCode`} initialValue={at(props, 'initialValue.WebCode')[0] || (fieldProps?.WebCode?.show_all ? { key: 'ALL', label: '所有来源' } : undefined)}>
<SiteSelect {...fieldProps.WebCode} />
<SiteSelect {...fieldProps.WebCode} labelInValue={true} />
</Form.Item>
),
item(
@ -274,18 +277,10 @@ function getFields(props) {
'DateType',
99,
<Form.Item name={`DateType`} initialValue={at(props, 'initialValue.DateType')[0] || { key: 'applyDate', label: '提交日期' }}>
<DateTypeSelect />
<DateTypeSelect labelInValue={true} />
</Form.Item>,
2
),
item(
'dates',
99,
<Form.Item>
<DatePickerCharts isform={true} {...fieldProps.dates} form={form} />
</Form.Item>,
midCol
),
item(
'years',
99,
@ -303,11 +298,26 @@ function getFields(props) {
</Form.Item>,
2
),
item(
'dates',
99,
<Form.Item>
<DatePickerCharts isform={true} {...fieldProps.dates} form={form} />
</Form.Item>,
midCol
),
item(
'operator',
99,
<Form.Item name={'operator'}>
<SearchInput autoGet url="/service-Analyse2/GetOperatorInfo" map={{ 'op_id': 'key', 'cn_name': 'label' }} resultkey={'result'} placeholder="输入搜索顾问: 中/英名字" />
<Form.Item name={'operator'} dependencies={['DepartmentList']} >
<SearchInput
autoGet
url="/service-Analyse2/GetOperatorInfo"
map={{ 'op_id': 'key', 'cn_name': 'label' }}
resultkey={'result'}
placeholder="输入搜索顾问: 中/英名字"
param={{ dept_id: (form.getFieldValue('DepartmentList')?.value || '').replace('ALL', ''), ...(fieldProps?.operator?.param || {}) }}
/>
</Form.Item>
),
item(

@ -26,7 +26,7 @@ class SiteSelect extends Component {
}
store?.handleChange_webcode(value);
}}
labelInValue={true}
labelInValue={false}
maxTagCount={1}
maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`}
allowClear={_mode != null}

@ -55,7 +55,7 @@ export const groups = [
{ value: '31', key: '31', label: '花梨鹰', code: '', children: [] },
];
export const groupsMappedByCode = groups.reduce((a, c) => ({ ...a, [String(c.code || c.key)]: c }), {});
export const leafGroup = groups.slice(3);
/**
* 来源
*/
@ -107,7 +107,7 @@ export const dataFieldAlias = dataFieldOptions.reduce(
(a, c) => ({
...a,
[c.value]: { ...c, alias: c.label, formatter: (v) => c.formatter(v) },
[c.nestkey.v]: { ...c, value: c.nestkey.v, alias: `${c.label}目标`, formatter: (v) => c.formatter(v) },
[c.nestkey.v]: { ...c, value: c.nestkey.v, alias: `${c.label}目标`, label: `${c.label}目标`, formatter: (v) => c.formatter(v) },
}),
{}
);
@ -118,7 +118,7 @@ export const dataFieldAlias = dataFieldOptions.reduce(
export const KPIObjects = [
{ key: 'overview', value: 'overview', label: '海纳' },
{ key: 'bu', value: 'bu', label: '事业部', data: bu },
{ key: 'dept', value: 'dept', label: '小组', data: groups },
{ key: 'dept', value: 'dept', label: '小组', data: leafGroup },
{ key: 'du', value: 'du', label: '销售小组', data: deptUnits },
{ key: 'operator', value: 'operator', label: '顾问' },
{ key: 'destination', value: 'destination', label: '目的地' },

@ -1,15 +1,13 @@
{
"get|/service-web/baseinfo/operator/test": {
"get|/service-Analyse2/GetDestinationInfo/test": {
"errcode": 0,
"errmsg": "",
"data": null,
"loading": null,
"result|10": [
{
"mobile": "13@integer(99999999,999999999)",
"op_id": "@integer(10,99)",
"id": "@integer(10,99)",
"cn_name": "@cname",
"email": "@email",
"en_name": "@first",
"code": "@word(2,3)",
"key": "@increment"

@ -10,23 +10,6 @@ class DatePickerStore {
makeAutoObservable(this);
}
formValues = {
'DepartmentList': { 'key': 'ALL', 'label': '所有小组' },
'WebCode': { 'key': 'ALL', 'label': '所有来源' },
'IncludeTickets': { 'key': '1', 'label': '含门票' },
'DateType': { 'key': 'applyDate', 'label': '提交日期' },
'year': moment(),
};
formValuesToSub = {
DepartmentList: 'ALL',
WebCode: 'ALL',
IncludeTickets: '1',
DateType: 'applyDate',
Date1: moment().startOf('year').format('YYYY-MM-DD'),
Date2: moment().endOf('year').format('YYYY-MM-DD 23:59'),
};
start_date = moment().startOf('week').subtract(7, 'days');
end_date = moment().endOf('week').subtract(7, 'days');
start_date_cp = false;
@ -57,6 +40,26 @@ class DatePickerStore {
return [moment(this.start_date).subtract(1, 'year'), moment(this.end_date).subtract(1, 'year')];
}
formValues = {
'DepartmentList': { 'key': 'ALL', 'label': '所有小组' },
'WebCode': { 'key': 'ALL', 'label': '所有来源' },
'IncludeTickets': { 'key': '1', 'label': '含门票' },
'DateType': { 'key': 'applyDate', 'label': '提交日期' },
'years': this.start_date,
// 'months': [moment(), moment()],
'dates': [this.start_date, this.end_date],
};
formValuesToSub = {
DepartmentList: 'ALL',
WebCode: 'ALL',
IncludeTickets: '1',
DateType: 'applyDate',
Date1: this.start_date.format('YYYY-MM-DD'),
Date2: this.end_date.format('YYYY-MM-DD 23:59:59'),
};
setFormValues(data){
this.formValues = data;
}

@ -19,6 +19,14 @@ const modelMapper = {
en_name: { key: 'label_alias' },
},
},
'destination': {
url: '/service-Analyse2/GetDestinationInfo/test',
mapper: {
id: [{ key: 'key' }, { key: 'value' }],
cn_name: { key: 'label' },
en_name: { key: 'label_alias' },
},
},
'vendor': {
url: '/service-web/QueryData/GetVEIName',
mapper: {
@ -48,7 +56,7 @@ class DictData {
runInAction(() => {
this[mkey].loading = false;
this[mkey].dataSource = objectMapper(json.result, modelMapper[mkey].mapper);
console.log({ loading: false, ...json }, model, 'DictData', toJS(this[mkey]));
// console.log({ loading: false, ...json }, model, 'DictData', toJS(this[mkey]));
});
}
return this[mkey];

@ -1,6 +1,6 @@
import { makeAutoObservable, runInAction, toJS } from 'mobx';
import * as req from '../utils/request';
import { isEmpty, sortBy } from '../utils/commons';
import { isEmpty, pick, sortBy } from '../utils/commons';
const modelMapper = {
'tourDays': { url: '/service-Analyse2/GetTradeApartByTourDays' },
@ -17,7 +17,10 @@ class Distribution {
makeAutoObservable(this);
}
async getData(param){
/**
* 各个类型的分布
*/
getApartData = async (param) => {
const mkey = this.curTab;
this[mkey] = { loading: true, dataSource: [] };
const json = await req.fetchJSON(modelMapper[mkey].url, param);
@ -30,10 +33,29 @@ class Distribution {
});
}
return this[mkey];
};
}
/**
* 明细
*/
getDetailData = async (param) => {
this.detailData.loading = true;
const json = await req.fetchJSON('/service-Analyse2/GetTradeApartDetail', param);
if (json.errcode === 0) {
runInAction(() => {
this.detailData.loading = false;
this.detailData.dataSource = json.result;
const daysData = json.result.filter(ele => ele.confirmDays).map(row => pick(row, ['o_id', 'tourdays', 'applyDays', 'personNum', 'country', 'startDate']));
this.scatterDays = daysData;
});
}
return this.detailData;
};
resetData = () => {
// this.detailData = { loading: false, dataSource: [] };
// this.scatterDays = [];
resetData() {
this.tourDays = { loading: false, dataSource: [] };
this.PML = { loading: false, dataSource: [] };
this.ConfirmDays = { loading: false, dataSource: [] };
@ -41,7 +63,7 @@ class Distribution {
this.PersonNum = { loading: false, dataSource: [] };
this.destination = { loading: false, dataSource: [] };
this.GlobalDestination = { loading: false, dataSource: [] };
}
};
curTab = 'tourDays';
setCurTab(v) {
@ -50,6 +72,9 @@ class Distribution {
pageLoading = false;
detailData = { loading: false, dataSource: [], };
scatterDays = [];
tourDays = { loading: false, dataSource: [] };
PML = { loading: false, dataSource: [] };
ConfirmDays = { loading: false, dataSource: [] };

@ -44,7 +44,7 @@ class KPI {
this.listLoading = false;
this.originData = json.result;
const yearData = parseKPI(json.result, ['subject', 'object_id']);
console.log(111, yearData, yearData[this.settingYear]);
// console.log(111, yearData, yearData[this.settingYear]);
this.pageData = yearData?.[this.settingYear]?.[this.settingSubject] || [];
});
}

@ -15,7 +15,7 @@ class Trade {
fetchSummaryData(queryData) {
this.summaryData.loading = true;
queryData.groupType = 'overview';
// queryData.groupDateType = 'year';
queryData.groupDateType = 'year';
this.fetchTradeData(queryData).then((json) => {
if (json.errcode === 0) {
runInAction(() => {

@ -289,7 +289,7 @@ export const sortBy = (key) => {
export function merge(...objects) {
const isDeep = objects.some(obj => obj !== null && typeof obj === 'object');
const result = objects[0] ?? {};
const result = objects[0] || (isDeep ? {} : objects[0]);
for (let i = 1; i < objects.length; i++) {
const obj = objects[i];
@ -301,7 +301,7 @@ export function merge(...objects) {
if (isDeep) {
if (Array.isArray(val)) {
result[key] = [...result[key] || [], ...val];
result[key] = [].concat(Array.isArray(result[key]) ? result[key] : [result[key]], val);
} else if (typeof val === 'object') {
result[key] = merge(result[key], val);
} else {

@ -0,0 +1,90 @@
import { useContext, useEffect, useMemo } from 'react';
import { observer } from 'mobx-react';
import { stores_Context } from '../config';
import { Row, Col, Spin, Space, Radio, Tabs, Table } from 'antd';
import { empty } from '../utils/commons';
import { dataFieldAlias } from '../libs/ht';
import Scatter from './../components/Scatter';
import SearchForm from './../components/search/SearchForm';
import { Histogram } from '@ant-design/plots';
export default observer((props) => {
const { date_picker_store: searchFormStore, DistributionStore } = useContext(stores_Context);
const { formValues, formValuesToSub } = searchFormStore;
const { curTab, scatterDays, detailData } = DistributionStore;
const detailRefresh = (obj) => {
DistributionStore.getDetailData({
...(obj || formValuesToSub),
});
};
useEffect(() => {
if (empty(detailData.dataSource)) {
detailRefresh();
}
}, []);
const ScatterConfig = {
xField: 'startDate',
yField: 'tourdays',
colorField: 'country',
size: 4,
// xAxis: {
// min: 0,
// max: 30,
// },
yAxis: {
min: 0,
max: 10,
},
// quadrant: {
// xBaseline: 15,
// yBaseline: 5,
// },
tooltip: false,
legend: {
position: 'right-top',
},
};
const HistogramConfig = {
binField: 'personNum',
binWidth: 1,
};
return (
<>
<Row gutter={16} style={{ margin: '-16px -8px' }}>
{/* style={{ margin: '-16px -8px', padding: 0 }} */}
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
},
shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'dates', 'country'],
fieldProps: {
DepartmentList: { show_all: true },
WebCode: { show_all: true },
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
detailRefresh(obj);
}}
/>
</Col>
</Row>
<section>
<Spin spinning={detailData.loading}>
<Scatter {...ScatterConfig} dataSource={scatterDays} />
</Spin>
</section>
{/* <section>
<Spin spinning={detailData.loading}>
<Histogram {...HistogramConfig} data={scatterDays} />
</Spin>
</section> */}
</>
);
});

@ -1,31 +1,31 @@
import { useContext, useEffect, useMemo } from 'react';
import { useContext, useEffect } from 'react';
import { observer } from 'mobx-react';
import { stores_Context } from '../config';
import { Row, Col, Spin, Space, Radio, Tabs, Table } from 'antd';
import { Row, Col, Spin, Tabs, Table } from 'antd';
import { RingProgress } from '@ant-design/plots';
import SearchForm from './../components/search/SearchForm';
import "./kpi.css";
import { empty } from '../utils/commons';
import { dataFieldAlias } from '../libs/ht';
import './kpi.css';
const apartOptions = [
{ key: 'tourDays', value: 'tourDays', label: '团天数', },
{ key: 'PML', value: 'PML', label: '单团毛利', },
{ 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: 'tourDays', value: 'tourDays', label: '团天数' },
{ key: 'PML', value: 'PML', label: '单团毛利' },
{ 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: '海外目的地' },
];
export default observer((props) => {
export default observer(() => {
const { date_picker_store: searchFormStore, DistributionStore } = useContext(stores_Context);
const { formValues, formValuesToSub } = searchFormStore;
const { curTab } = DistributionStore;
const pageRefresh = (obj) => {
DistributionStore.getData({
DateType: 'applyDate',
Date1: searchFormStore.start_date.startOf('year').format('YYYY-MM-DD'),
Date2: searchFormStore.end_date.endOf('year').format('YYYY-MM-DD 23:59'),
DistributionStore.getApartData({
...(obj || formValuesToSub),
});
};
@ -41,7 +41,6 @@ export default observer((props) => {
return () => {};
}, [formValuesToSub]);
const onTabsChange = (tab) => {
DistributionStore.setCurTab(tab);
};
@ -49,69 +48,62 @@ export default observer((props) => {
height: 60,
width: 60,
autoFit: false,
// percent: Number(_)/100,
color: ['#5B8FF9', '#E8EDF3'],
};
const columns = [
{
title: '',
dataIndex: 'label',
},
{ title: '团数', dataIndex: 'ConfirmOrder'},
{ title: '业绩', dataIndex: 'SumML', render1: (v) => `1`},
{ title: '团数占比', dataIndex: 'ConfirmOrderPercent', render: (v) => <RingProgress {...RingProgressConfig} percent={v/100} /> },
{ title: '业绩占比', dataIndex: 'SumMLPercent', render: (v) => <RingProgress {...RingProgressConfig} percent={v/100} />},
{ title: '', dataIndex: 'label' },
{ title: '团数', dataIndex: 'ConfirmOrder' },
{ title: '业绩', dataIndex: 'SumML', render: (v) => dataFieldAlias.SumML.formatter(v) },
{ title: '团数占比', dataIndex: 'ConfirmOrderPercent', render: (v) => <RingProgress {...RingProgressConfig} percent={v / 100} /> },
{ title: '业绩占比', dataIndex: 'SumMLPercent', render: (v) => <RingProgress {...RingProgressConfig} percent={v / 100} /> },
];
return (
<>
<Row gutter={16} style={{ margin: '-16px -8px' }}>
{/* style={{ margin: '-16px -8px', padding: 0 }} */}
<Col className="gutter-row" span={24}>
<SearchForm
defaultValue={{
initialValue: {
...formValues,
},
shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'years'],
shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'dates'],
fieldProps: {
DepartmentList: { show_all: true },
WebCode: { show_all: true },
years: { hide_vs: true },
dates: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
onSubmit={(_err, obj) => {
pageRefresh(obj);
}}
/>
</Col>
</Row>
<section>
<Tabs
onChange={onTabsChange}
type="card"
items={apartOptions.map((ele, i) => {
// const ObjectItemPanel = objectComponents[ele.key];
return {
...ele,
children: (
<Spin spinning={DistributionStore.pageLoading}>
{/* <ObjectItemPanel title={ele.label} {...{ curObject, onSearchSubmit, objects: retObjects }} /> */}
<Table
id="table_to_xlsx_sale"
dataSource={DistributionStore[curTab].dataSource}
columns={columns}
size="small"
rowKey={(record) => record.label}
loading={DistributionStore[curTab].loading}
pagination={false}
scroll={{ x: '100%' }}
/>
</Spin>
),
};
})}
/>
<section>
<Tabs
onChange={onTabsChange}
type="card"
items={apartOptions.map((ele) => {
return {
...ele,
children: (
<Spin spinning={DistributionStore.pageLoading}>
<Table
id="table_to_xlsx_sale"
dataSource={DistributionStore[curTab].dataSource}
columns={columns}
size="small"
rowKey={(record) => record.label}
loading={DistributionStore[curTab].loading}
pagination={false}
scroll={{ x: '100%' }}
/>
</Spin>
),
};
})}
/>
</section>
</>
);

@ -1,12 +1,13 @@
import { useContext, useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { Row, Col, Spin, Space, Radio } from 'antd';
import { CheckCircleTwoTone, MoneyCollectTwoTone, FlagTwoTone, SmileTwoTone, } from '@ant-design/icons';
import { CheckCircleTwoTone, MoneyCollectTwoTone, FlagTwoTone, SmileTwoTone } from '@ant-design/icons';
import { stores_Context } from '../config';
import { useNavigate } from 'react-router-dom';
import StatisticCard from '../components/StatisticCard';
import Bullet from '../components/BulletWithSort';
import Waterfall from '../components/Waterfall';
import LineWithKPI from '../components/LineWithKPI';
import DataFieldRadio from '../components/DataFieldRadio';
import { datePartOptions } from './../components/DateGroupRadio/date';
import SearchForm from './../components/search/SearchForm';
@ -30,7 +31,7 @@ export default observer(() => {
// const navigate = useNavigate();
const { TradeStore, date_picker_store: searchFormStore } = useContext(stores_Context);
const { sideData, summaryData, BuData, topData, timeData, timeLineKey } = TradeStore;
const { formValues, } = searchFormStore;
const { formValues } = searchFormStore;
useEffect(() => {
if (empty(summaryData.dataSource)) {
@ -92,7 +93,7 @@ export default observer(() => {
},
},
label: {
formatter: (v) => summaryData.kpi.value === 0 ? (dataFieldAlias.SumML?.formatter(v.SumML) || v.SumML) : ((v.SumML / summaryData.kpi.value) * 100).toFixed(2) + '%',
formatter: (v) => (summaryData.kpi.value === 0 ? dataFieldAlias.SumML?.formatter(v.SumML) || v.SumML : ((v.SumML / summaryData.kpi.value) * 100).toFixed(2) + '%'),
},
};
@ -108,7 +109,8 @@ export default observer(() => {
autoHide: true,
autoRotate: false,
},
}
},
legend: false,
};
const lineConfigSet = {
@ -123,10 +125,11 @@ export default observer(() => {
smooth: true,
point: {
size: 4,
shape: "cicle",
shape: 'cicle',
},
legend: false,
meta: { ...cloneDeep(dataFieldAlias)
meta: {
...cloneDeep(dataFieldAlias),
// [extProps.yField]: {
// alias: dataFieldAlias[extProps.yField]?.alias || extProps.yField,
// formatter: (v) => dataFieldAlias[extProps.yField]?.formatter(v) || v,
@ -139,7 +142,7 @@ export default observer(() => {
const handleChangetimeDataField = (key) => {
setTimeDataField(key);
setLineConfig({
...lineConfig,
...cloneDeep(lineConfig),
yField: key,
tooltip: {
customItems: (originalItems) => {
@ -151,7 +154,7 @@ export default observer(() => {
});
};
const [dateField, setDateField] = useState(timeLineKey);
const handleChangeDateType = ({target: {value}}) => {
const handleChangeDateType = ({ target: { value } }) => {
setDateField(value);
TradeStore.setTimeLineKey(value);
if (!isEmpty(TradeStore.searchPayloadHome)) {
@ -203,7 +206,8 @@ export default observer(() => {
<Radio.Group options={datePartOptions} optionType="button" onChange={handleChangeDateType} value={dateField} />
</Space>
<Spin spinning={timeData.loading}>
<Line {...lineConfig} data={timeData.dataSource} />
{/* <Line {...lineConfig} data={timeData.dataSource} /> */}
<LineWithKPI dataSource={timeData.dataSource} config={lineConfig} />
</Spin>
</section>
<section>
@ -212,7 +216,7 @@ export default observer(() => {
<Row gutter={layoutProps3.gutter}>
<Col {...layoutProps3}>
<Bullet {...BUConfig} dataSource={BuData?.dataSource || []} />
<h3 style={{ textAlign: 'center' }}>{`各事业部总业绩`}</h3>
<h3 style={{ textAlign: 'center' }}>{`各事业部总业绩`}</h3>
</Col>
{Object.keys(sideData.dataSource).map((key) => (
<Col {...layoutProps3} key={key}>

@ -2,7 +2,7 @@ import { useContext, useEffect, useState } from 'react';
import { stores_Context } from './../config';
import { observer } from 'mobx-react';
import { Row, Col, Tabs, Spin } from 'antd';
import { flush, objectMapper } from './../utils/commons';
import { flush, isEmpty, objectMapper } from './../utils/commons';
import { KPIObjects } from './../libs/ht';
import ObjectPanel from '../components/kpi/ObjectPanel';
import OverviewPanel from './../components/kpi/OverviewPanel';
@ -23,7 +23,7 @@ const objectFilterKey = {
// 'du': 'du',
// 'operator': 'operator',
// 'destination': 'destination',
// 'country': 'country',
'country': 'country',
};
export default observer((props) => {
const { KPIStore, DictDataStore, date_picker_store: searchFormStore } = useContext(stores_Context);
@ -42,13 +42,13 @@ export default observer((props) => {
}, [curObject]);
const [retObjects, setRetObjects] = useState([]);
const onSearchSubmit = (obj, formVal={}) => {
const onSearchSubmit = async (obj, formVal={}) => {
const getkpiParam = objectMapper(obj, {
DateType: { key: 'date_type' },
Date1: { key: 'start_date' },
Date2: { key: 'end_date' },
HTBusinessUnits: { key: 'object_id' },
DepartmentList: { key: 'object_id' },
DepartmentList: { key: curObject === 'dept' ? 'object_id' : 'dept_id' },
businessUnits: { key: 'object_id' },
WebCode: { key: 'object_id' },
operator: { key: 'object_id' },
@ -56,7 +56,24 @@ export default observer((props) => {
});
Object.assign(getkpiParam, { object: curObject });
KPIStore.setSettingYear(formVal?.year?.year() || KPIStore.settingYear);
console.log('invoke on search', obj, formVal, getkpiParam);
// console.log('invoke on search', obj, formVal, getkpiParam);
if (curObject === 'operator') {
const searchOperator = await DictDataStore.fetchDictData('operator', {
is_assign: 1,
dept_id: (obj?.DepartmentList || '').replace('ALL', ''),
q: searchFormStore.formValues?.operator?.label || '',
});
if ((!isEmpty(getkpiParam.dept_id) && getkpiParam.dept_id !== 'ALL')
|| !isEmpty(searchFormStore.formValues?.operator)) {
// && isEmpty(getkpiParam.object_id)
setRetObjects(searchOperator.dataSource);
getkpiParam.object_id = searchOperator.dataSource.map((ele) => ele.key).join(',');
delete getkpiParam.dept_id;
}
}
getKPIList(getkpiParam);
};
const getKPIList = (getkpiParam) => {
KPIStore.getList(getkpiParam).then((data) => {
// setDataSource(data);
if (objectFilterKey?.[curObject]) {

Loading…
Cancel
Save