feat(settlement): 合伙人账单生成后直接同步并支持打款凭证
去掉总部发送与合伙人确认,财务打款后上传凭证,合伙人即可查看状态。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -575,7 +575,7 @@ export default function DashboardPage() {
|
||||
<Col xs={24} sm={12} lg={8}>
|
||||
<Card
|
||||
loading={loading}
|
||||
title="待审核合伙人打款"
|
||||
title="待打款合伙人账单"
|
||||
extra={<Link to="/finance/partner-bills">去处理</Link>}
|
||||
>
|
||||
<Statistic
|
||||
@@ -584,9 +584,9 @@ export default function DashboardPage() {
|
||||
valueStyle={{ color: (stats?.pendingBills ?? 0) > 0 ? '#fa8c16' : undefined }}
|
||||
/>
|
||||
<Typography.Text type="secondary">
|
||||
合伙人已确认申请,待总部通过或驳回
|
||||
账单已同步合伙人,待财务打款
|
||||
{(stats?.pendingPartnerDraftBills ?? 0) > 0
|
||||
? `(另有 ${stats?.pendingPartnerDraftBills} 笔待合伙人确认)`
|
||||
? `(另有 ${stats?.pendingPartnerDraftBills} 笔零元账单未同步)`
|
||||
: ''}
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Descriptions,
|
||||
Drawer,
|
||||
Form,
|
||||
Input,
|
||||
Modal,
|
||||
Select,
|
||||
Space,
|
||||
@@ -29,6 +28,7 @@ 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 = {
|
||||
@@ -56,6 +56,7 @@ type Row = {
|
||||
periodEnd: string;
|
||||
rejectReason?: string | null;
|
||||
paymentRef?: string | null;
|
||||
paymentProofUrls?: string[];
|
||||
paidAt?: string | null;
|
||||
orderItems?: BillItemRow[];
|
||||
redeemItems?: BillItemRow[];
|
||||
@@ -80,7 +81,7 @@ type Row = {
|
||||
type PartnerOption = { id: string; companyName?: string | null; name?: string | null; phone?: string };
|
||||
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
PENDING_REVIEW: '待审核',
|
||||
PENDING_REVIEW: '零元未同步',
|
||||
AWAITING_CONFIRM: '待合伙人确认',
|
||||
UNPAID: '未打款',
|
||||
PAID: '已打款',
|
||||
@@ -131,13 +132,11 @@ export default function PartnerBillsPage() {
|
||||
const [genOpen, setGenOpen] = useState(false);
|
||||
const [genForm] = Form.useForm();
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [rejectOpen, setRejectOpen] = useState(false);
|
||||
const [rejectTarget, setRejectTarget] = useState<Row | null>(null);
|
||||
const [rejectForm] = Form.useForm();
|
||||
const [rejecting, setRejecting] = useState(false);
|
||||
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
||||
const [detail, setDetail] = useState<Row | null>(null);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [payModal, setPayModal] = useState<{ ids: string[]; amountHint?: number } | null>(null);
|
||||
const [paySubmitting, setPaySubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
void request<Paginated<PartnerOption>>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
|
||||
@@ -158,101 +157,14 @@ export default function PartnerBillsPage() {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ partnerId: values.partnerId, weekStartYmd }),
|
||||
});
|
||||
message.success('账单已生成(待审核)');
|
||||
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) {
|
||||
let paymentRef = '';
|
||||
Modal.confirm({
|
||||
title: '确认打款?',
|
||||
content: (
|
||||
<div>
|
||||
<div>
|
||||
将标记 {ids.length} 笔账单为已打款
|
||||
{amountHint != null ? `,合计约 ¥${amountHint.toFixed(2)}` : ''}。
|
||||
</div>
|
||||
<Input
|
||||
placeholder="打款凭证号(可选)"
|
||||
style={{ marginTop: 8 }}
|
||||
onChange={(e) => {
|
||||
paymentRef = e.target.value;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
okText: '确认打款',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
const body = JSON.stringify({ paymentRef: paymentRef.trim() || undefined });
|
||||
if (ids.length === 1) {
|
||||
await request(`/admin/partner-bills/${ids[0]}/mark-paid`, { method: 'POST', body });
|
||||
} 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);
|
||||
}
|
||||
setPayModal({ ids, amountHint });
|
||||
}
|
||||
|
||||
async function openDetail(id: string) {
|
||||
@@ -282,10 +194,7 @@ export default function PartnerBillsPage() {
|
||||
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 canSend = selectedRows.filter(
|
||||
(r) => (r.status === 'PENDING_REVIEW' || r.status === 'REJECTED') && Number(r.totalAmount) > 0,
|
||||
);
|
||||
const canPay = selectedRows.filter((r) => r.status === 'UNPAID');
|
||||
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<Row> = [
|
||||
@@ -381,24 +290,9 @@ export default function PartnerBillsPage() {
|
||||
<Button type="link" size="small" onClick={() => void openDetail(row.id)}>
|
||||
明细
|
||||
</Button>
|
||||
{(row.status === 'PENDING_REVIEW' || row.status === 'REJECTED') && Number(row.totalAmount) > 0 && (
|
||||
<Button type="link" size="small" onClick={() => sendBills([row.id])}>
|
||||
发送
|
||||
</Button>
|
||||
)}
|
||||
{row.status === 'UNPAID' && (
|
||||
<>
|
||||
<Button type="link" size="small" onClick={() => markPaid([row.id], Number(row.totalAmount))}>
|
||||
确认打款
|
||||
</Button>
|
||||
<Button type="link" size="small" danger onClick={() => openReject(row)}>
|
||||
驳回
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{row.status === 'AWAITING_CONFIRM' && (
|
||||
<Button type="link" size="small" danger onClick={() => openReject(row)}>
|
||||
驳回
|
||||
{row.status === 'UNPAID' && Number(row.totalAmount) > 0 && (
|
||||
<Button type="link" size="small" onClick={() => markPaid([row.id], Number(row.totalAmount))}>
|
||||
确认打款
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
@@ -414,7 +308,7 @@ export default function PartnerBillsPage() {
|
||||
<AdminListHeader
|
||||
title="合伙人账单"
|
||||
settings={settingsButton}
|
||||
description="每周一 8:00 自动生成上周账单(待审核)→ 发送合伙人确认 → 未打款 → 已打款。零元账单不同步给合伙人。"
|
||||
description="每周一 8:00 自动生成上周账单。应付大于 0 的账单直接同步到合伙人(未打款),财务打款后标记已打款并上传凭证。零元账单不同步给合伙人。"
|
||||
/>
|
||||
|
||||
{summary && (
|
||||
@@ -490,11 +384,6 @@ export default function PartnerBillsPage() {
|
||||
导出 Excel
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button disabled={!canSend.length} onClick={() => sendBills(canSend.map((r) => r.id))}>
|
||||
批量发送 ({canSend.length})
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -552,40 +441,44 @@ export default function PartnerBillsPage() {
|
||||
/>
|
||||
</Form.Item>
|
||||
<Button type="primary" htmlType="submit" block>
|
||||
生成(待审核)
|
||||
生成并同步
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="驳回打款申请"
|
||||
open={rejectOpen}
|
||||
onCancel={() => {
|
||||
setRejectOpen(false);
|
||||
setRejectTarget(null);
|
||||
<FinancePayProofModal
|
||||
open={!!payModal}
|
||||
title="确认打款?"
|
||||
hint={`将标记 ${payModal?.ids.length ?? 0} 笔账单为已打款${
|
||||
payModal?.amountHint != null ? `,合计约 ¥${payModal.amountHint.toFixed(2)}` : ''
|
||||
}。合伙人可看到打款状态和凭证。`}
|
||||
okText="确认打款"
|
||||
confirmLoading={paySubmitting}
|
||||
onCancel={() => setPayModal(null)}
|
||||
onOk={async (payload) => {
|
||||
if (!payModal) return;
|
||||
const { ids } = payModal;
|
||||
setPaySubmitting(true);
|
||||
try {
|
||||
const body = JSON.stringify({ ...payload, ...(ids.length > 1 ? { ids } : {}) });
|
||||
if (ids.length === 1) {
|
||||
await request(`/admin/partner-bills/${ids[0]}/mark-paid`, { method: 'POST', body });
|
||||
} else {
|
||||
await request('/admin/partner-bills/batch-mark-paid', { method: 'POST', body });
|
||||
}
|
||||
message.success('已标记打款');
|
||||
setPayModal(null);
|
||||
setSelectedKeys([]);
|
||||
if (detail && ids.includes(detail.id)) {
|
||||
const d = await request<Row>(`/admin/partner-bills/${detail.id}`);
|
||||
setDetail(d);
|
||||
}
|
||||
reload();
|
||||
} finally {
|
||||
setPaySubmitting(false);
|
||||
}
|
||||
}}
|
||||
footer={null}
|
||||
destroyOnClose
|
||||
>
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
|
||||
账单 {rejectTarget?.billNo} · {rejectTarget ? partnerName(rejectTarget) : ''}
|
||||
</Typography.Paragraph>
|
||||
<Form form={rejectForm} layout="vertical" onFinish={(v) => void submitReject(v)}>
|
||||
<Form.Item
|
||||
name="reason"
|
||||
label="驳回理由"
|
||||
rules={[
|
||||
{ required: true, message: '请填写驳回理由' },
|
||||
{ max: 500, message: '不超过 500 字' },
|
||||
]}
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="请说明驳回原因" maxLength={500} showCount />
|
||||
</Form.Item>
|
||||
<Button type="primary" danger htmlType="submit" block loading={rejecting}>
|
||||
确认驳回
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal>
|
||||
/>
|
||||
|
||||
<Drawer
|
||||
title="合伙人账单明细"
|
||||
@@ -619,9 +512,12 @@ export default function PartnerBillsPage() {
|
||||
<Descriptions.Item label="打款时间">
|
||||
{detail.paidAt ? fmtTime(detail.paidAt) : '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="打款凭证">
|
||||
<Descriptions.Item label="打款凭证号">
|
||||
{detail.paymentRef ? String(detail.paymentRef) : '—'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="打款凭证">
|
||||
<PaymentProofGallery urls={detail.paymentProofUrls} />
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Tabs
|
||||
style={{ marginTop: 16 }}
|
||||
|
||||
Reference in New Issue
Block a user