|
|
import { fixTo4Decimals, fixTo1Decimals, fixToInt, groupBy, sortBy, cloneDeep, pick, unique, flush, fixTo2Decimals, isEmpty } from '../utils/commons';
|
|
|
|
|
|
/**
|
|
|
* 事业部
|
|
|
*/
|
|
|
export const biz = [
|
|
|
{ key: '0', label: '公共开支', code: '' },
|
|
|
{ key: '1', label: 'GH事业部', code: '' },
|
|
|
{ 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 = [
|
|
|
{ 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 }), {});
|
|
|
export const groupsMappedByKey = groups.reduce((a, c) => ({ ...a, [String(c.key)]: c }), {});
|
|
|
export const leafGroup = groups.slice(3);
|
|
|
export const overviewGroup = groups.slice(0, 3); // todo: 花梨鹰 APP Trippest
|
|
|
/**
|
|
|
* 来源
|
|
|
*/
|
|
|
export const sites = [
|
|
|
{ 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: 'GHToB 海外', code: 'GHTOBHW' },
|
|
|
{ value: '176', key: '176', label: 'GHToB 中国', code: 'GHTOBZG' },
|
|
|
{ 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: '178,179,180,181,182,183', key: '178,179,180,181,182,183', label: '国际(GH)', code: 'gh_gm,gh_jp,gh_vc,gh_vac,gh_ru,gh_it'},
|
|
|
{ value: '184', key: '184', label: '站外渠道', code: 'ZWQD' },
|
|
|
{ value: '11', key: '11', label: '日语', code: 'JP' },
|
|
|
{ value: '179', key: '179', label: 'GH-日语', code: 'gh_jp'}, // www.globalhighlights.jp
|
|
|
{ value: '12', key: '12', label: '西语', code: 'VAC' },
|
|
|
{ value: '122', key: '122', label: '西语海外', code: 'VACHW' },
|
|
|
{ value: '181', key: '181', label: 'GH-西语', code: 'gh_vac'}, // www.globalhighlights.es
|
|
|
{ value: '20', key: '20', label: '意大利', code: 'IT' },
|
|
|
{ value: '200', key: '200', label: '意大利海外', code: 'ITHW' },
|
|
|
{ value: '183', key: '183', label: 'GH-意语', code: 'gh_it'}, // www.globalhighlights.it
|
|
|
{ value: '21', key: '21', label: '德语', code: 'GM' },
|
|
|
{ value: '211', key: '211', label: '德语海外', code: 'GMHW' },
|
|
|
{ value: '178', key: '178', label: 'GH-德语', code: 'gh_gm'}, // wwww.globalhighlights.de
|
|
|
{ value: '10', key: '10', label: '俄语', code: 'RU' },
|
|
|
{ value: '100', key: '100', label: '俄语海外', code: 'RUHW' },
|
|
|
{ value: '182', key: '182', label: 'GH-俄语', code: 'gh_ru'}, // www.globalhighlights.ru
|
|
|
{ value: '18', key: '18', label: '法语', code: 'VC' },
|
|
|
{ value: '188', key: '188', label: '法语海外', code: 'VCHW' },
|
|
|
{ value: '180', key: '180', label: 'GH-法语', code: 'gh_vc'}, // www.globalhighlights.fr
|
|
|
{ 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 sitesMappedByCode = sites.reduce((a, c) => ({ ...a, [String(c.code)]: { ...c, key: c.code, value: c.code } }), {});
|
|
|
export const dateTypes = [
|
|
|
{ key: 'applyDate', value: 'applyDate', label: '提交日期' },
|
|
|
{ key: 'confirmDate', value: 'confirmDate', label: '确认日期' },
|
|
|
{ key: 'startDate', value: 'startDate', label: '走团日期' },
|
|
|
];
|
|
|
|
|
|
/**
|
|
|
* 结果字段
|
|
|
*/
|
|
|
export const dataFieldOptions = [
|
|
|
{ label: '营收', value: 'transactions', formatter: (v) => `${fixTo1Decimals((v || 0) / 10000)} 万`, nestkey: { p: 'transactionsKPIrates', v: 'transactionsKPIvalue' } },
|
|
|
{ label: '毛利', value: 'SumML', formatter: (v) => `${fixTo1Decimals((v || 0) / 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: 'SumPersonNum', formatter: (v) => v, nestkey: {} },
|
|
|
// todo: more...
|
|
|
];
|
|
|
/**
|
|
|
* 结果字段alias
|
|
|
*/
|
|
|
export const dataFieldAlias = dataFieldOptions.reduce(
|
|
|
(a, c) => ({
|
|
|
...a,
|
|
|
[c.value]: { ...c, alias: c.label, formatter: (v) => c.formatter(v) },
|
|
|
[c.nestkey.v]: { ...c, value: c.nestkey.v, alias: `${c.label}目标`, label: `${c.label}目标`, formatter: (v) => c.formatter(v), nestkey: { o: c.value } },
|
|
|
}),
|
|
|
{}
|
|
|
);
|
|
|
|
|
|
/**
|
|
|
* KPI对象
|
|
|
*/
|
|
|
export const KPIObjects = [
|
|
|
{ key: 'overview', value: 'overview', label: '海纳', data: [{ key: 'ALL', value: 'ALL', label: '海纳' }, ...overviewGroup] },
|
|
|
{
|
|
|
key: 'bizarea',
|
|
|
value: 'bizarea',
|
|
|
label: '国内外业务',
|
|
|
data: [
|
|
|
{ key: 'inside', value: 'inside', label: '国内' },
|
|
|
{ key: 'outside', value: 'outside', label: '海外' },
|
|
|
],
|
|
|
},
|
|
|
{ key: 'bu', value: 'bu', label: 'HT事业部', data: bu },
|
|
|
{ key: 'dept', value: 'dept', label: '小组', data: leafGroup },
|
|
|
{ key: 'du', value: 'du', label: '销售小组', data: deptUnits },
|
|
|
{ key: 'operator', value: 'operator', label: '顾问' },
|
|
|
{ key: 'destination', value: 'destination', label: '目的地 城市' },
|
|
|
// { key: 'destination', value: 'destination', label: '目的地 国籍' },
|
|
|
{ key: 'country', value: 'country', label: '客源 国籍' },
|
|
|
{
|
|
|
key: 'guestgrouptype',
|
|
|
value: 'guestgrouptype',
|
|
|
label: '客群类别',
|
|
|
data: [
|
|
|
{ key: '146001', value: '146001', label: '夫妻' },
|
|
|
{ key: '146002', value: '146002', label: '家庭' },
|
|
|
{ key: '146003', value: '146003', label: 'Solo' },
|
|
|
{ key: '146004', value: '146004', label: '组织' },
|
|
|
{ key: '146005', value: '146005', 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: '人数' },
|
|
|
];
|
|
|
|
|
|
/**
|
|
|
* 计算指标值的分段区间
|
|
|
* @param {number} value
|
|
|
* @returns
|
|
|
*/
|
|
|
const calcPPPriceRange = (value) => {
|
|
|
if (value < 0) {
|
|
|
return '--';
|
|
|
}
|
|
|
|
|
|
const step = 30; // step = 30 USD
|
|
|
const start = Math.floor(value / step) * step;
|
|
|
const end = start + step;
|
|
|
|
|
|
if (value >= 301) {
|
|
|
return `301-Infinity`;
|
|
|
}
|
|
|
|
|
|
return `${start === 0 ? start : (start+1)}-${end}`;
|
|
|
};
|
|
|
/**
|
|
|
* 数据透视计算
|
|
|
* @param {object[]} data
|
|
|
* @param {any[]} groupbyKeys
|
|
|
* @returns
|
|
|
*/
|
|
|
export const pivotBy = (_data, [rows, columns, date]) => {
|
|
|
console.time('pivot3----');
|
|
|
console.log('pivotBy', [rows, columns, date]);
|
|
|
const groupbyKeys = flush([].concat(rows, columns, [date]));
|
|
|
// if (groupbyKeys.includes('PPPriceRange')) {
|
|
|
// }
|
|
|
// 补充计算的字段
|
|
|
let data = cloneDeep(_data).map(ele => {
|
|
|
ele.PPPrice = (Number(ele.orderState) === 1 && ele.tourdays && ele.personNum) ? fixToInt(ele.quotePrice / ele.tourdays / ele.personNum) : -1;
|
|
|
ele.PPPriceRange = calcPPPriceRange(ele.PPPrice);
|
|
|
ele.IsOld_txt = ele.IsOld === '1' ? '老客户' : '否';
|
|
|
ele.isCusCommend_txt = ele.isCusCommend === '1' ? '老客户推荐' : '否';
|
|
|
const hasOld = (ele.IsOld === '1' || ele.isCusCommend === '1') ? 1 : 0;
|
|
|
ele.hasOld = hasOld;
|
|
|
ele.hasOld_txt = hasOld === 1 ? '老客户(推荐)' : '';
|
|
|
ele.SumML_ctxt1 = ele.ML > 10000 ? '1W+' : '1W-';
|
|
|
ele.SumML_ctxt1_5 = ele.ML > 15000 ? '1.5W+' : '1.5W-';
|
|
|
ele.SumML_ctxt2 = ele.ML > 20000 ? '2W+' : '2W-';
|
|
|
ele.SumML_ctxt3 = ele.ML > 30000 ? '3W+' : '3W-';
|
|
|
ele.SumML_ctxt4 = ele.ML > 40000 ? '4W+' : '4W-';
|
|
|
return ele;
|
|
|
});
|
|
|
// 数组的字段值, 拆分处理
|
|
|
if (groupbyKeys.includes('destinationCountry_AsJOSN')) {
|
|
|
data = _data.reduce((r, v, i) => {
|
|
|
const vjson = isEmpty(v.destinationCountry_AsJOSN) ? [] : v.destinationCountry_AsJOSN;
|
|
|
const xv = (vjson).reduce((rv, cv, vi) => {
|
|
|
rv.push({...v, destinationCountry_AsJOSN: cv, key: vi === 0 ? v.key : `${v.key}@${cv}`});
|
|
|
return rv;
|
|
|
}, []);
|
|
|
|
|
|
r = r.concat(xv);
|
|
|
return r;
|
|
|
}, []);
|
|
|
}
|
|
|
|
|
|
const getKeys = (keys) => keys.map((keyField) => [...new Set(data.map((f) => f[keyField]))]);
|
|
|
const [rowsKeys, columnsKeys, dateKeys] = [getKeys(rows), getKeys(columns), [getKeys([date])[0].filter(s => s)]];
|
|
|
// console.log('rowsKeys', rowsKeys, 'columnsKeys', columnsKeys, 'dateKeys', dateKeys);
|
|
|
|
|
|
const calcTradeFields = (dataObj, keepKeys = [], seriesKey = '') => {
|
|
|
const outerKeys = [];
|
|
|
const _keepKeys = [...keepKeys, seriesKey];
|
|
|
const DataGroupByKeys = {};
|
|
|
|
|
|
Object.keys(dataObj).forEach((colKey) => {
|
|
|
const _len = dataObj[colKey].length;
|
|
|
const _rowKey = dataObj[colKey].map((v) => v.key).join('_');
|
|
|
outerKeys.push(_rowKey);
|
|
|
|
|
|
const initialData = {
|
|
|
...pick(dataObj[colKey][0], _keepKeys),
|
|
|
...(keepKeys.length === 0
|
|
|
? { rowLabel: '总' }
|
|
|
: {
|
|
|
rowLabel: cloneDeep(keepKeys)
|
|
|
// .slice(0, -1)
|
|
|
.map((_k) => dataObj[colKey][0][_k])
|
|
|
.join('»'),
|
|
|
}),
|
|
|
_label: colKey || '(空)',
|
|
|
key: _rowKey,
|
|
|
|
|
|
_a:dataObj[colKey].map((v) => `${v.PPPrice}: ${v.PPPriceRange}`).join(', '),
|
|
|
_b:dataObj[colKey].map((v) => `${v.orderState}: ${v.quotePrice}/ ${v.tourdays}/ ${v.personNum}`).join(', '),
|
|
|
|
|
|
SumOrder: _len,
|
|
|
|
|
|
SumPersonNum: 0,
|
|
|
ConfirmPersonNum: 0,
|
|
|
ConfirmOrder: 0,
|
|
|
transactions: 0,
|
|
|
SumML: 0,
|
|
|
SumML_txt: '',
|
|
|
quotePrice: 0,
|
|
|
tourdays: 0,
|
|
|
applyDays: 0,
|
|
|
confirmDays: 0,
|
|
|
SingleML: 0,
|
|
|
OrderValue: 0,
|
|
|
PPPrice: 0,
|
|
|
AvgPPPrice: 0,
|
|
|
confirmTourdays: 0,
|
|
|
PPPriceRange: '',
|
|
|
unitPPPriceRange: '',
|
|
|
};
|
|
|
|
|
|
const calculatedData = dataObj[colKey].reduce((r, v) => {
|
|
|
r.SumPersonNum += v.personNum;
|
|
|
r.ConfirmPersonNum += Number(v.orderState) === 1 ? v.personNum : 0;
|
|
|
r.ConfirmOrder += Number(v.orderState) === 1 ? 1 : 0;
|
|
|
r.transactions += v.transactions;
|
|
|
r.SumML += Number(v.orderState) === 1 ? v.ML : 0;
|
|
|
r.quotePrice += Number(v.orderState) === 1 ? v.quotePrice : 0;
|
|
|
r.tourdays += v.tourdays;
|
|
|
r.applyDays += v.applyDays;
|
|
|
r.confirmDays += v.confirmDays;
|
|
|
r.PPPrice += Number(v.orderState) === 1 ? v.PPPrice : 0;
|
|
|
r.confirmTourdays += Number(v.orderState) === 1 ? v.tourdays : 0;
|
|
|
return r;
|
|
|
}, initialData);
|
|
|
|
|
|
// Calculations
|
|
|
calculatedData.tourdays = Math.ceil(calculatedData.tourdays / _len);
|
|
|
calculatedData.confirmTourdays = calculatedData.ConfirmOrder > 0 ? Math.ceil(calculatedData.confirmTourdays / calculatedData.ConfirmOrder) : '0';
|
|
|
calculatedData.applyDays = Math.ceil(calculatedData.applyDays / _len);
|
|
|
calculatedData.confirmDays = Math.ceil(calculatedData.confirmDays / _len);
|
|
|
const _rowCalc = {
|
|
|
ConfirmRates: calculatedData.ConfirmOrder ? fixTo4Decimals(calculatedData.ConfirmOrder / calculatedData.SumOrder*100) : 0,
|
|
|
OrderValue: calculatedData.SumOrder ? fixToInt(calculatedData.SumML / calculatedData.SumOrder) : 0,
|
|
|
SingleML: calculatedData.ConfirmOrder ? fixToInt(calculatedData.SumML / calculatedData.ConfirmOrder) : 0,
|
|
|
|
|
|
AvgPPPrice: calculatedData.ConfirmOrder ? fixToInt(calculatedData.PPPrice / calculatedData.ConfirmOrder) : -1,
|
|
|
unitPPPrice:
|
|
|
calculatedData.confirmTourdays && calculatedData.ConfirmPersonNum ? fixToInt(calculatedData.quotePrice / calculatedData.confirmTourdays / calculatedData.ConfirmPersonNum) : -1,
|
|
|
};
|
|
|
// Formatter
|
|
|
calculatedData.transactions = fixTo2Decimals(calculatedData.transactions);
|
|
|
calculatedData.SumML = fixTo2Decimals(calculatedData.SumML);
|
|
|
calculatedData.SumML_txt = dataFieldAlias.SumML.formatter(calculatedData.SumML);
|
|
|
calculatedData.quotePrice = fixTo2Decimals(calculatedData.quotePrice);
|
|
|
calculatedData.ConfirmRates_txt = dataFieldAlias.ConfirmRates.formatter(_rowCalc.ConfirmRates);
|
|
|
// calculatedData.SingleML = fixTo2Decimals(calculatedData.SingleML);
|
|
|
calculatedData.PPPrice = fixTo2Decimals(calculatedData.PPPrice);
|
|
|
calculatedData.PPPriceRange = calcPPPriceRange(_rowCalc.AvgPPPrice);
|
|
|
calculatedData.unitPPPriceRange = calcPPPriceRange(_rowCalc.unitPPPrice);
|
|
|
|
|
|
DataGroupByKeys[colKey] = { ...calculatedData, ..._rowCalc };
|
|
|
});
|
|
|
|
|
|
return { groupByKeys: DataGroupByKeys, key: outerKeys.join('_') };
|
|
|
};
|
|
|
|
|
|
const groupData = groupBy(data, (row) => groupbyKeys.map((kk) => `${row[kk]}`).join('=@='));
|
|
|
const rowsNcolumnsItems = calcTradeFields(groupData, [...rows, ...columns], date);
|
|
|
const pivotResult = Object.values(rowsNcolumnsItems.groupByKeys);
|
|
|
|
|
|
const transposeData = (keys, dataProp, [dataKey, colKeys]=[]) =>
|
|
|
Object.keys(dataProp)
|
|
|
.map((rowKey) => {
|
|
|
const rowLabel = keys.length === 0 ? '总' : keys.map(ekey => dataProp[rowKey][0][ekey]).join('»');
|
|
|
const _colKey = dataKey || 'dataKey';
|
|
|
const _colData = groupBy(dataProp[rowKey], (crow) => (colKeys || keys).map((kk) => `${crow[kk]}`).join('=@='));
|
|
|
const _columnsObj = calcTradeFields(_colData);
|
|
|
return { ...pick(dataProp[rowKey][0], keys), [_colKey]: _columnsObj.groupByKeys, key: _columnsObj.key, rowLabel };
|
|
|
})
|
|
|
.map((everyR) => {
|
|
|
const _colKey = dataKey || 'dataKey';
|
|
|
const allColumns = Object.values(everyR[_colKey]).reduce((r, c) => r.concat([c]), []);
|
|
|
const summaryCalc = [
|
|
|
'ConfirmOrder',
|
|
|
'SumOrder',
|
|
|
'SumML',
|
|
|
'transactions',
|
|
|
'SumPersonNum',
|
|
|
'ConfirmPersonNum',
|
|
|
'quotePrice',
|
|
|
'tourdays',
|
|
|
'applyDays',
|
|
|
'confirmDays',
|
|
|
'PPPrice', 'AvgPPPrice',
|
|
|
'confirmTourdays',
|
|
|
].reduce((r, skey) => ({ ...r,
|
|
|
[skey]: allColumns.reduce((a, c) => (fixTo2Decimals(a + c[skey])), 0),
|
|
|
[`${skey}_arr`]: allColumns.reduce((a, c) => (a.concat(c[skey])), []),
|
|
|
}),everyR);
|
|
|
|
|
|
summaryCalc.tourdays = Math.ceil(summaryCalc.tourdays / allColumns.length);
|
|
|
summaryCalc.confirmTourdays = summaryCalc.ConfirmOrder > 0 ? Math.ceil(summaryCalc.confirmTourdays / summaryCalc.ConfirmOrder) : '0';
|
|
|
summaryCalc.applyDays = Math.ceil(summaryCalc.applyDays / allColumns.length);
|
|
|
summaryCalc.confirmDays = Math.ceil(summaryCalc.confirmDays / allColumns.length);
|
|
|
summaryCalc.ConfirmRates = summaryCalc.ConfirmOrder ? fixTo2Decimals(summaryCalc.ConfirmOrder / summaryCalc.SumOrder*100) : 0;
|
|
|
summaryCalc.OrderValue = summaryCalc.SumOrder ? fixToInt(summaryCalc.SumML / summaryCalc.SumOrder) : 0;
|
|
|
summaryCalc.SingleML = summaryCalc.ConfirmOrder ? fixTo2Decimals(summaryCalc.SumML / summaryCalc.ConfirmOrder) : 0;
|
|
|
summaryCalc.AvgPPPrice = Math.ceil(summaryCalc.AvgPPPrice / allColumns.length);
|
|
|
summaryCalc.PPPriceRange = calcPPPriceRange(summaryCalc.AvgPPPrice);
|
|
|
summaryCalc.unitPPPrice = summaryCalc.confirmTourdays && summaryCalc.ConfirmPersonNum ? fixToInt(summaryCalc.quotePrice / summaryCalc.confirmTourdays / summaryCalc.ConfirmPersonNum) : -1;
|
|
|
summaryCalc.unitPPPriceRange = calcPPPriceRange(summaryCalc.unitPPPrice);
|
|
|
|
|
|
summaryCalc.SumML_txt = dataFieldAlias.SumML.formatter(summaryCalc.SumML);
|
|
|
summaryCalc.ConfirmRates_txt = dataFieldAlias.ConfirmRates.formatter(summaryCalc.ConfirmRates);
|
|
|
|
|
|
return { ...everyR, ...summaryCalc };
|
|
|
});
|
|
|
|
|
|
const rowsData = groupBy(data, (row) => rows.map((kk) => `${row[kk]}`).join('=@='));
|
|
|
const summaryRows = transposeData(rows, rowsData, ['columns', columns]);
|
|
|
|
|
|
const columnsData = groupBy(data, (row) => columns.map((kk) => `${row[kk]}`).join('=@='));
|
|
|
const summaryColumns = transposeData(columns, columnsData, ['rows', rows]);
|
|
|
|
|
|
const rowsMixcolumns = flush([].concat(rows, columns));
|
|
|
const rowsMixcolumnsData = groupBy(data, (row) => rowsMixcolumns.map((kk) => `${row[kk]}`).join('=@='));
|
|
|
const summaryMix = transposeData(rowsMixcolumns, rowsMixcolumnsData);
|
|
|
|
|
|
console.timeEnd('pivot3----');
|
|
|
return { data: pivotResult, columnValues: [rowsKeys, columnsKeys, dateKeys], summaryRows, summaryColumns, pivotKeys: groupbyKeys, summaryMix };
|
|
|
};
|
|
|
|
|
|
// todo: 优化 pivotBy 速度
|
|
|
export const pivotBy3 = (data, [rows, columns, date]) => {
|
|
|
console.log('pivotBy', [rows, columns, date]);
|
|
|
console.time('pivot2');
|
|
|
// const rowKeys = new Set(data.map(row => row[rows[0]]));
|
|
|
const rowKeys = rows.map((keyField) => {
|
|
|
const keyu = new Set(data.map((f) => f[keyField]));
|
|
|
return keyu;
|
|
|
});
|
|
|
const colKeys = new Set(data.map(row => row[columns[0]]));
|
|
|
const dateKeys = new Set(data.map(row => row[date]));
|
|
|
|
|
|
const aggregatedData = {};
|
|
|
|
|
|
data.forEach(row => {
|
|
|
const rowKey = row[rows[0]] ?? '__total';
|
|
|
const colKey = row[columns[0]] ?? '__total';
|
|
|
const dateKey = row[date];
|
|
|
|
|
|
if (!aggregatedData[rowKey]) {
|
|
|
aggregatedData[rowKey] = {};
|
|
|
}
|
|
|
|
|
|
// if (!aggregatedData[rowKey][colKey]) {
|
|
|
// aggregatedData[rowKey][colKey] = {};
|
|
|
// }
|
|
|
|
|
|
if (!aggregatedData[rowKey][colKey]) {
|
|
|
aggregatedData[rowKey][colKey] = {
|
|
|
SumOrder: 0,
|
|
|
// other aggregated fields
|
|
|
SumPersonNum: 0,
|
|
|
ConfirmOrder: 0,
|
|
|
transactions: 0,
|
|
|
SumML: 0,
|
|
|
// ...
|
|
|
quotePrice: 0,
|
|
|
tourdays: 0,
|
|
|
applyDays: 0,
|
|
|
confirmDays: 0,
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
aggregatedData[rowKey][colKey].SumOrder++;
|
|
|
aggregatedData[rowKey][colKey].SumPersonNum += row.personNum;
|
|
|
aggregatedData[rowKey][colKey].ConfirmOrder += Number(row.orderState === 1);
|
|
|
aggregatedData[rowKey][colKey].transactions += row.transactions;
|
|
|
aggregatedData[rowKey][colKey].SumML += row.ML;
|
|
|
// aggregate other fields
|
|
|
|
|
|
});
|
|
|
|
|
|
const summarizedData = [];
|
|
|
|
|
|
// Generate summary rows
|
|
|
for (const rowKey of rowKeys) {
|
|
|
|
|
|
const rowAggregations = {
|
|
|
SumOrder: 0,
|
|
|
// other aggregated fields
|
|
|
SumPersonNum: 0,
|
|
|
ConfirmOrder: 0,
|
|
|
transactions: 0,
|
|
|
SumML: 0,
|
|
|
// ...
|
|
|
quotePrice: 0,
|
|
|
tourdays: 0,
|
|
|
applyDays: 0,
|
|
|
confirmDays: 0,
|
|
|
};
|
|
|
|
|
|
// Calculate aggregates over colKey
|
|
|
for (const colKey in aggregatedData[rowKey]) {
|
|
|
rowAggregations.SumOrder += aggregatedData[rowKey][colKey].SumOrder;
|
|
|
rowAggregations.SumPersonNum += aggregatedData[rowKey][colKey].SumPersonNum;
|
|
|
rowAggregations.ConfirmOrder += aggregatedData[rowKey][colKey].ConfirmOrder;
|
|
|
rowAggregations.transactions += aggregatedData[rowKey][colKey].transactions;
|
|
|
rowAggregations.SumML += aggregatedData[rowKey][colKey].SumML;
|
|
|
// ...aggregate all other fields
|
|
|
}
|
|
|
|
|
|
const row = {
|
|
|
[rows[0]]: rowKey,
|
|
|
...rowAggregations
|
|
|
};
|
|
|
|
|
|
summarizedData.push(row);
|
|
|
}
|
|
|
|
|
|
// Generate summary columns
|
|
|
for (const colKey of colKeys) {
|
|
|
const colAggregations = {
|
|
|
SumOrder: 0,
|
|
|
// other aggregated fields
|
|
|
SumPersonNum: 0,
|
|
|
ConfirmOrder: 0,
|
|
|
transactions: 0,
|
|
|
SumML: 0,
|
|
|
// ...
|
|
|
quotePrice: 0,
|
|
|
tourdays: 0,
|
|
|
applyDays: 0,
|
|
|
confirmDays: 0,
|
|
|
};
|
|
|
|
|
|
// Calculate aggregates over rowKey
|
|
|
for (const rowKey in aggregatedData) {
|
|
|
if (aggregatedData[rowKey][colKey]) {
|
|
|
colAggregations.SumOrder += aggregatedData[rowKey][colKey].SumOrder;
|
|
|
colAggregations.SumPersonNum += aggregatedData[rowKey][colKey].SumPersonNum;
|
|
|
colAggregations.ConfirmOrder += aggregatedData[rowKey][colKey].ConfirmOrder;
|
|
|
colAggregations.transactions += aggregatedData[rowKey][colKey].transactions;
|
|
|
colAggregations.SumML += aggregatedData[rowKey][colKey].SumML;
|
|
|
// ...aggregate all other fields
|
|
|
}
|
|
|
}
|
|
|
|
|
|
const col = {
|
|
|
[columns[0]]: colKey,
|
|
|
...colAggregations
|
|
|
};
|
|
|
|
|
|
summarizedData.push(col);
|
|
|
}
|
|
|
|
|
|
console.timeEnd('pivot2');
|
|
|
console.log('pivot2 ddd', aggregatedData);
|
|
|
return {
|
|
|
data: [], // aggregatedData,
|
|
|
columnValues: [rowKeys, colKeys, dateKeys],
|
|
|
summaryRows: summarizedData.filter(r => r[rows[0]]),
|
|
|
summaryColumns: summarizedData.filter(c => c[columns[0]])
|
|
|
};
|
|
|
|
|
|
};
|