import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Button, Card, DatePicker, Descriptions, Drawer, Form, Modal, Select, Space, Statistic, Table, Tabs, Tag, Tooltip, Typography, message, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import dayjs, { type Dayjs } from 'dayjs'; import { request, type Paginated } from '../lib/api'; import { ADMIN_OPTIONS_PAGE_SIZE, fmtTime } from '../lib/constants'; import { downloadExcelCsv } from '../lib/exportExcel'; import { appendExportColumns } from '../lib/export-columns'; import { useAdminList } from '../lib/useAdminList'; import { useAdminListColumns } from '../lib/useAdminListColumns'; import { AdminListHeader } from '../components/AdminListHeader'; import { AdminPrimaryLink } from '../components/AdminPrimaryLink'; import { FinancePayProofModal, PaymentProofGallery } from '../components/FinancePayProof'; type BillItemRow = { id: string; kind: 'ORDER' | 'REDEEM'; refId: string; refNo: string; title?: string | null; extra?: string | null; baseAmount: number; rate: number; commission: number; occurredAt: string; }; type Row = { id: string; billNo: string; orderCommission: number; redeemCommission: number; totalAmount: number; status: string; billDate: string; periodStart: string; periodEnd: string; rejectReason?: string | null; paymentRef?: string | null; paymentProofUrls?: string[]; paidAt?: string | null; orderItems?: BillItemRow[]; redeemItems?: BillItemRow[]; partner?: { companyName?: string; name?: string; phone?: string; bankAccountName?: string | null; bankAccountNo?: string | null; bankBranch?: string | null; }; partnerAccount?: { companyName?: string; name?: string; phone?: string; bankAccountName?: string | null; bankAccountNo?: string | null; bankBranch?: string | null; }; }; type PartnerOption = { id: string; companyName?: string | null; name?: string | null; phone?: string }; const STATUS_LABELS: Record = { PENDING_REVIEW: '零元未同步', AWAITING_CONFIRM: '待合伙人确认', UNPAID: '未打款', PAID: '已打款', REJECTED: '已驳回', }; const STATUS_COLORS: Record = { PENDING_REVIEW: 'gold', AWAITING_CONFIRM: 'blue', UNPAID: 'red', PAID: 'green', REJECTED: 'red', }; function partnerDashLabel(p?: { companyName?: string | null; name?: string | null; phone?: string | null; } | null): string { if (!p) return '—'; const company = (p.companyName || '').trim(); const person = (p.name || '').trim(); if (company && person) return `${company}-${person}`; return company || person || (p.phone || '').trim() || '—'; } function weekMonday(d: Dayjs) { const wd = d.day(); return d.subtract(wd === 0 ? 6 : wd - 1, 'day').startOf('day'); } export default function PartnerBillsPage() { const navigate = useNavigate(); const [form] = Form.useForm(); const [filters, setFilters] = useState>({}); const [partners, setPartners] = useState([]); const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList( '/admin/partner-bills', () => { const qs = new URLSearchParams(); if (filters.status) qs.set('status', filters.status); if (filters.partnerId) qs.set('partnerId', filters.partnerId); if (filters.weekStartYmd) qs.set('weekStartYmd', filters.weekStartYmd); return qs; }, [filters], ); const [genOpen, setGenOpen] = useState(false); const [genForm] = Form.useForm(); const [exporting, setExporting] = useState(false); const [selectedKeys, setSelectedKeys] = useState([]); const [detail, setDetail] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); const [payModal, setPayModal] = useState<{ ids: string[]; amountHint?: number } | null>(null); const [paySubmitting, setPaySubmitting] = useState(false); useEffect(() => { void request>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`) .then((res) => setPartners(res.items)) .catch(() => {}); }, []); async function generateBill(values: { partnerId?: string; week: Dayjs }) { const weekStartYmd = weekMonday(values.week).format('YYYY-MM-DD'); if (!values.partnerId) { const result = await request<{ success: number; failed: number; total: number }>( '/admin/partner-bills/generate-all', { method: 'POST', body: JSON.stringify({ weekStartYmd }) }, ); message.success(`已生成 ${result.success}/${result.total} 份账单${result.failed ? `,失败 ${result.failed}` : ''}`); } else { await request('/admin/partner-bills/generate', { method: 'POST', body: JSON.stringify({ partnerId: values.partnerId, weekStartYmd }), }); message.success('账单已生成并同步到合伙人'); } setGenOpen(false); reload(); } function markPaid(ids: string[], amountHint?: number) { setPayModal({ ids, amountHint }); } async function openDetail(id: string) { const d = await request(`/admin/partner-bills/${id}`); setDetail(d); setDrawerOpen(true); } async function exportExcel() { setExporting(true); try { const qs = new URLSearchParams(); if (filters.status) qs.set('status', filters.status); if (filters.partnerId) qs.set('partnerId', filters.partnerId); if (filters.weekStartYmd) qs.set('weekStartYmd', filters.weekStartYmd); appendExportColumns(qs, exportColumnKeys); const result = await request<{ csv: string; count: number }>(`/admin/partner-bills/export?${qs}`); const suffix = filters.weekStartYmd || 'all'; downloadExcelCsv(result.csv, `合伙人账单_${suffix}.csv`); message.success(`已导出 ${result.count} 条`); } finally { setExporting(false); } } const summary = data?.summary; const partnerName = (r: Row) => partnerDashLabel(r.partnerAccount ?? r.partner); const partnerBank = (r: Row) => r.partnerAccount ?? r.partner; const selectedRows = (data?.items ?? []).filter((r) => selectedKeys.includes(r.id)); const canPay = selectedRows.filter((r) => r.status === 'UNPAID' && Number(r.totalAmount) > 0); const payAmount = canPay.reduce((s, r) => s + Number(r.totalAmount), 0); const baseColumns: ColumnsType = [ { title: '账单号', key: '账单号', dataIndex: 'billNo', width: 180, render: (v, row) => ( void openDetail(row.id)}>{v} ), }, { title: '合伙人', key: '合伙人', width: 200, render: (_, r) => partnerName(r), }, { title: ( 出账日 ), key: '出账日', dataIndex: 'billDate', width: 110, render: (v) => String(v || '').slice(0, 10), }, { title: '账期', key: '账期', width: 200, render: (_, r) => `${String(r.periodStart || '').slice(0, 10)} ~ ${String(r.periodEnd || '').slice(0, 10)}`, }, { title: '酒单佣金', key: '酒单佣金', dataIndex: 'orderCommission', width: 110, render: (v) => `¥${Number(v).toFixed(2)}`, }, { title: '核销佣金', key: '核销佣金', dataIndex: 'redeemCommission', width: 110, render: (v) => `¥${Number(v).toFixed(2)}`, }, { title: '合计应付', key: '合计应付', dataIndex: 'totalAmount', width: 130, render: (v) => Number(v) <= 0 ? '¥0.00(不同步)' : `¥${Number(v).toFixed(2)}`, }, { title: '收款户名', key: '收款户名', width: 100, render: (_, r) => partnerBank(r)?.bankAccountName || '—', }, { title: '收款账号', key: '收款账号', width: 140, render: (_, r) => partnerBank(r)?.bankAccountNo || '—', }, { title: '状态', key: '状态', dataIndex: 'status', width: 130, render: (s, row) => ( {STATUS_LABELS[s] || s} {s === 'REJECTED' && row.rejectReason ? ( {row.rejectReason} ) : null} ), }, { title: '操作', width: 220, fixed: 'right', render: (_, row) => ( {row.status === 'UNPAID' && Number(row.totalAmount) > 0 && ( )} ), }, ]; const { columns, settingsButton, settingsModal, exportColumnKeys } = useAdminListColumns('finance-partner-bills', baseColumns, { page, pageSize }); return (
{settingsModal} {summary && ( )}
{ setFilters({ partnerId: v.partnerId || '', status: v.status || '', weekStartYmd: v.week ? weekMonday(v.week).format('YYYY-MM-DD') : '', }); setPage(1); }} > ({ value, label }))} />
{ setPage(p); setPageSize(ps); }, }} /> setGenOpen(false)} footer={null}>
v ? ( navigate(`/orders?orderNo=${encodeURIComponent(v)}`)} > {v} ) : ( '—' ), }, { title: '商品', dataIndex: 'title' }, { title: '数量', dataIndex: 'extra', width: 70 }, { title: '实付', dataIndex: 'baseAmount', render: (v: number) => `¥${Number(v).toFixed(2)}`, }, { title: '费率', dataIndex: 'rate', render: (v: number) => `${(Number(v) * 100).toFixed(2)}%`, }, { title: '佣金', dataIndex: 'commission', render: (v: number) => `¥${Number(v).toFixed(2)}`, }, ]} /> 酒单佣金 ¥{Number(detail.orderCommission).toFixed(2)} ), }, { key: 'redeems', label: `核销订单 (${detail.redeemItems?.length ?? 0})`, children: ( <>
`¥${Number(v).toFixed(2)}`, }, { title: '费率', dataIndex: 'rate', render: (v: number) => `${(Number(v) * 100).toFixed(2)}%`, }, { title: '佣金', dataIndex: 'commission', render: (v: number) => `¥${Number(v).toFixed(2)}`, }, ]} /> 核销佣金 ¥{Number(detail.redeemCommission).toFixed(2)} ), }, ]} /> 收款账户 {partnerBank(detail)?.bankAccountName || '—'} {partnerBank(detail)?.bankAccountNo || '—'} {partnerBank(detail)?.bankBranch || '—'} )} ); }