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]]) }; };