feat: add ETC records and bill drilldowns
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Loader2, ReceiptText, Route, Search } from 'lucide-react';
|
||||
import Blur from '../../components/Blur';
|
||||
import { fetchEtcBills, fetchEtcRecords } from './api';
|
||||
import type { EtcBillResponse, EtcTollRecordResponse } from './types';
|
||||
import type { EtcDetailView } from './etc-drill-context';
|
||||
|
||||
function fmtMoney(value: number): string {
|
||||
return `¥${value.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
}
|
||||
|
||||
function costTypeLabel(value: number): string {
|
||||
if (value === 1) return '客户承担';
|
||||
if (value === 2) return '我方承担';
|
||||
return '承担方待确认';
|
||||
}
|
||||
|
||||
function paymentLabel(receivable: number, paid: number): string {
|
||||
if (receivable > 0 && paid >= receivable) return '已收';
|
||||
if (paid > 0) return '部分收款';
|
||||
return '未收';
|
||||
}
|
||||
|
||||
export default function ETCDetails({
|
||||
view,
|
||||
onViewChange,
|
||||
}: {
|
||||
view: EtcDetailView;
|
||||
onViewChange: (view: EtcDetailView) => void;
|
||||
}) {
|
||||
const [draftSearch, setDraftSearch] = useState('');
|
||||
const [search, setSearch] = useState('');
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [records, setRecords] = useState<EtcTollRecordResponse | null>(null);
|
||||
const [bills, setBills] = useState<EtcBillResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const query = {
|
||||
page,
|
||||
limit: 20,
|
||||
startDate: startDate || undefined,
|
||||
endDate: endDate || undefined,
|
||||
search: search || undefined,
|
||||
};
|
||||
const request = view === 'records' ? fetchEtcRecords(query) : fetchEtcBills(query);
|
||||
request
|
||||
.then(result => {
|
||||
if (cancelled) return;
|
||||
if (view === 'records') setRecords(result as EtcTollRecordResponse);
|
||||
else setBills(result as EtcBillResponse);
|
||||
})
|
||||
.catch(loadError => {
|
||||
if (!cancelled) setError(loadError instanceof Error ? loadError.message : String(loadError));
|
||||
})
|
||||
.finally(() => { if (!cancelled) setLoading(false); });
|
||||
return () => { cancelled = true; };
|
||||
}, [endDate, page, search, startDate, view]);
|
||||
|
||||
const activeData = view === 'records' ? records : bills;
|
||||
const totalPages = activeData?.totalPages || 1;
|
||||
const applySearch = () => {
|
||||
setPage(1);
|
||||
setSearch(draftSearch.trim());
|
||||
};
|
||||
const changeView = (next: EtcDetailView) => {
|
||||
setPage(1);
|
||||
onViewChange(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="overflow-hidden rounded-lg border border-slate-100 bg-white shadow-sm">
|
||||
<header className="flex flex-col gap-3 border-b border-slate-100 px-3 py-3 md:px-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-black text-slate-900">ETC 明细下钻</h2>
|
||||
<p className="mt-1 text-[10px] font-bold text-slate-400">
|
||||
{view === 'records' ? '通行流水与费用承担' : '客户账期、应收与收款'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex rounded-lg bg-slate-100 p-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => changeView('records')}
|
||||
className={`inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-[10px] font-black ${view === 'records' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500'}`}
|
||||
>
|
||||
<Route size={13} />通行记录
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => changeView('bills')}
|
||||
className={`inline-flex h-8 items-center gap-1.5 rounded-md px-3 text-[10px] font-black ${view === 'bills' ? 'bg-white text-blue-600 shadow-sm' : 'text-slate-500'}`}
|
||||
>
|
||||
<ReceiptText size={13} />结算账单
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 sm:grid-cols-[140px_140px_minmax(0,1fr)_36px]">
|
||||
<input
|
||||
type="date"
|
||||
aria-label="ETC 开始日期"
|
||||
value={startDate}
|
||||
onChange={event => { setPage(1); setStartDate(event.target.value); }}
|
||||
className="h-9 min-w-0 rounded-lg border border-slate-200 bg-white px-2 text-[11px] font-bold text-slate-700 outline-none focus:border-blue-300"
|
||||
/>
|
||||
<input
|
||||
type="date"
|
||||
aria-label="ETC 结束日期"
|
||||
value={endDate}
|
||||
onChange={event => { setPage(1); setEndDate(event.target.value); }}
|
||||
className="h-9 min-w-0 rounded-lg border border-slate-200 bg-white px-2 text-[11px] font-bold text-slate-700 outline-none focus:border-blue-300"
|
||||
/>
|
||||
<input
|
||||
type="search"
|
||||
aria-label={view === 'records' ? '搜索车牌、客户或流水号' : '搜索客户或账单号'}
|
||||
placeholder={view === 'records' ? '车牌 / 客户 / 流水号' : '客户 / 账单号'}
|
||||
value={draftSearch}
|
||||
onChange={event => setDraftSearch(event.target.value)}
|
||||
onKeyDown={event => { if (event.key === 'Enter') applySearch(); }}
|
||||
className="h-9 min-w-0 rounded-lg border border-slate-200 bg-white px-3 text-[11px] font-bold text-slate-700 outline-none placeholder:text-slate-300 focus:border-blue-300"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={applySearch}
|
||||
className="flex h-9 w-9 items-center justify-center rounded-lg bg-slate-900 text-white hover:bg-blue-600"
|
||||
aria-label="应用 ETC 明细搜索"
|
||||
title="搜索"
|
||||
>
|
||||
<Search size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{activeData ? (
|
||||
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-slate-100 bg-slate-50/70 px-3 py-2 text-[10px] font-bold text-slate-500 md:px-4">
|
||||
<span>{activeData.total} 条结果</span>
|
||||
{view === 'records' && records ? (
|
||||
<span>通行费 {fmtMoney(records.summary.tollAmount)} · 服务费 {fmtMoney(records.summary.serviceFee)} · 合计 {fmtMoney(records.summary.totalAmount)}</span>
|
||||
) : view === 'bills' && bills ? (
|
||||
<span>应收 {fmtMoney(bills.summary.receivableAmount)} · 已收 {fmtMoney(bills.summary.paidAmount)}</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{error ? (
|
||||
<div className="m-4 rounded-lg border border-rose-100 bg-rose-50 p-3 text-xs font-bold text-rose-700">{error}</div>
|
||||
) : loading && !activeData ? (
|
||||
<div className="flex items-center justify-center gap-2 py-14 text-xs font-bold text-slate-400">
|
||||
<Loader2 size={16} className="animate-spin" />正在加载 ETC 明细
|
||||
</div>
|
||||
) : view === 'records' && records ? (
|
||||
records.items.length === 0 ? (
|
||||
<div className="py-14 text-center">
|
||||
<Route size={22} className="mx-auto text-slate-300" />
|
||||
<div className="mt-3 text-xs font-black text-slate-500">暂无匹配的通行记录</div>
|
||||
<div className="mt-1 text-[10px] font-bold text-slate-300">首次同步后将按通行时间倒序展示</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="hidden overflow-x-auto md:block">
|
||||
<table className="w-full min-w-[820px] text-left text-[11px]">
|
||||
<thead className="bg-white text-[10px] font-black text-slate-400">
|
||||
<tr><th className="px-4 py-2">通行时间</th><th className="px-3 py-2">车牌 / 客户</th><th className="px-3 py-2">入口 → 出口</th><th className="px-3 py-2">承担</th><th className="px-4 py-2 text-right">费用</th></tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{records.items.map(item => (
|
||||
<tr key={item.id}>
|
||||
<td className="px-4 py-3 font-bold text-slate-600">{item.transTime}</td>
|
||||
<td className="px-3 py-3"><div className="font-black text-slate-800"><Blur>{item.plate}</Blur></div><div className="mt-1 text-[9px] font-bold text-slate-400"><Blur>{item.customerName || '未关联客户'}</Blur></div></td>
|
||||
<td className="px-3 py-3 font-bold text-slate-600">{item.entryStation || '-'} → {item.exitStation || '-'}</td>
|
||||
<td className="px-3 py-3 font-bold text-slate-600">{costTypeLabel(item.costType)}</td>
|
||||
<td className="px-4 py-3 text-right"><div className="font-black text-slate-800">{fmtMoney(item.totalAmount)}</div><div className="mt-1 text-[9px] font-bold text-slate-400">通行 {fmtMoney(item.tollAmount)} · 服务 {fmtMoney(item.serviceFee)}</div></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="divide-y divide-slate-100 md:hidden">
|
||||
{records.items.map(item => (
|
||||
<article key={item.id} className="px-3 py-3">
|
||||
<div className="flex items-start justify-between gap-3"><div><div className="font-mono text-xs font-black text-slate-800"><Blur>{item.plate}</Blur></div><div className="mt-1 text-[9px] font-bold text-slate-400">{item.transTime}</div></div><div className="text-right text-sm font-black text-slate-800">{fmtMoney(item.totalAmount)}</div></div>
|
||||
<div className="mt-2 text-[10px] font-bold text-slate-500">{item.entryStation || '-'} → {item.exitStation || '-'}</div>
|
||||
<div className="mt-1 flex justify-between gap-2 text-[9px] font-bold text-slate-400"><Blur>{item.customerName || '未关联客户'}</Blur><span>{costTypeLabel(item.costType)}</span></div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
) : view === 'bills' && bills ? (
|
||||
bills.items.length === 0 ? (
|
||||
<div className="py-14 text-center">
|
||||
<ReceiptText size={22} className="mx-auto text-slate-300" />
|
||||
<div className="mt-3 text-xs font-black text-slate-500">暂无匹配的结算账单</div>
|
||||
<div className="mt-1 text-[10px] font-bold text-slate-300">生成账单后将按账期结束日倒序展示</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="hidden overflow-x-auto md:block">
|
||||
<table className="w-full min-w-[760px] text-left text-[11px]">
|
||||
<thead className="text-[10px] font-black text-slate-400"><tr><th className="px-4 py-2">账单 / 客户</th><th className="px-3 py-2">账期</th><th className="px-3 py-2">通行</th><th className="px-3 py-2">收款状态</th><th className="px-4 py-2 text-right">应收 / 已收</th></tr></thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{bills.items.map(item => (
|
||||
<tr key={item.id}>
|
||||
<td className="px-4 py-3"><div className="font-black text-slate-800">{item.billCode}</div><div className="mt-1 text-[9px] font-bold text-slate-400"><Blur>{item.customerName || '未指定客户'}</Blur></div></td>
|
||||
<td className="px-3 py-3 font-bold text-slate-600">{item.periodStart} → {item.periodEnd}</td>
|
||||
<td className="px-3 py-3 font-bold text-slate-600">{item.tollCount} 次 · {fmtMoney(item.tollAmount)}</td>
|
||||
<td className="px-3 py-3 font-bold text-slate-600">{paymentLabel(item.receivableAmount, item.paidAmount)}</td>
|
||||
<td className="px-4 py-3 text-right"><div className="font-black text-slate-800">{fmtMoney(item.receivableAmount)}</div><div className="mt-1 text-[9px] font-bold text-slate-400">已收 {fmtMoney(item.paidAmount)}</div></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="divide-y divide-slate-100 md:hidden">
|
||||
{bills.items.map(item => (
|
||||
<article key={item.id} className="px-3 py-3">
|
||||
<div className="flex items-start justify-between gap-3"><div><div className="text-xs font-black text-slate-800">{item.billCode}</div><div className="mt-1 text-[9px] font-bold text-slate-400"><Blur>{item.customerName || '未指定客户'}</Blur></div></div><div className="text-right"><div className="text-sm font-black text-slate-800">{fmtMoney(item.receivableAmount)}</div><div className="mt-1 text-[9px] font-bold text-slate-400">已收 {fmtMoney(item.paidAmount)}</div></div></div>
|
||||
<div className="mt-2 text-[10px] font-bold text-slate-500">{item.periodStart} → {item.periodEnd}</div>
|
||||
<div className="mt-1 flex justify-between text-[9px] font-bold text-slate-400"><span>{item.tollCount} 次通行</span><span>{paymentLabel(item.receivableAmount, item.paidAmount)}</span></div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
) : null}
|
||||
|
||||
{activeData ? (
|
||||
<footer className="flex items-center justify-between border-t border-slate-100 px-3 py-3 md:px-4">
|
||||
<span className="text-[10px] font-bold text-slate-400">第 {page} / {totalPages} 页</span>
|
||||
<div className="flex gap-2">
|
||||
<button type="button" onClick={() => setPage(current => Math.max(1, current - 1))} disabled={page <= 1 || loading} className="flex h-8 w-8 items-center justify-center rounded-lg border border-slate-200 text-slate-500 disabled:opacity-30" aria-label="上一页" title="上一页"><ChevronLeft size={14} /></button>
|
||||
<button type="button" onClick={() => setPage(current => Math.min(totalPages, current + 1))} disabled={page >= totalPages || loading} className="flex h-8 w-8 items-center justify-center rounded-lg border border-slate-200 text-slate-500 disabled:opacity-30" aria-label="下一页" title="下一页"><ChevronRight size={14} /></button>
|
||||
</div>
|
||||
</footer>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { CircleCheck, CircleX, Clock3, Database, ReceiptText, RefreshCw, Route, Truck } from 'lucide-react';
|
||||
import { fetchEtcOverview } from './api';
|
||||
import type { EtcOverviewResponse } from './types';
|
||||
import { EmptyState, ErrorState, LoadingState, MetricTile, SurfaceCard } from '../../components/ui/surface';
|
||||
import { ErrorState, LoadingState, MetricTile, SurfaceCard } from '../../components/ui/surface';
|
||||
import ETCDetails from './ETCDetails';
|
||||
import { buildEtcDetailUrl, parseEtcDetailView, type EtcDetailView } from './etc-drill-context';
|
||||
|
||||
function fmtMoney(value: number): string {
|
||||
return `¥${value.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}`;
|
||||
@@ -20,6 +22,18 @@ export default function ETCView() {
|
||||
const [data, setData] = useState<EtcOverviewResponse | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [detailView, setDetailView] = useState<EtcDetailView>(() => parseEtcDetailView(window.location.search));
|
||||
|
||||
const selectDetailView = useCallback((next: EtcDetailView) => {
|
||||
window.history.pushState(null, '', buildEtcDetailUrl(window.location, next));
|
||||
setDetailView(next);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handlePopState = () => setDetailView(parseEtcDetailView(window.location.search));
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
return () => window.removeEventListener('popstate', handlePopState);
|
||||
}, []);
|
||||
|
||||
const load = async (force = false) => {
|
||||
if (force) setRefreshing(true);
|
||||
@@ -41,7 +55,6 @@ export default function ETCView() {
|
||||
if (!data) return <LoadingState label="正在检查 ETC 接入状态" />;
|
||||
|
||||
const { integration, kpi } = data;
|
||||
const hasData = kpi.passageCount > 0;
|
||||
const collectionRate = kpi.receivableAmount > 0 ? kpi.paidAmount / kpi.receivableAmount * 100 : 0;
|
||||
|
||||
return (
|
||||
@@ -73,10 +86,10 @@ export default function ETCView() {
|
||||
</SurfaceCard>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||
<MetricTile icon={ReceiptText} label="ETC 总费用" value={fmtMoney(kpi.totalAmount)} helper={`通行费 ${fmtMoney(kpi.tollAmount)} · 服务费 ${fmtMoney(kpi.serviceFee)}`} />
|
||||
<MetricTile icon={Route} label="通行次数" value={kpi.passageCount} unit="次" helper={kpi.latestTransactionTime ? `最新 ${kpi.latestTransactionTime}` : '暂无通行记录'} tone="emerald" />
|
||||
<MetricTile icon={Truck} label="通行车辆" value={kpi.vehicleCount} unit="辆" helper="按车牌去重" tone="amber" />
|
||||
<MetricTile icon={Database} label="账单应收" value={fmtMoney(kpi.receivableAmount)} helper={`${kpi.billCount} 账单 · 已收 ${fmtMoney(kpi.paidAmount)} · ${collectionRate.toFixed(1)}%`} tone="slate" />
|
||||
<button type="button" onClick={() => selectDetailView('records')} className="text-left" title="查看通行记录"><MetricTile icon={ReceiptText} label="ETC 总费用" value={fmtMoney(kpi.totalAmount)} helper={`通行费 ${fmtMoney(kpi.tollAmount)} · 服务费 ${fmtMoney(kpi.serviceFee)}`} /></button>
|
||||
<button type="button" onClick={() => selectDetailView('records')} className="text-left" title="查看通行记录"><MetricTile icon={Route} label="通行次数" value={kpi.passageCount} unit="次" helper={kpi.latestTransactionTime ? `最新 ${kpi.latestTransactionTime}` : '暂无通行记录'} tone="emerald" /></button>
|
||||
<button type="button" onClick={() => selectDetailView('records')} className="text-left" title="查看通行记录"><MetricTile icon={Truck} label="通行车辆" value={kpi.vehicleCount} unit="辆" helper="按车牌去重" tone="amber" /></button>
|
||||
<button type="button" onClick={() => selectDetailView('bills')} className="text-left" title="查看结算账单"><MetricTile icon={Database} label="账单应收" value={fmtMoney(kpi.receivableAmount)} helper={`${kpi.billCount} 账单 · 已收 ${fmtMoney(kpi.paidAmount)} · ${collectionRate.toFixed(1)}%`} tone="slate" /></button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
@@ -99,12 +112,7 @@ export default function ETCView() {
|
||||
</SurfaceCard>
|
||||
</div>
|
||||
|
||||
{!hasData && (
|
||||
<EmptyState
|
||||
title="ETC 尚无可统计数据"
|
||||
description="当前通行记录、账单、同步配置和同步日志均为空;完成提供方配置与首次同步后,本页将直接展示真实指标。"
|
||||
/>
|
||||
)}
|
||||
<ETCDetails view={detailView} onViewChange={selectDetailView} />
|
||||
|
||||
{error && <ErrorState message={error} />}
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
ElectricKpi, ElectricDailyRow, ElectricMonthGroup,
|
||||
ElectricChargeOrderResponse,
|
||||
EtcOverviewResponse,
|
||||
EtcBillResponse, EtcTollRecordResponse,
|
||||
CustomerType, DateQuickPick, ElectricVehicleScope,
|
||||
} from './types';
|
||||
|
||||
@@ -73,3 +74,31 @@ export function fetchElectricOrders(date: string, customer: ElectricVehicleScope
|
||||
export function fetchEtcOverview(force = false): Promise<EtcOverviewResponse> {
|
||||
return fetchJson<EtcOverviewResponse>(`${BASE}/etc/overview${force ? '?force=1' : ''}`);
|
||||
}
|
||||
|
||||
export interface EtcListQuery {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
startDate?: string;
|
||||
endDate?: string;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
function etcListParams(query: EtcListQuery): string {
|
||||
const params = new URLSearchParams();
|
||||
if (query.page) params.set('page', String(query.page));
|
||||
if (query.limit) params.set('limit', String(query.limit));
|
||||
if (query.startDate) params.set('startDate', query.startDate);
|
||||
if (query.endDate) params.set('endDate', query.endDate);
|
||||
if (query.search) params.set('search', query.search);
|
||||
return params.toString();
|
||||
}
|
||||
|
||||
export function fetchEtcRecords(query: EtcListQuery): Promise<EtcTollRecordResponse> {
|
||||
const params = etcListParams(query);
|
||||
return fetchJson<EtcTollRecordResponse>(`${BASE}/etc/records${params ? `?${params}` : ''}`);
|
||||
}
|
||||
|
||||
export function fetchEtcBills(query: EtcListQuery): Promise<EtcBillResponse> {
|
||||
const params = etcListParams(query);
|
||||
return fetchJson<EtcBillResponse>(`${BASE}/etc/bills${params ? `?${params}` : ''}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { buildEtcDetailUrl, parseEtcDetailView } from './etc-drill-context.js';
|
||||
|
||||
test('parses only published ETC detail views', () => {
|
||||
assert.equal(parseEtcDetailView('?etcView=bills'), 'bills');
|
||||
assert.equal(parseEtcDetailView('?etcView=unknown'), 'records');
|
||||
});
|
||||
|
||||
test('updates ETC drill state without dropping other URL context', () => {
|
||||
assert.equal(buildEtcDetailUrl({
|
||||
pathname: '/energy',
|
||||
search: '?electricScope=all&etcView=records',
|
||||
hash: '#etc',
|
||||
}, 'bills'), '/energy?electricScope=all&etcView=bills#etc');
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
export type EtcDetailView = 'records' | 'bills';
|
||||
|
||||
interface LocationParts {
|
||||
pathname: string;
|
||||
search: string;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export function parseEtcDetailView(search: string): EtcDetailView {
|
||||
return new URLSearchParams(search).get('etcView') === 'bills' ? 'bills' : 'records';
|
||||
}
|
||||
|
||||
export function buildEtcDetailUrl(location: LocationParts, view: EtcDetailView): string {
|
||||
const params = new URLSearchParams(location.search);
|
||||
params.set('etcView', view);
|
||||
const query = params.toString();
|
||||
return `${location.pathname}${query ? `?${query}` : ''}${location.hash}`;
|
||||
}
|
||||
@@ -146,3 +146,60 @@ export interface EtcOverviewResponse {
|
||||
latestTransactionTime: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EtcListFilters {
|
||||
startDate: string | null;
|
||||
endDate: string | null;
|
||||
search: string;
|
||||
}
|
||||
|
||||
export interface EtcTollRecord {
|
||||
id: number;
|
||||
recordCode: string;
|
||||
plate: string;
|
||||
customerName: string | null;
|
||||
entryStation: string | null;
|
||||
exitStation: string | null;
|
||||
transTime: string;
|
||||
tollAmount: number;
|
||||
serviceFee: number;
|
||||
totalAmount: number;
|
||||
costType: number;
|
||||
contractMatched: boolean;
|
||||
reviewStatus: number;
|
||||
}
|
||||
|
||||
export interface EtcTollRecordResponse {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
filters: EtcListFilters;
|
||||
summary: { tollAmount: number; serviceFee: number; totalAmount: number };
|
||||
items: EtcTollRecord[];
|
||||
}
|
||||
|
||||
export interface EtcBill {
|
||||
id: number;
|
||||
billCode: string;
|
||||
customerName: string | null;
|
||||
periodStart: string;
|
||||
periodEnd: string;
|
||||
tollCount: number;
|
||||
tollAmount: number;
|
||||
serviceFee: number;
|
||||
receivableAmount: number;
|
||||
paidAmount: number;
|
||||
paymentStatus: number;
|
||||
reviewStatus: number;
|
||||
}
|
||||
|
||||
export interface EtcBillResponse {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
filters: EtcListFilters;
|
||||
summary: { receivableAmount: number; paidAmount: number };
|
||||
items: EtcBill[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user