diff --git a/src/App.css b/src/App.css index 2460a39..e9e982f 100644 --- a/src/App.css +++ b/src/App.css @@ -6,4 +6,31 @@ .align_left{ text-align: left; -} \ No newline at end of file +} +.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; +} diff --git a/src/App.jsx b/src/App.jsx index 28f12ec..1ce1708 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -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: 目标, icon: }, + { key: 'distribution', label: 统计分布, icon: }, ]; return ( @@ -162,6 +164,7 @@ const App = () => { } /> } /> + } /> }> } /> } /> diff --git a/src/components/Bullet.jsx b/src/components/BulletWithSort.jsx similarity index 67% rename from src/components/Bullet.jsx rename to src/components/BulletWithSort.jsx index c2e22d7..a2d7c73 100644 --- a/src/components/Bullet.jsx +++ b/src/components/BulletWithSort.jsx @@ -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); diff --git a/src/components/Data.jsx b/src/components/Data.jsx index f907dd2..d503f07 100644 --- a/src/components/Data.jsx +++ b/src/components/Data.jsx @@ -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 ? ( + '-' + ) : (
{data1} vs {data2} diff --git a/src/components/DateFieldRadio.jsx b/src/components/DataFieldRadio.jsx similarity index 87% rename from src/components/DateFieldRadio.jsx rename to src/components/DataFieldRadio.jsx index b14047f..773ec5d 100644 --- a/src/components/DateFieldRadio.jsx +++ b/src/components/DataFieldRadio.jsx @@ -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); } diff --git a/src/components/DateGroupRadio/date.js b/src/components/DateGroupRadio/date.js index 904f5d5..6d08642 100644 --- a/src/components/DateGroupRadio/date.js +++ b/src/components/DateGroupRadio/date.js @@ -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); }; diff --git a/src/components/DateGroupRadio/index.jsx b/src/components/DateGroupRadio/index.jsx index e8b1fd2..16a325f 100644 --- a/src/components/DateGroupRadio/index.jsx +++ b/src/components/DateGroupRadio/index.jsx @@ -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 }; diff --git a/src/components/StatisticCard.jsx b/src/components/StatisticCard.jsx index 9a771f8..e6fe898 100644 --- a/src/components/StatisticCard.jsx +++ b/src/components/StatisticCard.jsx @@ -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 ? : ); + const ValueIcon = props.icon; + const valueStyle = { color: (props?.VSrate || -1) < 0 ? '#3f8600' : '#cf1322' }; + const VSIcon = () => ((props?.VSrate || -1) < 0 ? : ); return ( { ) } + prefix={} + // title={{props.title}} {...props} + value={props.valueSuffix ? `${props.value} ${props.valueSuffix}` : props.value} /> {props.showProgress !== false && `${props.KPIrate}%`} />} diff --git a/src/components/Waterfall.jsx b/src/components/Waterfall.jsx index 9fc15f0..2ec6bd6 100644 --- a/src/components/Waterfall.jsx +++ b/src/components/Waterfall.jsx @@ -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 ( <> diff --git a/src/components/kpi/KPISettings.jsx b/src/components/kpi/KPISettings.jsx new file mode 100644 index 0000000..ecec5d4 --- /dev/null +++ b/src/components/kpi/KPISettings.jsx @@ -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

TODO

; +}; + +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 ( + <> + + + { + console.log('invoke kpi setting search'); + if (typeof onSearchSubmit === 'function') { + onSearchSubmit(obj); + } + }} + /> + + + + + { + KPIStore.setSettingSubject(sub); + }} + items={KPISubjects.map((ele, i) => { + const SubjectTableComponent = subjectComponents[ele.key]; + return { + ...ele, + children: + }; + })} + /> + + + + ); +}); diff --git a/src/components/kpi/ObjectPanel.jsx b/src/components/kpi/ObjectPanel.jsx new file mode 100644 index 0000000..a78c231 --- /dev/null +++ b/src/components/kpi/ObjectPanel.jsx @@ -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 ( + <> + + + ); +}); diff --git a/src/components/kpi/OverviewPanel.jsx b/src/components/kpi/OverviewPanel.jsx new file mode 100644 index 0000000..49f8760 --- /dev/null +++ b/src/components/kpi/OverviewPanel.jsx @@ -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 ( + + + {calcV} + + ); + }, + [] + ); + const RenderMonthCell = (row, mon) => { + return ( + +
+ {fixTo2Decimals(row?.[`M${mon}Percent`])} + % +
+
{numberConvert10K(fixTo4Decimals((Number(row?.yearValue) * row?.[`M${mon}Percent`]) / 100))}
+
+ ); + }; + 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 ; + }, + 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 ( + <> + + + { + // TradeStore.setStateSearch(form); + // pageRefresh(obj); + onSearchSubmit(obj, form); + setEditOpen(false); + setEditableRowKeys([]); + }} + /> + + + { + return [ + { + makeInitialTable(e); + }} + />, + , + ]; + }} + 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); + }, + }} + /> + + ); +}); diff --git a/src/components/kpi/SubjectTable/Profit.jsx b/src/components/kpi/SubjectTable/Profit.jsx new file mode 100644 index 0000000..777adb4 --- /dev/null +++ b/src/components/kpi/SubjectTable/Profit.jsx @@ -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 ( + + + {calcV} + + ); + }, + [] + ); + const RenderMonthCell = (row, mon) => { + return ( + +
+ {fixTo2Decimals(row?.[`M${mon}Percent`])} + % +
+
{numberConvert10K(fixTo4Decimals((Number(row?.yearValue) * row?.[`M${mon}Percent`]) / 100))}
+
+ ); + }; + 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 ; + }, + 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: ( + + 操作 + { + makeInitialTable(e); + }} + /> + + ), + 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 ( + <> + + + { + 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); + }, + }} + /> + + + + + + + ); +}); diff --git a/src/components/kpi/SumProfitPanel.jsx b/src/components/kpi/SumProfitPanel.jsx new file mode 100644 index 0000000..0818a00 --- /dev/null +++ b/src/components/kpi/SumProfitPanel.jsx @@ -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 ( + <> + + + { + // TradeStore.setStateSearch(form); + // pageRefresh(obj); + }} + /> + + + + + { + const id = String(i); + return { + ...ele, + children: `Content of tab ${id}`, + }; + })} + /> + + + + ); +}); diff --git a/src/components/search/BusinessSelect.jsx b/src/components/search/BusinessSelect.jsx index b97fcb3..cb94770 100644 --- a/src/components/search/BusinessSelect.jsx +++ b/src/components/search/BusinessSelect.jsx @@ -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 (
{ + if (typeof onChange === 'function') { + onChange(value); + } + // store?.bu_handleChange(value); + }} + labelInValue={true} + {...extProps} + > + {_show_all ? ALL 事业部 : ''} + {bu.map(ele => {ele.label})} + +
+ ); +}; +/** + * HT的事业部 + */ +export default observer(Business_unit); diff --git a/src/components/search/DataTypeSelect.jsx b/src/components/search/DataTypeSelect.jsx index e6f185d..731c81b 100644 --- a/src/components/search/DataTypeSelect.jsx +++ b/src/components/search/DataTypeSelect.jsx @@ -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 ( -
- -
); } } diff --git a/src/components/search/DatePickerCharts.jsx b/src/components/search/DatePickerCharts.jsx index 3712ff8..d6dc4f0 100644 --- a/src/components/search/DatePickerCharts.jsx +++ b/src/components/search/DatePickerCharts.jsx @@ -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} + ) : ( + + {children} + + ); + // 用于日期选择,计算上一时间段、同比时间等 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 (
+ { 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")], }} /> + {this.props.hide_vs ? ( "" ) : ( + { + 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")], }} - /> + /> )} diff --git a/src/components/search/GroupSelect.jsx b/src/components/search/GroupSelect.jsx index 95a0e6e..24daa00 100644 --- a/src/components/search/GroupSelect.jsx +++ b/src/components/search/GroupSelect.jsx @@ -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 (
+ {options} + + ); + } +} +export default observer(SearchInput); diff --git a/src/components/search/SearchForm.jsx b/src/components/search/SearchForm.jsx new file mode 100644 index 0000000..9dca78c --- /dev/null +++ b/src/components/search/SearchForm.jsx @@ -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" +
+ + + {getFields({ sort, initialValue, hides, shows, fieldProps, form })} + {/* 'textAlign': 'right' */} + + + + {/* */} + + + + +
+ ); +}); + +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, + + + + ), + item( + 'businessUnits', + 99, + + + + ), + item( + 'DepartmentList', + 99, + + + + ), + item( + 'WebCode', + 99, + + + + ), + item( + 'IncludeTickets', + 99, + // value={orders_store.include_tickets} onChange={orders_store.handleChange_include_tickets} + + + , + 2 + ), + // + item( + 'DateType', + 99, + + + , + 2 + ), + item( + 'dates', + 99, + + + , + midCol + ), + item( + 'years', + 99, + + {/* */} + + , + 2 + ), + item( + 'months', + 99, + + + , + 2 + ), + item( + 'operator', + 99, + + + + ), + item( + 'country', + 99, + + + + ), + ]; + 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 = ( + + {baseChildren[i].render} + + ); + children.push(Item); + } + return children; +} diff --git a/src/components/search/SiteSelect.jsx b/src/components/search/SiteSelect.jsx index abb138b..3d22f93 100644 --- a/src/components/search/SiteSelect.jsx +++ b/src/components/search/SiteSelect.jsx @@ -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 (
diff --git a/src/components/search/YearPickerCharts.jsx b/src/components/search/YearPickerCharts.jsx new file mode 100644 index 0000000..1a5a6f7 --- /dev/null +++ b/src/components/search/YearPickerCharts.jsx @@ -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 ( +
+ + + + { + 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); + } + }} + /> + + + {this.props.hide_vs ? ( + '' + ) : ( + + + { + 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); + }} + /> + + + )} + +
+ ); + } +} + +export default observer(DatePickerCharts); diff --git a/src/components/search/search.css b/src/components/search/search.css new file mode 100644 index 0000000..9f8f546 --- /dev/null +++ b/src/components/search/search.css @@ -0,0 +1,4 @@ +.ant-form-item{ + margin: 0; + margin-bottom: 8px; +} diff --git a/src/libs/ht.js b/src/libs/ht.js index 9f973df..b8b1cbf 100644 --- a/src/libs/ht.js +++ b/src/libs/ht.js @@ -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: '人数' }, +]; diff --git a/src/mock/2.0/1.json b/src/mock/2.0/1.json deleted file mode 100644 index 9b25a55..0000000 --- a/src/mock/2.0/1.json +++ /dev/null @@ -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\")" - } - ] - } -} diff --git a/src/mock/2.0/baseinfo.json b/src/mock/2.0/baseinfo.json new file mode 100644 index 0000000..e02e285 --- /dev/null +++ b/src/mock/2.0/baseinfo.json @@ -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" + } + ] + } +} diff --git a/src/mock/2.0/trade.json b/src/mock/2.0/trade.json index a6bcb13..6d65617 100644 --- a/src/mock/2.0/trade.json +++ b/src/mock/2.0/trade.json @@ -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": "", diff --git a/src/stores/DatePickerStore.js b/src/stores/DatePickerStore.js index ff7ac74..d4f24eb 100644 --- a/src/stores/DatePickerStore.js +++ b/src/stores/DatePickerStore.js @@ -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; diff --git a/src/stores/DictData.js b/src/stores/DictData.js new file mode 100644 index 0000000..cdef2cd --- /dev/null +++ b/src/stores/DictData.js @@ -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; diff --git a/src/stores/Distribution.js b/src/stores/Distribution.js new file mode 100644 index 0000000..e583983 --- /dev/null +++ b/src/stores/Distribution.js @@ -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; diff --git a/src/stores/Index.js b/src/stores/Index.js index 45aed26..48dfba1 100644 --- a/src/stores/Index.js +++ b/src/stores/Index.js @@ -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); } diff --git a/src/stores/KPI.js b/src/stores/KPI.js index ec63aa2..40e5b69 100644 --- a/src/stores/KPI.js +++ b/src/stores/KPI.js @@ -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; diff --git a/src/stores/OrdersStore.js b/src/stores/OrdersStore.js index 50ab2a4..e8baed2 100644 --- a/src/stores/OrdersStore.js +++ b/src/stores/OrdersStore.js @@ -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; }); }) diff --git a/src/stores/Trade.js b/src/stores/Trade.js index 113d473..0806e18 100644 --- a/src/stores/Trade.js +++ b/src/stores/Trade.js @@ -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; diff --git a/src/utils/commons.js b/src/utils/commons.js index 5dddfc1..6a9adff 100644 --- a/src/utils/commons.js +++ b/src/utils/commons.js @@ -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); +}; diff --git a/src/utils/request.js b/src/utils/request.js index e9289eb..d954743 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -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; + }); +} diff --git a/src/views/Distribution.jsx b/src/views/Distribution.jsx new file mode 100644 index 0000000..68a07f0 --- /dev/null +++ b/src/views/Distribution.jsx @@ -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) => }, + { title: '业绩占比', dataIndex: 'SumMLPercent', render: (v) => }, + ]; + return ( + <> + + {/* style={{ margin: '-16px -8px', padding: 0 }} */} + + { + pageRefresh(obj); + }} + /> + + +
+ { + // const ObjectItemPanel = objectComponents[ele.key]; + return { + ...ele, + children: ( + + {/* */} + + record.label} + loading={DistributionStore[curTab].loading} + pagination={false} + scroll={{ x: '100%' }} + /> + + ), + }; + })} + /> + + + ); +}); diff --git a/src/views/Home.jsx b/src/views/Home.jsx index 75b7d6b..409955b 100644 --- a/src/views/Home.jsx +++ b/src/views/Home.jsx @@ -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, , , , ,,]; +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 ( <> + + {/* style={{ margin: '-16px -8px', padding: 0 }} */} + + { + TradeStore.setStateSearch(obj); + pageRefresh(obj); + }} + /> + +

年度业绩

-
- {summaryData.dataSource.map((item) => ( + {summaryData.dataSource.map((item, i) => (
- + ))} +
+ +

走势

+ + +
+ + + +

市场进度

- +
- + +

{`各事业部总业绩`}

{Object.keys(sideData.dataSource).map((key) => (
- + +

{`${key}每月业绩`}

))} @@ -146,9 +233,9 @@ export default observer(() => { {topSeries.map((item) => (
- +

{item.label}

- +
))} diff --git a/src/views/KPI.jsx b/src/views/KPI.jsx index 557386c..288e041 100644 --- a/src/views/KPI.jsx +++ b/src/views/KPI.jsx @@ -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 ( <> - ({ - key: pageData.length+1, // Number(Date.now().toString()), - ...initialRow, - object_name: '', - value:0 - }), - } - : false - } - toolBarRender={() => { - return [ - { - setEditOpen(e); - setEditableRowKeys(e ? pageData.map((ele) => ele.key) : []); - // KPIStore.setEditableRowsKeys(e ? pageData.map((ele) => ele.key) : []); - }} - />, - , - ]; - }} - 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() - } - }} - /> + +
+ { + const ObjectItemPanel = objectComponents[ele.key]; + return { + ...ele, + children: ( + + + + ), + }; + })} + /> + + ); }); diff --git a/src/views/Orders.jsx b/src/views/Orders.jsx index 50588cf..917a8d5 100644 --- a/src/views/Orders.jsx +++ b/src/views/Orders.jsx @@ -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; diff --git a/src/views/kpi.css b/src/views/kpi.css new file mode 100644 index 0000000..ac1d799 --- /dev/null +++ b/src/views/kpi.css @@ -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; +}