import { useEffect, useState } from 'react'; import { Button, Card, DatePicker, Form, Input, Modal, Select, Space, Statistic, Table, Tag, 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 { useAdminList } from '../lib/useAdminList'; type Row = { id: string; billNo: string; orderCommission: number; redeemCommission: number; totalAmount: number; status: string; periodStart: string; periodEnd: string; rejectReason?: string | null; partner?: { companyName?: string; phone?: string }; partnerAccount?: { companyName?: string; phone?: string }; }; type PartnerOption = { id: string; companyName: string; 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', }; export default function PartnerBillsPage() { 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.year) qs.set('year', filters.year); if (filters.month) qs.set('month', filters.month); return qs; }, [filters], ); const [genOpen, setGenOpen] = useState(false); const [genForm] = Form.useForm(); const [exporting, setExporting] = useState(false); const [rejectOpen, setRejectOpen] = useState(false); const [rejectTarget, setRejectTarget] = useState(null); const [rejectForm] = Form.useForm(); const [rejecting, setRejecting] = useState(false); const [selectedKeys, setSelectedKeys] = useState([]); useEffect(() => { void request>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`) .then((res) => setPartners(res.items)) .catch(() => {}); }, []); async function generateBill(values: { partnerId?: string; month: Dayjs }) { const year = values.month.year(); const month = values.month.month() + 1; if (!values.partnerId) { const result = await request<{ success: number; failed: number; total: number }>( '/admin/partner-bills/generate-all', { method: 'POST', body: JSON.stringify({ year, month }) }, ); 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, year, month }), }); message.success('账单已生成(待审核)'); } setGenOpen(false); reload(); } function sendBills(ids: string[]) { Modal.confirm({ title: '发送给合伙人?', content: `将发送 ${ids.length} 笔账单到合伙人端,状态变为「待合伙人确认」。`, okText: '确认发送', cancelText: '取消', onOk: async () => { if (ids.length === 1) { await request(`/admin/partner-bills/${ids[0]}/send`, { method: 'POST' }); } else { await request('/admin/partner-bills/batch-send', { method: 'POST', body: JSON.stringify({ ids }), }); } message.success('已发送'); setSelectedKeys([]); reload(); }, }); } function markPaid(ids: string[], amountHint?: number) { Modal.confirm({ title: '确认打款?', content: `将标记 ${ids.length} 笔账单为已打款${amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。`, okText: '确认打款', cancelText: '取消', onOk: async () => { if (ids.length === 1) { await request(`/admin/partner-bills/${ids[0]}/mark-paid`, { method: 'POST', body: JSON.stringify({ paymentRef: `PAY-${Date.now()}` }), }); } else { await request('/admin/partner-bills/batch-mark-paid', { method: 'POST', body: JSON.stringify({ ids }), }); } message.success('已标记打款'); setSelectedKeys([]); reload(); }, }); } function openReject(row: Row) { Modal.confirm({ title: '驳回该账单?', content: '驳回后合伙人需重新等待总部发送。请在下一步填写理由。', okText: '继续填写理由', cancelText: '取消', onOk: () => { setRejectTarget(row); rejectForm.resetFields(); setRejectOpen(true); }, }); } async function submitReject(values: { reason: string }) { if (!rejectTarget) return; setRejecting(true); try { await request(`/admin/partner-bills/${rejectTarget.id}/reject`, { method: 'POST', body: JSON.stringify({ reason: values.reason.trim() }), }); message.success('已驳回'); setRejectOpen(false); setRejectTarget(null); reload(); } finally { setRejecting(false); } } 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.year) qs.set('year', filters.year); if (filters.month) qs.set('month', filters.month); const result = await request<{ csv: string; count: number }>(`/admin/partner-bills/export?${qs}`); const suffix = filters.year && filters.month ? `${filters.year}-${filters.month}` : 'all'; downloadExcelCsv(result.csv, `合伙人账单_${suffix}.csv`); message.success(`已导出 ${result.count} 条`); } finally { setExporting(false); } } const summary = data?.summary; const partnerName = (r: Row) => r.partner?.companyName || r.partnerAccount?.companyName || '—'; const selectedRows = (data?.items ?? []).filter((r) => selectedKeys.includes(r.id)); const canSend = selectedRows.filter((r) => r.status === 'PENDING_REVIEW' || r.status === 'REJECTED'); const canPay = selectedRows.filter((r) => r.status === 'UNPAID'); const payAmount = canPay.reduce((s, r) => s + Number(r.totalAmount), 0); const columns: ColumnsType = [ { title: '账单号', dataIndex: 'billNo', width: 180, ellipsis: true }, { title: '合伙人', width: 160, ellipsis: true, render: (_, r) => partnerName(r), }, { title: '账期', width: 200, render: (_, r) => `${fmtTime(r.periodStart).slice(0, 10)} ~ ${fmtTime(r.periodEnd).slice(0, 10)}`, }, { title: '酒单佣金', dataIndex: 'orderCommission', width: 110, render: (v) => `¥${Number(v).toFixed(2)}`, }, { title: '核销佣金', dataIndex: 'redeemCommission', width: 110, render: (v) => `¥${Number(v).toFixed(2)}`, }, { title: '合计应付', dataIndex: 'totalAmount', width: 110, render: (v) => `¥${Number(v).toFixed(2)}`, }, { title: '状态', 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 === 'PENDING_REVIEW' || row.status === 'REJECTED') && ( )} {row.status === 'UNPAID' && ( <> )} {row.status === 'AWAITING_CONFIRM' && ( )} ), }, ]; return (
合伙人账单 每月 1 日 8:00 自动生成上月账单(待审核)→ 发送合伙人确认 → 未打款 → 已打款 {summary && ( )}
{ setFilters({ partnerId: v.partnerId || '', status: v.status || '', year: v.month ? String(v.month.year()) : '', month: v.month ? String(v.month.month() + 1) : '', }); setPage(1); }} > ({ value, label }))} />
{ setPage(p); setPageSize(ps); }, }} /> setGenOpen(false)} footer={null}>