ci/woodpecker/push/woodpecker Pipeline was canceled
Co-authored-by: HiFox Agent <agents-noreply@hifox.com>
375 lines
18 KiB
TypeScript
375 lines
18 KiB
TypeScript
import type { Hono } from 'hono';
|
|
import type { RowDataPacket } from 'mysql2';
|
|
import type hydrogenPool from '../../hydrogen-db.js';
|
|
import type { cached } from './cache.js';
|
|
import {
|
|
HYDROGEN_FUEL_ONLY_WHERE,
|
|
HYDROGEN_FUEL_ONLY_WHERE_B,
|
|
HYDROGEN_LOCAL,
|
|
HYDROGEN_MIN_DATE,
|
|
HYDROGEN_TABLE,
|
|
} from './constants.js';
|
|
|
|
export interface HydrogenOverviewDependencies {
|
|
hydrogenPool: Pick<typeof hydrogenPool, 'query'>;
|
|
cached: typeof cached;
|
|
}
|
|
|
|
type VehicleScope = 'all' | 'lingniu' | 'external';
|
|
type VerifyScope = 'all' | 'verified';
|
|
|
|
interface OverviewFilter {
|
|
cacheKey: string;
|
|
ledgerSql: string;
|
|
billSql: string;
|
|
params: number[];
|
|
stationId: number | null;
|
|
vehicleScope: VehicleScope;
|
|
verifyScope: VerifyScope;
|
|
}
|
|
|
|
function overviewFilter(
|
|
stationIdParam: string | undefined,
|
|
vehicleScopeParam: string | undefined,
|
|
verifyScopeParam: string | undefined,
|
|
): OverviewFilter {
|
|
const stationId = Number(stationIdParam);
|
|
const safeStationId = Number.isInteger(stationId) && stationId > 0 ? stationId : null;
|
|
const vehicleScope: VehicleScope = vehicleScopeParam === 'lingniu' || vehicleScopeParam === 'external'
|
|
? vehicleScopeParam
|
|
: 'all';
|
|
const verifyScope: VerifyScope = verifyScopeParam === 'verified' ? 'verified' : 'all';
|
|
const clauses: string[] = [];
|
|
const params: number[] = [];
|
|
|
|
if (safeStationId) {
|
|
clauses.push('station_id = ?');
|
|
params.push(safeStationId);
|
|
}
|
|
if (vehicleScope === 'lingniu') clauses.push('vehicle_id IS NOT NULL');
|
|
if (vehicleScope === 'external') clauses.push('vehicle_id IS NULL');
|
|
// Only use the ledger's persisted verification state; never infer verification
|
|
// from import source or payment status.
|
|
if (verifyScope === 'verified') {
|
|
clauses.push("LOWER(COALESCE(NULLIF(TRIM(verify_status), ''), 'unverified')) = 'verified'");
|
|
}
|
|
|
|
const suffix = clauses.length ? ` AND ${clauses.join(' AND ')}` : '';
|
|
const billSuffix = suffix
|
|
.replaceAll('station_id', 'b.station_id')
|
|
.replaceAll('vehicle_id', 'b.vehicle_id')
|
|
.replaceAll('verify_status', 'b.verify_status');
|
|
const cacheParts = [
|
|
safeStationId ? `station=${safeStationId}` : '',
|
|
vehicleScope !== 'all' ? `vehicle=${vehicleScope}` : '',
|
|
verifyScope !== 'all' ? `verify=${verifyScope}` : '',
|
|
].filter(Boolean);
|
|
|
|
return {
|
|
cacheKey: cacheParts.length ? `&${cacheParts.join('&')}` : '',
|
|
ledgerSql: suffix,
|
|
billSql: billSuffix,
|
|
params,
|
|
stationId: safeStationId,
|
|
vehicleScope,
|
|
verifyScope,
|
|
};
|
|
}
|
|
|
|
// 氢能总览:KPI + 趋势 + 站点/区域/客户汇总。筛选只使用账本中可验证的站点和车辆归属字段。
|
|
export function registerHydrogenOverviewRoute(
|
|
app: Hono,
|
|
{ hydrogenPool, cached }: HydrogenOverviewDependencies,
|
|
) {
|
|
app.get('/hydrogen/overview', async (c) => {
|
|
const yearParam = c.req.query('year');
|
|
const force = c.req.query('force') === '1';
|
|
const filter = overviewFilter(c.req.query('stationId'), c.req.query('vehicleScope'), c.req.query('verifyScope'));
|
|
const today = new Date();
|
|
const todayYear = today.getFullYear();
|
|
const requestedYear = yearParam ? Number(yearParam) || todayYear : todayYear;
|
|
|
|
const data = await cached(`hydrogen/overview?year=${requestedYear}${filter.cacheKey}`, async () => {
|
|
// 可选年份(数据自 HYDROGEN_MIN_DATE 起)
|
|
const [yearListRows] = await hydrogenPool.query<RowDataPacket[]>(
|
|
`SELECT YEAR(${HYDROGEN_LOCAL}) AS y,
|
|
DATE_FORMAT(MAX(${HYDROGEN_LOCAL}), '%Y-%m-%d %H:%i:%s') AS latestLedgerTime
|
|
FROM ${HYDROGEN_TABLE}
|
|
WHERE ${HYDROGEN_FUEL_ONLY_WHERE} AND ${HYDROGEN_LOCAL} >= ?
|
|
GROUP BY YEAR(${HYDROGEN_LOCAL})
|
|
ORDER BY y DESC`,
|
|
[HYDROGEN_MIN_DATE],
|
|
);
|
|
const availableYears = yearListRows.map(r => Number(r.y)).filter(y => y > 0);
|
|
// 数据源时间来自账本,不等同于前端请求完成时间。
|
|
const year = availableYears.includes(requestedYear) ? requestedYear : (availableYears[0] ?? todayYear);
|
|
// The watermark is calculated with the active scope below. The year list
|
|
// remains global metadata so users can still select a year with no matching
|
|
// verified/vehicle/station records.
|
|
let latestLedgerTime: string | null = null;
|
|
const isCurrentYear = year === todayYear;
|
|
|
|
// KPI(按 year 分桶;月/日仅在 isCurrentYear 时取本月/今日)。
|
|
// yearProfit 是客户单毛利:仅以存在对客价/对客收入的订单成本抵扣对客收入。
|
|
const [kpiRows] = await hydrogenPool.query<RowDataPacket[]>(
|
|
`SELECT DATE_FORMAT(MAX(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? THEN ${HYDROGEN_LOCAL} END), '%Y-%m-%d %H:%i:%s') AS latestLedgerTime,
|
|
SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ?
|
|
THEN amount_kg ELSE 0 END) AS yearKg,
|
|
SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ?
|
|
THEN cost_total ELSE 0 END) AS yearFee,
|
|
SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? AND (COALESCE(customer_price, 0) > 0 OR COALESCE(fee_total, 0) > 0)
|
|
THEN cost_total ELSE 0 END) AS yearCustomerCost,
|
|
SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ?
|
|
THEN fee_total ELSE 0 END) AS yearRevenue,
|
|
SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? AND COALESCE(customer_price, 0) <= 0 AND COALESCE(fee_total, 0) <= 0
|
|
THEN amount_kg ELSE 0 END) AS ourYearKg,
|
|
SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? AND COALESCE(customer_price, 0) <= 0 AND COALESCE(fee_total, 0) <= 0
|
|
THEN cost_total ELSE 0 END) AS ourYearFee,
|
|
SUM(CASE WHEN YEAR(${HYDROGEN_LOCAL}) = ? AND (COALESCE(customer_price, 0) > 0 OR COALESCE(fee_total, 0) > 0)
|
|
THEN amount_kg ELSE 0 END) AS customerYearKg,
|
|
SUM(CASE WHEN ? = 1 AND DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
|
|
THEN amount_kg ELSE 0 END) AS monthKg,
|
|
SUM(CASE WHEN ? = 1 AND DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
|
|
THEN cost_total ELSE 0 END) AS monthFee,
|
|
SUM(CASE WHEN ? = 1 AND DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
|
|
AND (COALESCE(customer_price, 0) > 0 OR COALESCE(fee_total, 0) > 0)
|
|
THEN cost_total ELSE 0 END) AS monthCustomerCost,
|
|
SUM(CASE WHEN ? = 1 AND DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
|
|
THEN fee_total ELSE 0 END) AS monthRevenue,
|
|
SUM(CASE WHEN ? = 1 AND DATE(${HYDROGEN_LOCAL}) = CURDATE()
|
|
THEN amount_kg ELSE 0 END) AS todayKg,
|
|
SUM(CASE WHEN ? = 1 AND DATE(${HYDROGEN_LOCAL}) = CURDATE()
|
|
THEN cost_total ELSE 0 END) AS todayFee,
|
|
SUM(CASE WHEN ? = 1 AND DATE(${HYDROGEN_LOCAL}) = CURDATE()
|
|
AND (COALESCE(customer_price, 0) > 0 OR COALESCE(fee_total, 0) > 0)
|
|
THEN cost_total ELSE 0 END) AS todayCustomerCost,
|
|
SUM(CASE WHEN ? = 1 AND DATE(${HYDROGEN_LOCAL}) = CURDATE()
|
|
THEN fee_total ELSE 0 END) AS todayRevenue,
|
|
SUM(CASE WHEN vehicle_id IS NOT NULL
|
|
THEN amount_kg ELSE 0 END) AS lingniuBornKg,
|
|
SUM(CASE WHEN vehicle_id IS NOT NULL
|
|
THEN cost_total ELSE 0 END) AS lingniuBornFee
|
|
FROM ${HYDROGEN_TABLE}
|
|
WHERE ${HYDROGEN_FUEL_ONLY_WHERE} AND ${HYDROGEN_LOCAL} >= ?${filter.ledgerSql}`,
|
|
[year, year, year, year, year, year, year, year,
|
|
isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0,
|
|
isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0, isCurrentYear ? 1 : 0,
|
|
HYDROGEN_MIN_DATE, ...filter.params],
|
|
);
|
|
const k = kpiRows[0] ?? {};
|
|
latestLedgerTime = typeof k.latestLedgerTime === 'string' ? k.latestLedgerTime : null;
|
|
const yearFee = Number(k.yearFee) || 0;
|
|
const yearCustomerCost = Number(k.yearCustomerCost) || 0;
|
|
const yearRevenue = Number(k.yearRevenue) || 0;
|
|
const monthFee = Number(k.monthFee) || 0;
|
|
const monthCustomerCost = Number(k.monthCustomerCost) || 0;
|
|
const monthRevenue = Number(k.monthRevenue) || 0;
|
|
const todayFee = Number(k.todayFee) || 0;
|
|
const todayCustomerCost = Number(k.todayCustomerCost) || 0;
|
|
const todayRevenue = Number(k.todayRevenue) || 0;
|
|
const kpi = {
|
|
yearKg: Number(k.yearKg) || 0,
|
|
yearFee,
|
|
yearRevenue,
|
|
yearProfit: yearRevenue - yearCustomerCost,
|
|
ourYearKg: Number(k.ourYearKg) || 0,
|
|
ourYearFee: Number(k.ourYearFee) || 0,
|
|
customerYearKg: Number(k.customerYearKg) || 0,
|
|
monthKg: Number(k.monthKg) || 0,
|
|
monthFee,
|
|
monthRevenue,
|
|
monthProfit: monthRevenue - monthCustomerCost,
|
|
todayKg: Number(k.todayKg) || 0,
|
|
todayFee,
|
|
todayRevenue,
|
|
todayProfit: todayRevenue - todayCustomerCost,
|
|
lingniuBornKg: Number(k.lingniuBornKg) || 0,
|
|
lingniuBornFee: Number(k.lingniuBornFee) || 0,
|
|
};
|
|
|
|
// Top5 加氢站(指定年份)
|
|
const [top5Rows] = await hydrogenPool.query<RowDataPacket[]>(
|
|
`SELECT b.station_id AS id,
|
|
COALESCE(MAX(s.station_short_name), MAX(s.station_name), MAX(b.station_name),
|
|
CASE WHEN b.station_id IS NULL THEN '未关联站点'
|
|
ELSE CONCAT('未知站点 #', b.station_id) END) AS name,
|
|
SUM(b.amount_kg) AS kg,
|
|
SUM(b.cost_total) AS fee
|
|
FROM ${HYDROGEN_TABLE} b
|
|
LEFT JOIN hydrogen_station s ON s.id = b.station_id AND s.del_flag = '0'
|
|
WHERE ${HYDROGEN_FUEL_ONLY_WHERE_B}
|
|
AND b.${HYDROGEN_LOCAL} >= ?
|
|
AND YEAR(b.${HYDROGEN_LOCAL}) = ?${filter.billSql}
|
|
GROUP BY b.station_id
|
|
ORDER BY kg DESC
|
|
LIMIT 5`,
|
|
[HYDROGEN_MIN_DATE, year, ...filter.params],
|
|
);
|
|
const top5KgSum = kpi.yearKg || 1;
|
|
const top5 = top5Rows.map((r, i) => ({
|
|
rank: i + 1,
|
|
id: Number(r.id) || 0,
|
|
name: r.name as string,
|
|
kg: Number(r.kg) || 0,
|
|
fee: Number(r.fee) || 0,
|
|
share: (Number(r.kg) || 0) / top5KgSum,
|
|
}));
|
|
|
|
// 加氢站全量汇总(同年所有站,按加氢量降序)
|
|
const [stationFullRows] = await hydrogenPool.query<RowDataPacket[]>(
|
|
`SELECT b.station_id AS id,
|
|
COALESCE(MAX(s.station_short_name), MAX(s.station_name), MAX(b.station_name),
|
|
CASE WHEN b.station_id IS NULL THEN '未关联站点'
|
|
ELSE CONCAT('未知站点 #', b.station_id) END) AS name,
|
|
COALESCE(MAX(d.NAME), '未归属') AS province,
|
|
SUM(b.amount_kg) AS kg,
|
|
SUM(b.fee_total) AS revenue
|
|
FROM ${HYDROGEN_TABLE} b
|
|
LEFT JOIN hydrogen_station s ON s.id = b.station_id AND s.del_flag = '0'
|
|
LEFT JOIN common_district d
|
|
ON CONVERT(d.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci
|
|
= CONVERT(s.province USING utf8mb4) COLLATE utf8mb4_unicode_ci
|
|
AND d.STATUS = 'VALID'
|
|
WHERE ${HYDROGEN_FUEL_ONLY_WHERE_B}
|
|
AND b.${HYDROGEN_LOCAL} >= ?
|
|
AND YEAR(b.${HYDROGEN_LOCAL}) = ?${filter.billSql}
|
|
GROUP BY b.station_id
|
|
ORDER BY kg DESC`,
|
|
[HYDROGEN_MIN_DATE, year, ...filter.params],
|
|
);
|
|
const stationKgSum = stationFullRows.reduce((s, r) => s + (Number(r.kg) || 0), 0) || 1;
|
|
const stationRevSum = stationFullRows.reduce((s, r) => s + (Number(r.revenue) || 0), 0) || 1;
|
|
const stations = stationFullRows.map(r => ({
|
|
id: Number(r.id) || 0,
|
|
name: r.name as string,
|
|
province: r.province as string,
|
|
kg: Number(r.kg) || 0,
|
|
revenue: Number(r.revenue) || 0,
|
|
share: (Number(r.kg) || 0) / stationKgSum,
|
|
revenueShare: (Number(r.revenue) || 0) / stationRevSum,
|
|
}));
|
|
|
|
// 区域占比(按城市,指定年份)— 取前 8,其余合并为"其他"
|
|
const [regionRows] = await hydrogenPool.query<RowDataPacket[]>(
|
|
`SELECT region, SUM(kg) AS kg FROM (
|
|
SELECT CASE
|
|
WHEN NULLIF(TRIM(d.NAME), '') IS NOT NULL THEN d.NAME
|
|
WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%嘉兴%' OR COALESCE(s.station_name, b.station_name, '') LIKE '%平湖%' THEN '嘉兴'
|
|
WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%广州%' THEN '广州'
|
|
WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%佛山%' THEN '佛山'
|
|
WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%成都%' THEN '成都'
|
|
WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%重庆%' THEN '重庆'
|
|
WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%乌鲁木齐%' THEN '乌鲁木齐'
|
|
WHEN COALESCE(s.station_name, b.station_name, '') LIKE '%昆山%' THEN '昆山'
|
|
ELSE '未归属区域'
|
|
END AS region,
|
|
b.amount_kg AS kg
|
|
FROM ${HYDROGEN_TABLE} b
|
|
LEFT JOIN hydrogen_station s ON s.id = b.station_id AND s.del_flag = '0'
|
|
-- 历史站点城市编码与行政区字典的排序规则不同,显式统一避免线上连接报错。
|
|
LEFT JOIN common_district d
|
|
ON CONVERT(d.CODE USING utf8mb4) COLLATE utf8mb4_unicode_ci
|
|
= CONVERT(s.city USING utf8mb4) COLLATE utf8mb4_unicode_ci
|
|
AND d.STATUS = 'VALID'
|
|
WHERE ${HYDROGEN_FUEL_ONLY_WHERE_B}
|
|
AND b.${HYDROGEN_LOCAL} >= ?
|
|
AND YEAR(b.${HYDROGEN_LOCAL}) = ?${filter.billSql}
|
|
) r
|
|
GROUP BY region
|
|
ORDER BY kg DESC`,
|
|
[HYDROGEN_MIN_DATE, year, ...filter.params],
|
|
);
|
|
const totalKg = regionRows.reduce((sum, r) => sum + (Number(r.kg) || 0), 0) || 1;
|
|
const TOP_REGIONS = 8;
|
|
const top = regionRows.slice(0, TOP_REGIONS);
|
|
const restKg = regionRows.slice(TOP_REGIONS).reduce((s, r) => s + (Number(r.kg) || 0), 0);
|
|
const regions = [
|
|
...top.map(r => ({
|
|
region: r.region as string,
|
|
kg: Number(r.kg) || 0,
|
|
share: (Number(r.kg) || 0) / totalKg,
|
|
})),
|
|
...(restKg > 0 ? [{ region: '其他', kg: restKg, share: restKg / totalKg }] : []),
|
|
];
|
|
|
|
// 月度趋势(指定年份内 12 个月,缺失月补 0)含成本/收入/利润
|
|
// 利润 = 客户单收入 - 客户单成本(按 customer_price/fee_total 判断客户承担)
|
|
const [monthRows] = await hydrogenPool.query<RowDataPacket[]>(
|
|
`SELECT DATE_FORMAT(${HYDROGEN_LOCAL}, '%Y-%m') AS m,
|
|
ROUND(SUM(amount_kg), 2) AS kg,
|
|
ROUND(SUM(CASE WHEN vehicle_id IS NOT NULL THEN amount_kg ELSE 0 END), 2) AS lingniuKg,
|
|
ROUND(SUM(CASE WHEN vehicle_id IS NULL THEN amount_kg ELSE 0 END), 2) AS externalKg,
|
|
ROUND(SUM(cost_total), 2) AS fee,
|
|
ROUND(SUM(CASE WHEN COALESCE(customer_price, 0) > 0 OR COALESCE(fee_total, 0) > 0 THEN cost_total ELSE 0 END), 2) AS customerCost,
|
|
ROUND(SUM(fee_total), 2) AS revenue
|
|
FROM ${HYDROGEN_TABLE}
|
|
WHERE ${HYDROGEN_FUEL_ONLY_WHERE}
|
|
AND ${HYDROGEN_LOCAL} >= ?
|
|
AND YEAR(${HYDROGEN_LOCAL}) = ?${filter.ledgerSql}
|
|
GROUP BY m
|
|
ORDER BY m`,
|
|
[HYDROGEN_MIN_DATE, year, ...filter.params],
|
|
);
|
|
const monthMap = new Map<string, { kg: number; lingniuKg: number; externalKg: number; fee: number; revenue: number; customerCost: number }>();
|
|
for (const r of monthRows) {
|
|
monthMap.set(r.m as string, {
|
|
kg: Number(r.kg) || 0,
|
|
lingniuKg: Number(r.lingniuKg) || 0,
|
|
externalKg: Number(r.externalKg) || 0,
|
|
fee: Number(r.fee) || 0,
|
|
revenue: Number(r.revenue) || 0,
|
|
customerCost: Number(r.customerCost) || 0,
|
|
});
|
|
}
|
|
const lastMonth = isCurrentYear ? today.getMonth() + 1 : 12;
|
|
const monthly: { month: string; kg: number; lingniuKg: number; externalKg: number; fee: number; revenue: number; profit: number }[] = [];
|
|
for (let mi = 1; mi <= lastMonth; mi++) {
|
|
const key = `${year}-${String(mi).padStart(2, '0')}`;
|
|
const v = monthMap.get(key) || { kg: 0, lingniuKg: 0, externalKg: 0, fee: 0, revenue: 0, customerCost: 0 };
|
|
monthly.push({ month: key, kg: v.kg, lingniuKg: v.lingniuKg, externalKg: v.externalKg, fee: v.fee, revenue: v.revenue, profit: v.revenue - v.customerCost });
|
|
}
|
|
|
|
// 客户账单 Top(指定年份;按加氢量降序,前 30)。
|
|
// 同一客户可能同时存在客户承担与羚牛承担订单,必须显示为混合,不能按任一条记录覆盖。
|
|
const [customerRows] = await hydrogenPool.query<RowDataPacket[]>(
|
|
`SELECT COALESCE(NULLIF(TRIM(customer_name), ''), '未指定客户') AS name,
|
|
CASE
|
|
WHEN SUM(CASE WHEN COALESCE(customer_price, 0) > 0 OR COALESCE(fee_total, 0) > 0 THEN 1 ELSE 0 END) > 0
|
|
AND SUM(CASE WHEN COALESCE(customer_price, 0) <= 0 AND COALESCE(fee_total, 0) <= 0 THEN 1 ELSE 0 END) > 0 THEN 'mixed'
|
|
WHEN SUM(CASE WHEN COALESCE(customer_price, 0) > 0 OR COALESCE(fee_total, 0) > 0 THEN 1 ELSE 0 END) > 0 THEN 'customer'
|
|
ELSE 'lingniu'
|
|
END AS payer,
|
|
SUM(amount_kg) AS kg,
|
|
SUM(cost_total) AS cost,
|
|
SUM(fee_total) AS revenue
|
|
FROM ${HYDROGEN_TABLE}
|
|
WHERE ${HYDROGEN_FUEL_ONLY_WHERE}
|
|
AND ${HYDROGEN_LOCAL} >= ?
|
|
AND YEAR(${HYDROGEN_LOCAL}) = ?${filter.ledgerSql}
|
|
GROUP BY name
|
|
ORDER BY kg DESC
|
|
LIMIT 30`,
|
|
[HYDROGEN_MIN_DATE, year, ...filter.params],
|
|
);
|
|
const customers = customerRows.map(r => ({
|
|
name: r.name as string,
|
|
payer: (r.payer as string) === 'lingniu'
|
|
? 'lingniu' as const
|
|
: (r.payer as string) === 'mixed'
|
|
? 'mixed' as const
|
|
: 'customer' as const,
|
|
kg: Number(r.kg) || 0,
|
|
cost: Number(r.cost) || 0,
|
|
revenue: Number(r.revenue) || 0,
|
|
}));
|
|
|
|
return { kpi, top5, regions, monthly, customers, stations, availableYears, year, latestLedgerTime, filter: {
|
|
stationId: filter.stationId,
|
|
vehicleScope: filter.vehicleScope,
|
|
verifyScope: filter.verifyScope,
|
|
} };
|
|
}, { force });
|
|
return c.json(data);
|
|
});
|
|
}
|