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

163 lines
6.5 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { Plus, Search } from 'lucide-react';
import { Breadcrumb, Button, DateRangeInput, Input, ManualRechargeDialog, Pagination, Tag, type DateRangeValue } from '@/components/ui';
import { adminApi, type RechargeOrder, type TenantAccount, type TenantOption } from '@/api/adminApi';
import { formatDateTime } from '@/utils/dateTime';
import { formatCents } from '@/utils/currency';
function getDate(value: string) {
return value.slice(0, 10);
}
function RemarkCell({ value }: { value?: string }) {
return (
<div className={['admin-remark-cell', value ? '' : 'admin-remark-cell--empty'].filter(Boolean).join(' ')}>
{value || '暂无备注'}
</div>
);
}
export function AdminRechargeRecordsPage() {
const [records, setRecords] = useState<RechargeOrder[]>([]);
const [tenants, setTenants] = useState<TenantOption[]>([]);
const [accounts, setAccounts] = useState<TenantAccount[]>([]);
const [enterpriseKeyword, setEnterpriseKeyword] = useState('');
const [dateRange, setDateRange] = useState<DateRangeValue>({});
const [manualOpen, setManualOpen] = useState(false);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
async function loadData() {
setLoading(true);
setError('');
try {
const [nextTenants, nextAccounts, nextRecords] = await Promise.all([
adminApi.listTenants(),
adminApi.listAccounts(),
adminApi.listManualRecharges(),
]);
setTenants(nextTenants);
setAccounts(nextAccounts);
setRecords(nextRecords);
} catch (err) {
setError(err instanceof Error ? err.message : '充值记录加载失败');
setRecords([]);
} finally {
setLoading(false);
}
}
useEffect(() => {
void loadData();
}, []);
const filteredRows = useMemo(
() => records.filter((item) => {
const rechargeDate = getDate(item.paidAt ?? item.createdAt);
const tenantName = item.tenant?.name ?? tenants.find((tenant) => tenant.id === item.tenantId)?.name ?? item.tenantId;
const matchesEnterprise = !enterpriseKeyword || tenantName.includes(enterpriseKeyword);
const matchesStartDate = !dateRange.start || rechargeDate >= dateRange.start;
const matchesEndDate = !dateRange.end || rechargeDate <= dateRange.end;
return matchesEnterprise && matchesStartDate && matchesEndDate;
}),
[dateRange.end, dateRange.start, enterpriseKeyword, records, tenants],
);
const pageSize = 10;
const totalPages = Math.max(1, Math.ceil(filteredRows.length / pageSize));
const currentPage = Math.min(page, totalPages);
const visibleRows = filteredRows.slice((currentPage - 1) * pageSize, currentPage * pageSize);
useEffect(() => {
setPage(1);
}, [dateRange.end, dateRange.start, enterpriseKeyword, records.length]);
function resetFilters() {
setEnterpriseKeyword('');
setDateRange({});
}
return (
<section className="page-stack admin-recharge-page">
<div className="page-heading">
<div>
<Breadcrumb items={['数据详单', '充值记录']} />
<h1>充值记录</h1>
</div>
<Button icon={<Plus size={16} />} onClick={() => setManualOpen(true)}>人工充值</Button>
</div>
<div className="surface admin-recharge-filter">
<Input label="企业名称" onChange={(event) => setEnterpriseKeyword(event.target.value)} value={enterpriseKeyword} />
<DateRangeInput label="充值日期" onChange={setDateRange} value={dateRange} />
<div className="admin-recharge-filter__actions">
<Button icon={<Search size={16} />}>查询</Button>
<Button onClick={resetFilters} variant="ghost">重置</Button>
</div>
</div>
<div className="surface admin-recharge-table-card">
<div className="ui-table-wrap">
<table className="ui-table admin-recharge-table">
<thead>
<tr>
<th style={{ width: '240px' }}>企业名称</th>
<th style={{ width: '180px' }}>充值时间</th>
<th style={{ width: '130px' }}>充值金额</th>
<th style={{ width: '140px' }}>充值后余额</th>
<th style={{ width: '120px' }}>充值类型</th>
<th style={{ width: '300px' }}>备注</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>
) : filteredRows.length === 0 ? (
<tr><td className="ui-table__empty" colSpan={6}>暂无真实充值记录</td></tr>
) : visibleRows.map((record) => {
const tenantName = record.tenant?.name ?? tenants.find((tenant) => tenant.id === record.tenantId)?.name ?? record.tenantId;
return (
<tr key={record.id}>
<td><strong>{tenantName}</strong></td>
<td>{formatDateTime(record.paidAt ?? record.createdAt)}</td>
<td>¥{formatCents(record.amountCents)}</td>
<td>{record.balanceAfterCents === null || record.balanceAfterCents === undefined ? '-' : ${formatCents(record.balanceAfterCents)}`}</td>
<td><Tag tone="warning">人工充值</Tag></td>
<td><RemarkCell value={record.remark ?? undefined} /></td>
</tr>
);
})}
</tbody>
</table>
</div>
<Pagination
nextDisabled={currentPage >= totalPages}
onNext={() => setPage((value) => Math.min(totalPages, value + 1))}
onPrevious={() => setPage((value) => Math.max(1, value - 1))}
page={currentPage}
totalPages={totalPages}
onPageChange={setPage}
previousDisabled={currentPage <= 1}
total={filteredRows.length}
/>
</div>
<ManualRechargeDialog
initialTargetId={tenants.find((tenant) => tenant.status !== 'deleted')?.id}
onClose={() => setManualOpen(false)}
onCompleted={loadData}
open={manualOpen}
targets={tenants.filter((tenant) => tenant.status !== 'deleted').map((tenant) => ({
id: tenant.id,
name: tenant.name,
code: tenant.code,
balanceCents: accounts.find((account) => account.tenantId === tenant.id)?.balanceCents ?? 0,
}))}
/>
</section>
);
}