feat: add ETC records and bill drilldowns
This commit is contained in:
@@ -9,10 +9,14 @@ import {
|
||||
parseHydrogenCustomerName,
|
||||
parseHydrogenStationId,
|
||||
parseEnergyDate,
|
||||
parseEtcLimit,
|
||||
parseEtcPage,
|
||||
parseEtcSearch,
|
||||
type HydrogenCustomerKind,
|
||||
} from './query.js';
|
||||
import type { AuthUser } from '../../auth/types.js';
|
||||
import { canAccessEnergy } from '../../auth/types.js';
|
||||
import { observeDataSource } from '../../source-telemetry.js';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
@@ -867,4 +871,194 @@ app.get('/etc/overview', async (c) => {
|
||||
return c.json(data);
|
||||
});
|
||||
|
||||
app.get('/etc/records', async (c) => {
|
||||
const page = parseEtcPage(c.req.query('page'));
|
||||
const limit = parseEtcLimit(c.req.query('limit'));
|
||||
const search = parseEtcSearch(c.req.query('search'));
|
||||
if (search === null) return c.json({ error: 'search 最多 128 个字符' }, 400);
|
||||
|
||||
const startParam = c.req.query('startDate');
|
||||
const endParam = c.req.query('endDate');
|
||||
const parsedStart = startParam === undefined ? undefined : parseEnergyDate(startParam);
|
||||
const parsedEnd = endParam === undefined ? undefined : parseEnergyDate(endParam);
|
||||
if (parsedStart === null || parsedEnd === null) {
|
||||
return c.json({ error: 'startDate/endDate 必须是有效的 YYYY-MM-DD 日期' }, 400);
|
||||
}
|
||||
const startDate = parsedStart && parsedEnd && parsedStart > parsedEnd ? parsedEnd : parsedStart;
|
||||
const endDate = parsedStart && parsedEnd && parsedStart > parsedEnd ? parsedStart : parsedEnd;
|
||||
|
||||
const where = ["del_flag = '0'"];
|
||||
const params: Array<string | number> = [];
|
||||
if (startDate) {
|
||||
where.push('trans_time >= ?');
|
||||
params.push(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
where.push('trans_time < DATE_ADD(?, INTERVAL 1 DAY)');
|
||||
params.push(endDate);
|
||||
}
|
||||
if (search) {
|
||||
where.push('(plate_number LIKE ? OR customer_name LIKE ? OR record_code LIKE ?)');
|
||||
const pattern = `%${search}%`;
|
||||
params.push(pattern, pattern, pattern);
|
||||
}
|
||||
const whereSql = where.join(' AND ');
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const [[summaryRows], [detailRows]] = await observeDataSource('etcDatabase', () => Promise.all([
|
||||
pool.query<RowDataPacket[]>(
|
||||
`SELECT COUNT(*) AS total,
|
||||
SUM(toll_amount) AS tollAmount,
|
||||
SUM(service_fee) AS serviceFee,
|
||||
SUM(total_amount) AS totalAmount
|
||||
FROM etc_toll_record
|
||||
WHERE ${whereSql}`,
|
||||
params,
|
||||
),
|
||||
pool.query<RowDataPacket[]>(
|
||||
`SELECT id,
|
||||
record_code AS recordCode,
|
||||
plate_number AS plate,
|
||||
customer_name AS customerName,
|
||||
entry_station_name AS entryStation,
|
||||
exit_station_name AS exitStation,
|
||||
DATE_FORMAT(trans_time, '%Y-%m-%d %H:%i:%s') AS transTime,
|
||||
toll_amount AS tollAmount,
|
||||
service_fee AS serviceFee,
|
||||
total_amount AS totalAmount,
|
||||
cost_type AS costType,
|
||||
contract_matched AS contractMatched,
|
||||
review_status AS reviewStatus
|
||||
FROM etc_toll_record
|
||||
WHERE ${whereSql}
|
||||
ORDER BY trans_time DESC, id DESC
|
||||
LIMIT ? OFFSET ?`,
|
||||
[...params, limit, offset],
|
||||
),
|
||||
]));
|
||||
|
||||
const summary = summaryRows[0] ?? {};
|
||||
const total = Number(summary.total) || 0;
|
||||
return c.json({
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.max(1, Math.ceil(total / limit)),
|
||||
filters: { startDate: startDate || null, endDate: endDate || null, search },
|
||||
summary: {
|
||||
tollAmount: Math.round((Number(summary.tollAmount) || 0) * 100) / 100,
|
||||
serviceFee: Math.round((Number(summary.serviceFee) || 0) * 100) / 100,
|
||||
totalAmount: Math.round((Number(summary.totalAmount) || 0) * 100) / 100,
|
||||
},
|
||||
items: detailRows.map(row => ({
|
||||
id: Number(row.id),
|
||||
recordCode: String(row.recordCode),
|
||||
plate: String(row.plate),
|
||||
customerName: row.customerName ? String(row.customerName) : null,
|
||||
entryStation: row.entryStation ? String(row.entryStation) : null,
|
||||
exitStation: row.exitStation ? String(row.exitStation) : null,
|
||||
transTime: String(row.transTime),
|
||||
tollAmount: Math.round((Number(row.tollAmount) || 0) * 100) / 100,
|
||||
serviceFee: Math.round((Number(row.serviceFee) || 0) * 100) / 100,
|
||||
totalAmount: Math.round((Number(row.totalAmount) || 0) * 100) / 100,
|
||||
costType: Number(row.costType) || 0,
|
||||
contractMatched: Number(row.contractMatched) === 1,
|
||||
reviewStatus: Number(row.reviewStatus) || 0,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/etc/bills', async (c) => {
|
||||
const page = parseEtcPage(c.req.query('page'));
|
||||
const limit = parseEtcLimit(c.req.query('limit'));
|
||||
const search = parseEtcSearch(c.req.query('search'));
|
||||
if (search === null) return c.json({ error: 'search 最多 128 个字符' }, 400);
|
||||
|
||||
const startParam = c.req.query('startDate');
|
||||
const endParam = c.req.query('endDate');
|
||||
const parsedStart = startParam === undefined ? undefined : parseEnergyDate(startParam);
|
||||
const parsedEnd = endParam === undefined ? undefined : parseEnergyDate(endParam);
|
||||
if (parsedStart === null || parsedEnd === null) {
|
||||
return c.json({ error: 'startDate/endDate 必须是有效的 YYYY-MM-DD 日期' }, 400);
|
||||
}
|
||||
const startDate = parsedStart && parsedEnd && parsedStart > parsedEnd ? parsedEnd : parsedStart;
|
||||
const endDate = parsedStart && parsedEnd && parsedStart > parsedEnd ? parsedStart : parsedEnd;
|
||||
|
||||
const where = ["del_flag = '0'"];
|
||||
const params: Array<string | number> = [];
|
||||
if (startDate) {
|
||||
where.push('bill_period_end >= ?');
|
||||
params.push(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
where.push('bill_period_start <= ?');
|
||||
params.push(endDate);
|
||||
}
|
||||
if (search) {
|
||||
where.push('(customer_name LIKE ? OR bill_code LIKE ?)');
|
||||
const pattern = `%${search}%`;
|
||||
params.push(pattern, pattern);
|
||||
}
|
||||
const whereSql = where.join(' AND ');
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const [[summaryRows], [detailRows]] = await observeDataSource('etcDatabase', () => Promise.all([
|
||||
pool.query<RowDataPacket[]>(
|
||||
`SELECT COUNT(*) AS total,
|
||||
SUM(receivable_amount) AS receivableAmount,
|
||||
SUM(paid_amount) AS paidAmount
|
||||
FROM energy_etc_bill
|
||||
WHERE ${whereSql}`,
|
||||
params,
|
||||
),
|
||||
pool.query<RowDataPacket[]>(
|
||||
`SELECT id,
|
||||
bill_code AS billCode,
|
||||
customer_name AS customerName,
|
||||
DATE_FORMAT(bill_period_start, '%Y-%m-%d') AS periodStart,
|
||||
DATE_FORMAT(bill_period_end, '%Y-%m-%d') AS periodEnd,
|
||||
total_toll_count AS tollCount,
|
||||
total_toll_amount AS tollAmount,
|
||||
total_service_fee AS serviceFee,
|
||||
receivable_amount AS receivableAmount,
|
||||
paid_amount AS paidAmount,
|
||||
payment_status AS paymentStatus,
|
||||
review_status AS reviewStatus
|
||||
FROM energy_etc_bill
|
||||
WHERE ${whereSql}
|
||||
ORDER BY bill_period_end DESC, id DESC
|
||||
LIMIT ? OFFSET ?`,
|
||||
[...params, limit, offset],
|
||||
),
|
||||
]));
|
||||
|
||||
const summary = summaryRows[0] ?? {};
|
||||
const total = Number(summary.total) || 0;
|
||||
return c.json({
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.max(1, Math.ceil(total / limit)),
|
||||
filters: { startDate: startDate || null, endDate: endDate || null, search },
|
||||
summary: {
|
||||
receivableAmount: Math.round((Number(summary.receivableAmount) || 0) * 100) / 100,
|
||||
paidAmount: Math.round((Number(summary.paidAmount) || 0) * 100) / 100,
|
||||
},
|
||||
items: detailRows.map(row => ({
|
||||
id: Number(row.id),
|
||||
billCode: String(row.billCode),
|
||||
customerName: row.customerName ? String(row.customerName) : null,
|
||||
periodStart: String(row.periodStart),
|
||||
periodEnd: String(row.periodEnd),
|
||||
tollCount: Number(row.tollCount) || 0,
|
||||
tollAmount: Math.round((Number(row.tollAmount) || 0) * 100) / 100,
|
||||
serviceFee: Math.round((Number(row.serviceFee) || 0) * 100) / 100,
|
||||
receivableAmount: Math.round((Number(row.receivableAmount) || 0) * 100) / 100,
|
||||
paidAmount: Math.round((Number(row.paidAmount) || 0) * 100) / 100,
|
||||
paymentStatus: Number(row.paymentStatus) || 0,
|
||||
reviewStatus: Number(row.reviewStatus) || 0,
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
||||
Reference in New Issue
Block a user