'use strict'; /** * 数据报表 · 经营周报导出 * ---------------------------------------------------------------------------- * 参考 DataAgent ReportGeneratorNode 的「自包含 HTML 报告」产物形态: * 单文件、内联 ECharts、离线可打开,适合发群/汇报。 */ const fs = require('fs'); const path = require('path'); const svc = require('./napos-service.js'); const ECHARTS_SRC = path.join(__dirname, 'public', 'vendor', 'echarts.min.js'); function esc(v) { return String(v == null ? '' : v) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } function fmtMoney(v) { return v == null ? '—' : Number(v).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } function fmtInt(v) { return v == null ? '—' : Number(v).toLocaleString('zh-CN', { maximumFractionDigits: 0 }); } function fmtPct(v) { return v == null || v === '' ? '—' : (Number(v) * 100).toFixed(2) + '%'; } function fmtByType(type, v) { if (type === 'money') return fmtMoney(v); if (type === 'int') return fmtInt(v); if (type === 'pct') return fmtPct(v); if (type === 'num') return v == null ? '—' : Number(v).toFixed(2); if (type === 'date') return /^\d{8}$/.test(String(v)) ? `${v.slice(0, 4)}-${v.slice(4, 6)}-${v.slice(6, 8)}` : (v || '—'); return v == null ? '—' : String(v); } function buildReportHtml({ entity, from, to }) { const meta = svc.store.getEntity(entity) || { key: entity, name: entity, shops: [] }; const shopNameById = new Map((meta.shops || []).map((s) => [String(s.shopId), s.shopName])); const daily = svc.queryReport({ entity, from, to, scope: 'shop' }); const aggDate = svc.aggregateReport({ entity, from, to, group: 'date' }); const aggShop = svc.aggregateReport({ entity, from, to, group: 'shop' }); const s = daily.summary || {}; // 图表数据:日趋势(营业额/有效订单 双轴)+ 门店排行 Top15 const trendDates = aggDate.rows.map((r) => fmtByType('date', r.date)); const trendGross = aggDate.rows.map((r) => Number(r.grossAmt) || 0); const trendOrders = aggDate.rows.map((r) => Number(r.validOrderCnt) || 0); const topShops = aggShop.rows.slice(0, 15); const topNames = topShops.map((r) => shopNameById.get(String(r.shopId)) || r.shopName || r.shopId); const topGross = topShops.map((r) => Number(r.grossAmt) || 0); const tableRows = aggShop.rows.map((r, i) => { const name = esc(shopNameById.get(String(r.shopId)) || r.shopName || r.shopId); return `${i + 1}${name}${fmtMoney(r.grossAmt)}${fmtInt(r.validOrderCnt)}${fmtMoney(r.incomeAmt)}${fmtInt(r.expUv)}${fmtInt(r.orderUserCnt)}${fmtPct(r.clkExpRate)}${fmtMoney(r.perActualPay)}`; }).join(''); const kpi = [ ['营业额(元)', fmtMoney(s.grossAmt), '#ff6b35'], ['收入(元)', fmtMoney(s.incomeAmt), '#1a8a4c'], ['有效订单', fmtInt(s.validOrderCnt), '#ff6b35'], ['曝光人数', fmtInt(s.expUv), '#6b6560'], ['下单人数', fmtInt(s.orderUserCnt), '#6b6560'], ['进店转化率', fmtPct(s.clkExpRate), '#b8860b'], ['客单价(元)', fmtMoney(s.perActualPay), '#1a8a4c'], ].map(([label, value, color]) => `
${label}${value}
`).join(''); const echartsJs = fs.existsSync(ECHARTS_SRC) ? fs.readFileSync(ECHARTS_SRC, 'utf8') : ''; return ` 经营周报 · ${esc(meta.name)} · ${esc(from)}~${esc(to)}

经营周报 · ${esc(meta.name)}

区间 ${esc(from)} ~ ${esc(to)} · 覆盖 ${s.shopCount || 0} 家门店 · ${s.dayCount || 0} 天 · 生成于 ${new Date().toLocaleString('zh-CN')} · 智慧运营中台
${kpi}

日趋势(营业额 / 有效订单)

门店排行 · 营业额 Top ${topNames.length}

门店汇总(按营业额降序)

${tableRows || ''}
#门店营业额(元)有效订单收入(元)曝光人数下单人数进店转化率客单价(元)
区间内无数据
`; } module.exports = { buildReportHtml };