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

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

@ -7,3 +7,30 @@
.align_left{
text-align: left;
}
.align_center{
text-align: center;
}
.m-1{
margin: 1em;
}
.mt-1{
margin-top: 1em;
}
.mb-1{
margin-bottom: 1em;
}
.ml-1{
margin-left: 1em;
}
.mr-1{
margin-right: 1em;
}
.mb-n1{
margin-bottom: -1em;
}
.p-none{
padding: 0;
}
.p-s1{
padding: .5em;
}

@ -10,7 +10,7 @@ import {
DollarOutlined,
AreaChartOutlined,
WechatOutlined,
UserOutlined, FlagOutlined,
UserOutlined, FlagOutlined, PieChartOutlined
} from '@ant-design/icons';
import { Layout, Menu, Image, Spin } from 'antd';
import { BrowserRouter, Route, Routes, NavLink } from 'react-router-dom';
@ -36,6 +36,7 @@ import { stores_Context } from './config';
import { observer } from 'mobx-react';
import ExchangeRate from './charts/ExchangeRate';
import KPI from './views/KPI';
import Distribution from './views/Distribution';
const App = () => {
const { Content, Footer, Sider } = Layout;
@ -121,6 +122,7 @@ const App = () => {
],
},
{ key: 'kpi', label: <NavLink to="/kpi">目标</NavLink>, icon: <FlagOutlined /> },
{ key: 'distribution', label: <NavLink to="/distribution">统计分布</NavLink>, icon: <PieChartOutlined /> },
];
return (
@ -162,6 +164,7 @@ const App = () => {
<Routes>
<Route path="/" element={<Home />} />
<Route path="/kpi" element={<KPI />} />
<Route path="/distribution" element={<Distribution />} />
<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 />} />

@ -2,17 +2,21 @@ import { useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { Bullet } from '@ant-design/plots';
import { sortBy, merge } from '../utils/commons';
import { dataFieldAlias } from './../libs/ht';
import { dataFieldAlias } from '../libs/ht';
// const layoutLabel = {
// 'vertical':
// };
export default observer((props) => {
const { dataSource, ...extProps } = props;
const { dataSource, itemLength, ...extProps } = props;
// , number -> array
const dataParser = (origin) => {
const { measureField, rangeField, targetField } = extProps;
const maxKPI = Math.max(...(origin || []).map((ele) => ele[targetField]));
const maxValue = Math.max(...(origin || []).map((ele) => ele[measureField]));
const _max = Math.max(maxKPI, maxValue);
const sortData = origin.sort(sortBy(measureField));
const sortData = origin.sort(sortBy(measureField)).slice(-itemLength);
//
const _parseData = sortData?.map((ele) => ({ ...ele, [rangeField]: [0, Math.ceil(_max / 0.9)], [measureField]: [ele[measureField]] }));
return _parseData;
@ -26,21 +30,28 @@ export default observer((props) => {
const config = merge({
color: {
range: ['#FFbcb8', '#FFe0b0', '#bfeec8'],
range: [ '#FFF3E1', '#FFe0b0', '#bfeec8'], // '#FFbcb8', '#FFe0b0',
measure: '#5B8FF9',
target: '#FF9845',
},
label: {
// target: true,
// measure: {
// position: 'middle',
target: false,
measure: {
// position: extProps?.vertical === 'vertical' ? 'top' : 'right',
// style: {
// fill: '#fff',
// fill: '#063CAA',
// },
// },
formatter: (v) => {
return dataFieldAlias[extProps.measureField]?.formatter(v[extProps.measureField]) || v;
}
},
},
xAxis: {
line: null,
label: {
autoHide: false,
autoRotate: true,
},
},
yAxis: false,
// legend
@ -76,7 +87,7 @@ export default observer((props) => {
tooltip: {
customItems: (originalItems) => {
// process originalItems,
return originalItems.map((ele) => ({ ...ele, name: dataFieldAlias[ele.name]?.alias || ele.name }));
return originalItems.map((ele) => ({ ...ele, value: dataFieldAlias[ele.name]?.formatter(Number(ele.value)), name: dataFieldAlias[ele.name]?.alias || ele.name }));
},
},
}, extProps);

@ -1,10 +1,13 @@
import { Tag } from 'antd';
import { CaretUpOutlined, CaretDownOutlined } from '@ant-design/icons';
export const VSTag = (diffPercent, diffData, data1, data2) => {
export const VSTag = (props) => {
const { diffPercent, diffData, data1, data2 } = props;
const CaretIcon = parseInt(diffPercent) < 0 ? CaretDownOutlined : CaretUpOutlined;
const tagColor = parseInt(diffPercent) < 0 ? "gold" : "lime";
return parseInt(diffPercent) === 0 ? "-" : (
const tagColor = parseInt(diffPercent) < 0 ? 'gold' : 'lime';
return parseInt(diffPercent) === 0 ? (
'-'
) : (
<span>
<div>
{data1} vs {data2}

@ -1,11 +1,11 @@
import { observer } from 'mobx-react';
import { Radio, Select } from 'antd';
import { dataFieldOptions } from './../libs/ht';
import { dataFieldOptions } from '../libs/ht';
export default observer((props) => {
const { visible, dataRaw, dataMapper, fieldMapper, onChange, ...extProps } = props;
const handleChange = (value) => {
console.log('handleChange', value);
// console.log('handleChange', value);
if (typeof onChange === 'function') {
onChange(value);
}

@ -4,7 +4,7 @@ export const datePartOptions = [
{ label: '日', value: 'day' },
{ label: '周', value: 'week' },
{ label: '月', value: 'month' },
{ label: '季', value: 'quarter' },
{ label: '季', value: 'season' },
{ label: '年', value: 'year' },
];
export const datePartMethod = {
@ -20,7 +20,7 @@ export const datePartMethod = {
const dateKey = `${year}-W${String(week).padStart(2, '0')}`;
return { dateKey, 'groupKey': dateKey, date };
},
'quarter': (date) => {
'season': (date) => {
const dateO = moment(date);
// const key = dateO.format('YYYY-Q');
const key = `${dateO.year()}-${String(dateO.quarter()).padStart(2, 'Q')}`;
@ -117,16 +117,19 @@ export const resultDataCb = (dataRaw, dateGroup, { data1, data2 }, fieldMapper,
}));
const parse2 = parseDateType(_data2, dateGroup, fieldMapper);
const parseData2 = parse2.data.map((ele) => ({
[fieldMapper.dateKey]: ele.groupKey,
[fieldMapper.dateKey]: ele[fieldMapper.dateKey],
// [fieldMapper.dateKey]: ele.groupKey,
[fieldMapper.valueKey]: ele.value,
[fieldMapper.seriesKey]: ele[fieldMapper.seriesKey],
groups: _data2[0].groups,
dateKey: ele.dateKey,
dateRange: ele.dateRange,
dateGroup: ele[fieldMapper.dateKey],
}));
const useKeys = parseData1.map((ele) => ele[fieldMapper.dateKey]);
const reindecData2 = parseData2.map((ele, index) => ({ ...ele, [fieldMapper.dateKey]: useKeys[index] || `X.${ele[fieldMapper.dateKey]}`, dateKey: ele.dateKey }));
const retData = [].concat(parseData1, reindecData2);
const reindexData2 = parseData2.map((ele, index) => ({ ...ele, [fieldMapper.dateKey]: useKeys[index] || `_${ele[fieldMapper.dateKey]}`, dateKey: ele.dateKey }));
const retData = [].concat(parseData1, reindexData2 );
const avg1 = parse1.avgVal;
// console.log('callback', dateGroup, retData);
cb(dateGroup, retData, avg1);
};

@ -2,6 +2,10 @@ import { Radio } from 'antd';
import { observer } from 'mobx-react';
import { datePartOptions, resultDataCb } from './date';
/**
* 仅前端转换数据
* 把按天的数据转换成 按周,,,
*/
export default observer((props) => {
const { visible, dataRaw, dataMapper, fieldMapper, onChange, ...extProps } = props;
const _dataMapper = dataMapper || { 'data1': null, data2: null };

@ -1,10 +1,11 @@
import { observer } from "mobx-react";
import { observer } from 'mobx-react';
import { Card, Statistic, Progress } from 'antd';
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
export default observer((props) => {
const valueStyle = { color: props.VSrate < 0 ? '#3f8600' : '#cf1322' };
const VSIcon = () => (props.VSrate < 0 ? <ArrowDownOutlined /> : <ArrowUpOutlined />);
const ValueIcon = props.icon;
const valueStyle = { color: (props?.VSrate || -1) < 0 ? '#3f8600' : '#cf1322' };
const VSIcon = () => ((props?.VSrate || -1) < 0 ? <ArrowDownOutlined /> : <ArrowUpOutlined />);
return (
<Card>
<Statistic
@ -19,7 +20,10 @@ export default observer((props) => {
</>
)
}
prefix={<ValueIcon twoToneColor="#89B67F" />}
// title={<Space><ValueIcon twoToneColor="#89B67F" /><span>{props.title}</span></Space>}
{...props}
value={props.valueSuffix ? `${props.value} ${props.valueSuffix}` : props.value}
/>
{props.showProgress !== false && <Progress percent={props.KPIrate} size="small" format={(percent) => `${props.KPIrate}%`} />}
</Card>

@ -5,13 +5,13 @@ 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]));
const yMax = (Math.max(line?.value || 0, ...dataSource.map((ele) => ele[extProps.yField])))*1;
const annotationsLine = line
? [
{
type: 'text',
position: ['start', line.value],
content: `${line.label} ${line.value / 1000} K`,
content: `${line.label} ${line.value / 10000}`,
// offsetX: -15,
style: {
fill: '#F4664A',
@ -36,6 +36,7 @@ export default observer((props) => {
/** 展示总计 */
total: {
// label: `${title}`,
label: `${title}`,
style: {
fill: '#96a6a6',
@ -57,6 +58,16 @@ export default observer((props) => {
formatter: (v) => dataFieldAlias[extProps.yField]?.formatter(v) || v,
},
},
xAxis: {
type: 'cat',
},
tooltip: {
customItems: (originalItems) => {
// process originalItems,
const items = originalItems.map((ele) => ({ ...ele, title: `${ele.title} ${ele.data.groupsLabel}`, name: dataFieldAlias[ele.name]?.alias || ele.name }));
return items;
},
},
}, extProps);
return (
<>

@ -0,0 +1,79 @@
import { useContext } from 'react';
import { observer } from 'mobx-react';
import { stores_Context } from './../../config';
import { Typography, Row, Col, Tabs, } from 'antd';
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';
const Todo = (props) => {
return <h2>TODO</h2>;
};
const subjectComponents = {
'sum_profit': ProfitTable,
'in_order_count': Todo,
'confirm_order_count': Todo,
'depart_order_count': Todo,
'confirm_rates': Todo,
'praise_rates': Todo,
'sum_person_num': Todo,
};
export const KPIObjectsMapped = KPIObjects.reduce((a, c) => ({ ...a, [String(c.key)]: c }), {});
const { Text } = Typography;
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,
fieldProps: {},
hides: [],
shows: ['DateType', 'years'],
...props.searchProps,
};
const fieldProps = merge(_fieldProps, { years: { hide_vs: true } });
const { curObject, objects, KPISubjects, onSearchSubmit } = props;
return (
<>
<Row gutter={16} className="mb-1">
<Col className="gutter-row mb-n1 p-none" span={24}>
<SearchForm
defaultValue={{
initialValue,
shows,
fieldProps,
}}
confirmText="查询"
onSubmit={(_err, obj, form, str) => {
console.log('invoke kpi setting search');
if (typeof onSearchSubmit === 'function') {
onSearchSubmit(obj);
}
}}
/>
</Col>
</Row>
<Row gutter={16} className="mb-1 p-s1">
<Col className="gutter-row mb-n1 p-none" span={24} style={{ backgroundColor: '#ffffff' }}>
<Tabs
tabPosition={'left'}
onChange={(sub) => {
KPIStore.setSettingSubject(sub);
}}
items={KPISubjects.map((ele, i) => {
const SubjectTableComponent = subjectComponents[ele.key];
return {
...ele,
children: <SubjectTableComponent title={ele.label} {...{ curObject, objects, onSearchSubmit }} />
};
})}
/>
</Col>
</Row>
</>
);
});

@ -0,0 +1,23 @@
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 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'] },
};
export default observer((props) => {
const searchProps = searchFormItemSet?.[props.curObject] || {};
return (
<>
<KPISettings {...{ searchProps, objects: bu, KPISubjects }} {...props} />
</>
);
});

@ -0,0 +1,263 @@
import { useContext, useState, useEffect, useMemo } from 'react';
import { observer } from 'mobx-react';
import { toJS } from 'mobx';
import moment from 'moment';
import { stores_Context } from './../../config';
import { Button, Table, Switch, Input, Space, Typography, Row, Col, Spin, Radio, Tabs, message } from 'antd';
import { EditableProTable, ProCard, ProFormField } from '@ant-design/pro-components';
import SearchForm from './../search/SearchForm';
import { bu } from './../../libs/ht';
import { isEmpty, objectMapper, fixToInt, fixTo2Decimals, fixTo4Decimals, cloneDeep, numberFormatter } from './../../utils/commons';
const { Text } = Typography;
const initialPercentKey = new Array(12).fill(1).reduce((r, v, i) => ({ ...r, [`M${i + 1}Percent`]: [8, 9].includes(i) ? 10 : 8 }), {});
const numberConvert10K = (number, scale = 10) => {
return fixTo2Decimals((number/(1000*scale))) + '万';
};
export default observer((props) => {
const { KPIStore, date_picker_store: searchFormStore } = useContext(stores_Context);
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'),
});
return () => {};
}, []);
const onSearchSubmit = (obj, formVal={}) => {
const getkpiParam = objectMapper(obj, { DateType: { key: 'date_type' }, Date1: { key: 'start_date' }, Date2: { key: 'end_date' } });
Object.assign(getkpiParam, { object: curObject });
KPIStore.setSettingYear(formVal?.year?.year() || KPIStore.settingYear);
KPIStore.getList(getkpiParam).then((data) => {
setDataSource(data);
});
};
const [editOpen, setEditOpen] = useState(false);
const [editableRowsKeys, setEditableRowKeys] = useState([]);
const PercentInput = useMemo(
() =>
// eslint-disable-next-line react/display-name
({ value, onChange, record, ...extProps }) => {
// // eslint-disable-next-line react-hooks/rules-of-hooks
const [inputValue, setInputValue] = useState(value);
const handleInputChange = (e) => {
setInputValue(e.target.value);
onChange?.(e.target.value);
};
const calcV = inputValue ? numberConvert10K(fixTo4Decimals((Number(record?.yearValue) * inputValue) / 100)) : 0;
return (
<Space direction={'vertical'}>
<Input key={'input'} suffix="%" type={'number'} value={inputValue} onChange={handleInputChange} step={0.1} />
<Text type={'secondary'}>{calcV}</Text>
</Space>
);
},
[]
);
const RenderMonthCell = (row, mon) => {
return (
<Space direction={'vertical'}>
<div>
{fixTo2Decimals(row?.[`M${mon}Percent`])}
<span>%</span>
</div>
<div>{numberConvert10K(fixTo4Decimals((Number(row?.yearValue) * row?.[`M${mon}Percent`]) / 100))}</div>
</Space>
);
};
const monthCol = new Array(12).fill(1).map((_, index) => {
return {
title: `${index + 1}`,
dataIndex: `M${index + 1}Percent`,
valueType: 'digit',
width: '6.5em',
// fieldProps: { min: 0, max: 100, style: { width: '4em' } },
renderFormItem: ({ dataIndex, ...item }, { record, isEditable, ...e }, form) => {
return <PercentInput {...{ record }} month={index + 1} key={`M${index + 1}`} />;
},
render: (_, row) => RenderMonthCell(row, index + 1),
};
});
const columns = [
{
title: '年度目标',
dataIndex: 'yearValue',
valueType: 'digit',
fieldProps: { style: { width: '100%' }, step: 10000 * 100 },
formItemProps: {
style: { width: '100%' },
},
render: (_, row, ...a) => numberConvert10K(row.yearValue),
},
...monthCol,
{
title: '操作',
valueType: 'option',
// width: 250,
render: () => {
return null;
},
},
];
const onTableChange = (...argrs) => {
setEditableRowKeys(argrs[0].map((ele) => ele.key));
setDataSource(argrs[0]);
};
const onTableSubmit = () => {
const tableData = dataSource.reduce((r, curObj) => {
const allMonth = new Array(12).fill(1).map((_, index) => {
const mIndex = index + 1;
const mVal = (Number(curObj.yearValue) * Number(curObj[`M${mIndex}Percent`])) / 100;
const startM = moment([KPIStore.settingYear, index, 1]);
const pick = (({ object, object_name, object_id, subject, date_type }) => ({
object,
object_name,
object_id,
subject,
date_type,
}))(curObj);
return {
...pick,
start_date: startM.format('YYYY-MM-DD'),
end_date: startM.endOf('M').format('YYYY-MM-DD HH:mm'),
value: mVal,
kpi_id: curObj.kpiDataMapped?.[`M${mIndex}`]?.kpi_id || undefined,
key: undefined,
};
});
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,
}))(dataSource?.[0] || {});
tableData.unshift(yearRow);
console.log('sub', tableData, delKpiIds);
// return false; // debug:
KPIStore.onSubmit(tableData, {delQueue: delKpiIds}).then((res) => {
if (res) {
message.success('保存成功');
setEditOpen(false);
setEditableRowKeys([]);
setDelKpiIds([]);
onSearchSubmit(searchFormStore.formValuesToSub);
return false;
}
message.error('失败, 请重试');
});
};
const initialRow = monthCol.reduce(
(r, v) => ({
...r,
[v.dataIndex]: 0,
yearValue: 10000*100,
object: curObject,
object_name: '',
object_id: -1,
subject: 'sum_profit',
date_type: searchFormStore.formValuesToSub.DateType,
kpiDataMapped: {},
key: Date.now().toString(32)
}),
{}
); // v.formItemProps.initialValue
const makeInitialTable = (e) => {
setEditOpen(e);
if (e && isEmpty(dataSource)) {
const _initialRow = Object.assign({}, initialRow, initialPercentKey);
setDataSource([_initialRow]);
setEditableRowKeys([_initialRow.key]);
return false;
}
setEditableRowKeys(e ? dataSource.map((ele) => ele.key) : []);
};
const [delKpiIds, setDelKpiIds] = useState([]);
return (
<>
<Row gutter={16} className="mb-1" style1={{ margin: '0 0 1em' }}>
<Col className="gutter-row m-n1 p-none" span={24} style1={{ margin: '0 0 -16px 0', padding: 0 }}>
<SearchForm
defaultValue={{
'initialValue': {},
shows: ['DateType', 'years'],
'fieldProps': {
DepartmentList: { show_all: false },
WebCode: { show_all: false },
years: { hide_vs: true },
},
}}
confirmText="查询"
onSubmit={(_err, obj, form, str) => {
// TradeStore.setStateSearch(form);
// pageRefresh(obj);
onSearchSubmit(obj, form);
setEditOpen(false);
setEditableRowKeys([]);
}}
/>
</Col>
</Row>
<EditableProTable
key={KPIStore.settingYear}
headerTitle={`毛利`}
columns={columns}
rowKey="key"
scroll={{
x: 1000,
}}
value={dataSource}
onChange={onTableChange}
recordCreatorProps={false}
toolBarRender={() => {
return [
<Switch
unCheckedChildren="查看"
checkedChildren="编辑"
key={'openEdit'}
checked={editOpen}
onChange={(e) => {
makeInitialTable(e);
}}
/>,
<Button disabled={!editOpen} type="primary" key="save" onClick={onTableSubmit}>
保存数据
</Button>,
];
}}
editable={{
type: 'multiple',
editableKeys: editableRowsKeys,
actionRender: (row, config, defaultDoms) => {
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) => {
setEditableRowKeys(editableKeys);
},
}}
/>
</>
);
});

@ -0,0 +1,252 @@
import { useContext, useState, useEffect, useMemo } from 'react';
import { observer } from 'mobx-react';
import moment from 'moment';
import { stores_Context } from './../../../config';
import { Button, Switch, Input, Space, Typography, Row, Col, message } from 'antd';
import { EditableProTable } from '@ant-design/pro-components';
import { KPIObjects } from './../../../libs/ht';
import { isEmpty, fixTo2Decimals, fixTo4Decimals, cloneDeep, numberFormatter, fixToInt } from './../../../utils/commons';
export const KPIObjectsMapped = KPIObjects.reduce((a, c) => ({ ...a, [String(c.key)]: c }), {});
const { Text } = Typography;
const initialPercentKey = new Array(12).fill(1).reduce((r, v, i) => ({ ...r, [`M${i + 1}Percent`]: [8, 9].includes(i) ? 10 : 8 }), {});
const numberConvert10K = (number, scale = 10) => {
return fixTo2Decimals(number / (1000 * scale)) + '万';
};
export default observer((props) => {
const { KPIStore, date_picker_store: searchFormStore } = useContext(stores_Context);
const { curObject, objects, onSearchSubmit } = props;
const curObjectItem = KPIObjectsMapped[curObject];
const [dataSource, setDataSource] = useState(KPIStore.pageData);
const [editOpen, setEditOpen] = useState(false);
const [editableRowsKeys, setEditableRowKeys] = useState([]);
useEffect(() => {
setDataSource(KPIStore.pageData);
setEditOpen(false);
return () => {};
}, [KPIStore.pageData]);
const PercentInput = useMemo(
() =>
// eslint-disable-next-line react/display-name
({ value, onChange, record, ...extProps }) => {
// // eslint-disable-next-line react-hooks/rules-of-hooks
const [inputValue, setInputValue] = useState(value);
const handleInputChange = (e) => {
setInputValue(e.target.value);
onChange?.(e.target.value);
};
const calcV = inputValue ? numberConvert10K(fixToInt((Number(record?.yearValue) * inputValue) / 100)) : 0;
return (
<Space direction={'vertical'}>
<Input key={'input'} suffix="%" type={'number'} value={inputValue} onChange={handleInputChange} step={0.1} />
<Text type={'secondary'}>{calcV}</Text>
</Space>
);
},
[]
);
const RenderMonthCell = (row, mon) => {
return (
<Space direction={'vertical'}>
<div>
{fixTo2Decimals(row?.[`M${mon}Percent`])}
<span>%</span>
</div>
<div>{numberConvert10K(fixTo4Decimals((Number(row?.yearValue) * row?.[`M${mon}Percent`]) / 100))}</div>
</Space>
);
};
const monthCol = new Array(12).fill(1).map((_, index) => {
return {
title: `${index + 1}`,
dataIndex: `M${index + 1}Percent`,
valueType: 'digit',
width: '6.5em',
// fieldProps: { min: 0, max: 100, style: { width: '4em' } },
renderFormItem: ({ dataIndex, ...item }, { record, isEditable, ...e }, form) => {
return <PercentInput {...{ record }} month={index + 1} key={`M${index + 1}`} />;
},
render: (_, row) => RenderMonthCell(row, index + 1),
};
});
const columns = [
{
title: curObjectItem.label,
dataIndex: 'object_id',
editable: false,
render: (_, r) => r.object_name,
},
{
title: '年度目标',
dataIndex: 'yearValue',
valueType: 'digit',
fieldProps: { style: { width: '100%' }, step: 10000*100 },
formItemProps: {
style: { width: '100%' },
},
},
...monthCol,
{
title: (
<Space>
<span>操作</span>
<Switch
unCheckedChildren="查看"
checkedChildren="编辑"
key={'openEdit'}
// defaultChecked={true}
checked={editOpen}
onChange={(e) => {
makeInitialTable(e);
}}
/>
</Space>
),
valueType: 'option',
// width: 250,
render: () => {
return null;
},
},
];
const onTableChange = (...argrs) => {
setEditableRowKeys(argrs[0].map((ele) => ele.key));
setDataSource(argrs[0]);
};
const onTableSubmit = () => {
const tableData = dataSource.reduce((r, curObj) => {
const allMonth = new Array(12).fill(1).map((_, index) => {
const mIndex = index + 1;
const mVal = (Number(curObj.yearValue) * Number(curObj[`M${mIndex}Percent`])) / 100;
const startM = moment([KPIStore.settingYear, index, 1]);
const pick = (({ object, object_name, object_id, subject, date_type }) => ({
object,
object_name,
object_id,
subject,
date_type,
}))(curObj);
return {
...pick,
start_date: startM.format('YYYY-MM-DD'),
end_date: startM.endOf('M').format('YYYY-MM-DD HH:mm'),
value: mVal,
kpi_id: curObj.kpiDataMapped?.[`M${mIndex}`]?.kpi_id || undefined,
key: undefined,
};
});
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,
}))(dataSource?.[0] || {});
tableData.unshift(yearRow);
console.log('sub', tableData, 'del:', delKpiIds);
// return false; // debug:
KPIStore.onSubmit(tableData, { delQueue: delKpiIds }).then((res) => {
if (res) {
message.success('保存成功');
setEditOpen(false);
setEditableRowKeys([]);
setDelKpiIds([]);
onSearchSubmit(searchFormStore.formValuesToSub);
return false;
}
message.error('失败, 请重试');
});
};
const initialRow = monthCol.reduce(
(r, v) => ({
...r,
[v.dataIndex]: 0,
yearValue: 10000 * 100,
object: curObject,
object_name: '',
object_id: -1,
subject: 'sum_profit',
date_type: searchFormStore.formValuesToSub.DateType,
kpiDataMapped: {},
key: Date.now().toString(32),
}),
{}
); // v.formItemProps.initialValue
const makeInitialTable = (e) => {
setEditOpen(e);
// todo: ,
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) : []);
};
const [delKpiIds, setDelKpiIds] = useState([]);
return (
<>
<Row gutter={16} className="mb-1 ">
<Col className="gutter-row mb-n1 p-none" span={24}>
<EditableProTable
key={KPIStore.settingYear}
// headerTitle={``}
columns={columns}
rowKey="key"
scroll={{
x: 1000,
}}
value={dataSource}
onChange={onTableChange}
recordCreatorProps={false}
editable={{
type: 'multiple',
editableKeys: editableRowsKeys,
actionRender: (row, config, defaultDoms) => {
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) => {
setEditableRowKeys(editableKeys);
},
}}
/>
</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>
</Col>
</Row>
</>
);
});

@ -0,0 +1,57 @@
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';
export default observer((props) => {
// const { } = useContext(stores_Context);
return (
<>
<Row gutter={16} className="mb-1">
<Col className="gutter-row mb-n1 p-none" span={24}>
<SearchForm
defaultValue={{
'initialValue': {
// ...searchPayloadHome,
},
// hides: ['businessUnits', 'months', 'WebCode', 'dates'],
shows: ['HTBusinessUnits', 'DateType', 'DepartmentList', 'years'],
'fieldProps': {
HTBusinessUnits: { show_all: true },
DepartmentList: { show_all: true },
WebCode: { show_all: false },
years: { hide_vs: true },
},
}}
confirmText="查询"
onSubmit={(_err, obj, form, str) => {
// TradeStore.setStateSearch(form);
// pageRefresh(obj);
}}
/>
</Col>
</Row>
<Row gutter={16} className="mb-1 p-s1">
<Col className="gutter-row mb-n1 p-none" span={24} style={{ backgroundColor: '#ffffff' }}>
<Tabs
tabPosition={'left'}
style={
{
// height: 220,
}
}
items={KPIObjects.map((ele, i) => {
const id = String(i);
return {
...ele,
children: `Content of tab ${id}`,
};
})}
/>
</Col>
</Row>
</>
);
});

@ -5,16 +5,23 @@ import { biz } from '../../libs/ht';
const Business_unit = (props) => {
const {store} = props;
const { store, mode, value, onChange, show_all, ...extProps } = props;
return (
<div>
<Select
mode={store.bu_select_mode}
mode={mode || store?.bu_select_mode || null}
allowClear
style={{width: '100%',}}
placeholder="选择事业部"
value={store.business_units}
onChange={(value) => store.bu_handleChange(value)}
value={value || store?.business_units || { key: '-1', label: 'ALL 事业部' }}
onChange={(value) => {
if (typeof onChange === 'function') {
onChange(value);
}
store?.bu_handleChange(value);
}}
labelInValue={true}
{...extProps}
>
{props.show_all ? <Select.Option key="-1" value="ALL">ALL 事业部</Select.Option> : ''}
{biz.map(ele => <Select.Option key={ele.key} value={ele.label}>{ele.label}</Select.Option>)}
@ -22,5 +29,7 @@ const Business_unit = (props) => {
</div>
);
};
/**
* 基于组织架构
*/
export default observer(Business_unit);

@ -0,0 +1,36 @@
import React, {Component} from 'react';
import {Select} from 'antd';
import {observer} from "mobx-react";
import { biz, bu } from '../../libs/ht';
const Business_unit = (props) => {
const { store, 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 } // { key: '-1', label: 'ALL ' }
onChange={(value) => {
if (typeof onChange === 'function') {
onChange(value);
}
// store?.bu_handleChange(value);
}}
labelInValue={true}
{...extProps}
>
{_show_all ? <Select.Option key="-1" value="ALL">ALL 事业部</Select.Option> : ''}
{bu.map(ele => <Select.Option key={ele.key} value={ele.label}>{ele.label}</Select.Option>)}
</Select>
</div>
);
};
/**
* HT的事业部
*/
export default observer(Business_unit);

@ -8,15 +8,30 @@ class DataTypeSelect extends Component {
super(props);
}
// debug:
// componentDidMount() {
// console.log(this.props, 'DataTypeSelect props');
// }
handleChange = (value) => {
if (this.props.store) {
this.props.store.onChange_datetype(value);
}
if (this.props.onChange) {
this.props.onChange(value);
}
};
render() {
const store = this.props.store;
return (
<div>
<Select
value={store.date_type}
<Select labelInValue={true}
// value={store.date_type}
value={this.props?.value || store?.date_type}
style={{ width: '100%' }}
placeholder="选择日期类型"
onChange={(value) => store.onChange_datetype(value)}
// onChange={(value) => store.onChange_datetype(value)}
onChange={this.handleChange}
>
{dateTypes.map((ele) => (
<Select.Option key={ele.key} value={ele.key}>
@ -24,7 +39,6 @@ class DataTypeSelect extends Component {
</Select.Option>
))}
</Select>
</div>
);
}
}

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { Col, DatePicker, Row } from "antd";
import { Col, DatePicker, Row, Form } from "antd";
import { observer } from "mobx-react";
import * as config from "../../config";
import moment from "moment";
@ -7,6 +7,16 @@ import "moment/locale/zh-cn";
import locale from "antd/es/date-picker/locale/zh_CN";
import { stores_Context } from "../../config";
// 1.0 Form
const SectionWrapper = ({ isform, id, title, children, right, ...extProps }) =>
!isform ? (
<>{children}</>
) : (
<Form.Item noStyle {...extProps}>
{children}
</Form.Item>
);
//
class DatePickerCharts extends Component {
static contextType = stores_Context;
@ -17,16 +27,20 @@ class DatePickerCharts extends Component {
render() {
const { date_picker_store } = this.context;
const { isform } = this.props;
const defaultV = [date_picker_store.start_date, date_picker_store.end_date];
const defaultVdiff = [date_picker_store.start_date_cp, date_picker_store.end_date_cp];
return (
<div>
<Row>
<Col span={24}>
<SectionWrapper {...{isform: isform || false, initialValue: defaultV, name: `applyDate`}} >
<DatePicker.RangePicker
style={{width: '100%'}}
format={config.DATE_FORMAT}
locale={locale}
allowClear={false}
value={[date_picker_store.start_date, date_picker_store.end_date]}
{...(isform ? {} : {value: defaultV})}
onChange={(e) => {
date_picker_store.onChange_dataPicker(e);
if (typeof this.props.onChange === 'function') {
@ -46,19 +60,27 @@ class DatePickerCharts extends Component {
去年: [moment().subtract(1, "year").startOf("year"), moment().subtract(1, "year").endOf("year")],
}}
/>
</SectionWrapper>
</Col>
{this.props.hide_vs ? (
""
) : (
<Col span={24}>
<SectionWrapper {...{isform: isform || false, initialValue: defaultVdiff, name: `applyDate2`}} >
<DatePicker.RangePicker
bordered={false}
style={{width: '100%'}}
format={config.DATE_FORMAT}
locale={locale}
value={[date_picker_store.start_date_cp, date_picker_store.end_date_cp]}
{...(isform ? {} : {value: defaultVdiff})}
placeholder={["对比 Start date", "End date"]}
onChange={date_picker_store.onChange_dataPicker_cp}
// onChange={date_picker_store.onChange_dataPicker_cp}
onChange={(value) => {
if (typeof this.props.onChange === 'function') {
this.props.onChange(value);
}
date_picker_store?.onChange_dataPicker_cp(value);
}}
ranges={{
上一时间段: date_picker_store.previous_date(),
去年同期: date_picker_store.previous_year(),
@ -68,7 +90,7 @@ class DatePickerCharts extends Component {
前三个月: [moment().subtract(5, "month").startOf("month"), moment().subtract(3, "month").endOf("month")],
去年: [moment().subtract(1, "year").startOf("year"), moment().subtract(1, "year").endOf("year")],
}}
/>
/></SectionWrapper>
</Col>
)}
</Row>

@ -10,23 +10,29 @@ class GroupSelect extends Component {
render() {
const { store, mode, value, onChange, show_all, ...extProps } = this.props;
const _mode = mode || store?.group_select_mode || null;
const _show_all = ['tags', 'multiple'].includes(_mode) ? false : show_all;
return (
<div>
<Select
mode={mode || store?.group_select_mode || null}
mode={_mode}
style={{ width: '100%' }}
placeholder="选择小组"
value={value || store?.groups || '0'}
value={value || store?.groups || undefined} // { key: '0', label: '' }
onChange={(value) => {
if (typeof onChange === 'function') {
onChange(value);
}
store?.group_handleChange(value);
}}
labelInValue={true}
maxTagCount={1}
maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`}
allowClear={_mode != null}
{...extProps}
>
{show_all ? (
<Select.Option key="0" value="0">
{_show_all ? (
<Select.Option key="ALL" value="ALL">
所有小组
</Select.Option>
) : (

@ -0,0 +1,133 @@
import React from 'react';
import { Select } from 'antd';
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';
const { Option } = Select;
let timeout;
let currentValue;
function curl(opts, callback) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
currentValue = opts.value;
function fake() {
// console.log(currentValue, opts.value);
if (currentValue === opts.value && opts.value === '空') {
const _p = [{ 'key': '0', 'label': '空' }];
return callback(_p);
}
const param = {
code: 'utf-8',
q: opts.value,
};
// const str = new URLSearchParams({
// code: 'utf-8',
// q: opts.value,
// }).toString();
fetchJSON(`${opts.url}`, param)
.then(d => {
if (currentValue === opts.value) {
const result = objectMapper(d.result, opts.map) || [];
callback(result);
}
});
}
timeout = setTimeout(fake, 300);
}
/**
* 异步请求的下拉菜单, 可搜索
* @property {array} defaultOptions 默认选项 [{key: '', label: '' }]
* @property {string} url
* @property {object} map 异步结果的字段转换定义
* @property {boolean} autoGet 首次默认请求
* @property {string} resultkey 结果的根字段
*/
class SearchInput extends React.Component {
constructor(props) {
super(props);
this.state = {
data: this.props.defaultOptions || [],
value: undefined,
autoData: this.props.defaultOptions || [],
};
}
componentDidMount() {
if (this.props.autoGet === true) {
const { map, resultkey } = 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) =>
this.setState({ data, autoData: data }, () => (typeof this.props.onSearchAfter === 'function' ? this.props.onSearchAfter(data, this.state.value) : ''))
);
}
}
componentDidUpdate(prevProps) {
if (this.props.value !== prevProps.value) {
this.setState({ value: undefined });
}
}
handleClear = () => {
this.setState({ data: this.state.autoData });
};
handleSearch = value => {
if ( ! this.props.url && this.props.defaultOptions?.length) {
const f = this.props.defaultOptions.filter(r => String(r.label).indexOf(value) !== -1);
this.setState({ data: f || [] });
return false;
}
const { map, resultkey } = 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) =>
this.setState({ data }, () => (typeof this.props.onSearchAfter === 'function' ? this.props.onSearchAfter(data, this.state.value) : ''))
);
} else {
this.setState({ data: this.state.autoData || [] });
}
};
handleChange = (value, option) => {
this.setState({ value }, () => this.props.onChange(value, option));
};
render() {
const options = this.state.data.map(d => <Option key={d.key} extradata={d.options}>{d.label}</Option>);
const { onSearchAfter, defaultOptions, autoGet, ...props } = this.props;
return (
<Select
{...props}
style={this.props.style || { width: '100%' }}
showSearch
labelInValue
value={this.state.value || this.props.value}
placeholder={this.props.placeholder}
defaultActiveFirstOption={false}
showArrow={false}
filterOption={false}
onSearch={this.handleSearch}
onChange={this.handleChange}
notFoundContent={null}
allowClear={true}
onClear={this.handleClear}
>
{options}
</Select>
);
}
}
export default observer(SearchInput);

@ -0,0 +1,366 @@
import { createContext, useContext, useEffect } from 'react';
import { toJS } from 'mobx';
import { observer } from 'mobx-react';
import { DATE_FORMAT, stores_Context } from './../../config';
import { SearchOutlined } from '@ant-design/icons';
import { Form, Row, Col, Select, Button, Space, DatePicker } from 'antd';
import moment from 'moment';
// import locale from 'antd/es/date-picker/locale/zh_CN';
import BusinessSelect from './BusinessSelect';
import BusinessUnitSelect from './BusinessUnitSelect';
import GroupSelect from './GroupSelect';
import SiteSelect from './SiteSelect';
import DateTypeSelect from './DataTypeSelect';
import DatePickerCharts from './DatePickerCharts';
import YearPickerCharts from './YearPickerCharts';
import SearchInput from './Input';
import { objectMapper, at, empty } from './../../utils/commons';
import './search.css';
const EditableContext = createContext();
const Option = Select.Option;
/**
* 搜索表单
* @property defaultValue { initialValue, fieldProps, hides, shows, sort }
* * {object} initialValue 默认值
* * {object} fieldProps 表单项属性
* * {array} hides 隐藏的表单项
* * {array} shows 显示的表单项
* * {object} sort 表单项排序
* @property onSubmit
*/
export default observer((props) => {
const { date_picker_store: searchFormStore } = useContext(stores_Context);
const [form] = Form.useForm();
const { sort, initialValue, hides, shows, fieldProps } = {
sort: '',
initialValue: '',
fieldProps: '',
hides: [],
shows: [],
...props.defaultValue,
};
const { onSubmit, confirmText } = props;
const formValuesMapper = (values) => {
console.log('Received values of form, origin form value: ', values);
const destinationObject = {
'DateType': {
key: 'DateType',
transform: (value) => value?.key || '',
default: '',
},
'HTBusinessUnits': {
key: 'HTBusinessUnits',
transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? (!isNaN(parseInt(value.key), 10) ? value.key : '') : '-1';
},
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';
},
default: '',
},
'DepartmentList': {
key: 'DepartmentList',
transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.key : 'ALL';
},
default: '',
},
'WebCode': {
key: 'WebCode',
transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.key : 'ALL';
},
default: '',
},
'IncludeTickets': {
key: 'IncludeTickets',
transform: (value) => value?.key || '',
default: '',
},
'operator': {
key: 'operator',
transform: (value) => value?.key || '',
default: '',
},
'applyDate': [
{
key: 'Date1',
transform: (value) => (value === '' || !Array.isArray(value) ? undefined : moment(value[0]).format(DATE_FORMAT)),
default: '',
},
{
key: 'Date2',
transform: (value) => (value === '' || !Array.isArray(value) ? undefined : moment(value[1]).format(`${DATE_FORMAT} 23:59:59`)),
default: '',
},
],
'applyDate2': [
{
key: 'DateDiff1',
transform: (value) => (value === '' || !Array.isArray(value) ? undefined : value[0] ? moment(value[0]).format(DATE_FORMAT) : undefined),
default: '',
},
{
key: 'DateDiff2',
transform: (value) => (value === '' || !Array.isArray(value) ? undefined : value[1] ? moment(value[1]).format(`${DATE_FORMAT} 23:59:59`) : undefined),
default: '',
},
],
'year': [
{
key: 'Date1',
transform: (value) => (value ? moment(value).format(`YYYY-01-01`) : undefined),
default: '',
},
{
key: 'Date2',
transform: (value) => (value ? moment(value).format(`YYYY-12-31 23:59:59`) : undefined),
default: '',
},
],
'yearDiff': [
{
key: 'DateDiff1',
transform: (value) => (value ? moment(value).format(`YYYY-01-01`) : undefined),
default: '',
},
{
key: 'DateDiff2',
transform: (value) => (value ? moment(value).format(`YYYY-12-31 23:59:59`) : undefined),
default: '',
},
],
'country': {
key: 'country',
transform: (value) => value?.key || '',
default: '',
},
};
let dest = {};
const { applyDate, applyDate2, year, yearDiff, ...omittedValue } = values;
dest = { ...omittedValue, ...objectMapper(values, destinationObject) };
for (const key in dest) {
if (Object.prototype.hasOwnProperty.call(dest, key)) {
dest[key] = typeof dest[key] === 'string' ? (dest[key] || '').trim() : dest[key];
}
}
// 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) => {
const dest = formValuesMapper(values);
const str = new URLSearchParams(dest).toString();
searchFormStore.setFormValues(values);
searchFormStore.setFormValuesToSub(dest);
if (typeof onSubmit === 'function') {
onSubmit(null, dest, values, str);
}
};
const onReset = () => {
form.setFieldsValue({
// 'DateType': undefined,
});
};
const onValuesChange = (...args) => {
const [changedValues, allValues] = args;
console.log('form onValuesChange', args);
searchFormStore.setFormValues(allValues);
};
return (
// layout="inline"
<Form form={form} name="advanced_search" className="orders-search-form" onFinish={onFinish} onValuesChange={onValuesChange}>
<EditableContext.Provider value={form}>
<Row gutter={10} style={{ background: '#f9fafa', margin: '0px 0px 10px 0px', padding: '16px 8px' }}>
{getFields({ sort, initialValue, hides, shows, fieldProps, form })}
{/* 'textAlign': 'right' */}
<Col flex="1 0 120px" style={{ padding: '0px 5px' }}>
<Space align="center">
<Button size={'middle'} type="primary" icon={<SearchOutlined />} htmlType="submit">
{confirmText || '统计'}
</Button>
{/* <Button size="small" onClick={onReset}>
重置
</Button> */}
</Space>
</Col>
</Row>
</EditableContext.Provider>
</Form>
);
});
function getFields(props) {
const { fieldProps, form } = props;
const bigCol = 4 * 2;
const midCol = 6;
const layoutProps = {
gutter: { xs: 8, sm: 8, lg: 16 },
lg: { span: 4 },
sm: { span: 12 },
xs: { span: 24 },
};
const item = (name, sort = 0, render, col) => {
const customCol = col || 4;
return {
'key': '',
sort,
name,
render,
'hide': false,
'col': { lg: { span: customCol } },
};
};
let baseChildren = [];
baseChildren = [
item(
'HTBusinessUnits',
99,
<Form.Item name={`HTBusinessUnits`} initialValue={at(props, 'initialValue.HTBusinessUnits')[0] || undefined}>
<BusinessUnitSelect {...fieldProps.HTBusinessUnits} />
</Form.Item>
),
item(
'businessUnits',
99,
<Form.Item name={`businessUnits`} initialValue={at(props, 'initialValue.businessUnits')[0] || undefined}>
<BusinessSelect {...fieldProps.businessUnits} />
</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} />
</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} />
</Form.Item>
),
item(
'IncludeTickets',
99,
// value={orders_store.include_tickets} onChange={orders_store.handleChange_include_tickets}
<Form.Item name={`IncludeTickets`} initialValue={at(props, 'initialValue.IncludeTickets')[0] || { key: '1', label: '含门票' }}>
<Select style={{ width: '100%' }} placeholder="是否含门票" labelInValue>
<Option key="1" value="1">
含门票
</Option>
<Option key="0" value="0">
不含门票
</Option>
</Select>
</Form.Item>,
2
),
//
item(
'DateType',
99,
<Form.Item name={`DateType`} initialValue={at(props, 'initialValue.DateType')[0] || { key: 'applyDate', label: '提交日期' }}>
<DateTypeSelect />
</Form.Item>,
2
),
item(
'dates',
99,
<Form.Item>
<DatePickerCharts isform={true} {...fieldProps.dates} form={form} />
</Form.Item>,
midCol
),
item(
'years',
99,
<Form.Item>
{/* <DatePicker picker="year" placeholder='年份' /> */}
<YearPickerCharts {...fieldProps.years} />
</Form.Item>,
2
),
item(
'months',
99,
<Form.Item>
<DatePicker picker="month" placeholder="月份" />
</Form.Item>,
2
),
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>
),
item(
'country',
99,
<Form.Item name={'country'}>
<SearchInput autoGet url="/service-Analyse2/GetCountryInfo" map={{ 'c_id': 'key', 'cn_name': 'label' }} resultkey={'result'} placeholder="输入搜索国籍: 中/英名字" />
</Form.Item>
),
];
baseChildren = baseChildren
.map((x) => {
x.hide = false;
if (props.sort === undefined) {
return x;
}
const tmpSort = props.sort;
for (const key in tmpSort) {
if (Object.prototype.hasOwnProperty.call(tmpSort, key)) {
if (x.name === key) {
x.sort = tmpSort[key];
}
}
}
return x;
})
.map((x) => {
if (props.hides.length === 0 && props.shows.length === 0) {
return x;
}
if (props.hides.length === 0) {
x.hide = !props.shows.includes(x.name);
} else if (props.shows.length === 0) {
x.hide = props.hides.includes(x.name);
}
return x;
})
.filter((x) => !x.hide)
.sort((a, b) => {
return a.sort < b.sort ? -1 : 1;
});
const children = [];
const leftStyle = {}; // { borderRight: '1px solid #dedede' };
for (let i = 0; i < baseChildren.length; i++) {
let style = { padding: '0px 2px' };
style = i % 2 === 0 && baseChildren[i].col === 12 ? { ...style, ...leftStyle } : style;
style = !baseChildren[i].hide ? { ...style, display: 'block' } : { ...style, display: 'none' };
const Item = (
<Col key={String(i)} style={style} {...layoutProps} {...baseChildren[i].col}>
{baseChildren[i].render}
</Col>
);
children.push(Item);
}
return children;
}

@ -11,22 +11,28 @@ class SiteSelect extends Component {
render() {
const { store, mode, value, onChange, show_all, ...extProps } = this.props;
return (
const _mode = mode || store?.group_select_mode || null;
const _show_all = ['tags', 'multiple'].includes(_mode) ? false : show_all;
return (
<div>
<Select
mode={mode || store?.site_select_mode || null}
mode={_mode}
style={{width: '100%'}}
placeholder="选择来源"
defaultValue={value || store?.webcode || 'ALL'}
defaultValue={value || store?.webcode || undefined }
onChange={(value) => {
if (typeof onChange === 'function') {
onChange(value);
}
store?.handleChange_webcode(value);
}}
labelInValue={true}
maxTagCount={1}
maxTagPlaceholder={(omittedValues) => ` + ${omittedValues.length} 更多...`}
allowClear={_mode != null}
{...extProps}
>
{show_all===true ? <Select.Option key="1" value="ALL">所有来源</Select.Option> : ''}
{_show_all===true ? <Select.Option key="1" value="ALL">所有来源</Select.Option> : ''}
{sites.map(ele => <Select.Option key={ele.key} value={ele.code}>{ele.label}</Select.Option>)}
</Select>
</div>

@ -0,0 +1,70 @@
import React, { Component } from 'react';
import { Col, DatePicker, Row, Form } from 'antd';
import { observer } from 'mobx-react';
import * as config from '../../config';
import moment from 'moment';
import 'moment/locale/zh-cn';
import locale from 'antd/es/date-picker/locale/zh_CN';
import { stores_Context } from '../../config';
//
class DatePickerCharts extends Component {
static contextType = stores_Context;
constructor(props) {
super(props);
}
render() {
const { date_picker_store } = this.context;
return (
<div>
<Row>
<Col span={24}>
<Form.Item name={`year`} noStyle initialValue={date_picker_store.start_date}>
<DatePicker
picker="year"
style={{ width: '100%' }}
format={`YYYY`}
locale={locale}
allowClear={false}
onChange={(e) => {
const fullYear = [e.clone().set('month', 0).set('date', 1), e.clone().set('month', 11).set('date', 31)];
date_picker_store.onChange_dataPicker(fullYear);
if (typeof this.props.onChange === 'function') {
this.props.onChange(fullYear);
}
}}
/>
</Form.Item>
</Col>
{this.props.hide_vs ? (
''
) : (
<Col span={24}>
<Form.Item name={`yearDiff`} noStyle initialValue={date_picker_store.start_date_cp}>
<DatePicker
picker="year"
bordered={false}
style={{ width: '100%' }}
format={`YYYY`}
locale={locale}
placeholder={"对比 Year"}
onChange={(value) => {
const fullYear = [value.clone().set('month', 0).set('date', 1), value.clone().set('month', 11).set('date', 31)];
if (typeof this.props.onChange === 'function') {
this.props.onChange(fullYear);
}
date_picker_store?.onChange_dataPicker_cp(fullYear);
}}
/>
</Form.Item>
</Col>
)}
</Row>
</div>
);
}
}
export default observer(DatePickerCharts);

@ -0,0 +1,4 @@
.ant-form-item{
margin: 0;
margin-bottom: 8px;
}

@ -7,29 +7,52 @@ export const biz = [
{ key: '2', label: '国际事业部', code: '' },
{ key: '4', label: '孵化学院', code: '' },
];
/**
* HT 事业部
*/
export const bu = [
{ key: '91001', value: '91001', label: 'CH事业部' },
{ key: '91002', value: '91002', label: '商旅事业部' },
{ key: '91003', value: '91003', label: '国际事业部' },
{ key: '91004', value: '91004', label: 'CT事业部' },
{ key: '91005', value: '91005', label: '德语事业部' },
{ key: '91006', value: '91006', label: 'AH亚洲项目组' },
{ key: '91009', value: '91009', label: 'Trippest项目组' },
{ key: '91010', value: '91010', label: '花梨鹰' },
{ key: '91012', value: '91012', label: '西语组' },
];
/**
* HT 销售小组
*/
export const deptUnits = [
{ key: '43001', value: '43001', label: '英文A组(骆梅玉)' },
{ key: '43002', value: '43002', label: '英文B组(王健)' },
{ key: '43003', value: '43003', label: '目的地组(杨新玲)' },
{ key: '43005', value: '43005', label: '其他' },
];
/**
* 小组
*/
export const groups = [
{ key: '1,2,28,7,33', label: 'GH事业部', code: 'GH', children: [] },
{ key: '8,9,11,12,20,21', label: '国际事业部', code: 'INT', children: [] },
{ key: '10,18,16,30', label: '孵化学院', code: '', children: [] },
{ key: '1', label: 'CH直销', code: '', children: [] },
{ key: '2', label: 'CH大客户', code: '', children: [] },
{ key: '28', label: 'AH亚洲项目组', code: 'AH', children: [] },
{ key: '33', label: 'GH项目组', code: '', children: [] },
{ key: '7', label: '市场推广', code: '', children: [] },
{ key: '8', label: '德语', code: '', children: [] },
{ key: '9', label: '日语', code: '', children: [] },
{ key: '11', label: '法语', code: '', children: [] },
{ key: '12', label: '西语', code: '', children: [] },
{ key: '20', label: '俄语', code: '', children: [] },
{ key: '21', label: '意语', code: '', children: [] },
{ key: '10', label: '商旅', code: '', children: [] },
{ key: '18', label: 'CT', code: 'CT', children: [] },
{ key: '16', label: 'APP', code: 'APP', children: [] },
{ key: '30', label: 'Trippest', code: 'TP', children: [] },
{ key: '31', label: '花梨鹰', code: '', children: [] },
{ value: '1,2,28,7,33', key: '1,2,28,7,33', label: 'GH事业部', code: 'GH', children: [1, 2, 28, 7, 33] },
{ value: '8,9,11,12,20,21', key: '8,9,11,12,20,21', label: '国际事业部', code: 'INT', children: [8, 9, 11, 12, 20, 21] },
{ value: '10,18,16,30', key: '10,18,16,30', label: '孵化学院', code: '', children: [10, 18, 16, 30] },
{ value: '1', key: '1', label: 'CH直销', code: '', children: [] },
{ value: '2', key: '2', label: 'CH大客户', code: '', children: [] },
{ value: '28', key: '28', label: 'AH亚洲项目组', code: 'AH', children: [] },
{ value: '33', key: '33', label: 'GH项目组', code: '', children: [] },
{ value: '7', key: '7', label: '市场推广', code: '', children: [] },
{ value: '8', key: '8', label: '德语', code: '', children: [] },
{ value: '9', key: '9', label: '日语', code: '', children: [] },
{ value: '11', key: '11', label: '法语', code: '', children: [] },
{ value: '12', key: '12', label: '西语', code: '', children: [] },
{ value: '20', key: '20', label: '俄语', code: '', children: [] },
{ value: '21', key: '21', label: '意语', code: '', children: [] },
{ value: '10', key: '10', label: '商旅', code: '', children: [] },
{ value: '18', key: '18', label: 'CT', code: 'CT', children: [] },
{ value: '16', key: '16', label: 'APP', code: 'APP', children: [] },
{ value: '30', key: '30', label: 'Trippest', code: 'TP', children: [] },
{ value: '31', key: '31', label: '花梨鹰', code: '', children: [] },
];
export const groupsMappedByCode = groups.reduce((a, c) => ({ ...a, [String(c.code || c.key)]: c }), {});
@ -37,44 +60,44 @@ export const groupsMappedByCode = groups.reduce((a, c) => ({ ...a, [String(c.cod
* 来源
*/
export const sites = [
{ key: '2', label: 'CHT', code: 'CHT' },
{ key: '8', label: 'AH', code: 'AH' },
{ key: '163', label: 'GH', code: 'GH' },
{ key: '28', label: '客运中国', code: 'GHKYZG' },
{ key: '7', label: '客运海外', code: 'GHKYHW' },
{ key: '172', label: 'ToB业务', code: 'GHTOB' },
{ key: '11,12,20,21,10,18', label: '国际(入境)', code: 'JP,VAC,IT,GM,RU,VC' },
{ key: '122,200,211,100,188', label: '国际(海外)', code: 'VACHW,ITHW,GMHW,RUHW,VCHW' },
{ key: '11', label: '日语', code: 'JP' },
{ key: '12', label: '西语', code: 'VAC' },
{ key: '122', label: '西语海外', code: 'VACHW' },
{ key: '20', label: '意大利', code: 'IT' },
{ key: '200', label: '意大利海外', code: 'ITHW' },
{ key: '21', label: '德语', code: 'GM' },
{ key: '211', label: '德语海外', code: 'GMHW' },
{ key: '10', label: '俄语', code: 'RU' },
{ key: '100', label: '俄语海外', code: 'RUHW' },
{ key: '18', label: '法语', code: 'VC' },
{ key: '188', label: '法语海外', code: 'VCHW' },
{ key: '16', label: 'CT', code: 'CT' },
{ key: '30', label: 'TP', code: 'trippest' },
{ key: '31', label: '花梨鹰', code: 'HLY' },
{ value: '2', key: '2', label: 'CHT', code: 'CHT' },
{ value: '8', key: '8', label: 'AH', code: 'AH' },
{ value: '163', key: '163', label: 'GH', code: 'GH' },
{ value: '28', key: '28', label: '客运中国', code: 'GHKYZG' },
{ value: '7', key: '7', label: '客运海外', code: 'GHKYHW' },
{ value: '172', key: '172', label: 'GH TO B业务', code: 'GHTOB' },
{ 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: '11', key: '11', label: '日语', code: 'JP' },
{ value: '12', key: '12', label: '西语', code: 'VAC' },
{ value: '122', key: '122', label: '西语海外', code: 'VACHW' },
{ value: '20', key: '20', label: '意大利', code: 'IT' },
{ value: '200', key: '200', label: '意大利海外', code: 'ITHW' },
{ value: '21', key: '21', label: '德语', code: 'GM' },
{ value: '211', key: '211', label: '德语海外', code: 'GMHW' },
{ value: '10', key: '10', label: '俄语', code: 'RU' },
{ value: '100', key: '100', label: '俄语海外', code: 'RUHW' },
{ value: '18', key: '18', label: '法语', code: 'VC' },
{ value: '188', key: '188', label: '法语海外', code: 'VCHW' },
{ value: '16', key: '16', label: 'CT', code: 'CT' },
{ value: '30', key: '30', label: 'TP', code: 'trippest' },
{ value: '31', key: '31', label: '花梨鹰', code: 'HLY' },
];
export const dateTypes = [
{ key: 'applyDate', label: '提交日期' },
{ key: 'ConfirmDate', label: '确认日期' },
{ key: 'startDate', label: '走团日期' },
{ key: 'applyDate', value: 'applyDate', label: '提交日期' },
{ key: 'ConfirmDate', value: 'ConfirmDate', label: '确认日期' },
{ key: 'startDate', value: 'startDate', label: '走团日期' },
];
/**
* 结果字段
*/
export const dataFieldOptions = [
{ label: '毛利', value: 'SumML', formatter: (v) => `${v / 1000} K` },
{ label: '订单数', value: 'OrderCount', formatter: (v) => v },
{ label: '成交数', value: 'CJCount', formatter: (v) => v },
// { label: '成交人数', value: 'CJPersonNum', formatter: (v) => v },
{ label: '成交率', value: 'CJrate', formatter: (v) => v },
{ label: '毛利', value: 'SumML', formatter: (v) => `${v / 10000}`, nestkey: { p: 'MLKPIrates', v: 'MLKPIvalue' } },
{ label: '订单数', value: 'SumOrder', formatter: (v) => v, nestkey: { p: 'OrderKPIrates', v: 'OrderKPIvalue' } },
{ label: '成交数', value: 'ConfirmOrder', formatter: (v) => v, nestkey: { p: 'ConfirmOrderKPIrates', v: 'ConfirmOrderKPIvalue' } },
{ label: '成交率', value: 'ConfirmRates', formatter: (v) => `${v} %`, nestkey: { p: 'ConfirmRatesKPIrates', v: 'ConfirmRatesKPIvalue' } },
// { label: '人数', value: 'CJPersonNum', formatter: (v) => v },
// todo: more...
];
/**
@ -83,8 +106,37 @@ export const dataFieldOptions = [
export const dataFieldAlias = dataFieldOptions.reduce(
(a, c) => ({
...a,
[c.value]: { alias: c.label, formatter: (v) => c.formatter(v) },
[`${c.value}KPI`]: { alias: `${c.label}目标`, formatter: (v) => c.formatter(v) },
[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) },
}),
{}
);
/**
* KPI对象
*/
export const KPIObjects = [
{ key: 'overview', value: 'overview', label: '海纳' },
{ key: 'bu', value: 'bu', label: '事业部', data: bu },
{ key: 'dept', value: 'dept', label: '小组', data: groups },
{ key: 'du', value: 'du', label: '销售小组', data: deptUnits },
{ key: 'operator', value: 'operator', label: '顾问' },
{ key: 'destination', value: 'destination', label: '目的地' },
{ key: 'country', value: 'country', label: '国籍' },
];
export const KPISubjects = [
{ key: 'sum_profit', value: 'sum_profit', label: '毛利' },
{ key: 'in_order_count', value: 'in_order_count', label: '订单数' },
{ key: 'confirm_order_count', value: 'confirm_order_count', label: '成团' },
{ key: 'depart_order_count', value: 'depart_order_count', label: '走团' },
{ key: 'confirm_rates', value: 'confirm_rates', label: '成型率' },
{ key: 'praise_rates', value: 'praise_rates', label: '表扬率' },
// { key: 'first_reply_rates', value: 'first_reply_rates', label: '首报回复率'},
// { key: 'quote_rates', value: 'quote_rates', label: '报价率'},
// { key: 'first_post_time', value: 'first_post_time', label: '订单到首邮发送时间'},
// { key: 'reply_rates_wechat', value: 'reply_rates_wechat', label: '微信回复率'},
// { key: 'reply_rates_wa', value: 'reply_rates_wa', label: 'WA回复率'},
// { key: 'reply_eff_wechat', value: 'reply_eff_wechat', label: '微信回复效率'},
// { key: 'reply_eff_wa', value: 'reply_eff_wa', label: 'WA回复效率'},
{ key: 'sum_person_num', value: 'sum_person_num', label: '人数' },
];

@ -1,27 +0,0 @@
{
"get|/service-web/QueryData/GetOrderCount/test": {
"errcode": "",
"errmsg": "",
"data": null,
"ordercount1|10": [
{
"id|10-99": 1,
"key|10-99": 1,
"orderCount|1-100": 5,
"WebCode|1": ["ah", "cht"],
"groups": "groups1",
"ApplyDate": "@date(\"2023-08-dd\")"
}
],
"ordercount2|10": [
{
"id|100-200": 1,
"key|100-200": 1,
"orderCount|1-100": 5,
"WebCode|1": ["ah", "cht"],
"groups": "groups2",
"ApplyDate": "@date(\"2023-08-dd\")"
}
]
}
}

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

@ -79,7 +79,7 @@
{}
]
},
"get|/service-web/QueryData/GetTradeProcess": {
"get|/service-Analyse2/GetTradeProcess/test": {
"errcode": 0,
"errmsg": "",
"loading": false,
@ -87,6 +87,7 @@
"result1|24": [
{
"groups|1": "@pick([\"inside\",\"outside\"])",
"groupsLabel|1": "@pick([\"inside\",\"outside\"])",
"groupDateVal": "@date('2023-MM')",
"SumML": "@increment(1000)",
"SumMLVSrate": "@float(0,70,2,2)",
@ -104,6 +105,27 @@
{}
]
},
"get|/service-web/QueryData/GetTradeSection": {
"errcode": 0,
"errmsg": "",
"loading": false,
"data": null,
"result1|24": [
{
"groups|1": "@pick([\"1\",\"2_5\"])",
"groupDateVal": "@date('2023-MM')",
"key": "@increment",
"SumML": "@integer(99,9999)",
"SumOrder": "@integer(99,9999)",
"SumPersonNum": "@integer(99,9999)",
"ConfirmOrder": "@integer(99,9999)",
"ConfirmRates": "@float(20,100,0,2)",
"AvgML": "@integer(99,9999)"
}],
"result2": [
{}
]
},
"get|/service-Analyse2/getkpi/test": {
"errcode": 0,
"errmsg": "",

@ -1,6 +1,8 @@
import {makeAutoObservable} from "mobx";
import moment from "moment";
/**
* 管理搜索组件的状态
*/
class DatePickerStore {
constructor(rootStore) {
@ -8,6 +10,23 @@ 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;
@ -38,8 +57,14 @@ class DatePickerStore {
return [moment(this.start_date).subtract(1, 'year'), moment(this.end_date).subtract(1, 'year')];
}
}
setFormValues(data){
this.formValues = data;
}
setFormValuesToSub(data){
this.formValuesToSub = data;
}
}
export default DatePickerStore;

@ -0,0 +1,63 @@
import { makeAutoObservable, runInAction, toJS } from 'mobx';
import * as req from '../utils/request';
import { isEmpty, sortBy, objectMapper } from '../utils/commons';
const modelMapper = {
'operator': {
url: '/service-Analyse2/GetOperatorInfo',
mapper: {
op_id: [{ key: 'key' }, { key: 'value' }],
cn_name: { key: 'label' },
en_name: { key: 'label_alias' },
},
},
'country': {
url: '/service-Analyse2/GetCountryInfo',
mapper: {
c_id: [{ key: 'key' }, { key: 'value' }],
cn_name: { key: 'label' },
en_name: { key: 'label_alias' },
},
},
'vendor': {
url: '/service-web/QueryData/GetVEIName',
mapper: {
CAV_VEI_SN: [{ key: 'key' }, { key: 'value' }],
VEI2_CompanyBN: { key: 'label' },
},
},
'creditcardbilltype': {
url: '/service-web/QueryData/GetCreditCardBillType',
mapper: {
cb_billtype: [{ key: 'key' }, { key: 'value' }, { key: 'label' }],
},
},
};
class DictData {
constructor(appStore) {
this.appStore = appStore;
makeAutoObservable(this);
}
async fetchDictData(model = '', param={}) {
const mkey = model.toLowerCase();
this[mkey] = { loading: true, dataSource: [] };
const json = await req.fetchJSON(modelMapper[mkey].url, param);
if (json.errcode === 0) {
runInAction(() => {
this[mkey].loading = false;
this[mkey].dataSource = objectMapper(json.result, modelMapper[mkey].mapper);
console.log({ loading: false, ...json }, model, 'DictData', toJS(this[mkey]));
});
}
return this[mkey];
}
data = {};
operator = { loading: false, dataSource: [] };
country = { loading: false, dataSource: [] };
vendor = { loading: false, dataSource: [] };
creditcardbilltype = { loading: false, dataSource: [] };
}
export default DictData;

@ -0,0 +1,61 @@
import { makeAutoObservable, runInAction, toJS } from 'mobx';
import * as req from '../utils/request';
import { isEmpty, sortBy } from '../utils/commons';
const modelMapper = {
'tourDays': { url: '/service-Analyse2/GetTradeApartByTourDays' },
'PML': { url: '/service-Analyse2/GetTradeApartByPML' },
'ConfirmDays': { url: '/service-Analyse2/GetTradeApartByConfirmDays' },
'ApplyDays': { url: '/service-Analyse2/GetTradeApartByApplyDays' },
'PersonNum': { url: '/service-Analyse2/GetTradeApartByPersonNum' },
'destination': { url: '/service-Analyse2/GetTradeApartByDestination' },
'GlobalDestination': { url: '/service-Analyse2/GetTradeApartByGlobalDestination' },
};
class Distribution {
constructor(appStore){
this.appStore = appStore;
makeAutoObservable(this);
}
async getData(param){
const mkey = this.curTab;
this[mkey] = { loading: true, dataSource: [] };
const json = await req.fetchJSON(modelMapper[mkey].url, param);
if (json.errcode === 0) {
runInAction(() => {
const dataLength = json.result.length;
this[mkey].loading = false;
this[mkey].originData = json.result;
this[mkey].dataSource = dataLength > 20 ? json.result.slice(0, 30) : json.result;
});
}
return this[mkey];
}
resetData() {
this.tourDays = { loading: false, dataSource: [] };
this.PML = { loading: false, dataSource: [] };
this.ConfirmDays = { loading: false, dataSource: [] };
this.ApplyDays = { loading: false, dataSource: [] };
this.PersonNum = { loading: false, dataSource: [] };
this.destination = { loading: false, dataSource: [] };
this.GlobalDestination = { loading: false, dataSource: [] };
}
curTab = 'tourDays';
setCurTab(v) {
this.curTab = v;
}
pageLoading = false;
tourDays = { loading: false, dataSource: [] };
PML = { loading: false, dataSource: [] };
ConfirmDays = { loading: false, dataSource: [] };
ApplyDays = { loading: false, dataSource: [] };
PersonNum = { loading: false, dataSource: [] };
destination = { loading: false, dataSource: [] };
GlobalDestination = { loading: false, dataSource: [] };
}
export default Distribution;

@ -12,6 +12,8 @@ import WhatsAppStore from "./WhatsApp";
import CustomerServicesStore from "./CustomerServices";
import TradeStore from "./Trade";
import KPI from "./KPI";
import DictData from "./DictData";
import Distribution from "./Distribution";
class Index {
constructor() {
this.dashboard_store = new DashboardStore(this);
@ -27,6 +29,8 @@ class Index {
this.customerServicesStore = new CustomerServicesStore(this);
this.TradeStore = new TradeStore(this);
this.KPIStore = new KPI(this);
this.DictDataStore = new DictData(this);
this.DistributionStore = new Distribution(this);
makeAutoObservable(this);
}

@ -1,106 +1,127 @@
import { makeAutoObservable, runInAction, toJS } from 'mobx';
import * as req from '../utils/request';
import { isEmpty, sortBy, groupBy } from '../utils/commons';
import { isEmpty, sortBy, groupBy, cloneDeep, fixTo4Decimals, flush } from '../utils/commons';
import moment from 'moment';
const currentYear = moment().year();
class KPI {
constructor(appStore) {
this.appStore = appStore;
makeAutoObservable(this);
}
saveOrUpdate() {
console.log('ssssssssssss');
console.log(this.pageData);
const tableData = this.pageData.reduce((r, curObj) => {
const allMonth = new Array(12).fill(1).map((_, index) => {
const mIndex = index+1;
const startM = moment([this.settingYear, index, 1]);
const pick = (({
object, object_name, object_id, subject,date_type,
}) => ({
object, object_name, object_id, subject,date_type,
}))(curObj);
return {
...pick,
start_date: startM.format('YYYY-MM-DD'),
end_date: startM.endOf('M').format('YYYY-MM-DD HH:mm:ss'),
value: curObj[`M${mIndex}`],
// ...(curObj[`M${mIndex}_id`] ? { kpi_id: curObj[`M${mIndex}_id`] } : {}),
kpi_id: curObj[`M${mIndex}_id`] || undefined,
key: undefined,
};
});
console.log('cccccccccc', allMonth);
return r.concat(allMonth);
}, []);
console.log('ppppp', tableData);
const data = {
'kpis': tableData
// [
// {
// 'kpi_id': '1',
// 'object': 'sales',
// 'subject': 'orderCount',
// 'object_name': '刘莎',
// 'object_id': '15',
// 'date_type': 'confirmDate',
// 'start_date': '2024-01-01',
// 'end_date': '2024-03-31 23:59',
// 'value': '10000',
// },
// ],
};
// req.postJSON('/service-Analyse2/setkpi_multi/test', data).then((json) => {
req.postJSON('/service-Analyse2/setkpi_multi', data).then((json) => {
if (json.errcode === 0) {
runInAction(() => {
console.log({ loading: false, ...json }, 'post kpi');
});
}
});
async delByID(ids) {
const data = { 'kpi_ids': ids };
const json = await req.delJSON('/service-Analyse2/delkpi_multi', data);
return json.errcode === 0;
}
async saveOrUpdate(tableData) {
const data = { 'kpis': tableData };
const json = await req.postJSON('/service-Analyse2/setkpi_multi', data);
return json.errcode === 0;
}
async onSubmit(tableData, { delQueue }) {
const flushData = tableData.filter(row => !isEmpty(row.value) || !isEmpty(row?.kpi_id));
const postRes = isEmpty(flushData) ? true : await this.saveOrUpdate(flushData);
const delRes = isEmpty(flush(delQueue)) ? true : await this.delByID(delQueue);
return postRes && delRes;
}
getList() {
const param = {
date_type: 'confirmDate',
start_date: '2024-01-01',
end_date: '2024-12-01',
getList(param = {}) {
const _param = {
date_type: 'applyDate',
start_date: '2020-01-01',
end_date: '2024-12-31 23:59:59',
...param,
object_id: [0, -1, 'ALL'].includes(param?.object_id || 0) ? '' : param.object_id,
};
// req.fetchJSON('/service-Analyse2/getkpi/test', param).then((json) => {
req.fetchJSON('/service-Analyse2/getkpi', param).then((json) => {
this.listLoading = true;
this.pageData = [];
return req.fetchJSON('/service-Analyse2/getkpi', _param).then((json) => {
if (json.errcode === 0) {
runInAction(() => {
const result = json.result.map((row) => ({ ...row, yearIndex: moment(row.start_date).year(), monthIndex: moment(row.start_date).month() + 1 }));
const byYear = groupBy(result, (ele) => ele.yearIndex);
const yearData = {};
Object.keys(byYear).map((_yearVal) => {
const _objRet = groupBy(byYear[_yearVal], (ele) => `${ele.object_id}`);
Object.keys(_objRet).map((_obj) => {
_objRet[_obj] = _objRet[_obj].reduce((r, v) => ({ ...r, ...v, [`M${v.monthIndex}`]: v.value, [`M${v.monthIndex}_id`]: v.kpi_id }), {});
return _obj;
});
Object.assign(yearData, { [_yearVal]: Object.values(_objRet) });
return _yearVal;
});
console.log(111, yearData);
this.pageData = yearData[this.settingYear];
this.listLoading = false;
this.originData = json.result;
const yearData = parseKPI(json.result, ['subject', 'object_id']);
console.log(111, yearData, yearData[this.settingYear]);
this.pageData = yearData?.[this.settingYear]?.[this.settingSubject] || [];
});
}
return this.pageData;
});
}
handleTableEdit(data) {
console.log('handle change ', data);
this.pageData = data;
settingYear = moment().year();
setSettingYear(v) {
this.settingYear = v;
}
settingYear = 2024;
data = [];
objectData = [];
listLoading = false;
setListLoading(v) {
this.listLoading = v;
}
settingSubject = 'sum_profit';
setSettingSubject(v) {
this.settingSubject = v;
}
originData =[];
pageData = [];
}
/**
* 把kpi数据按照对象和类型转换格式
* @param {array} keyArr 转换的字段的顺序, 返回结果的key层级
* @example
* * parseKPI(json.result, ['object_id', 'subject']);
* // => { 2023: { 114: [{...}, {...}], 216: [...] } }
* * parseKPI(json.result, ['subject', 'object_id']);
* // => { 2023: { in_order_count: [{...}, {...}], sum_profit: [...] } }
*/
export const parseKPI = (kpis, keyArr = []) => {
const result = kpis.map((row) => ({
...row,
yearIndex: moment(row.start_date).year(),
monthIndex: moment(row.start_date).month() + 1,
monthRange: [moment(row.start_date).month() + 1, moment(row.end_date).month() + 1],
fullYear: moment(row.start_date).month() === 0 && moment(row.end_date).month() === 11,
}));
const byYear = groupBy(result, (ele) => ele.yearIndex);
const yearData = {};
const initialPercentKey = new Array(12).fill(1).reduce((r, v, i) => ({ ...r, [`M${i+1}Percent`]: 0 }), {});
const ret = {};
const [key0, key1] = keyArr;
Object.keys(byYear).map((_yearVal) => {
const _subjectRet = groupBy(byYear[_yearVal], (ele) => `${ele[key0]}`);
Object.keys(_subjectRet).map((_subject) => {
const subjectObject = groupBy(_subjectRet[_subject], (row) => row[key1]);
const afterGroup = Object.keys(subjectObject).map((oID) => {
const _ByFull = subjectObject[oID].reduce((r, v) => {
(r[String(v.fullYear)] || (r[String(v.fullYear)] = [])).push(v);
return r;
}, {});
const kpiYear = (_ByFull?.true || []).reduce((r, v) => {
// r.push({ monthIndex: v.monthIndex, yearIndex: v.yearIndex, value: v.value, kpi_id: v.kpi_id });
return { monthIndex: v.monthIndex, yearIndex: v.yearIndex, value: v.value, kpi_id: v.kpi_id };
}, {});
const kpiData = (_ByFull?.false || []).reduce((r, v) => {
r.push({ monthIndex: v.monthIndex, yearIndex: v.yearIndex, value: v.value, kpi_id: v.kpi_id, percentVal: (fixTo4Decimals(v.value/kpiYear.value)*100) });
return r;
}, []);
const kpiDataMapped = kpiData.reduce((r, v) => ({...r, [`M${v.monthIndex}`]: v }), {});
const kpiDataFlat = kpiData.reduce((r, v) => ({...r, [`M${v.monthIndex}Val`]: v.value, [`M${v.monthIndex}Percent`]: v.percentVal}), {});
const { start_date, end_date, kpi_id, value, unit, monthIndex, monthRange, ...objectEle } = subjectObject[oID][0];
const allKey = !isEmpty(kpiData) ? kpiData.map(ek => ek.kpi_id).join('_') : `${Object.values(kpiYear).join('_')}`;
return { ...cloneDeep(initialPercentKey), ...objectEle, ...kpiDataFlat, kpiData, kpiDataMapped, kpiYear, yearValue: kpiYear?.value || 0, key: allKey };
});
ret[_subject] = afterGroup;
return 1;
});
Object.assign(yearData, { [_yearVal]: ret });
return _yearVal;
});
return yearData;
};
export default KPI;

@ -1,4 +1,4 @@
import { makeAutoObservable, runInAction } from "mobx";
import { makeAutoObservable, runInAction, toJS } from "mobx";
import { CaretUpOutlined, CaretDownOutlined } from "@ant-design/icons";
import { Tag } from "antd";
import * as config from "../config";
@ -121,16 +121,46 @@ class OrdersStore {
(r[v.ApplyDate] || (r[v.ApplyDate] = [])).push(v);
return r;
}, {});
this.orderCountData = Object.keys(groupByDate)
const _data = Object.keys(groupByDate)
.reduce((r, _d) => {
const summaryVal = groupByDate[_d].reduce((rows, row) => rows + row.orderCount, 0);
r.push({ ...groupByDate[_d][0], orderCount: summaryVal });
const xAxisGroup = groupByDate[_d].reduce((a, v) => {
(a[v.groups] || (a[v.groups] = [])).push(v);
return a;
}, {});
Object.keys(xAxisGroup).map((_group) => {
const summaryVal = xAxisGroup[_group].reduce((rows, row) => rows + row.orderCount, 0);
r.push({ ...xAxisGroup[_group][0], orderCount: summaryVal });
return _group;
});
return r;
}, [])
.map((row) => ({ xField: row.ApplyDate, yField: row.orderCount, seriesField: row.groups }));
this.orderCountData = _data;
this.avgLine1 = avg1;
};
summaryAllWebcode = (json) => {
const groupByDate1 = json.ordercount1.reduce((r, v) => {
(r[v.ApplyDate] || (r[v.ApplyDate] = [])).push(v);
return r;
}, {});
const ordercount1 = Object.keys(groupByDate1).reduce((r, _date) => {
const summaryVal = groupByDate1[_date].reduce((rows, row) => rows + row.orderCount, 0);
r.push({ ...groupByDate1[_date][0], orderCount: summaryVal });
return r;
}, []);
const groupByDate2 = json.ordercount2.reduce((r, v) => {
(r[v.ApplyDate] || (r[v.ApplyDate] = [])).push(v);
return r;
}, {});
const ordercount2 = Object.keys(groupByDate2).reduce((r, _date) => {
const summaryVal = groupByDate2[_date].reduce((rows, row) => rows + row.orderCount, 0);
r.push({ ...groupByDate2[_date][0], orderCount: summaryVal });
return r;
}, []);
return { ...json, ordercount1, ordercount2 };
};
parseOrderCount = (orderCountData, dateGroup) => {
resultDataCb(orderCountData, dateGroup, this.orderCountDataMapper, this.orderCountDataFieldMapper, this.onChangeDateGroup);
};
@ -175,9 +205,10 @@ class OrdersStore {
.then(response => response.json())
.then(json => {
runInAction(() => {
this.orderCountDataRaw = json;
const data = this.summaryAllWebcode(json);
this.orderCountDataRaw = data;
// 第一次得到数据
this.parseOrderCount(json, 'day');
this.parseOrderCount(data, 'day');
this.loading = false;
});
})

@ -1,13 +1,7 @@
import { makeAutoObservable, runInAction } from 'mobx';
import { makeAutoObservable, runInAction, toJS } from 'mobx';
import * as req from '../utils/request';
import { isEmpty, sortBy } from '../utils/commons';
/**
* 计算变化值
*/
const calcRate = (r1, r2) => {
//
};
import { isEmpty, sortBy, pick } from '../utils/commons';
import { dataFieldAlias } from './../libs/ht';
class Trade {
constructor(rootStore) {
@ -15,71 +9,154 @@ class Trade {
makeAutoObservable(this);
}
fetchSummaryData() {
/**
* 年度总额
*/
fetchSummaryData(queryData) {
this.summaryData.loading = true;
req.fetchJSON('/service-web/QueryData/GetTradeSummary').then((json) => {
queryData.groupType = 'overview';
// queryData.groupDateType = 'year';
this.fetchTradeData(queryData).then((json) => {
if (json.errcode === 0) {
runInAction(() => {
const summary = json.result1?.[0] || {};
const summaryData = {
loading: false,
dataSource: [
{ title: '成团', value: json.result1?.SumOrder, VSrate: json.result1?.SumOrderrate, KPIrate: json.result1?.SumOrderKPIrate, hasKPI: !isEmpty(json.result1?.SumOrderKPIrate) },
{ title: '毛利', value: json.result1?.SumML, VSrate: json.result1?.SumMLrate, KPIrate: json.result1?.SumMLKPIrate, hasKPI: !isEmpty(json.result1?.SumMLKPIrate) },
{ title: '完成率', value: `${json.result1?.SumMLKPIrate}%`, hasKPI: false },
{
title: '成团',
value: summary?.ConfirmOrder,
valueSuffix: summary?.ConfirmRates ? ` / ${summary.ConfirmRates} %` : undefined,
// VSrate: summary?.ConfirmOrderrate,
KPIrate: summary?.[dataFieldAlias.ConfirmOrder.nestkey.p],
// hasKPI: !isEmpty(summary?.[dataFieldAlias.ConfirmOrder.nestkey.p]),
hasKPI: false
},
{ title: '毛利', value: summary?.SumML, KPIrate: summary?.[dataFieldAlias.SumML.nestkey.p], hasKPI: false },
{ title: '完成率', value: `${summary?.[dataFieldAlias.SumML.nestkey.p] || ''}%`, hasKPI: false },
{
title: '人数',
value: json.result1?.SumPersonNum,
VSrate: json.result1?.SumPersonNumrate,
KPIrate: json.result1?.SumPersonNumKPIrate,
hasKPI: !isEmpty(json.result1?.SumPersonNumKPIrate),
value: summary?.SumPersonNum,
// VSrate: summary?.SumPersonNumrate,
// KPIrate: summary?.[dataFieldAlias.SumPersonNum.nestkey.p],
hasKPI: false, // // !isEmpty(summary?.[dataFieldAlias.SumPersonNum.nestkey.p]),
},
],
};
this.summaryData = summaryData;
const kpi = { label: '', value: summary.MLKPIvalue };
this.summaryData.kpi = kpi;
});
}
});
}
fetchTradeDataByMonth() {
/**
* 时间轴
*/
fetchTradeDataByDate(queryData) {
this.timeData.loading = true;
queryData = queryData || this.searchPayloadHome;
Object.assign(queryData, { groupType: 'overview', groupDateType: this.timeLineKey });
this.fetchTradeData(queryData).then((json) => {
if (json.errcode === 0) {
runInAction(() => {
const data = json.result1;
// 标注KPI
this.timeData.loading = false;
this.timeData.dataSource = data;
});
}
});
}
/**
* 事业部年度
*/
fetchTradeDataByBU(queryData) {
this.BuData.loading = true;
Object.assign(queryData, { groupType: 'bu', groupDateType: 'year' });
this.fetchTradeData(queryData).then((json) => {
if (json.errcode === 0) {
runInAction(() => {
const data = json.result1;
// 标注KPI
this.BuData.loading = false;
this.BuData.dataSource = data;
});
}
});
}
/**
* 业务区域, 按月
*/
fetchTradeDataByMonth(queryData) {
this.sideData.loading = true;
req.fetchJSON('/service-web/QueryData/GetTradeProcess').then((json) => {
// req.fetchJSON('/service-web/QueryData/GetTradeByMonth').then((json) => {
Object.assign(queryData, { groupType: 'bizarea', groupDateType: 'month' });
this.fetchTradeData(queryData).then((json) => {
if (json.errcode === 0) {
runInAction(() => {
const sortResult = json.result1.sort(sortBy('groupDateVal'));
const groupsData = sortResult.reduce((r, v) => {
(r[v.groups] || (r[v.groups] = [])).push(v);
if (v.groupsLabel ) { // && ['91001', '91006'].includes(v.groupsKey)
(r[v.groupsLabel] || (r[v.groupsLabel] = [])).push(v);
}
return r;
}, {});
console.log(groupsData, 'groupsData');
const kpi = { label: '', value: 1200000 }; // 标注KPI
this.sideData.loading = false;
this.sideData.dataSource = groupsData;
this.sideData.monthData = sortResult;
this.sideData.kpi = kpi;
});
}
});
}
fetchTradeDataByType(orderType) {
this.topData[orderType] = { loading: true, dataSource: [] };
req.fetchJSON('/service-web/QueryData/GetTradeOrderByType').then((json) => {
/**
* TOP
*/
fetchTradeDataByType(orderType, queryData) {
this.topData[orderType] = { loading: true, dataSource: [], originData: [] };
Object.assign(queryData, { groupType: orderType, groupDateType: 'year' });
this.fetchTradeData(queryData).then((json) => {
if (json.errcode === 0) {
runInAction(() => {
this.topData[orderType].loading = false;
this.topData[orderType].dataSource = json.result1;
console.log({ loading: false, ...json }, orderType, 'topData');
});
}
});
}
summaryData = { loading: false, dataSource: [] };
sideData = { loading: false, dataSource: {}, kpi: {}, monthData: [] };
/**
* 获取业绩数据
*/
async fetchTradeData(queryData) {
const json = await req.fetchJSON('/service-Analyse2/GetTradeProcess', queryData);
if (json.errcode === 0) {
return json;
}
return null;
}
setStateSearch(body) {
this.searchPayloadHome = body;
}
timeLineKey = 'week';
setTimeLineKey(v) {
this.timeLineKey = v;
}
summaryData = { loading: false, dataSource: [], kpi: {}, };
timeData = { loading: false, dataSource: [] };
BuData = { loading: false, dataSource: [] };
sideData = { loading: false, dataSource: {}, monthData: [] };
dataForSort = {};
topData = {};
defaultDataSubject = 'CJCount';
searchPayloadHome = {};
}
export default Trade;

@ -319,8 +319,7 @@ export function merge(...objects) {
/**
* 数组分组
* - 相当于 lodash _.groupBy
*
* https://www.lodashjs.com/docs/lodash.groupBy#_groupbycollection-iteratee_identity
* @see https://www.lodashjs.com/docs/lodash.groupBy#_groupbycollection-iteratee_identity
*/
export function groupBy(array, callback) {
return array.reduce((groups, item) => {
@ -334,3 +333,176 @@ export function groupBy(array, callback) {
return groups;
}, {});
}
/**
* 创建一个从 object 中选中的属性的对象
* @param {*} object
* @param {array} keys
*/
export function pick(object, keys) {
return keys.reduce((obj, key) => {
if (object && Object.prototype.hasOwnProperty.call(object, key)) {
obj[key] = object[key];
}
return obj;
}, {});
}
/**
* 深拷贝
*/
export function cloneDeep(value) {
if (typeof value !== 'object' || value === null) {
return value;
}
const result = Array.isArray(value) ? [] : {};
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
result[key] = cloneDeep(value[key]);
}
}
return result;
}
/**
* 向零四舍五入, 固定精度设置
*/
function curriedFix(precision = 0) {
return function(number) {
// Shift number by precision places
const shift = Math.pow(10, precision);
const shiftedNumber = number * shift;
// Round to nearest integer
const roundedNumber = Math.round(shiftedNumber);
// Shift back decimal place
return roundedNumber / shift;
};
}
/**
* 向零四舍五入, 保留2位小数
*/
export const fixTo2Decimals = curriedFix(2);
/**
* 向零四舍五入, 保留4位小数
*/
export const fixTo4Decimals = curriedFix(4);
export const fixToInt = curriedFix(0);
/**
* 映射
* @example
* const keyMap = {
a: [{key: 'a1'}, {key: 'a2', transform: v => v * 2}],
b: {key: 'b1'}
};
const result = objectMapper({a: 1, b: 3}, keyMap);
// result = {a1: 1, a2: 2, b1: 3}
*
*/
export function objectMapper(input, keyMap) {
// Loop through array mapping
if (Array.isArray(input)) {
return input.map((obj) => objectMapper(obj, keyMap));
}
if (typeof input === 'object') {
const mappedObj = {};
Object.keys(input).forEach((key) => {
// Keep original keys not in keyMap
if (!keyMap[key]) {
mappedObj[key] = input[key];
}
// Handle array of maps
if (Array.isArray(keyMap[key])) {
keyMap[key].forEach((map) => {
let value = input[key];
if (map.transform) value = map.transform(value);
mappedObj[map.key] = value;
});
// Handle single map
} else {
const map = keyMap[key];
if (map) {
let value = input[key];
if (map.transform) value = map.transform(value);
mappedObj[map.key || key] = value;
}
}
});
return mappedObj;
}
return input;
}
/**
* 创建一个对应于对象路径的值数组
*/
export function at(obj, path) {
let result;
if (Array.isArray(obj)) {
// array case
const indexes = path.split('.').map((i) => parseInt(i));
result = [];
for (let i = 0; i < indexes.length; i++) {
result.push(obj[indexes[i]]);
}
} else {
// object case
const indexes = path.split('.').map((i) => i);
result = [obj];
for (let i = 0; i < indexes.length; i++) {
result = [result[0][indexes[i]]];
}
}
return result;
}
/**
* 删除 null/undefined
*/
export function flush(collection) {
let result, len, i;
if (!collection) {
return undefined;
}
if (Array.isArray(collection)) {
result = [];
len = collection.length;
for (i = 0; i < len; i++) {
const elem = collection[i];
if (elem != null) {
result.push(elem);
}
}
return result;
}
if (typeof collection === 'object') {
result = {};
const keys = Object.keys(collection);
len = keys.length;
for (i = 0; i < len; i++) {
const key = keys[i];
const value = collection[key];
if (value != null) {
result[key] = value;
}
}
return result;
}
return undefined;
}
/**
* 千分位 格式化数字
*/
export const numberFormatter = (number) => {
return new Intl.NumberFormat().format(number);
};

@ -78,3 +78,18 @@ export function postStream(url, obj) {
throw error;
});
}
export function delJSON(url, obj) {
const host = /^https?:\/\//i.test(url) ? '': HT_HOST;
return fetch(`${host}${url}`, {
method: 'DELETE',
body: JSON.stringify(obj),
headers: {
'Content-type': 'application/json; charset=UTF-8'
}
}).then(checkStatus)
.then(response => response.json())
.catch(error => {
throw error;
});
}

@ -0,0 +1,118 @@
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 { RingProgress } from '@ant-design/plots';
import SearchForm from './../components/search/SearchForm';
import "./kpi.css";
import { empty } from '../utils/commons';
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: '海外目的地', },
];
export default observer((props) => {
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'),
...(obj || formValuesToSub),
});
};
useEffect(() => {
if (empty(DistributionStore[curTab].dataSource)) {
pageRefresh();
}
}, [curTab]);
useEffect(() => {
DistributionStore.resetData();
return () => {};
}, [formValuesToSub]);
const onTabsChange = (tab) => {
DistributionStore.setCurTab(tab);
};
const RingProgressConfig = {
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} />},
];
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'],
fieldProps: {
DepartmentList: { show_all: true },
WebCode: { show_all: true },
years: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
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>
</>
);
});

@ -1,42 +1,54 @@
import { useContext, useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { Row, Col, Spin, Space } from 'antd';
import { Row, Col, Spin, Space, Radio } from 'antd';
import { CheckCircleTwoTone, MoneyCollectTwoTone, FlagTwoTone, SmileTwoTone, } from '@ant-design/icons';
import { stores_Context } from '../config';
import { useNavigate } from 'react-router-dom';
// import { SlackOutlined, SketchOutlined, AntCloudOutlined, RedditOutlined, GithubOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
import StatisticCard from '../components/StatisticCard';
import Bullet from '../components/Bullet';
import Bullet from '../components/BulletWithSort';
import Waterfall from '../components/Waterfall';
import Column from '../components/Column';
import DataFieldRadio from './../components/DateFieldRadio';
import DatePickerCharts from './../components/search/DatePickerCharts';
import { empty } from './../utils/commons';
import DataFieldRadio from '../components/DataFieldRadio';
import { datePartOptions } from './../components/DateGroupRadio/date';
import SearchForm from './../components/search/SearchForm';
import { empty, cloneDeep, isEmpty } from './../utils/commons';
import { dataFieldAlias } from './../libs/ht';
import { Line } from '@ant-design/charts';
import './home.css';
export default observer(() => {
const navigate = useNavigate();
const { TradeStore } = useContext(stores_Context);
const { sideData, summaryData, topData } = TradeStore;
const topSeries = [
{ key: 'country', label: '国籍' },
{ key: 'dept', label: '小组' },
{ key: 'operator', label: '顾问' },
{ key: 'destination', label: '目的地' },
// { key: 'GuestGroupType', label: '' },
];
const topSeries = [
{ key: 'Country', label: '国籍' },
{ key: 'Area', label: '目的地' },
{ key: 'Sales', label: '顾问' },
{ key: 'GuestGroupType', label: '客群类别' },
];
// const iconSets = [CheckCircleTwoTone, <MoneyCollectTwoTone />, <FlagTwoTone />, <ClockCircleTwoTone />, <DashboardTwoTone />,<SmileTwoTone />,];
const iconSets = [CheckCircleTwoTone, MoneyCollectTwoTone, FlagTwoTone, SmileTwoTone];
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;
useEffect(() => {
if (empty(summaryData.dataSource)) {
TradeStore.fetchSummaryData();
TradeStore.fetchTradeDataByMonth();
for (const iterator of topSeries) {
TradeStore.fetchTradeDataByType(iterator.key);
}
// pageRefresh();
}
return () => {};
}, []);
const pageRefresh = (queryData) => {
TradeStore.fetchSummaryData(queryData);
TradeStore.fetchTradeDataByDate(queryData);
TradeStore.fetchTradeDataByBU(queryData);
TradeStore.fetchTradeDataByMonth(queryData);
for (const iterator of topSeries) {
TradeStore.fetchTradeDataByType(iterator.key, queryData);
}
};
const layoutProps = {
gutter: { xs: 8, sm: 8, lg: 16 },
lg: { span: 6 },
@ -55,82 +67,157 @@ export default observer(() => {
const [BulletConfig, setBulletConfig] = useState({
measureField: 'SumML', //
rangeField: 'SumMLRange', //
targetField: 'SumMLKPI', //
xField: 'OrderType',
targetField: 'MLKPIvalue', //
xField: 'groupsLabel',
});
const handleChangeValueKey = (key) => {
setValueKey(key);
setBulletConfig({
measureField: key,
rangeField: `${key}Range`,
targetField: `${key}KPI`,
xField: 'OrderType',
// targetField: `${key}KPI`,
targetField: dataFieldAlias[key].nestkey.v,
xField: 'groupsLabel',
});
};
const WaterfallConfig = {
xField: 'groupDateVal',
yField: 'SumML',
seriesField: 'groupsLabel',
meta: {
groupDateVal: {
alias: '月份',
// type: 'cat',
},
},
label: {
formatter: (v) => ((v.SumML / sideData.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) + '%',
},
};
const ColumnConfig = {
const BUConfig = {
measureField: 'SumML', //
rangeField: 'SumMLRange', //
targetField: 'MLKPIvalue', //
xField: 'groupsLabel',
layout: 'vertical',
xAxis: {
line: null,
label: {
autoHide: true,
autoRotate: false,
},
}
};
const lineConfigSet = {
// data: orders_data.data,
padding: 'auto',
xField: 'groupDateVal',
yField: 'SumML',
seriesField: 'groups',
label: {
formatter: (v) => ((v.SumML / sideData.kpi.value) * 100).toFixed(2) + '%',
seriesField: 'groupsLabel',
xAxis: {
type: 'cat',
},
smooth: true,
point: {
size: 4,
shape: "cicle",
},
legend: false,
// annotations: sideData.monthData.map((d, ...args) => {
// console.log('aaa', d, args);
// return {
// type: 'dataMarker',
// position: d,
// point: {
// style: {
// stroke: '#FF6B3B',
// lineWidth: 1.5,
// },
// },
// };
// }),
meta: { ...cloneDeep(dataFieldAlias)
// [extProps.yField]: {
// alias: dataFieldAlias[extProps.yField]?.alias || extProps.yField,
// formatter: (v) => dataFieldAlias[extProps.yField]?.formatter(v) || v,
// max: Math.ceil(yMax / 0.95),
// },
},
};
const [timeDataField, setTimeDataField] = useState('SumML');
const [lineConfig, setLineConfig] = useState(cloneDeep(lineConfigSet));
const handleChangetimeDataField = (key) => {
setTimeDataField(key);
setLineConfig({
...lineConfig,
yField: key,
tooltip: {
customItems: (originalItems) => {
// process originalItems,
const items = originalItems.map((ele) => ({ ...ele, name: dataFieldAlias[key]?.alias || key }));
return items;
},
},
});
};
const [dateField, setDateField] = useState(timeLineKey);
const handleChangeDateType = ({target: {value}}) => {
setDateField(value);
TradeStore.setTimeLineKey(value);
if (!isEmpty(TradeStore.searchPayloadHome)) {
TradeStore.fetchTradeDataByDate();
}
};
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'],
fieldProps: {
DepartmentList: { show_all: true },
WebCode: { show_all: true },
years: { hide_vs: true },
},
}}
onSubmit={(_err, obj, form, str) => {
TradeStore.setStateSearch(obj);
pageRefresh(obj);
}}
/>
</Col>
</Row>
<section>
<Space>
<h2>年度业绩</h2>
<DatePickerCharts hide_vs={true} />
</Space>
<Spin spinning={summaryData.loading}>
<Row gutter={layoutProps.gutter}>
{summaryData.dataSource.map((item) => (
{summaryData.dataSource.map((item, i) => (
<Col {...layoutProps} key={item.title}>
<StatisticCard {...item} showProgress={item.hasKPI} />
<StatisticCard {...item} showProgress={item.hasKPI} icon={iconSets[i]} />
</Col>
))}
</Row>
</Spin>
</section>
<section>
<Space gutter={16} size={'large'}>
<h3>走势</h3>
<DataFieldRadio value={timeDataField} onChange={handleChangetimeDataField} />
<Radio.Group options={datePartOptions} optionType="button" onChange={handleChangeDateType} value={dateField} />
</Space>
<Spin spinning={timeData.loading}>
<Line {...lineConfig} data={timeData.dataSource} />
</Spin>
</section>
<section>
<h3>市场进度</h3>
<Spin spinning={sideData.loading}>
<Spin spinning={BuData.loading}>
<Row gutter={layoutProps3.gutter}>
<Col {...layoutProps3}>
<Column {...ColumnConfig} dataSource={sideData.monthData} line={sideData.kpi} />
<Bullet {...BUConfig} dataSource={BuData?.dataSource || []} />
<h3 style={{ textAlign: 'center' }}>{`各事业部总业绩`}</h3>
</Col>
{Object.keys(sideData.dataSource).map((key) => (
<Col {...layoutProps3} key={key}>
<Waterfall {...WaterfallConfig} title={key} dataSource={sideData.dataSource[key]} line={sideData.kpi} />
<Waterfall key={key} {...WaterfallConfig} title={key} dataSource={sideData.dataSource[key]} line={summaryData.kpi} />
<h3 style={{ textAlign: 'center' }}>{`${key}每月业绩`}</h3>
</Col>
))}
</Row>
@ -146,9 +233,9 @@ export default observer(() => {
<Row gutter={layoutProps.gutter}>
{topSeries.map((item) => (
<Col {...layoutProps} key={item.key}>
<Spin spinning={topData[item.key]?.loading}>
<Spin spinning={topData[item.key]?.loading || false}>
<h3 style={{ textAlign: 'center' }}>{item.label}</h3>
<Bullet {...BulletConfig} dataSource={topData[item.key]?.dataSource || []} />
<Bullet {...BulletConfig} dataSource={topData[item.key]?.dataSource || []} itemLength={10} />
</Spin>
</Col>
))}

@ -1,156 +1,94 @@
import { useContext, useEffect, useState } from 'react';
import { stores_Context } from './../config';
import { observer } from 'mobx-react';
// import type { ProColumns } from '@ant-design/pro-components';
import { EditableProTable, ProCard, ProFormField } from '@ant-design/pro-components';
import { Button, Table, Switch } from 'antd';
import { stores_Context } from '../config';
import { isEmpty } from './../utils/commons';
import { Row, Col, Tabs, Spin } from 'antd';
import { flush, objectMapper } from './../utils/commons';
import { KPIObjects } from './../libs/ht';
import ObjectPanel from '../components/kpi/ObjectPanel';
import OverviewPanel from './../components/kpi/OverviewPanel';
import './kpi.css';
const objectComponents = {
'overview': OverviewPanel,
'bu': ObjectPanel,
'dept': ObjectPanel,
'du': ObjectPanel,
'operator': ObjectPanel,
'destination': ObjectPanel,
'country': ObjectPanel,
};
const objectFilterKey = {
'bu': 'HTBusinessUnits',
'dept': 'DepartmentList',
// 'du': 'du',
// 'operator': 'operator',
// 'destination': 'destination',
// 'country': 'country',
};
export default observer((props) => {
const { KPIStore } = useContext(stores_Context);
const { settingYear, pageData, } = KPIStore;
const { KPIStore, DictDataStore, date_picker_store: searchFormStore } = useContext(stores_Context);
// useEffect(() => {
// return () => {};
// }, []);
const [curObject, setCurObject] = useState('overview');
const onObjectChange = (object) => {
setCurObject(object);
setRetObjects([]);
};
useEffect(() => {
// KPIStore.saveOrUpdate();
KPIStore.getList();
onSearchSubmit(searchFormStore.formValuesToSub);
return () => {};
}, []);
const [editOpen, setEditOpen] = useState(false); // test:
const [editableRowsKeys, setEditableRowKeys] = useState([]);
const monthCol = new Array(12).fill(1).map((_, index) => {
return {
title: `${index + 1}`,
dataIndex: `M${index + 1}`,
valueType: 'percent',
width: '5em',
formItemProps: {
initialValue: [8, 9].includes(index) ? 10 : 8,
},
fieldProps: { min: 0, max: 100, style: { width: '4em' } },
};
});
const initialRow = monthCol.reduce((r, v) => ({...r, [v.dataIndex]: v.formItemProps.initialValue}), {});
const columns = [
{
title: '对象',
dataIndex: 'object_name',
valueType: 'select',
// ...valueEnum
},
// {
// title: 'Name',
// dataIndex: 'title',
// //...form rules
// formItemProps: {
// rules: [
// {
// required: true,
// whitespace: true,
// message: '',
// },
// ],
// },
// },
{
title: '目标',
dataIndex: 'value',
valueType: 'digit',
fieldProps: { style: { width: '100%' } },
formItemProps: {
style: { width: '100%' },
},
},
...monthCol,
{
title: '完成进度',
dataIndex: 'place',
valueType: 'percent',
editable: false,
width: '6em',
},
{
title: '操作',
valueType: 'option',
// width: 250,
render: () => {
return null;
},
},
];
const onChange = (...argrs) => {
console.log(argrs, 'who who who');
setEditableRowKeys(argrs[0].map((ele) => ele.key));
// KPIStore.setEditableRowsKeys(argrs[0].map((ele) => ele.key));
KPIStore.handleTableEdit(argrs[0]);
}, [curObject]);
const [retObjects, setRetObjects] = useState([]);
const onSearchSubmit = (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' },
businessUnits: { key: 'object_id' },
WebCode: { key: 'object_id' },
operator: { key: 'object_id' },
country: { key: 'object_id' },
});
Object.assign(getkpiParam, { object: curObject });
KPIStore.setSettingYear(formVal?.year?.year() || KPIStore.settingYear);
console.log('invoke on search', obj, formVal, getkpiParam);
KPIStore.getList(getkpiParam).then((data) => {
// setDataSource(data);
if (objectFilterKey?.[curObject]) {
const selectItem = searchFormStore.formValues[objectFilterKey[curObject]];
if (selectItem) {
selectItem.value = selectItem.key;
}
setRetObjects(flush([selectItem]));
}
});
};
return (
<>
<EditableProTable
key={settingYear}
headerTitle={settingYear}
columns={columns}
rowKey="key"
scroll={{
x: 1000,
}}
value={pageData}
onChange={onChange}
recordCreatorProps={
editOpen
? {
newRecordType: 'dataSource',
record: () => ({
key: pageData.length+1, // Number(Date.now().toString()),
...initialRow,
object_name: '',
value:0
}),
}
: false
}
toolBarRender={() => {
return [
<Switch
unCheckedChildren="查看"
checkedChildren="编辑"
key={'openEdit'}
// defaultChecked={true}
checked={editOpen}
onChange={(e) => {
setEditOpen(e);
setEditableRowKeys(e ? pageData.map((ele) => ele.key) : []);
// KPIStore.setEditableRowsKeys(e ? pageData.map((ele) => ele.key) : []);
}}
/>,
<Button
disabled={!editOpen}
type="primary"
key="save"
onClick={() => {
// dataSource api
KPIStore.saveOrUpdate();
}}
>
保存数据
</Button>,
];
}}
editable={{
type: 'multiple',
editableKeys: editableRowsKeys,
actionRender: (row, config, defaultDoms) => {
// console.log(row, config, defaultDoms);
return [defaultDoms.delete];
},
onValuesChange: (record, recordList) => {
console.log('on edit');
onChange(recordList);
},
onChange: (editableKeys, editableRows) => {
onChange(editableRows);
// KPIStore.setEditableRowsKeys()
}
}}
/>
<Row>
<Col span={24}>
<Tabs
onChange={onObjectChange}
type="card"
items={KPIObjects.map((ele, i) => {
const ObjectItemPanel = objectComponents[ele.key];
return {
...ele,
children: (
<Spin spinning={KPIStore.listLoading}>
<ObjectItemPanel title={ele.label} {...{ curObject, onSearchSubmit, objects: retObjects }} />
</Spin>
),
};
})}
/>
</Col>
</Row>
</>
);
});

@ -272,7 +272,7 @@ class Orders extends Component {
tooltip: {
customItems: (originalItems) => {
// process originalItems,
return originalItems.map(ele => ({...ele, name: ele.data?.seriesKey || ele.data?.xField}));
return originalItems.map(ele => ({...ele, name: ele.data?.seriesField || ele.data?.xField}));
},
title: (title, datum) => {
let ret = title;

@ -0,0 +1,17 @@
.ant-tabs.ant-tabs-card > .ant-tabs-nav {
margin-bottom: 0;
}
.ant-tabs.ant-tabs-left .ant-tabs-content-holder{
padding: 0;
}
.ant-tabs-content.ant-tabs-content-left .ant-tabs-tabpane.ant-tabs-tabpane-active{
padding-left: 0;
}
.ant-tabs-content.ant-tabs-content-left .ant-tabs-tabpane .ant-pro-table .ant-pro-card-body{
padding: 0;
}
.ant-form-item-control-input .ant-input-affix-wrapper{
padding-left: 4px;
padding-right: 4px;
}
Loading…
Cancel
Save