Merge remote-tracking branch 'origin/feature/2.0'
commit
e5ac09e030
@ -0,0 +1,53 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import { Line } from '@ant-design/plots';
|
||||
import { merge, isEmpty } from '../utils/commons';
|
||||
import { dataFieldAlias } from '../libs/ht';
|
||||
|
||||
const uniqueByKey = (array, key, pickLast) => {
|
||||
const seen = new Map();
|
||||
const isPickLast = pickLast === true;
|
||||
|
||||
return array.filter(item => {
|
||||
const k = item[key];
|
||||
const storedItem = seen.get(k);
|
||||
|
||||
if(storedItem) {
|
||||
if(isPickLast) {
|
||||
seen.set(k, item); // update with last item
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
seen.set(k, item);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
export default observer((props) => {
|
||||
const { config, dataSource, ...extProps } = props;
|
||||
const kpiKey = dataFieldAlias[config.yField]?.nestkey?.v;
|
||||
const _data = dataSource.reduce((r, v) => {
|
||||
r.push(v);
|
||||
if ( ! isEmpty(v[kpiKey])) { // 有设目标才多显示一条虚线 颜色: #F4664A
|
||||
r.push({...v, [config.yField]: v[kpiKey], [config.seriesField]: dataFieldAlias[kpiKey].label, extraLine: true,});
|
||||
}
|
||||
return r;
|
||||
}, []);
|
||||
const mergeLineConfig = merge({
|
||||
color: ['#598cf3', '#F4664A', '#FAAD14'],
|
||||
lineStyle: (data) => {
|
||||
// console.log(data);
|
||||
if (data[config.seriesField].includes('目标')) {
|
||||
return {
|
||||
lineDash: [4, 4],
|
||||
opacity: 0.5,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
opacity: 1,
|
||||
};
|
||||
},
|
||||
}, config);
|
||||
return <Line {...mergeLineConfig} data={_data} />;
|
||||
});
|
@ -0,0 +1,42 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { sortBy, merge } from '../utils/commons';
|
||||
import { dataFieldAlias } from '../libs/ht';
|
||||
import { Mix, Scatter } from '@ant-design/plots';
|
||||
|
||||
export default observer((props) => {
|
||||
const { dataSource, ...extProps } = props;
|
||||
const config = merge(
|
||||
{
|
||||
appendPadding: 10,
|
||||
// xField: 'Revenue (Millions)',
|
||||
// yField: 'Rating',
|
||||
shape: 'circle',
|
||||
// colorField: 'Genre',
|
||||
size: 4,
|
||||
yAxis: {
|
||||
nice: true,
|
||||
line: {
|
||||
style: {
|
||||
stroke: '#aaa',
|
||||
},
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
min: -100,
|
||||
grid: {
|
||||
line: {
|
||||
style: {
|
||||
stroke: '#eee',
|
||||
},
|
||||
},
|
||||
},
|
||||
line: {
|
||||
style: {
|
||||
stroke: '#aaa',
|
||||
},
|
||||
},
|
||||
},
|
||||
}, extProps);
|
||||
return <Scatter {...config} data={dataSource} />;
|
||||
});
|
@ -1,23 +1,22 @@
|
||||
import { useContext } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
// import { stores_Context } from '../config';
|
||||
import { Table } from 'antd';
|
||||
import KPISettings from './KPISettings';
|
||||
import { bu, KPISubjects } from '../../libs/ht';
|
||||
|
||||
const sort = { DateType: 10, years: 11 };
|
||||
const yearInitial = {};
|
||||
const searchFormItemSet = {
|
||||
'bu': { shows: ['DateType', 'years', 'HTBusinessUnits'] },
|
||||
'dept': { shows: ['DateType', 'years', 'DepartmentList'], fieldProps: { DepartmentList: { allowClear: true } } },
|
||||
'operator': { shows: ['DateType', 'years', 'DepartmentList'] }, // , 'operator'
|
||||
'destination': { shows: ['DateType', 'years', 'destination'] },
|
||||
'country': { shows: ['DateType', 'years', 'country'] },
|
||||
'bu': { shows: ['DateType', 'years', 'HTBusinessUnits'], sort },
|
||||
'dept': { shows: ['DateType', 'years', 'DepartmentList'], sort, fieldProps: { DepartmentList: { allowClear: true,isLeaf: true, show_all: false } }, },
|
||||
'operator': { shows: ['DateType', 'years', 'DepartmentList', 'operator'], fieldProps: { DepartmentList: { allowClear: true, isLeaf: true }, operator: { param: { is_assign: 1 } } }, sort },
|
||||
'destination': { shows: ['DateType', 'years', 'destination'], sort },
|
||||
'country': { shows: ['DateType', 'years', 'country'], sort },
|
||||
};
|
||||
|
||||
export default observer((props) => {
|
||||
const searchProps = searchFormItemSet?.[props.curObject] || {};
|
||||
return (
|
||||
<>
|
||||
<KPISettings {...{ searchProps, objects: bu, KPISubjects }} {...props} />
|
||||
<KPISettings {...{ searchProps, KPISubjects }} {...props} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -0,0 +1,263 @@
|
||||
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 = 1) => {
|
||||
return fixTo2Decimals(number / (1000 * scale)) + 'K';
|
||||
};
|
||||
|
||||
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);
|
||||
setEditableRowKeys([]);
|
||||
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 * 1 },
|
||||
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,
|
||||
group_date_type: 'month',
|
||||
};
|
||||
});
|
||||
const yearRow = (({ object, object_name, object_id, subject, date_type, yearValue, kpiYear }) => ({
|
||||
object,
|
||||
object_name,
|
||||
object_id,
|
||||
subject,
|
||||
date_type,
|
||||
value: yearValue,
|
||||
start_date: moment([KPIStore.settingYear, 0, 1]).format('YYYY-MM-DD'),
|
||||
end_date: moment([KPIStore.settingYear, 11, 1]).endOf('M').format('YYYY-MM-DD HH:mm'),
|
||||
kpi_id: kpiYear?.kpi_id || undefined,
|
||||
group_date_type: 'year',
|
||||
}))(curObj);
|
||||
return r.concat(allMonth, yearRow);
|
||||
}, []);
|
||||
// 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 * 1,
|
||||
object: curObject,
|
||||
object_name: '',
|
||||
object_id: -1,
|
||||
subject: KPIStore.settingSubject,
|
||||
date_type: searchFormStore.formValuesToSub.DateType,
|
||||
kpiDataMapped: {},
|
||||
key: Date.now().toString(32),
|
||||
group_date_type: 'month',
|
||||
}),
|
||||
{}
|
||||
); // v.formItemProps.initialValue
|
||||
const makeInitialTable = (e) => {
|
||||
setEditOpen(e);
|
||||
// test: 单独设置之后, 清空筛选会导致无法批量设置新的
|
||||
const _initialRow = Object.assign({}, initialRow, initialPercentKey);
|
||||
const _objects = isEmpty(objects) ? curObjectItem?.data || [] : objects;
|
||||
const _initialTable = _objects.map((obj) => ({
|
||||
...cloneDeep(_initialRow),
|
||||
object_name: obj.label,
|
||||
object_id: obj.value,
|
||||
key: Date.now().toString(32) + obj.value,
|
||||
}));
|
||||
const mergePageData = Object.values(
|
||||
Object.assign(
|
||||
{},
|
||||
_initialTable.reduce((r, v) => ({ ...r, [v.object_name]: v }), {}),
|
||||
dataSource.reduce((r, v) => ({ ...r, [v.object_name]: v }), {})
|
||||
)
|
||||
);
|
||||
if (e && isEmpty(dataSource)) {
|
||||
setDataSource(_initialTable);
|
||||
setEditableRowKeys(_initialTable.map((ele) => ele.key));
|
||||
return false;
|
||||
}
|
||||
setDataSource(mergePageData);
|
||||
setEditableRowKeys(e ? mergePageData.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) => {
|
||||
const rowKpiIds = (_row?.kpiData || []).map((ele) => ele.kpi_id);
|
||||
rowKpiIds.push(_row?.kpiYear?.kpi_id);
|
||||
setDelKpiIds(rowKpiIds);
|
||||
},
|
||||
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}>
|
||||
<Space size={'large'}>
|
||||
<Button className="mt-1 mb-1 align_center" disabled={!editOpen} type="primary" key="save" onClick={onTableSubmit}>
|
||||
保存数据
|
||||
</Button>
|
||||
{!editOpen && <Button className="mt-1 mb-1 align_center" disabled={false} type={'ghost'} key="initTable" onClick={() => {makeInitialTable(true);}}>
|
||||
编辑设置
|
||||
</Button>}
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
});
|
@ -0,0 +1,263 @@
|
||||
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}Val`]: [8, 9].includes(i) ? 10 : 8 }), {});
|
||||
const numberConvert10K = (number, scale = 1) => {
|
||||
return fixTo2Decimals(number / (1000 * scale)) + 'K';
|
||||
};
|
||||
|
||||
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(() => {
|
||||
// console.log(KPIStore.pageData);
|
||||
setDataSource(KPIStore.pageData);
|
||||
setEditableRowKeys([]);
|
||||
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);
|
||||
};
|
||||
return (
|
||||
<Input key={'input'} suffix="%" type={'number'} value={inputValue} onChange={handleInputChange} step={0.1} />
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
const RenderMonthCell = (row, mon) => {
|
||||
return (
|
||||
<Space direction={'vertical'}>
|
||||
<div>
|
||||
{fixTo2Decimals(row?.[`M${mon}Val`])}
|
||||
<span>%</span>
|
||||
</div>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
const monthCol = new Array(12).fill(1).map((_, index) => {
|
||||
return {
|
||||
title: `${index + 1}月`,
|
||||
dataIndex: `M${index + 1}Val`,
|
||||
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: 'percent',
|
||||
formItemProps: {
|
||||
style: { width: '100%' },
|
||||
},
|
||||
renderFormItem: () => <PercentInput />
|
||||
},
|
||||
...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 mVal = Number(curObj[`M${mIndex}Val`]);
|
||||
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,
|
||||
group_date_type: 'month',
|
||||
unit: '%',
|
||||
};
|
||||
});
|
||||
const yearRow = (({ object, object_name, object_id, subject, date_type, yearValue, kpiYear }) => ({
|
||||
object,
|
||||
object_name,
|
||||
object_id,
|
||||
subject,
|
||||
date_type,
|
||||
value: yearValue,
|
||||
start_date: moment([KPIStore.settingYear, 0, 1]).format('YYYY-MM-DD'),
|
||||
end_date: moment([KPIStore.settingYear, 11, 1]).endOf('M').format('YYYY-MM-DD HH:mm'),
|
||||
kpi_id: kpiYear?.kpi_id || undefined,
|
||||
group_date_type: 'year',
|
||||
unit: '%',
|
||||
}))(curObj);
|
||||
return r.concat(allMonth, 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: 15,
|
||||
object: curObject,
|
||||
object_name: '',
|
||||
object_id: -1,
|
||||
subject: KPIStore.settingSubject,
|
||||
date_type: searchFormStore.formValuesToSub.DateType,
|
||||
kpiDataMapped: {},
|
||||
key: Date.now().toString(32),
|
||||
group_date_type: 'month',
|
||||
unit: '%',
|
||||
}),
|
||||
{}
|
||||
); // v.formItemProps.initialValue
|
||||
const makeInitialTable = (e) => {
|
||||
setEditOpen(e);
|
||||
// test: 单独设置之后, 清空筛选会导致无法批量设置新的
|
||||
const _initialRow = Object.assign({}, initialRow, initialPercentKey);
|
||||
const _objects = isEmpty(objects) ? curObjectItem?.data || [] : objects;
|
||||
const _initialTable = _objects.map((obj) => ({
|
||||
...cloneDeep(_initialRow),
|
||||
object_name: obj.label,
|
||||
object_id: obj.value,
|
||||
key: Date.now().toString(32) + obj.value,
|
||||
}));
|
||||
const mergePageData = Object.values(
|
||||
Object.assign(
|
||||
{},
|
||||
_initialTable.reduce((r, v) => ({ ...r, [v.object_name]: v }), {}),
|
||||
dataSource.reduce((r, v) => ({ ...r, [v.object_name]: v }), {})
|
||||
)
|
||||
);
|
||||
if (e && isEmpty(dataSource)) {
|
||||
setDataSource(_initialTable);
|
||||
setEditableRowKeys(_initialTable.map((ele) => ele.key));
|
||||
return false;
|
||||
}
|
||||
setDataSource(mergePageData);
|
||||
setEditableRowKeys(e ? mergePageData.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) => {
|
||||
const rowKpiIds = (_row?.kpiData || []).map((ele) => ele.kpi_id);
|
||||
rowKpiIds.push(_row?.kpiYear?.kpi_id);
|
||||
setDelKpiIds(rowKpiIds);
|
||||
},
|
||||
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}>
|
||||
<Space size={'large'}>
|
||||
<Button className="mt-1 mb-1 align_center" disabled={!editOpen} type="primary" key="save" onClick={onTableSubmit}>
|
||||
保存数据
|
||||
</Button>
|
||||
{!editOpen && <Button className="mt-1 mb-1 align_center" disabled={false} type={'ghost'} key="initTable" onClick={() => {makeInitialTable(true);}}>
|
||||
编辑设置
|
||||
</Button>}
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
});
|
@ -0,0 +1,86 @@
|
||||
import { makeAutoObservable, runInAction, toJS } from 'mobx';
|
||||
import * as req from '../utils/request';
|
||||
import { isEmpty, pick, 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 各个类型的分布
|
||||
*/
|
||||
getApartData = async (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];
|
||||
};
|
||||
|
||||
/**
|
||||
* 明细
|
||||
*/
|
||||
getDetailData = async (param) => {
|
||||
this.detailData.loading = true;
|
||||
const json = await req.fetchJSON('/service-Analyse2/GetTradeApartDetail', param);
|
||||
if (json.errcode === 0) {
|
||||
runInAction(() => {
|
||||
this.detailData.loading = false;
|
||||
this.detailData.dataSource = json.result;
|
||||
const daysData = json.result.filter(ele => ele.confirmDays).map(row => pick(row, ['o_id', 'tourdays', 'applyDays', 'personNum', 'country', 'startDate']));
|
||||
this.scatterDays = daysData;
|
||||
});
|
||||
}
|
||||
return this.detailData;
|
||||
};
|
||||
|
||||
resetData = () => {
|
||||
// this.detailData = { loading: false, dataSource: [] };
|
||||
// this.scatterDays = [];
|
||||
|
||||
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;
|
||||
|
||||
detailData = { loading: false, dataSource: [], };
|
||||
scatterDays = [];
|
||||
|
||||
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;
|
@ -0,0 +1,90 @@
|
||||
import { useContext, useEffect, useMemo } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { stores_Context } from '../config';
|
||||
import { Row, Col, Spin, Space, Radio, Tabs, Table } from 'antd';
|
||||
import { empty } from '../utils/commons';
|
||||
import { dataFieldAlias } from '../libs/ht';
|
||||
import Scatter from './../components/Scatter';
|
||||
import SearchForm from './../components/search/SearchForm';
|
||||
import { Histogram } from '@ant-design/plots';
|
||||
|
||||
export default observer((props) => {
|
||||
const { date_picker_store: searchFormStore, DistributionStore } = useContext(stores_Context);
|
||||
const { formValues, formValuesToSub } = searchFormStore;
|
||||
const { curTab, scatterDays, detailData } = DistributionStore;
|
||||
|
||||
const detailRefresh = (obj) => {
|
||||
DistributionStore.getDetailData({
|
||||
...(obj || formValuesToSub),
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (empty(detailData.dataSource)) {
|
||||
detailRefresh();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const ScatterConfig = {
|
||||
xField: 'startDate',
|
||||
yField: 'tourdays',
|
||||
colorField: 'country',
|
||||
size: 4,
|
||||
// xAxis: {
|
||||
// min: 0,
|
||||
// max: 30,
|
||||
// },
|
||||
yAxis: {
|
||||
min: 0,
|
||||
max: 10,
|
||||
},
|
||||
// quadrant: {
|
||||
// xBaseline: 15,
|
||||
// yBaseline: 5,
|
||||
// },
|
||||
tooltip: false,
|
||||
legend: {
|
||||
position: 'right-top',
|
||||
},
|
||||
};
|
||||
const HistogramConfig = {
|
||||
binField: 'personNum',
|
||||
binWidth: 1,
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Row gutter={16} style={{ margin: '-16px -8px' }}>
|
||||
{/* style={{ margin: '-16px -8px', padding: 0 }} */}
|
||||
<Col className="gutter-row" span={24}>
|
||||
<SearchForm
|
||||
defaultValue={{
|
||||
initialValue: {
|
||||
...formValues,
|
||||
},
|
||||
shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'dates', 'country'],
|
||||
fieldProps: {
|
||||
DepartmentList: { show_all: true },
|
||||
WebCode: { show_all: true },
|
||||
dates: { hide_vs: true },
|
||||
},
|
||||
}}
|
||||
onSubmit={(_err, obj, form, str) => {
|
||||
detailRefresh(obj);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<section>
|
||||
<Spin spinning={detailData.loading}>
|
||||
<Scatter {...ScatterConfig} dataSource={scatterDays} />
|
||||
</Spin>
|
||||
</section>
|
||||
{/* <section>
|
||||
<Spin spinning={detailData.loading}>
|
||||
<Histogram {...HistogramConfig} data={scatterDays} />
|
||||
</Spin>
|
||||
</section> */}
|
||||
</>
|
||||
);
|
||||
});
|
@ -0,0 +1,110 @@
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { stores_Context } from '../config';
|
||||
import { Row, Col, Spin, Tabs, Table } from 'antd';
|
||||
import { RingProgress } from '@ant-design/plots';
|
||||
import SearchForm from './../components/search/SearchForm';
|
||||
import { empty } from '../utils/commons';
|
||||
import { dataFieldAlias } from '../libs/ht';
|
||||
|
||||
import './kpi.css';
|
||||
|
||||
const apartOptions = [
|
||||
{ key: 'tourDays', value: 'tourDays', label: '团天数' },
|
||||
{ key: 'PML', value: 'PML', label: '单团毛利' },
|
||||
{ key: 'ConfirmDays', value: 'ConfirmDays', label: '成团周期' },
|
||||
{ key: 'ApplyDays', value: 'ApplyDays', label: '预定周期' },
|
||||
{ key: 'PersonNum', value: 'PersonNum', label: '人等' },
|
||||
{ key: 'destination', value: 'destination', label: '国内目的地' },
|
||||
{ key: 'GlobalDestination', value: 'GlobalDestination', label: '海外目的地' },
|
||||
];
|
||||
|
||||
export default observer(() => {
|
||||
const { date_picker_store: searchFormStore, DistributionStore } = useContext(stores_Context);
|
||||
const { formValues, formValuesToSub } = searchFormStore;
|
||||
const { curTab } = DistributionStore;
|
||||
|
||||
const pageRefresh = (obj) => {
|
||||
DistributionStore.getApartData({
|
||||
...(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,
|
||||
color: ['#5B8FF9', '#E8EDF3'],
|
||||
};
|
||||
const columns = [
|
||||
{ title: '', dataIndex: 'label' },
|
||||
{ title: '团数', dataIndex: 'ConfirmOrder' },
|
||||
{ title: '业绩', dataIndex: 'SumML', render: (v) => dataFieldAlias.SumML.formatter(v) },
|
||||
{ title: '团数占比', dataIndex: 'ConfirmOrderPercent', render: (v) => <RingProgress {...RingProgressConfig} percent={v / 100} /> },
|
||||
{ title: '业绩占比', dataIndex: 'SumMLPercent', render: (v) => <RingProgress {...RingProgressConfig} percent={v / 100} /> },
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<Row gutter={16} style={{ margin: '-16px -8px' }}>
|
||||
<Col className="gutter-row" span={24}>
|
||||
<SearchForm
|
||||
defaultValue={{
|
||||
initialValue: {
|
||||
...formValues,
|
||||
},
|
||||
shows: ['DateType', 'DepartmentList', 'WebCode', 'IncludeTickets', 'dates'],
|
||||
fieldProps: {
|
||||
DepartmentList: { show_all: true },
|
||||
WebCode: { show_all: true },
|
||||
dates: { hide_vs: true },
|
||||
},
|
||||
}}
|
||||
onSubmit={(_err, obj) => {
|
||||
pageRefresh(obj);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<section>
|
||||
<Tabs
|
||||
onChange={onTabsChange}
|
||||
type="card"
|
||||
items={apartOptions.map((ele) => {
|
||||
return {
|
||||
...ele,
|
||||
children: (
|
||||
<Spin spinning={DistributionStore.pageLoading}>
|
||||
<Table
|
||||
id="table_to_xlsx_sale"
|
||||
dataSource={DistributionStore[curTab].dataSource}
|
||||
columns={columns}
|
||||
size="small"
|
||||
rowKey={(record) => record.label}
|
||||
loading={DistributionStore[curTab].loading}
|
||||
pagination={false}
|
||||
scroll={{ x: '100%' }}
|
||||
/>
|
||||
</Spin>
|
||||
),
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
});
|
Loading…
Reference in New Issue