import type { CostDim, FleetScope, H2OrderRow, LeaseKind, OpsKind, } from '../types'; import { COST_DIM_LABEL, LEASE_KIND_LABEL, OPS_KIND_LABEL } from '../types'; export function filterOrders( rows: H2OrderRow[], year: number, verifyScope: 'all' | 'verified', fleetScope: FleetScope, ): H2OrderRow[] { return rows.filter((r) => { if (!r.occurredAt.startsWith(String(year))) return false; if (verifyScope === 'verified' && r.verifyStatus !== 'verified') return false; if (fleetScope === 'own' && r.fleet !== 'own') return false; if (fleetScope === 'external' && r.fleet !== 'external') return false; return true; }); } export function sumAmount(rows: H2OrderRow[]): number { return rows.reduce((s, r) => s + r.amount, 0); } export function sumKg(rows: H2OrderRow[]): number { return rows.reduce((s, r) => s + r.quantityKg, 0); } export function companyCostRows(rows: H2OrderRow[]): H2OrderRow[] { return rows.filter((r) => r.borneBy === 'company'); } export function dimAmount(rows: H2OrderRow[], dim: CostDim): number { return sumAmount(companyCostRows(rows).filter((r) => r.costDim === dim)); } export function leaseSubAmount(rows: H2OrderRow[], kind: LeaseKind): number { return sumAmount( companyCostRows(rows).filter((r) => r.costDim === 'lease' && r.leaseKind === kind), ); } export function opsSubAmount(rows: H2OrderRow[], kind: OpsKind): number { return sumAmount( companyCostRows(rows).filter((r) => r.costDim === 'ops' && r.opsKind === kind), ); } export interface DimCard { key: CostDim; label: string; amount: number; subs: { key: string; label: string; amount: number }[]; } export function costDimCards(rows: H2OrderRow[]): DimCard[] { return [ { key: 'lease', label: COST_DIM_LABEL.lease, amount: dimAmount(rows, 'lease'), subs: [ { key: 'company_borne', label: LEASE_KIND_LABEL.company_borne, amount: leaseSubAmount(rows, 'company_borne') }, { key: 'package_h2', label: LEASE_KIND_LABEL.package_h2, amount: leaseSubAmount(rows, 'package_h2') }, ], }, { key: 'logistics', label: COST_DIM_LABEL.logistics, amount: dimAmount(rows, 'logistics'), subs: [], }, { key: 'ops', label: COST_DIM_LABEL.ops, amount: dimAmount(rows, 'ops'), subs: [ { key: 'abnormal', label: OPS_KIND_LABEL.abnormal, amount: opsSubAmount(rows, 'abnormal') }, { key: 'transfer', label: OPS_KIND_LABEL.transfer, amount: opsSubAmount(rows, 'transfer') }, ], }, ]; } export function pendingAmount(rows: H2OrderRow[]): number { return dimAmount(rows, 'pending'); } export function unverified(rows: H2OrderRow[]): { amount: number; count: number } { const list = rows.filter((r) => r.verifyStatus === 'unverified'); return { amount: sumAmount(list), count: list.length }; } export function formatYuan(n: number): string { return `¥${n.toLocaleString('zh-CN', { maximumFractionDigits: 0 })}`; } export function formatKg(n: number): string { return `${n.toLocaleString('zh-CN', { maximumFractionDigits: 2 })} kg`; } export function costDimLabel(row: H2OrderRow): string { if (row.costDim === 'lease' && row.leaseKind) { return `${COST_DIM_LABEL.lease} · ${LEASE_KIND_LABEL[row.leaseKind]}`; } if (row.costDim === 'ops' && row.opsKind) { return `${COST_DIM_LABEL.ops} · ${OPS_KIND_LABEL[row.opsKind]}`; } return COST_DIM_LABEL[row.costDim]; } export type DimFilter = | { dim: CostDim; sub?: string } | null; export function applyDimFilter(rows: H2OrderRow[], filter: DimFilter): H2OrderRow[] { if (!filter) return rows; return rows.filter((r) => { if (r.borneBy !== 'company') return false; if (r.costDim !== filter.dim) return false; if (!filter.sub) return true; if (filter.dim === 'lease') return r.leaseKind === filter.sub; if (filter.dim === 'ops') return r.opsKind === filter.sub; return true; }); } /** 统计/明细共用:维度筛后的我司成本行;无维度筛则全部我司行 */ export function companyRowsForStats(rows: H2OrderRow[], filter: DimFilter): H2OrderRow[] { if (!filter) return companyCostRows(rows); return applyDimFilter(rows, filter); } export interface StationMonthRow { stationId: string; stationName: string; month: string; amount: number; quantityKg: number; unverifiedAmount: number; } export function stationMonthAgg(rows: H2OrderRow[]): StationMonthRow[] { const map = new Map(); rows.forEach((r) => { const month = r.occurredAt.slice(0, 7); const key = `${r.stationId}|${month}`; const list = map.get(key) ?? []; list.push(r); map.set(key, list); }); return Array.from(map.entries()) .map(([key, list]) => { const [stationId, month] = key.split('|'); return { stationId, stationName: list[0].stationName, month, amount: sumAmount(list), quantityKg: sumKg(list), unverifiedAmount: sumAmount(list.filter((x) => x.verifyStatus === 'unverified')), }; }) .sort((a, b) => b.amount - a.amount); } export interface CustomerAttrRow { customerId: string; customerName: string; borneLabel: string; quantityKg: number; companyCost: number; unverifiedAmount: number; } export function customerAttrAgg(rows: H2OrderRow[]): CustomerAttrRow[] { const map = new Map(); rows.forEach((r) => { const list = map.get(r.customerId) ?? []; list.push(r); map.set(r.customerId, list); }); return Array.from(map.entries()) .map(([customerId, list]) => { const company = list.filter((x) => x.borneBy === 'company'); const customer = list.filter((x) => x.borneBy === 'customer'); let borneLabel = '混合'; if (company.length && !customer.length) borneLabel = '我司'; else if (customer.length && !company.length) borneLabel = '客户'; return { customerId, customerName: list[0].customerName, borneLabel, quantityKg: sumKg(list), companyCost: sumAmount(company), unverifiedAmount: sumAmount(list.filter((x) => x.verifyStatus === 'unverified')), }; }) .sort((a, b) => b.companyCost - a.companyCost || b.quantityKg - a.quantityKg); } export const SOURCE_LABEL: Record = { api: 'API', manual: '补录', fence: '围栏', }; /** 总览 KPI:在宿主示意量级上按当前筛选(年/车辆/核对)等比缩放,保证卡片跟随顶栏筛选 */ export function computeHostKpi( filtered: H2OrderRow[], year: number, allOrders: H2OrderRow[], base: { totalKgT: number; companyKgT: number; customerKgT: number; totalFeeWan: number; companyFeeWan: number; customerFeeWan: number; profitWan: number; incomeWan: number; costWan: number; monthKgT: number; monthFeeWan: number; monthYearPct: number; dayKg: number; dayFee: number; dayMonthPct: number; }, ) { const round2 = (n: number) => Math.round(n * 100) / 100; const baseline = filterOrders(allOrders, year, 'all', 'all'); const baseKg = sumKg(baseline) || 1; const fKg = sumKg(filtered); const ratio = fKg / baseKg; const companyKg = sumKg(filtered.filter((r) => r.borneBy === 'company')); const customerKg = sumKg(filtered.filter((r) => r.borneBy === 'customer')); const split = companyKg + customerKg || 1; const companyShare = companyKg / split; const customerShare = customerKg / split; const monthRows = filtered.filter((r) => r.occurredAt.startsWith(`${year}-08`)); const dayRows = filtered.filter((r) => r.occurredAt.startsWith(`${year}-08-08`)); const monthKg = sumKg(monthRows); const dayKgVal = sumKg(dayRows); const monthAmt = sumAmount(monthRows); const dayAmt = sumAmount(dayRows); const yearKg = fKg || 1; const monthKgShare = monthKg / yearKg; const dayMonthShare = monthKg > 0 ? dayKgVal / monthKg : 0; const totalKgT = round2(base.totalKgT * ratio); const totalFeeWan = round2(base.totalFeeWan * ratio); const incomeWan = round2(base.incomeWan * ratio); const costWan = round2(base.costWan * ratio); const profitWan = round2(base.profitWan * ratio); const monthKgT = round2(totalKgT * monthKgShare); const monthFeeWan = round2(totalFeeWan * monthKgShare); return { totalKgT, companyKgT: round2(totalKgT * companyShare), customerKgT: round2(totalKgT * customerShare), totalFeeWan, companyFeeWan: round2(totalFeeWan * companyShare), customerFeeWan: round2(totalFeeWan * customerShare), profitWan, incomeWan, costWan, monthKgT, monthFeeWan, monthYearPct: round2(monthKgShare * 100), dayKg: round2(dayKgVal > 0 ? dayKgVal : base.dayKg * ratio * Math.max(dayMonthShare, 0.01)), dayFee: Math.round(dayAmt > 0 ? dayAmt : base.dayFee * ratio * Math.max(dayMonthShare, 0.01)), dayMonthPct: round2((dayMonthShare || base.dayMonthPct / 100) * 100), profitRatePct: incomeWan > 0 ? round2((profitWan / incomeWan) * 100) : 0, }; }