You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dashboard/src/libs/ht.js

522 lines
22 KiB
JavaScript

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: '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 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);
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.applyDays = Math.ceil(calculatedData.applyDays / _len);
calculatedData.confirmDays = Math.ceil(calculatedData.confirmDays / _len);
const _rowCalc = {
ConfirmRates: calculatedData.ConfirmOrder ? fixTo4Decimals(calculatedData.ConfirmOrder / calculatedData.SumOrder) : 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.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]])
};
};