import type { HydrogenDailyRow } from '../types'; export { formatYmd, getQuickRange, getRangeModeLabel, normalizeRange, QUICK_PICK_OPTIONS, type RangeMode, } from '../daily-range/model'; export function summarizeHydrogenRows(rows: HydrogenDailyRow[] | null) { const source = rows ?? []; // 图表固定按日期升序;复制数组避免改变接口返回及表格原始顺序。 const trendData = [...source].sort((left, right) => left.date.localeCompare(right.date)); const totalKg = source.reduce((total, row) => total + row.totalKg, 0); const totalFee = source.reduce((total, row) => total + row.totalFee, 0); const activeDays = source.filter(row => row.totalKg > 0).length; const stationIds = new Set(); source.forEach(row => row.stations.forEach(station => stationIds.add(station.id))); const peakDay = trendData.reduce( (best, item) => (!best || item.totalKg > best.totalKg ? item : best), null, ); const lowDay = trendData .filter(item => item.totalKg > 0) .reduce( (low, item) => (!low || item.totalKg < low.totalKg ? item : low), null, ); return { trendData, totalKg, totalFee, activeDays, stationCount: stationIds.size, avgKg: activeDays > 0 ? totalKg / activeDays : 0, avgPrice: totalKg > 0 ? totalFee / totalKg : 0, peakDay, lowDay, zeroDays: source.filter(row => row.totalKg === 0).length, }; } export interface HydrogenDailyStationOption { id: number; name: string; totalKg: number; totalFee: number; } export type HydrogenDailyVehicleScope = 'all' | 'lingniu' | 'external'; export type HydrogenDailyBoardScope = 'global' | 'station'; export interface HydrogenDailyTrendPoint { date: string; totalKg: number; lingniuKg: number; externalKg: number; } function round2(value: number): number { return Math.round(value * 100) / 100; } /** * “全部车辆”由羚牛与外部车辆两次真实查询合并得到。合并只累加账本 * 已返回的加氢量和成本,不引入原型里的演示数据。 */ export function mergeHydrogenDailyRows( lingniuRows: HydrogenDailyRow[] | null, externalRows: HydrogenDailyRow[] | null, ): HydrogenDailyRow[] | null { if (lingniuRows === null || externalRows === null) return null; const rowsByDate = new Map(); for (const row of [...lingniuRows, ...externalRows]) { const rows = rowsByDate.get(row.date) ?? []; rows.push(row); rowsByDate.set(row.date, rows); } const merged = [...rowsByDate.entries()].map(([date, rows]) => { const stationsById = new Map(); for (const row of rows) { for (const station of row.stations) { const current = stationsById.get(station.id); const kg = round2((current?.kg ?? 0) + station.kg); const fee = round2((current?.fee ?? 0) + station.fee); stationsById.set(station.id, { id: station.id, name: station.name || current?.name || `站点 #${station.id}`, kg, fee, // 跨车辆归属聚合后展示实际成本加权均价。 pricePerKg: kg > 0 ? round2(fee / kg) : 0, chainPct: 0, }); } } const stations = [...stationsById.values()].sort((left, right) => right.kg - left.kg); return { date, totalKg: round2(stations.reduce((sum, station) => sum + station.kg, 0)), totalFee: round2(stations.reduce((sum, station) => sum + station.fee, 0)), chainPct: 0, customerType: 'lingniu' as const, stations, }; }); return recomputeHydrogenDailyChains(merged); } function recomputeHydrogenDailyChains(rows: HydrogenDailyRow[]): HydrogenDailyRow[] { const ascending = [...rows] .map(row => ({ ...row, stations: row.stations.map(station => ({ ...station })) })) .sort((left, right) => left.date.localeCompare(right.date)); let previousTotalKg = 0; const stationPreviousKg = new Map(); for (const row of ascending) { row.chainPct = previousTotalKg > 0 ? (row.totalKg - previousTotalKg) / previousTotalKg : 0; previousTotalKg = row.totalKg; for (const station of row.stations) { const previousStationKg = stationPreviousKg.get(station.id) ?? 0; station.chainPct = previousStationKg > 0 ? (station.kg - previousStationKg) / previousStationKg : 0; stationPreviousKg.set(station.id, station.kg); } } return ascending.sort((left, right) => right.date.localeCompare(left.date)); } /** Build one stable station selector from the current date range. */ export function getHydrogenDailyStations(rows: HydrogenDailyRow[] | null): HydrogenDailyStationOption[] { const stations = new Map(); for (const row of rows ?? []) { for (const station of row.stations) { const current = stations.get(station.id); stations.set(station.id, { id: station.id, name: station.name, totalKg: (current?.totalKg ?? 0) + station.kg, totalFee: (current?.totalFee ?? 0) + station.fee, }); } } return [...stations.values()].sort((left, right) => right.totalKg - left.totalKg || left.name.localeCompare(right.name)); } /** * Daily endpoint returns all stations in the selected date range. This keeps * station drill-down local and recomputes each day's totals and chain change. */ export function filterHydrogenRowsByStation( rows: HydrogenDailyRow[] | null, stationId: number | null, ): HydrogenDailyRow[] | null { if (rows === null || stationId === null) return rows; const filtered = rows.map(row => { const stations = row.stations.filter(station => station.id === stationId); return { ...row, totalKg: stations.reduce((sum, station) => sum + station.kg, 0), totalFee: stations.reduce((sum, station) => sum + station.fee, 0), stations, }; }); return recomputeHydrogenDailyChains(filtered); } /** Build the prototype's blue/orange stacked daily series from real queries. */ export function buildHydrogenDailyTrend( lingniuRows: HydrogenDailyRow[] | null, externalRows: HydrogenDailyRow[] | null, vehicleScope: HydrogenDailyVehicleScope, stationId: number | null, ): HydrogenDailyTrendPoint[] { if (lingniuRows === null || externalRows === null) return []; const lingniu = filterHydrogenRowsByStation(lingniuRows, stationId) ?? []; const external = filterHydrogenRowsByStation(externalRows, stationId) ?? []; const byDate = new Map(); for (const row of lingniu) { byDate.set(row.date, { date: row.date, lingniuKg: vehicleScope === 'external' ? 0 : row.totalKg, externalKg: 0, totalKg: vehicleScope === 'external' ? 0 : row.totalKg, }); } for (const row of external) { const current = byDate.get(row.date) ?? { date: row.date, lingniuKg: 0, externalKg: 0, totalKg: 0, }; const externalKg = vehicleScope === 'lingniu' ? 0 : row.totalKg; current.externalKg = externalKg; current.totalKg = round2(current.lingniuKg + externalKg); byDate.set(row.date, current); } return [...byDate.values()].sort((left, right) => left.date.localeCompare(right.date)); }