todo: KPI 设置和查看

feature/2.0-sales-trade
Lei OT 2 years ago
parent 31481fd301
commit 836d4b9597

@ -10,7 +10,7 @@ import {
DollarOutlined,
AreaChartOutlined,
WechatOutlined,
UserOutlined,
UserOutlined, FlagOutlined,
} from '@ant-design/icons';
import { Layout, Menu, Image, Spin } from 'antd';
import { BrowserRouter, Route, Routes, NavLink } from 'react-router-dom';
@ -35,6 +35,7 @@ import Logo from './logo.png';
import { stores_Context } from './config';
import { observer } from 'mobx-react';
import ExchangeRate from './charts/ExchangeRate';
import KPI from './views/KPI';
const App = () => {
const { Content, Footer, Sider } = Layout;
@ -119,6 +120,7 @@ const App = () => {
},
],
},
{ key: 'kpi', label: <NavLink to="/kpi">目标</NavLink>, icon: <FlagOutlined /> },
];
return (
@ -159,6 +161,7 @@ const App = () => {
>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/kpi" element={<KPI />} />
<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 />} />

@ -104,19 +104,40 @@
{}
]
},
"get|/service-web/QueryData/Getkpi": {
"get|/service-web/QueryData/GetTradeSection": {
"errcode": 0,
"errmsg": "",
"loading": false,
"data": null,
"result1|10": [
"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": "",
"loading": false,
"data": null,
"result|10": [
{
"object|1": "@pick([\"dept\",\"sales\", \"group\"])",
"subject|1": "@pick([\"OrderCount\",\"SumML\", \"AvgML\", \"SuccessRate\"])",
"object_id": "@integer(10,100)",
"object_name": "@cname",
"date_type": "@pick([\"ConfirmDate\",\"startDate\", \"applyDate\"])",
"start_date": "@date(\"yyyy-MM-dd\")",
"start_date": "@date(\"2023-MM-dd\")",
"end_date": "@datetime(\"yyyy-MM-dd HH:mm:ss\")",
"value": "@integer(99,9999)",
"key": "@increment"
@ -124,5 +145,11 @@
"result2": [
{}
]
},
"post|/service-Analyse2/setkpi_multi/test": {
"errcode": 0,
"errmsg": "",
"loading": false,
"data": null
}
}

@ -11,7 +11,7 @@ import WechatStore from "./Wechat";
import WhatsAppStore from "./WhatsApp";
import CustomerServicesStore from "./CustomerServices";
import TradeStore from "./Trade";
import KPI from "./KPI";
class Index {
constructor() {
this.dashboard_store = new DashboardStore(this);
@ -26,6 +26,7 @@ class Index {
this.whatsAppStore = new WhatsAppStore(this);
this.customerServicesStore = new CustomerServicesStore(this);
this.TradeStore = new TradeStore(this);
this.KPIStore = new KPI(this);
makeAutoObservable(this);
}

@ -0,0 +1,135 @@
import { makeAutoObservable, runInAction, toJS } from 'mobx';
import * as req from '../utils/request';
import { isEmpty, sortBy, groupBy, cloneDeep } from '../utils/commons';
import moment from 'moment';
const currentYear = moment().year();
class KPI {
constructor(appStore) {
this.appStore = appStore;
makeAutoObservable(this);
}
saveOrUpdate(x) {
console.log('ssssssssssss', x);
console.log(toJS(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 };
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');
});
}
});
}
getList() {
const param = {
date_type: 'applyDate',
start_date: '2020-01-01',
end_date: '2024-12-31 23:59:59',
};
// return req.fetchJSON('/service-Analyse2/getkpi/test', param).then((json) => {
return req.fetchJSON('/service-Analyse2/getkpi', param).then((json) => {
if (json.errcode === 0) {
runInAction(() => {
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 = 2023;
settingSubject = 'sum_profit';
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: (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];
return { ...cloneDeep(initialPercentKey), ...objectEle, ...kpiDataFlat, kpiData, kpiDataMapped, kpiYear, yearValue: kpiYear?.value || 0 };
});
ret[_subject] = afterGroup;
return 1;
});
Object.assign(yearData, { [_yearVal]: ret });
return _yearVal;
});
return yearData;
};
export default KPI;

@ -315,3 +315,129 @@ export function merge(...objects) {
return result;
}
/**
* 数组分组
* - 相当于 lodash _.groupBy
* @see https://www.lodashjs.com/docs/lodash.groupBy#_groupbycollection-iteratee_identity
*/
export function groupBy(array, callback) {
return array.reduce((groups, item) => {
const key = callback(item);
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(item);
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);
/**
* 映射
* @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;
}

@ -51,7 +51,8 @@ export function postForm(url, data) {
}
export function postJSON(url, obj) {
return fetch(url, {
const host = /^https?:\/\//i.test(url) ? '': HT_HOST;
return fetch(`${host}${url}`, {
method: 'POST',
body: JSON.stringify(obj),
headers: {

@ -0,0 +1,206 @@
import { useContext, useEffect, useState, useRef } from 'react';
import { observer, useLocalStore } from 'mobx-react';
import { toJS } from 'mobx';
// import type { ProColumns } from '@ant-design/pro-components';
import { EditableProTable, ProCard, ProFormField } from '@ant-design/pro-components';
import { Button, Table, Switch, Input, Space, Typography } from 'antd';
import { stores_Context } from '../config';
import { isEmpty, fixTo4Decimals, cloneDeep } 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 }), {});
export default observer((props) => {
const [dataSource, setDataSource] = useState([]);
const { KPIStore, DictDataStore } = useContext(stores_Context);
const { settingYear, } = KPIStore;
const { operator } = DictDataStore;
useEffect(() => {
// KPIStore.saveOrUpdate();
KPIStore.getList().then((data) => {
setDataSource(data);
});
DictDataStore.fetchDictData('operator');
return () => {};
}, []);
const [editOpen, setEditOpen] = useState(false); // test:
const [editableRowsKeys, setEditableRowKeys] = useState([]);
// console.log(toJS(KPIStore.pageData ), dataSource, '00000');
const PercentInput = ({ value, onChange, record, ...extProps }) => {
// console.log(extProps, '22222222');
const initialPercent = record.kpiDataMapped?.[`M${extProps.month}`]?.percentVal;
const [inputVal, setInputVal] = useState(value);
const calcV = inputVal ? fixTo4Decimals((Number(record?.yearValue) * inputVal) / 100) : 0;
const handleInputChange = ({ target: { value } }) => {
setInputVal(value);
};
const handleInputConfirm = () => {
// onChange?.(inputVal);
// setInputVal('');
};
return (
<Space direction={'vertical'}>
<Input suffix="%" type={'number'} value={value} onChange={onChange} />
{/* onBlur={handleInputConfirm} onPressEnter={handleInputConfirm} */}
{/* <span className='ant-typography ant-typography-secondary'>1</span> */}
<Text type={'secondary'}>{calcV}</Text>
</Space>
);
};
const RenderInput = (row, mon) => {
// console.log(toJS(row), mon);
return (
<Space direction={'vertical'}>
<div>{row.kpiDataMapped?.[`M${mon}`]?.percentVal}<span>%</span></div>
<div>{row.kpiDataMapped?.[`M${mon}`]?.value}</div>
</Space>
);
};
const monthCol = new Array(12).fill(1).map((_, index) => {
return {
title: `${index + 1}`,
dataIndex: `M${index + 1}Percent`,
valueType: 'percent',
width: '6.5em',
fieldProps: { min: 0, max: 100, style: { width: '4em' } },
renderFormItem: ({ dataIndex, ...item }, { record, isEditable ,...e }, form) => {
return <PercentInput {...{ record }} month={index + 1} />;
},
render: (_, row) => RenderInput(row, index + 1),
};
});
const initialRow = monthCol.reduce((r, v) => ({ ...r, [v.dataIndex]: 0 }), {}); // v.formItemProps.initialValue
const columns = [
{
title: '对象',
dataIndex: 'object_id',
valueType: 'select',
// ...valueEnum
// fieldProps: { labelInValue: true },
render: (_, r) => r.object_name
},
// {
// title: 'Name',
// dataIndex: 'title',
// //...form rules
// formItemProps: {
// rules: [
// {
// required: true,
// whitespace: true,
// message: '',
// },
// ],
// },
// },
{
title: '年度目标',
dataIndex: 'yearValue',
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 onTableChange = (...argrs) => {
console.log(argrs[0], 'who who who');
setEditableRowKeys(argrs[0].map((ele) => ele.key));
// KPIStore.setEditableRowsKeys(argrs[0].map((ele) => ele.key));
setDataSource(argrs[0]);
// KPIStore.handleTableEdit(argrs[0]);
};
return (
<>
<EditableProTable
key={settingYear}
headerTitle={settingYear}
columns={columns}
rowKey="key"
scroll={{
x: 1000,
}}
value={dataSource}
onChange={onTableChange}
recordCreatorProps={
editOpen
? {
newRecordType: 'dataSource',
record: () => ({
key: (Date.now().toString(32)), // dataSource.length + 1, // Number(Date.now().toString()),
...initialRow,
object_name: '',
value: 0,
yearValue: 0,
...cloneDeep(initialPercentKey),
}),
}
: false
}
toolBarRender={() => {
return [
<Switch
unCheckedChildren="查看"
checkedChildren="编辑"
key={'openEdit'}
// defaultChecked={true}
checked={editOpen}
onChange={(e) => {
setEditOpen(e);
setEditableRowKeys(e ? dataSource.map((ele) => ele.key) : []);
// KPIStore.setEditableRowsKeys(e ? dataSource.map((ele) => ele.key) : []);
}}
/>,
<Button
disabled={!editOpen}
type="primary"
key="save"
onClick={() => {
console.log(dataSource);
// dataSource api
KPIStore.saveOrUpdate(dataSource);
}}
>
保存数据
</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, onValuesChange');
onTableChange(recordList);
},
onChange: (editableKeys, editableRows) => {
console.log('editable onValuesChange');
onTableChange(editableRows);
// KPIStore.setEditableRowsKeys()
},
}}
/>
{(operator?.dataSource || []).map(ele => <div key={ele.value}>{ele.label} <span className="a">{ele.mobile}</span></div>)}
</>
);
});
Loading…
Cancel
Save