209 lines
7.2 KiB
TypeScript
209 lines
7.2 KiB
TypeScript
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<number>();
|
|
source.forEach(row => row.stations.forEach(station => stationIds.add(station.id)));
|
|
const peakDay = trendData.reduce<HydrogenDailyRow | null>(
|
|
(best, item) => (!best || item.totalKg > best.totalKg ? item : best),
|
|
null,
|
|
);
|
|
const lowDay = trendData
|
|
.filter(item => item.totalKg > 0)
|
|
.reduce<HydrogenDailyRow | null>(
|
|
(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<string, HydrogenDailyRow[]>();
|
|
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<number, HydrogenDailyRow['stations'][number]>();
|
|
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<number, number>();
|
|
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<number, HydrogenDailyStationOption>();
|
|
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<string, HydrogenDailyTrendPoint>();
|
|
|
|
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));
|
|
}
|