From 54fca99208da522828cce7cafa874ebe87c138b8 Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Mon, 3 Aug 2026 16:04:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(settlement):=20=E7=BB=9F=E4=B8=80=E9=97=A8?= =?UTF-8?q?=E5=BA=97=E8=B4=A6=E5=8D=95=E5=88=97=E8=A1=A8=E5=90=88=E5=B9=B6?= =?UTF-8?q?=20T+1=20=E4=B8=8E=E6=89=8B=E5=8A=A8=E6=8F=90=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 总部财务以门店账单为唯一入口;新增 store-settlements API,去掉独立提现审菜单并更新企微引导文案。 Co-authored-by: Cursor --- apps/admin-web/src/App.tsx | 11 +- apps/admin-web/src/layouts/AdminLayout.tsx | 1 - apps/admin-web/src/pages/DashboardPage.tsx | 2 +- apps/admin-web/src/pages/StoreBillsPage.tsx | 396 +++++++++++++++--- packages/shared-types/src/settlement.ts | 51 +++ .../settlement/settlement.controller.ts | 19 + .../modules/settlement/settlement.module.ts | 2 + .../modules/settlement/settlement.service.ts | 150 ++++++- 8 files changed, 561 insertions(+), 71 deletions(-) diff --git a/apps/admin-web/src/App.tsx b/apps/admin-web/src/App.tsx index 0e373ea..4f5adba 100644 --- a/apps/admin-web/src/App.tsx +++ b/apps/admin-web/src/App.tsx @@ -30,7 +30,6 @@ import ProductsPage from './pages/ProductsPage'; import ProductDetailTemplatesPage from './pages/ProductDetailTemplatesPage'; import ResourcesPage from './pages/ResourcesPage'; import StoreBillsPage from './pages/StoreBillsPage'; -import StoreWithdrawalsPage from './pages/StoreWithdrawalsPage'; import PartnerBillsPage from './pages/PartnerBillsPage'; import WineryBillsPage from './pages/WineryBillsPage'; import LogisticsBillsPage from './pages/LogisticsBillsPage'; @@ -97,13 +96,19 @@ export default function App() { } /> } /> } /> - } /> + } + /> } /> } /> } /> } /> } /> - } /> + } + /> } /> } /> } /> diff --git a/apps/admin-web/src/layouts/AdminLayout.tsx b/apps/admin-web/src/layouts/AdminLayout.tsx index b8240b9..525490f 100644 --- a/apps/admin-web/src/layouts/AdminLayout.tsx +++ b/apps/admin-web/src/layouts/AdminLayout.tsx @@ -74,7 +74,6 @@ const MENU_ITEMS: MenuProps['items'] = [ label: '财务', children: [ { key: '/finance/store-bills', label: '门店账单' }, - { key: '/finance/store-withdrawals', label: '门店提现审' }, { key: '/finance/partner-bills', label: '合伙人账单' }, { key: '/finance/winery-bills', label: '酒厂账单' }, { key: '/finance/logistics-bills', label: '物流对账' }, diff --git a/apps/admin-web/src/pages/DashboardPage.tsx b/apps/admin-web/src/pages/DashboardPage.tsx index 9a2d77c..2af146c 100644 --- a/apps/admin-web/src/pages/DashboardPage.tsx +++ b/apps/admin-web/src/pages/DashboardPage.tsx @@ -721,7 +721,7 @@ export default function DashboardPage() { 去处理} + extra={去处理} > = { - UNPAID: '未打款', - PAID: '已打款', -}; - const STATUS_COLORS: Record = { UNPAID: 'red', PAID: 'green', + PENDING_REVIEW: 'orange', + REJECTED: 'red', +}; + +const KIND_COLORS: Record = { + T1_BILL: 'blue', + WITHDRAW: 'purple', }; export default function StoreBillsPage() { + const [searchParams] = useSearchParams(); + const initialKind = searchParams.get('kind') === 'WITHDRAW' ? 'WITHDRAW' : ''; const [form] = Form.useForm(); - const [filters, setFilters] = useState>({}); + const [filters, setFilters] = useState>({ + kind: initialKind, + status: initialKind === 'WITHDRAW' ? 'PENDING_REVIEW' : '', + }); const [stores, setStores] = useState([]); const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( - '/admin/store-bills', + '/admin/store-settlements', () => { const qs = new URLSearchParams(); + if (filters.kind) qs.set('kind', filters.kind); if (filters.status) qs.set('status', filters.status); if (filters.storeId) qs.set('storeId', filters.storeId); if (filters.dateFrom) qs.set('dateFrom', filters.dateFrom); @@ -64,21 +83,38 @@ export default function StoreBillsPage() { [filters], ); const [detail, setDetail] = useState | null>(null); + const [detailKind, setDetailKind] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); const [exporting, setExporting] = useState(false); const [selectedKeys, setSelectedKeys] = useState([]); const [batchLoading, setBatchLoading] = useState(false); + const [overdueSummary, setOverdueSummary] = useState<{ + pendingCount: number; + overdueCount: number; + } | null>(null); + + useEffect(() => { + form.setFieldsValue({ + kind: filters.kind || undefined, + status: filters.status || undefined, + }); + }, [filters.kind, filters.status, form]); useEffect(() => { void request>(`/admin/stores?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`) .then((res) => setStores(res.items)) .catch(() => {}); + void request<{ pendingCount: number; overdueCount: number }>( + '/admin/store-withdrawals/overdue-summary', + ) + .then(setOverdueSummary) + .catch(() => {}); }, []); function confirmPay(ids: string[], amountHint?: number) { Modal.confirm({ title: '确认打款?', - content: `将确认 ${ids.length} 笔门店对账单${amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。此操作不可撤销。`, + content: `将确认 ${ids.length} 笔 T+1 门店对账单${amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。此操作不可撤销。`, okText: '确认打款', cancelText: '取消', onOk: async () => { @@ -102,9 +138,75 @@ export default function StoreBillsPage() { }); } - async function openDetail(id: string) { - const d = await request>(`/admin/store-bills/${id}`); + function approveWithdraw(id: string) { + let paymentRef = ''; + Modal.confirm({ + title: '审核通过并标记已结算?', + content: ( + { + paymentRef = e.target.value; + }} + /> + ), + okText: '通过并结算', + cancelText: '取消', + onOk: async () => { + await request(`/admin/store-withdrawals/${id}/approve`, { + method: 'POST', + body: JSON.stringify({ paymentRef: paymentRef.trim() || undefined }), + }); + message.success('已通过并标记已结算'); + setDrawerOpen(false); + reload(); + void request<{ pendingCount: number; overdueCount: number }>( + '/admin/store-withdrawals/overdue-summary', + ) + .then(setOverdueSummary) + .catch(() => {}); + }, + }); + } + + function rejectWithdraw(id: string) { + let reason = ''; + Modal.confirm({ + title: '驳回提现申请?', + content: ( + { + reason = e.target.value; + }} + /> + ), + okText: '确认驳回', + okButtonProps: { danger: true }, + cancelText: '取消', + onOk: async () => { + if (!reason.trim()) { + message.error('请填写驳回理由'); + throw new Error('reason required'); + } + await request(`/admin/store-withdrawals/${id}/reject`, { + method: 'POST', + body: JSON.stringify({ reason: reason.trim() }), + }); + message.success('已驳回'); + setDrawerOpen(false); + reload(); + }, + }); + } + + async function openDetail(row: Row) { + const path = + row.kind === 'WITHDRAW' ? `/admin/store-withdrawals/${row.id}` : `/admin/store-bills/${row.id}`; + const d = await request>(path); setDetail(d); + setDetailKind(row.kind); setDrawerOpen(true); } @@ -118,88 +220,134 @@ export default function StoreBillsPage() { if (filters.dateTo) qs.set('dateTo', filters.dateTo); const result = await request<{ csv: string; count: number }>(`/admin/store-bills/export?${qs}`); downloadExcelCsv(result.csv, `门店对账单_${filters.dateFrom || 'all'}_${filters.dateTo || 'all'}.csv`); - message.success(`已导出 ${result.count} 条`); + message.success(`已导出 ${result.count} 条 T+1 账单`); } finally { setExporting(false); } } const summary = data?.summary; - const selectedRows = (data?.items ?? []).filter((r) => selectedKeys.includes(r.id)); - const selectedAmount = selectedRows.reduce((s, r) => s + Number(r.payoutAmount), 0); + const selectedRows = (data?.items ?? []).filter( + (r) => + selectedKeys.includes(`${r.kind}-${r.id}`) && + r.kind === 'T1_BILL' && + r.status === 'UNPAID', + ); + const selectedAmount = selectedRows.reduce((s, r) => s + Number(r.amount), 0); const columns: ColumnsType = [ - { title: '账单号', dataIndex: 'billNo', width: 170, ellipsis: true }, { - title: '账单日', - dataIndex: 'billDate', - width: 110, - render: (v) => String(v || '').slice(0, 10), + title: '类型', + dataIndex: 'kind', + width: 100, + render: (k: StoreSettlementKind) => ( + {STORE_SETTLEMENT_KIND_LABELS[k] ?? k} + ), + }, + { + title: '单号', + dataIndex: 'billNo', + width: 180, + ellipsis: true, + render: (v, row) => ( + + {v} + {row.overdue ? 超时 : null} + + ), + }, + { + title: '日期', + dataIndex: 'date', + width: 160, + render: (v, row) => (row.kind === 'T1_BILL' ? String(v || '').slice(0, 10) : fmtTime(v)), }, { title: '门店', dataIndex: ['store', 'name'], width: 140, ellipsis: true }, { title: '登录手机', dataIndex: ['store', 'phone'], width: 120, render: (v) => v || '—' }, { title: '城市', dataIndex: ['store', 'cityName'], width: 90 }, - { title: '核销笔数', dataIndex: 'redeemCount', width: 90 }, { - title: '核销金额', - dataIndex: 'redeemAmount', - width: 110, - render: (v) => `¥${Number(v).toFixed(2)}`, - }, - { - title: '结算比例', - dataIndex: 'settlementRate', - width: 90, - render: (v) => `${Math.round(Number(v) * 100)}%`, + title: '笔数', + width: 80, + render: (_, row) => + row.kind === 'T1_BILL' ? (row.redeemCount ?? '—') : (row.payoutCount ?? '—'), }, { title: '应付金额', - dataIndex: 'payoutAmount', + dataIndex: 'amount', width: 110, render: (v) => `¥${Number(v).toFixed(2)}`, }, { title: '状态', dataIndex: 'status', - width: 90, - render: (s) => {STATUS_LABELS[s] || s}, + width: 100, + render: (s: string, row) => ( + {storeSettlementStatusLabel(row.kind, s)} + ), }, { title: '操作', - width: 160, + width: 200, fixed: 'right', render: (_, row) => ( - - {row.status === 'UNPAID' && ( - )} + {row.kind === 'WITHDRAW' && row.status === 'PENDING_REVIEW' && ( + <> + + + + )} ), }, ]; + const withdrawDetailItems = + (detail?.items as Array> | undefined) ?? []; + const storeAccount = detail?.storeAccount as + | { + name?: string; + phone?: string; + bankAccountName?: string; + bankAccountNo?: string; + bankBranch?: string; + } + | undefined; + return (
- 门店对账单 + 门店账单 - 按核销日汇总(每日 8:00 自动出账);未打款红色、已打款绿色 + 含 T+1 自动出账与门店手动提现;按类型筛选,详情与审核动作沿用原接口 + {overdueSummary && overdueSummary.overdueCount > 0 ? ( + + 手动提现超时未审 {overdueSummary.overdueCount} 笔(待审共 {overdueSummary.pendingCount}) + + ) : null} {summary && ( - + - + )} @@ -208,16 +356,37 @@ export default function StoreBillsPage() { form={form} layout="inline" style={{ marginBottom: 16 }} - onFinish={(v: { storeId?: string; status?: string; range?: [Dayjs, Dayjs] }) => { + initialValues={{ + kind: filters.kind || undefined, + status: filters.status || undefined, + }} + onFinish={(v: { + kind?: string; + storeId?: string; + status?: string; + range?: [Dayjs, Dayjs]; + }) => { setFilters({ + kind: v.kind || '', storeId: v.storeId || '', status: v.status || '', dateFrom: v.range?.[0] ? v.range[0].format('YYYY-MM-DD') : '', dateTo: v.range?.[1] ? v.range[1].format('YYYY-MM-DD') : '', }); setPage(1); + setSelectedKeys([]); }} > + + ({ value, label }))} + options={Object.entries(STORE_SETTLEMENT_STATUS_LABELS).map(([value, label]) => ({ + value, + label: + value === 'PAID' + ? '已打款/已结算' + : label, + }))} /> - + @@ -249,6 +424,7 @@ export default function StoreBillsPage() { form.resetFields(); setFilters({}); setPage(1); + setSelectedKeys([]); }} > 重置 @@ -256,33 +432,36 @@ export default function StoreBillsPage() { `${r.kind}-${r.id}`} className="admin-table-nowrap" loading={loading} columns={columns} dataSource={data?.items ?? []} + rowClassName={(row) => (row.overdue ? 'ant-table-row-selected' : '')} rowSelection={{ selectedRowKeys: selectedKeys, onChange: setSelectedKeys, - getCheckboxProps: (r) => ({ disabled: r.status !== 'UNPAID' }), + getCheckboxProps: (r) => ({ + disabled: !(r.kind === 'T1_BILL' && r.status === 'UNPAID'), + }), }} - scroll={{ x: 1200 }} + scroll={{ x: 1300 }} pagination={{ current: page, pageSize, @@ -295,14 +474,25 @@ export default function StoreBillsPage() { }} /> - setDrawerOpen(false)} width={520}> - {detail && ( + setDrawerOpen(false)} + width={detailKind === 'WITHDRAW' ? 640 : 520} + > + {detail && detailKind === 'T1_BILL' && ( <> {String(detail.billNo)} - {String(detail.billDate || '').slice(0, 10)} - ¥{Number(detail.payoutAmount ?? 0).toFixed(2)} - {STATUS_LABELS[String(detail.status)] || String(detail.status)} + + {String(detail.billDate || '').slice(0, 10)} + + + ¥{Number(detail.payoutAmount ?? 0).toFixed(2)} + + + {storeSettlementStatusLabel('T1_BILL', String(detail.status))} + {detail.paidAt ? fmtTime(String(detail.paidAt)) : '—'} @@ -318,7 +508,8 @@ export default function StoreBillsPage() { columns={[ { title: '核销单号', - render: (_, r) => String((r.redeemRecord as { redeemNo?: string })?.redeemNo || '—'), + render: (_, r) => + String((r.redeemRecord as { redeemNo?: string })?.redeemNo || '—'), }, { title: '金额', @@ -334,6 +525,83 @@ export default function StoreBillsPage() { /> )} + {detail && detailKind === 'WITHDRAW' && ( + <> + + {String(detail.withdrawNo)} + + ¥{Number(detail.amount ?? 0).toFixed(2)} + + + {storeSettlementStatusLabel('WITHDRAW', String(detail.status))} + + + {detail.appliedAt ? fmtTime(String(detail.appliedAt)) : '—'} + + + {detail.rejectReason ? String(detail.rejectReason) : '—'} + + + {detail.paymentRef ? String(detail.paymentRef) : '—'} + + + {storeAccount ? ( + <> + + 收款账户 + + + + {storeAccount.bankAccountName || '—'} + + + {storeAccount.bankAccountNo || '—'} + + + {storeAccount.bankBranch || '—'} + + + + ) : null} + + 提现明细 + +
{ + const payout = r.storePayout as + | { redeemRecord?: { redeemNo?: string }; payoutAmount?: number } + | undefined; + return String(payout?.redeemRecord?.redeemNo || '—'); + }, + }, + { + title: '应付', + render: (_, r) => { + const payout = r.storePayout as { payoutAmount?: number } | undefined; + return `¥${Number(payout?.payoutAmount ?? 0).toFixed(2)}`; + }, + }, + ]} + /> + {String(detail.status) === 'PENDING_REVIEW' ? ( + + + + + ) : null} + + )} ); diff --git a/packages/shared-types/src/settlement.ts b/packages/shared-types/src/settlement.ts index fca95d8..3697b0d 100644 --- a/packages/shared-types/src/settlement.ts +++ b/packages/shared-types/src/settlement.ts @@ -11,6 +11,44 @@ export const STORE_WITHDRAW_STATUS_LABELS: Record = PAID: '已结算', }; +/** 总部「门店账单」统一列表:T+1 出账 / 手动提现 */ +export type StoreSettlementKind = 'T1_BILL' | 'WITHDRAW'; + +export const STORE_SETTLEMENT_KIND_LABELS: Record = { + T1_BILL: 'T+1出账', + WITHDRAW: '手动提现', +}; + +export type StoreSettlementStatus = FinancePayStatus | StoreWithdrawStatus; + +export const STORE_SETTLEMENT_STATUS_LABELS: Record = { + UNPAID: '未打款', + PAID: '已打款', + PENDING_REVIEW: '待审核', + REJECTED: '已驳回', +}; + +export interface StoreSettlementRowDto { + kind: StoreSettlementKind; + id: string; + billNo: string; + storeId: string; + amount: number; + status: StoreSettlementStatus; + date: string; + overdue?: boolean; + redeemCount?: number | null; + redeemAmount?: number | null; + settlementRate?: number | null; + payoutCount?: number | null; + store?: { + id: string; + name: string; + cityName: string; + phone?: string | null; + }; +} + export interface StoreWithdrawBankAccountDto { bankAccountName?: string | null; bankAccountNo?: string | null; @@ -73,6 +111,19 @@ export const FINANCE_PAY_STATUS_LABELS: Record = { PAID: '已打款', }; +export function storeSettlementStatusLabel( + kind: StoreSettlementKind, + status: string, +): string { + if (kind === 'WITHDRAW' && status in STORE_WITHDRAW_STATUS_LABELS) { + return STORE_WITHDRAW_STATUS_LABELS[status as StoreWithdrawStatus]; + } + if (status in FINANCE_PAY_STATUS_LABELS) { + return FINANCE_PAY_STATUS_LABELS[status as FinancePayStatus]; + } + return STORE_SETTLEMENT_STATUS_LABELS[status as StoreSettlementStatus] ?? status; +} + export interface PartnerBillDto { id: string; billNo: string; diff --git a/server/dukang-api/src/modules/settlement/settlement.controller.ts b/server/dukang-api/src/modules/settlement/settlement.controller.ts index 58e63e6..171251f 100644 --- a/server/dukang-api/src/modules/settlement/settlement.controller.ts +++ b/server/dukang-api/src/modules/settlement/settlement.controller.ts @@ -224,6 +224,25 @@ export class AdminStorePayoutController { } } +@Controller('admin/store-settlements') +@UseGuards(HqAuthGuard) +export class AdminStoreSettlementController { + constructor(private readonly settlementService: SettlementService) {} + + @Get() + list(@Query() query: Record) { + return this.settlementService.listAdminStoreSettlements({ + page: query.page ? Number(query.page) : 1, + pageSize: query.pageSize ? Number(query.pageSize) : 20, + kind: query.kind, + status: query.status, + storeId: query.storeId, + dateFrom: query.dateFrom, + dateTo: query.dateTo, + }); + } +} + @Controller('admin/store-bills') @UseGuards(HqAuthGuard) export class AdminStoreBillController { diff --git a/server/dukang-api/src/modules/settlement/settlement.module.ts b/server/dukang-api/src/modules/settlement/settlement.module.ts index e0348e9..bfff4ae 100644 --- a/server/dukang-api/src/modules/settlement/settlement.module.ts +++ b/server/dukang-api/src/modules/settlement/settlement.module.ts @@ -9,6 +9,7 @@ import { AdminPartnerBillController, AdminStoreBillController, AdminStorePayoutController, + AdminStoreSettlementController, AdminStoreWithdrawController, AdminWineryBillController, PartnerMeController, @@ -26,6 +27,7 @@ import { ShopWithdrawController, AdminStoreWithdrawController, AdminStorePayoutController, + AdminStoreSettlementController, AdminStoreBillController, AdminPartnerBillController, AdminWineryBillController, diff --git a/server/dukang-api/src/modules/settlement/settlement.service.ts b/server/dukang-api/src/modules/settlement/settlement.service.ts index bb43b3b..163c15a 100644 --- a/server/dukang-api/src/modules/settlement/settlement.service.ts +++ b/server/dukang-api/src/modules/settlement/settlement.service.ts @@ -339,7 +339,7 @@ export class SettlementService { `单号:${created.withdrawNo}`, `金额:¥${Number(created.amount).toFixed(2)}`, `明细:${created.payoutCount} 笔未出账核销(已锁定,不进入次日 T+1 出账)`, - '请尽快在 HQ「财务 → 门店提现审」处理。', + '请尽快在 HQ「财务 → 门店账单(手动提现)」处理。', ].join('\n'), dedupeKey: `store_withdraw_applied|${created.id.toString()}`, dedupeTtlSec: 3600, @@ -594,7 +594,7 @@ export class SettlementService { detail: [ `待审 ${summary.pendingCount} 笔,超时 ${summary.overdueCount} 笔,超时金额 ¥${summary.overdueAmount.toFixed(2)}`, ...lines, - '请尽快在 HQ「财务 → 门店提现审」处理。', + '请尽快在 HQ「财务 → 门店账单(手动提现)」处理。', ].join('\n'), dedupeKey: `store_withdraw_overdue|${new Date().toISOString().slice(0, 10)}`, dedupeTtlSec: 6 * 3600, @@ -857,6 +857,152 @@ export class SettlementService { return { billDate: billDate.toISOString().slice(0, 10), created, skipped, payoutLinked: payouts.length }; } + /** + * 总部「门店账单」统一列表:T+1 StoreBill + 手动提现 StoreWithdrawRequest + * 中小数据量内存合并后分页。 + */ + async listAdminStoreSettlements(query: { + page?: number; + pageSize?: number; + kind?: string; + status?: string; + storeId?: string; + dateFrom?: string; + dateTo?: string; + }) { + const page = query.page ?? 1; + const pageSize = query.pageSize ?? 20; + const kind = query.kind === 'T1_BILL' || query.kind === 'WITHDRAW' ? query.kind : undefined; + const status = query.status?.trim() || undefined; + + const includeBills = + !kind || kind === 'T1_BILL' + ? !status || status === 'UNPAID' || status === 'PAID' + : false; + const includeWithdraws = + !kind || kind === 'WITHDRAW' + ? !status || + status === 'PENDING_REVIEW' || + status === 'REJECTED' || + status === 'PAID' + : false; + + const billWhere = includeBills + ? this.buildStoreBillWhere({ + status: status === 'UNPAID' || status === 'PAID' ? status : undefined, + storeId: query.storeId, + dateFrom: query.dateFrom, + dateTo: query.dateTo, + }) + : null; + + const withdrawWhere: Prisma.StoreWithdrawRequestWhereInput | null = includeWithdraws + ? (() => { + const where: Prisma.StoreWithdrawRequestWhereInput = {}; + if (status === 'PENDING_REVIEW' || status === 'REJECTED' || status === 'PAID') { + where.status = status; + } + if (query.storeId) where.storeId = BigInt(query.storeId); + if (query.dateFrom || query.dateTo) { + where.appliedAt = {}; + if (query.dateFrom) where.appliedAt.gte = new Date(query.dateFrom); + if (query.dateTo) { + const end = new Date(query.dateTo); + end.setHours(23, 59, 59, 999); + where.appliedAt.lte = end; + } + } + return where; + })() + : null; + + const bills = billWhere + ? await this.prisma.storeBill.findMany({ + where: billWhere, + include: { + store: { select: { id: true, name: true, cityName: true, phone: true } }, + }, + }) + : []; + const withdraws = withdrawWhere + ? await this.prisma.storeWithdrawRequest.findMany({ + where: withdrawWhere, + include: { + store: { select: { id: true, name: true, cityName: true, phone: true } }, + }, + }) + : []; + + const now = new Date(); + type UnifiedRow = { + kind: 'T1_BILL' | 'WITHDRAW'; + id: bigint; + billNo: string; + storeId: bigint; + amount: number; + status: string; + date: Date; + overdue?: boolean; + redeemCount?: number; + redeemAmount?: number; + settlementRate?: number; + payoutCount?: number; + store?: { id: bigint; name: string; cityName: string; phone: string | null }; + }; + + const rows: UnifiedRow[] = [ + ...bills.map((b) => ({ + kind: 'T1_BILL' as const, + id: b.id, + billNo: b.billNo, + storeId: b.storeId, + amount: Number(b.payoutAmount), + status: b.status, + date: b.billDate, + redeemCount: b.redeemCount, + redeemAmount: Number(b.redeemAmount), + settlementRate: Number(b.settlementRate), + store: b.store, + })), + ...withdraws.map((w) => ({ + kind: 'WITHDRAW' as const, + id: w.id, + billNo: w.withdrawNo, + storeId: w.storeId, + amount: Number(w.amount), + status: w.status, + date: w.appliedAt, + overdue: w.status === 'PENDING_REVIEW' ? isWithdrawOverdue(w.appliedAt, now) : false, + payoutCount: w.payoutCount, + store: w.store, + })), + ]; + + rows.sort((a, b) => { + const dt = b.date.getTime() - a.date.getTime(); + if (dt !== 0) return dt; + return Number(b.id - a.id); + }); + + const total = rows.length; + const slice = rows.slice((page - 1) * pageSize, page * pageSize); + const redeemAmount = bills.reduce((s, b) => s + Number(b.redeemAmount), 0); + const payoutAmount = rows.reduce((s, r) => s + r.amount, 0); + + return serializeBigInt({ + items: slice, + total, + page, + pageSize, + summary: { + count: total, + redeemAmount, + payoutAmount, + totalAmount: payoutAmount, + }, + }); + } + async listAdminStoreBills(query: { page?: number; pageSize?: number;