Files
lislgosms/src/apps/admin/AdminReconciliationReportsPage.tsx
T

101 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useMemo, useState } from 'react';
import { Search } from 'lucide-react';
import { adminApi, type DailyReconciliationReport, type EnterpriseApplication, type TenantOption } from '@/api/adminApi';
import { Breadcrumb, Button, DateRangeInput, Pagination, Select, Tag, type DateRangeValue } from '@/components/ui';
import { formatDateTime } from '@/utils/dateTime';
const pageSize = 20;
export function AdminReconciliationReportsPage() {
const [rows, setRows] = useState<DailyReconciliationReport[]>([]);
const [tenants, setTenants] = useState<TenantOption[]>([]);
const [applications, setApplications] = useState<EnterpriseApplication[]>([]);
const [dateRange, setDateRange] = useState<DateRangeValue>(defaultDateRange());
const [tenantId, setTenantId] = useState('');
const [applicationId, setApplicationId] = useState('');
const [page, setPage] = useState(1);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
Promise.all([adminApi.listTenants(), adminApi.listEnterpriseApplications()])
.then(([nextTenants, nextApplications]) => { setTenants(nextTenants); setApplications(nextApplications); })
.catch(() => { setTenants([]); setApplications([]); });
}, []);
useEffect(() => { void loadData(); }, [page, dateRange.start, dateRange.end, tenantId, applicationId]);
async function loadData() {
setLoading(true);
setError('');
try {
const response = await adminApi.listReconciliationReports({
dateFrom: dateRange.start,
dateTo: dateRange.end,
tenantId: tenantId || undefined,
applicationId: applicationId || undefined,
page,
pageSize,
});
setRows(response.items);
setTotal(response.total);
} catch (failure) {
setRows([]);
setTotal(0);
setError(failure instanceof Error ? failure.message : '对账单加载失败');
} finally {
setLoading(false);
}
}
const availableApplications = useMemo(
() => applications.filter((application) => !tenantId || application.tenantId === tenantId),
[applications, tenantId],
);
const totalPages = Math.max(1, Math.ceil(total / pageSize));
return (
<section className="page-stack">
<div className="page-heading">
<div><Breadcrumb items={['报表对账', '对账单']} /><h1>对账单</h1></div>
<Tag tone="info">T+1 生成 · 每日重算 T-4T-1</Tag>
</div>
<div className="surface" style={{ display: 'grid', gap: 16, gridTemplateColumns: 'minmax(280px, 1.4fr) minmax(200px, 1fr) minmax(220px, 1fr) auto', padding: 20, alignItems: 'end' }}>
<DateRangeInput label="发送日期" onChange={(value) => { setDateRange(value); setPage(1); }} value={dateRange} />
<Select label="企业" onChange={(event) => { setTenantId(event.target.value); setApplicationId(''); setPage(1); }} options={[{ label: '全部企业', value: '' }, ...tenants.map((tenant) => ({ label: tenant.name, value: tenant.id }))]} value={tenantId} />
<Select label="企业应用" onChange={(event) => { setApplicationId(event.target.value); setPage(1); }} options={[{ label: '全部应用', value: '' }, ...availableApplications.map((application) => ({ label: application.name, value: application.id }))]} value={applicationId} />
<Button icon={<Search size={16} />} onClick={() => void loadData()}>查询</Button>
</div>
<div className="surface">
<div className="ui-table-wrap">
<table className="ui-table">
<thead><tr><th>发送日期</th><th>企业</th><th>企业应用</th><th>日发送条数</th><th>成功条数</th><th>生成时间</th></tr></thead>
<tbody>
{error ? <tr><td className="ui-table__empty" colSpan={6}>{error}</td></tr>
: loading ? <tr><td className="ui-table__empty" colSpan={6}>正在加载真实对账数据...</td></tr>
: rows.length === 0 ? <tr><td className="ui-table__empty" colSpan={6}>暂无已生成的对账单</td></tr>
: rows.map((row) => <tr key={row.id}><td>{row.reportDate.slice(0, 10)}</td><td>{row.tenantName}</td><td>{row.applicationName}</td><td>{row.sentUnits.toLocaleString('zh-CN')}</td><td>{row.successUnits.toLocaleString('zh-CN')}</td><td>{formatDateTime(row.generatedAt)}</td></tr>)}
</tbody>
</table>
</div>
<Pagination page={page} totalPages={totalPages} total={total} onPageChange={setPage} onPrevious={() => setPage((value) => Math.max(1, value - 1))} onNext={() => setPage((value) => Math.min(totalPages, value + 1))} previousDisabled={page <= 1} nextDisabled={page >= totalPages} />
</div>
</section>
);
}
function defaultDateRange(): DateRangeValue {
const end = new Date();
end.setDate(end.getDate() - 1);
const start = new Date(end);
start.setDate(start.getDate() - 29);
return { start: localDate(start), end: localDate(end) };
}
function localDate(value: Date) {
return `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, '0')}-${String(value.getDate()).padStart(2, '0')}`;
}