import { useEffect, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import { Button, Card, DatePicker, Descriptions, Drawer, Form, Input, Modal, Select, Space, Statistic, Table, Tag, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import type { Dayjs } from 'dayjs'; import { STORE_SETTLEMENT_KIND_LABELS, STORE_SETTLEMENT_STATUS_LABELS, storeSettlementStatusLabel, type StoreSettlementKind, type StoreSettlementStatus, } from '@dukang/shared-types'; import { request, type Paginated } from '../lib/api'; import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants'; import { downloadExcelCsv } from '../lib/exportExcel'; import { useAdminList } from '../lib/useAdminList'; type Row = { kind: StoreSettlementKind; id: string; billNo: 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 }; }; type StoreOption = { id: string; name: string; phone: string }; 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>({ kind: initialKind, status: initialKind === 'WITHDRAW' ? 'PENDING_REVIEW' : '', }); const [stores, setStores] = useState([]); const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( '/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); if (filters.dateTo) qs.set('dateTo', filters.dateTo); return qs; }, [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} 笔 T+1 门店对账单${amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。此操作不可撤销。`, okText: '确认打款', cancelText: '取消', onOk: async () => { if (ids.length === 1) { await request(`/admin/store-bills/${ids[0]}/confirm`, { method: 'POST' }); } else { setBatchLoading(true); try { await request('/admin/store-bills/batch-confirm', { method: 'POST', body: JSON.stringify({ ids }), }); } finally { setBatchLoading(false); } } message.success('已确认打款'); setSelectedKeys([]); reload(); }, }); } 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); } async function exportExcel() { setExporting(true); try { const qs = new URLSearchParams(); if (filters.status) qs.set('status', filters.status); if (filters.storeId) qs.set('storeId', filters.storeId); if (filters.dateFrom) qs.set('dateFrom', filters.dateFrom); 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} 条 T+1 账单`); } finally { setExporting(false); } } const summary = data?.summary; 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: '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: '笔数', width: 80, render: (_, row) => row.kind === 'T1_BILL' ? (row.redeemCount ?? '—') : (row.payoutCount ?? '—'), }, { title: '应付金额', dataIndex: 'amount', width: 110, render: (v) => `¥${Number(v).toFixed(2)}`, }, { title: '状态', dataIndex: 'status', width: 100, render: (s: string, row) => ( {storeSettlementStatusLabel(row.kind, s)} ), }, { title: '操作', width: 200, fixed: 'right', render: (_, row) => ( {row.kind === 'T1_BILL' && 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 (
门店账单 含 T+1 自动出账与门店手动提现;按类型筛选,详情与审核动作沿用原接口 {overdueSummary && overdueSummary.overdueCount > 0 ? ( 手动提现超时未审 {overdueSummary.overdueCount} 笔(待审共 {overdueSummary.pendingCount}) ) : null} {summary && ( )}
{ 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: s.id, label: s.name || s.phone || s.id }))} />