@@ -575,7 +575,7 @@ export default function DashboardPage() {
|
|||||||
<Col xs={24} sm={12} lg={8}>
|
<Col xs={24} sm={12} lg={8}>
|
||||||
<Card
|
<Card
|
||||||
loading={loading}
|
loading={loading}
|
||||||
title="待审核合伙人打款"
|
title="待打款合伙人账单"
|
||||||
extra={<Link to="/finance/partner-bills">去处理</Link>}
|
extra={<Link to="/finance/partner-bills">去处理</Link>}
|
||||||
>
|
>
|
||||||
<Statistic
|
<Statistic
|
||||||
@@ -584,9 +584,9 @@ export default function DashboardPage() {
|
|||||||
valueStyle={{ color: (stats?.pendingBills ?? 0) > 0 ? '#fa8c16' : undefined }}
|
valueStyle={{ color: (stats?.pendingBills ?? 0) > 0 ? '#fa8c16' : undefined }}
|
||||||
/>
|
/>
|
||||||
<Typography.Text type="secondary">
|
<Typography.Text type="secondary">
|
||||||
合伙人已确认申请,待总部通过或驳回
|
账单已同步合伙人,待财务打款
|
||||||
{(stats?.pendingPartnerDraftBills ?? 0) > 0
|
{(stats?.pendingPartnerDraftBills ?? 0) > 0
|
||||||
? `(另有 ${stats?.pendingPartnerDraftBills} 笔待合伙人确认)`
|
? `(另有 ${stats?.pendingPartnerDraftBills} 笔零元账单未同步)`
|
||||||
: ''}
|
: ''}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
Descriptions,
|
Descriptions,
|
||||||
Drawer,
|
Drawer,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
|
||||||
Modal,
|
Modal,
|
||||||
Select,
|
Select,
|
||||||
Space,
|
Space,
|
||||||
@@ -29,6 +28,7 @@ import { useAdminList } from '../lib/useAdminList';
|
|||||||
import { useAdminListColumns } from '../lib/useAdminListColumns';
|
import { useAdminListColumns } from '../lib/useAdminListColumns';
|
||||||
import { AdminListHeader } from '../components/AdminListHeader';
|
import { AdminListHeader } from '../components/AdminListHeader';
|
||||||
import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
|
import { AdminPrimaryLink } from '../components/AdminPrimaryLink';
|
||||||
|
import { FinancePayProofModal, PaymentProofGallery } from '../components/FinancePayProof';
|
||||||
|
|
||||||
|
|
||||||
type BillItemRow = {
|
type BillItemRow = {
|
||||||
@@ -56,6 +56,7 @@ type Row = {
|
|||||||
periodEnd: string;
|
periodEnd: string;
|
||||||
rejectReason?: string | null;
|
rejectReason?: string | null;
|
||||||
paymentRef?: string | null;
|
paymentRef?: string | null;
|
||||||
|
paymentProofUrls?: string[];
|
||||||
paidAt?: string | null;
|
paidAt?: string | null;
|
||||||
orderItems?: BillItemRow[];
|
orderItems?: BillItemRow[];
|
||||||
redeemItems?: BillItemRow[];
|
redeemItems?: BillItemRow[];
|
||||||
@@ -80,7 +81,7 @@ type Row = {
|
|||||||
type PartnerOption = { id: string; companyName?: string | null; name?: string | null; phone?: string };
|
type PartnerOption = { id: string; companyName?: string | null; name?: string | null; phone?: string };
|
||||||
|
|
||||||
const STATUS_LABELS: Record<string, string> = {
|
const STATUS_LABELS: Record<string, string> = {
|
||||||
PENDING_REVIEW: '待审核',
|
PENDING_REVIEW: '零元未同步',
|
||||||
AWAITING_CONFIRM: '待合伙人确认',
|
AWAITING_CONFIRM: '待合伙人确认',
|
||||||
UNPAID: '未打款',
|
UNPAID: '未打款',
|
||||||
PAID: '已打款',
|
PAID: '已打款',
|
||||||
@@ -131,13 +132,11 @@ export default function PartnerBillsPage() {
|
|||||||
const [genOpen, setGenOpen] = useState(false);
|
const [genOpen, setGenOpen] = useState(false);
|
||||||
const [genForm] = Form.useForm();
|
const [genForm] = Form.useForm();
|
||||||
const [exporting, setExporting] = useState(false);
|
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 [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
||||||
const [detail, setDetail] = useState<Row | null>(null);
|
const [detail, setDetail] = useState<Row | null>(null);
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const [payModal, setPayModal] = useState<{ ids: string[]; amountHint?: number } | null>(null);
|
||||||
|
const [paySubmitting, setPaySubmitting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void request<Paginated<PartnerOption>>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
|
void request<Paginated<PartnerOption>>(`/admin/partners?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
|
||||||
@@ -158,101 +157,14 @@ export default function PartnerBillsPage() {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ partnerId: values.partnerId, weekStartYmd }),
|
body: JSON.stringify({ partnerId: values.partnerId, weekStartYmd }),
|
||||||
});
|
});
|
||||||
message.success('账单已生成(待审核)');
|
message.success('账单已生成并同步到合伙人');
|
||||||
}
|
}
|
||||||
setGenOpen(false);
|
setGenOpen(false);
|
||||||
reload();
|
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) {
|
function markPaid(ids: string[], amountHint?: number) {
|
||||||
let paymentRef = '';
|
setPayModal({ ids, amountHint });
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openDetail(id: string) {
|
async function openDetail(id: string) {
|
||||||
@@ -282,10 +194,7 @@ export default function PartnerBillsPage() {
|
|||||||
const partnerName = (r: Row) => partnerDashLabel(r.partnerAccount ?? r.partner);
|
const partnerName = (r: Row) => partnerDashLabel(r.partnerAccount ?? r.partner);
|
||||||
const partnerBank = (r: Row) => r.partnerAccount ?? r.partner;
|
const partnerBank = (r: Row) => r.partnerAccount ?? r.partner;
|
||||||
const selectedRows = (data?.items ?? []).filter((r) => selectedKeys.includes(r.id));
|
const selectedRows = (data?.items ?? []).filter((r) => selectedKeys.includes(r.id));
|
||||||
const canSend = selectedRows.filter(
|
const canPay = selectedRows.filter((r) => r.status === 'UNPAID' && Number(r.totalAmount) > 0);
|
||||||
(r) => (r.status === 'PENDING_REVIEW' || r.status === 'REJECTED') && Number(r.totalAmount) > 0,
|
|
||||||
);
|
|
||||||
const canPay = selectedRows.filter((r) => r.status === 'UNPAID');
|
|
||||||
const payAmount = canPay.reduce((s, r) => s + Number(r.totalAmount), 0);
|
const payAmount = canPay.reduce((s, r) => s + Number(r.totalAmount), 0);
|
||||||
|
|
||||||
const baseColumns: ColumnsType<Row> = [
|
const baseColumns: ColumnsType<Row> = [
|
||||||
@@ -381,24 +290,9 @@ export default function PartnerBillsPage() {
|
|||||||
<Button type="link" size="small" onClick={() => void openDetail(row.id)}>
|
<Button type="link" size="small" onClick={() => void openDetail(row.id)}>
|
||||||
明细
|
明细
|
||||||
</Button>
|
</Button>
|
||||||
{(row.status === 'PENDING_REVIEW' || row.status === 'REJECTED') && Number(row.totalAmount) > 0 && (
|
{row.status === 'UNPAID' && Number(row.totalAmount) > 0 && (
|
||||||
<Button type="link" size="small" onClick={() => sendBills([row.id])}>
|
<Button type="link" size="small" onClick={() => markPaid([row.id], Number(row.totalAmount))}>
|
||||||
发送
|
确认打款
|
||||||
</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)}>
|
|
||||||
驳回
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -414,7 +308,7 @@ export default function PartnerBillsPage() {
|
|||||||
<AdminListHeader
|
<AdminListHeader
|
||||||
title="合伙人账单"
|
title="合伙人账单"
|
||||||
settings={settingsButton}
|
settings={settingsButton}
|
||||||
description="每周一 8:00 自动生成上周账单(待审核)→ 发送合伙人确认 → 未打款 → 已打款。零元账单不同步给合伙人。"
|
description="每周一 8:00 自动生成上周账单。应付大于 0 的账单直接同步到合伙人(未打款),财务打款后标记已打款并上传凭证。零元账单不同步给合伙人。"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{summary && (
|
{summary && (
|
||||||
@@ -490,11 +384,6 @@ export default function PartnerBillsPage() {
|
|||||||
导出 Excel
|
导出 Excel
|
||||||
</Button>
|
</Button>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item>
|
|
||||||
<Button disabled={!canSend.length} onClick={() => sendBills(canSend.map((r) => r.id))}>
|
|
||||||
批量发送 ({canSend.length})
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -552,40 +441,44 @@ export default function PartnerBillsPage() {
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Button type="primary" htmlType="submit" block>
|
<Button type="primary" htmlType="submit" block>
|
||||||
生成(待审核)
|
生成并同步
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<FinancePayProofModal
|
||||||
title="驳回打款申请"
|
open={!!payModal}
|
||||||
open={rejectOpen}
|
title="确认打款?"
|
||||||
onCancel={() => {
|
hint={`将标记 ${payModal?.ids.length ?? 0} 笔账单为已打款${
|
||||||
setRejectOpen(false);
|
payModal?.amountHint != null ? `,合计约 ¥${payModal.amountHint.toFixed(2)}` : ''
|
||||||
setRejectTarget(null);
|
}。合伙人可看到打款状态和凭证。`}
|
||||||
|
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
|
<Drawer
|
||||||
title="合伙人账单明细"
|
title="合伙人账单明细"
|
||||||
@@ -619,9 +512,12 @@ export default function PartnerBillsPage() {
|
|||||||
<Descriptions.Item label="打款时间">
|
<Descriptions.Item label="打款时间">
|
||||||
{detail.paidAt ? fmtTime(detail.paidAt) : '—'}
|
{detail.paidAt ? fmtTime(detail.paidAt) : '—'}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="打款凭证">
|
<Descriptions.Item label="打款凭证号">
|
||||||
{detail.paymentRef ? String(detail.paymentRef) : '—'}
|
{detail.paymentRef ? String(detail.paymentRef) : '—'}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="打款凭证">
|
||||||
|
<PaymentProofGallery urls={detail.paymentProofUrls} />
|
||||||
|
</Descriptions.Item>
|
||||||
</Descriptions>
|
</Descriptions>
|
||||||
<Tabs
|
<Tabs
|
||||||
style={{ marginTop: 16 }}
|
style={{ marginTop: 16 }}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import PageHeader from '@dukang/shared-ui/PageHeader';
|
import PageHeader from '@dukang/shared-ui/PageHeader';
|
||||||
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
import PullToRefresh from '@dukang/shared-ui/PullToRefresh';
|
||||||
import type { PartnerBillDetailDto, PartnerBillDto, PartnerBillItemDto } from '@dukang/shared-types';
|
import type { PartnerBillDetailDto, PartnerBillDto, PartnerBillItemDto } from '@dukang/shared-types';
|
||||||
import { isLoggedIn, request } from '../lib/api';
|
import { isLoggedIn, request } from '../lib/api';
|
||||||
import { formatBillPeriod } from '../lib/settlement';
|
import { formatBillPeriod } from '../lib/settlement';
|
||||||
import { toastError, toastSuccess } from '../lib/toast';
|
|
||||||
import { usePartnerPageView } from '../lib/usePageView';
|
import { usePartnerPageView } from '../lib/usePageView';
|
||||||
|
|
||||||
function fmtMoney(n: number) {
|
function fmtMoney(n: number) {
|
||||||
@@ -14,16 +13,34 @@ function fmtMoney(n: number) {
|
|||||||
|
|
||||||
function billStatusLabel(status: string) {
|
function billStatusLabel(status: string) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'AWAITING_CONFIRM': return '待确认';
|
|
||||||
case 'UNPAID': return '未打款';
|
case 'UNPAID': return '未打款';
|
||||||
case 'PAID': return '已打款';
|
case 'PAID': return '已打款';
|
||||||
case 'REJECTED': return '已驳回';
|
|
||||||
default: return status;
|
default: return status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function canConfirm(status: string) {
|
function PaymentProofs({ urls, paymentRef }: { urls?: string[]; paymentRef?: string | null }) {
|
||||||
return status === 'AWAITING_CONFIRM';
|
const list = (urls ?? []).filter(Boolean);
|
||||||
|
if (!list.length && !paymentRef) return null;
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<p className="label-md text-muted" style={{ marginBottom: 8 }}>打款凭证</p>
|
||||||
|
{paymentRef ? <p className="body-md" style={{ marginBottom: 8 }}>凭证号 {paymentRef}</p> : null}
|
||||||
|
{list.length > 0 && (
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||||
|
{list.map((url) => (
|
||||||
|
<a key={url} href={url} target="_blank" rel="noreferrer">
|
||||||
|
<img
|
||||||
|
src={url}
|
||||||
|
alt="打款凭证"
|
||||||
|
style={{ width: 72, height: 72, objectFit: 'cover', borderRadius: 6, display: 'block' }}
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BillsPage() {
|
export default function BillsPage() {
|
||||||
@@ -31,10 +48,7 @@ export default function BillsPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [bills, setBills] = useState<PartnerBillDto[]>([]);
|
const [bills, setBills] = useState<PartnerBillDto[]>([]);
|
||||||
const [confirmed, setConfirmed] = useState(false);
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
||||||
const [detail, setDetail] = useState<PartnerBillDetailDto | null>(null);
|
const [detail, setDetail] = useState<PartnerBillDetailDto | null>(null);
|
||||||
|
|
||||||
async function loadBills() {
|
async function loadBills() {
|
||||||
@@ -55,18 +69,12 @@ export default function BillsPage() {
|
|||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
const billId = searchParams.get('id');
|
const billId = searchParams.get('id');
|
||||||
const actionable = useMemo(
|
|
||||||
() => bills.filter((b) => canConfirm(b.status)),
|
|
||||||
[bills],
|
|
||||||
);
|
|
||||||
const bill = billId
|
const bill = billId
|
||||||
? bills.find((b) => String(b.id) === billId) ?? actionable[0] ?? bills[0]
|
? bills.find((b) => String(b.id) === billId) ?? bills[0]
|
||||||
: actionable[0] ?? bills[0];
|
: bills.find((b) => b.status === 'UNPAID') ?? bills[0];
|
||||||
|
|
||||||
const status = String(bill?.status || '');
|
const status = String(bill?.status || '');
|
||||||
const showApplyForm = !!bill && canConfirm(status);
|
|
||||||
const isUnpaid = status === 'UNPAID';
|
const isUnpaid = status === 'UNPAID';
|
||||||
const isRejected = status === 'REJECTED';
|
|
||||||
const isPaid = status === 'PAID';
|
const isPaid = status === 'PAID';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -107,80 +115,42 @@ export default function BillsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function askConfirm(ids: string[]) {
|
const proofUrls = detail?.paymentProofUrls ?? bill?.paymentProofUrls;
|
||||||
if (!window.confirm(`确认 ${ids.length} 笔账单无误并提交?确认后状态将变为「未打款」,等待总部打款。`)) {
|
const paymentRef = detail?.paymentRef ?? bill?.paymentRef;
|
||||||
return;
|
|
||||||
}
|
|
||||||
void doConfirm(ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function doConfirm(ids: string[]) {
|
|
||||||
setSubmitting(true);
|
|
||||||
try {
|
|
||||||
if (ids.length === 1) {
|
|
||||||
await request('PARTNER_H5', `/partner/settlement/bills/${ids[0]}/confirm`, { method: 'POST' });
|
|
||||||
} else {
|
|
||||||
await request('PARTNER_H5', '/partner/settlement/bills/batch-confirm', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ ids }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
toastSuccess('已确认,等待总部打款');
|
|
||||||
setConfirmed(false);
|
|
||||||
setSelectedIds([]);
|
|
||||||
await loadBills();
|
|
||||||
} catch (e) {
|
|
||||||
toastError(e instanceof Error ? e.message : '提交失败');
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleSelect(id: string) {
|
|
||||||
setSelectedIds((prev) => (prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]));
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PullToRefresh onRefresh={loadBills} className="partner-bills-page">
|
<PullToRefresh onRefresh={loadBills} className="partner-bills-page">
|
||||||
<PageHeader title="账单确认" onBack={() => navigate('/center/settlement')} />
|
<PageHeader title="我的账单" onBack={() => navigate('/center/settlement')} />
|
||||||
|
|
||||||
<div className="partner-bill-stepper">
|
<div className="partner-bill-stepper">
|
||||||
<div className="partner-stepper-inner">
|
<div className="partner-stepper-inner">
|
||||||
<div className="partner-stepper-line" aria-hidden>
|
<div className="partner-stepper-line" aria-hidden>
|
||||||
<div
|
<div
|
||||||
className="partner-stepper-line-fill"
|
className="partner-stepper-line-fill"
|
||||||
style={{ width: isPaid ? '100%' : isUnpaid || isRejected ? '75%' : '50%' }}
|
style={{ width: isPaid ? '100%' : '50%' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="partner-step">
|
<div className="partner-step">
|
||||||
<div className="partner-step-circle partner-step-circle--sm partner-step-circle--done">
|
<div className="partner-step-circle partner-step-circle--sm partner-step-circle--done">
|
||||||
<span className="material-symbols-outlined" style={{ fontSize: 14, fontVariationSettings: "'FILL' 1" }}>check</span>
|
<span className="material-symbols-outlined" style={{ fontSize: 14, fontVariationSettings: "'FILL' 1" }}>check</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="partner-step-label partner-step-label--active">数据核算</span>
|
<span className="partner-step-label partner-step-label--active">账单出具</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="partner-step">
|
<div className="partner-step">
|
||||||
<div className={`partner-step-circle partner-step-circle--sm${showApplyForm || isUnpaid || isRejected || isPaid ? ' partner-step-circle--done' : ' partner-step-circle--active'}`}>
|
<div className={`partner-step-circle partner-step-circle--sm${isPaid ? ' partner-step-circle--done' : ' partner-step-circle--active'}`}>
|
||||||
{showApplyForm && !confirmed ? '2' : (
|
|
||||||
<span className="material-symbols-outlined" style={{ fontSize: 14, fontVariationSettings: "'FILL' 1" }}>check</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span className="partner-step-label partner-step-label--active">账单确认</span>
|
|
||||||
</div>
|
|
||||||
<div className="partner-step">
|
|
||||||
<div className={`partner-step-circle partner-step-circle--sm${isPaid ? ' partner-step-circle--done' : isUnpaid || isRejected ? ' partner-step-circle--active' : ''}`}>
|
|
||||||
{isPaid ? (
|
{isPaid ? (
|
||||||
<span className="material-symbols-outlined" style={{ fontSize: 14, fontVariationSettings: "'FILL' 1" }}>check</span>
|
<span className="material-symbols-outlined" style={{ fontSize: 14, fontVariationSettings: "'FILL' 1" }}>check</span>
|
||||||
) : '3'}
|
) : '2'}
|
||||||
</div>
|
</div>
|
||||||
<span className={`partner-step-label${isUnpaid || isRejected || isPaid ? ' partner-step-label--active' : ''}`}>
|
<span className={`partner-step-label${isUnpaid || isPaid ? ' partner-step-label--active' : ''}`}>
|
||||||
{isRejected ? '已驳回' : isPaid ? '已打款' : '总部打款'}
|
{isPaid ? '已打款' : '财务打款'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading && <div className="empty">加载中…</div>}
|
{loading && <div className="empty">加载中…</div>}
|
||||||
{!loading && !bill && <div className="empty">暂无待确认账单</div>}
|
{!loading && !bill && <div className="empty">暂无账单</div>}
|
||||||
|
|
||||||
{bill && (
|
{bill && (
|
||||||
<section className="partner-bill-card">
|
<section className="partner-bill-card">
|
||||||
@@ -192,26 +162,25 @@ export default function BillsPage() {
|
|||||||
<h2 className="headline-lg" style={{ fontSize: 20 }}>{String(bill.billNo || '周结算账单')}</h2>
|
<h2 className="headline-lg" style={{ fontSize: 20 }}>{String(bill.billNo || '周结算账单')}</h2>
|
||||||
<p className="label-md text-muted" style={{ marginTop: 4 }}>{formatBillPeriod(bill.periodStart, bill.periodEnd)}</p>
|
<p className="label-md text-muted" style={{ marginTop: 4 }}>{formatBillPeriod(bill.periodStart, bill.periodEnd)}</p>
|
||||||
</div>
|
</div>
|
||||||
<span className={`partner-status-pill${isRejected ? ' partner-status-pill--closed' : isUnpaid ? ' partner-status-pill--paused' : isPaid ? ' partner-status-pill--open' : ' partner-status-pill--paused'}`}>
|
<span className={`partner-status-pill${isUnpaid ? ' partner-status-pill--paused' : isPaid ? ' partner-status-pill--open' : ' partner-status-pill--paused'}`}>
|
||||||
{billStatusLabel(status)}
|
{billStatusLabel(status)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isRejected && bill.rejectReason && (
|
|
||||||
<div className="partner-info-banner" style={{ marginBottom: 16, background: 'rgba(166,29,36,0.06)' }}>
|
|
||||||
<span className="material-symbols-outlined text-primary" style={{ fontVariationSettings: "'FILL' 1" }}>error</span>
|
|
||||||
<div>
|
|
||||||
<p className="body-md text-primary" style={{ fontWeight: 600, marginBottom: 4 }}>账单已驳回</p>
|
|
||||||
<p className="body-md text-variant" style={{ lineHeight: 1.5 }}>{bill.rejectReason}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isUnpaid && (
|
{isUnpaid && (
|
||||||
<div className="partner-info-banner" style={{ marginBottom: 16 }}>
|
<div className="partner-info-banner" style={{ marginBottom: 16 }}>
|
||||||
<span className="material-symbols-outlined text-primary" style={{ fontVariationSettings: "'FILL' 1" }}>hourglass_top</span>
|
<span className="material-symbols-outlined text-primary" style={{ fontVariationSettings: "'FILL' 1" }}>hourglass_top</span>
|
||||||
<p className="body-md text-variant" style={{ lineHeight: 1.5 }}>
|
<p className="body-md text-variant" style={{ lineHeight: 1.5 }}>
|
||||||
您已确认账单,总部打款中,请耐心等待。
|
账单已同步,等待财务打款。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isPaid && (
|
||||||
|
<div className="partner-info-banner" style={{ marginBottom: 16 }}>
|
||||||
|
<span className="material-symbols-outlined text-primary" style={{ fontVariationSettings: "'FILL' 1" }}>check_circle</span>
|
||||||
|
<p className="body-md text-variant" style={{ lineHeight: 1.5 }}>
|
||||||
|
财务已打款{bill.paidAt ? `(${String(bill.paidAt).slice(0, 10)})` : ''}。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -226,94 +195,42 @@ export default function BillsPage() {
|
|||||||
|
|
||||||
{renderItemList('酒订单', detail?.orderItems, Number(detail?.orderCommission ?? bill.orderCommission ?? 0))}
|
{renderItemList('酒订单', detail?.orderItems, Number(detail?.orderCommission ?? bill.orderCommission ?? 0))}
|
||||||
{renderItemList('核销订单', detail?.redeemItems, Number(detail?.redeemCommission ?? bill.redeemCommission ?? 0))}
|
{renderItemList('核销订单', detail?.redeemItems, Number(detail?.redeemCommission ?? bill.redeemCommission ?? 0))}
|
||||||
|
{isPaid && <PaymentProofs urls={proofUrls} paymentRef={paymentRef} />}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{bills.length > 0 && (
|
{bills.length > 0 && (
|
||||||
<div style={{ padding: '0 20px' }}>
|
<div style={{ padding: '0 20px 24px' }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
<h3 className="headline-md" style={{ margin: '0 0 12px' }}>全部账单</h3>
|
||||||
<h3 className="headline-md" style={{ margin: 0 }}>全部账单</h3>
|
|
||||||
{actionable.length > 1 && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="partner-fill-max"
|
|
||||||
style={{ fontSize: 13 }}
|
|
||||||
disabled={!selectedIds.length || submitting}
|
|
||||||
onClick={() => askConfirm(selectedIds)}
|
|
||||||
>
|
|
||||||
批量确认 ({selectedIds.length})
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{bills.map((b) => {
|
{bills.map((b) => {
|
||||||
const id = String(b.id);
|
const id = String(b.id);
|
||||||
const selectable = canConfirm(b.status);
|
const active = bill && String(bill.id) === id;
|
||||||
return (
|
return (
|
||||||
<div
|
<button
|
||||||
key={id}
|
key={id}
|
||||||
|
type="button"
|
||||||
className="partner-store-card"
|
className="partner-store-card"
|
||||||
style={{ margin: '0 0 12px', display: 'flex', gap: 10, alignItems: 'flex-start' }}
|
style={{
|
||||||
|
margin: '0 0 12px',
|
||||||
|
width: '100%',
|
||||||
|
textAlign: 'left',
|
||||||
|
border: active ? '1px solid var(--md-sys-color-primary, #a61d24)' : undefined,
|
||||||
|
}}
|
||||||
|
onClick={() => navigate(`/center/bills?id=${id}`)}
|
||||||
>
|
>
|
||||||
{selectable ? (
|
<div className="partner-store-card-header" style={{ marginBottom: 0 }}>
|
||||||
<input
|
<div>
|
||||||
type="checkbox"
|
<p className="body-md">{b.billNo}</p>
|
||||||
checked={selectedIds.includes(id)}
|
<p className="label-md text-muted">{formatBillPeriod(b.periodStart, b.periodEnd)} · {billStatusLabel(b.status)}</p>
|
||||||
onChange={() => toggleSelect(id)}
|
|
||||||
style={{ marginTop: 4 }}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span style={{ width: 16 }} />
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
style={{ flex: 1, textAlign: 'left', background: 'none', border: 'none', padding: 0, cursor: 'pointer' }}
|
|
||||||
onClick={() => navigate(`/center/bills?id=${id}`)}
|
|
||||||
>
|
|
||||||
<div className="partner-store-card-header" style={{ marginBottom: 0 }}>
|
|
||||||
<div>
|
|
||||||
<p className="body-md">{b.billNo}</p>
|
|
||||||
<p className="label-md text-muted">{formatBillPeriod(b.periodStart, b.periodEnd)} · {billStatusLabel(b.status)}</p>
|
|
||||||
{b.status === 'REJECTED' && b.rejectReason ? (
|
|
||||||
<p className="label-md text-primary" style={{ marginTop: 4 }}>驳回:{b.rejectReason}</p>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<span className="amount-lg" style={{ fontSize: 18 }}>¥{Number(b.totalAmount).toFixed(2)}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
<span className="amount-lg" style={{ fontSize: 18 }}>¥{Number(b.totalAmount).toFixed(2)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showApplyForm && (
|
|
||||||
<>
|
|
||||||
<div className="partner-info-banner" style={{ marginTop: 16 }}>
|
|
||||||
<span className="material-symbols-outlined text-primary" style={{ fontVariationSettings: "'FILL' 1" }}>info</span>
|
|
||||||
<p className="body-md text-variant" style={{ lineHeight: 1.5 }}>
|
|
||||||
确认后账单变为「未打款」,由总部完成打款。如有异议请先联系城市运营经理。
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer className="partner-bill-footer">
|
|
||||||
<label className="partner-checkbox-row" style={{ marginBottom: 16 }}>
|
|
||||||
<input type="checkbox" checked={confirmed} onChange={(e) => setConfirmed(e.target.checked)} />
|
|
||||||
<span>我已核对数据无误,同意根据此账单进行结算</span>
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="partner-btn-primary"
|
|
||||||
disabled={!confirmed || submitting}
|
|
||||||
style={{ opacity: confirmed ? 1 : 0.5 }}
|
|
||||||
onClick={() => askConfirm([String(bill!.id)])}
|
|
||||||
>
|
|
||||||
{submitting ? '正在提交...' : '确认账单'}
|
|
||||||
{!submitting && <span className="material-symbols-outlined">payments</span>}
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</PullToRefresh>
|
</PullToRefresh>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,19 +62,14 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag
|
|||||||
}, [loadCenter]);
|
}, [loadCenter]);
|
||||||
|
|
||||||
const finance = useMemo(() => {
|
const finance = useMemo(() => {
|
||||||
const pending = bills.filter((b) => b.status === 'AWAITING_CONFIRM');
|
const unpaid = bills.filter((b) => b.status === 'UNPAID');
|
||||||
const settled = bills.filter((b) => b.status === 'PAID');
|
const settled = bills.filter((b) => b.status === 'PAID');
|
||||||
const paidCount = settled.length;
|
|
||||||
const rejectedCount = bills.filter((b) => b.status === 'REJECTED').length;
|
|
||||||
return {
|
return {
|
||||||
pendingBillCount: pending.length,
|
unpaidCount: unpaid.length,
|
||||||
rejectedCount,
|
pendingTotal: unpaid.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
||||||
pendingTotal: bills
|
|
||||||
.filter((b) => b.status === 'AWAITING_CONFIRM' || b.status === 'UNPAID' || b.status === 'REJECTED')
|
|
||||||
.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
|
||||||
settledTotal: settled.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
settledTotal: settled.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
||||||
balance: settled.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
balance: settled.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
||||||
paidCount,
|
paidCount: settled.length,
|
||||||
};
|
};
|
||||||
}, [bills]);
|
}, [bills]);
|
||||||
|
|
||||||
@@ -167,20 +162,16 @@ export default function CenterPage({ variant = 'primary', roleLabel }: CenterPag
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{finance.pendingBillCount > 0 && (
|
{finance.unpaidCount > 0 && (
|
||||||
<Link to="/center/bills" className="partner-bills-banner">
|
<Link to="/center/bills" className="partner-bills-banner">
|
||||||
<div className="partner-bills-banner-left">
|
<div className="partner-bills-banner-left">
|
||||||
<div className="partner-bills-icon">
|
<div className="partner-bills-icon">
|
||||||
<span className="material-symbols-outlined">receipt_long</span>
|
<span className="material-symbols-outlined">receipt_long</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="body-md text-primary" style={{ fontWeight: 600 }}>
|
<p className="body-md text-primary" style={{ fontWeight: 600 }}>待打款账单</p>
|
||||||
{finance.rejectedCount > 0 ? '待处理账单' : '待确认账单'}
|
|
||||||
</p>
|
|
||||||
<p className="label-md text-primary" style={{ opacity: 0.85 }}>
|
<p className="label-md text-primary" style={{ opacity: 0.85 }}>
|
||||||
{finance.rejectedCount > 0
|
您有 {finance.unpaidCount} 笔账单待财务打款
|
||||||
? `您有 ${finance.pendingBillCount} 笔账单待处理(含 ${finance.rejectedCount} 笔已驳回)`
|
|
||||||
: `您有 ${finance.pendingBillCount} 笔账单待确认`}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { PartnerBillDto, PartnerSettlementPreviewDto } from '@dukang/shared
|
|||||||
import { isLoggedIn, request } from '../lib/api';
|
import { isLoggedIn, request } from '../lib/api';
|
||||||
import { formatBillPeriod, formatIssueLabel } from '../lib/settlement';
|
import { formatBillPeriod, formatIssueLabel } from '../lib/settlement';
|
||||||
|
|
||||||
type StatusFilter = 'all' | 'pending' | 'settled' | 'reviewing' | 'rejected';
|
type StatusFilter = 'all' | 'unpaid' | 'paid';
|
||||||
|
|
||||||
function fmtMoney(n: number) {
|
function fmtMoney(n: number) {
|
||||||
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
@@ -14,20 +14,16 @@ function fmtMoney(n: number) {
|
|||||||
|
|
||||||
function billStatusLabel(status: string) {
|
function billStatusLabel(status: string) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'AWAITING_CONFIRM': return '待确认';
|
|
||||||
case 'UNPAID': return '未打款';
|
case 'UNPAID': return '未打款';
|
||||||
case 'PAID': return '已打款';
|
case 'PAID': return '已打款';
|
||||||
case 'REJECTED': return '已驳回';
|
|
||||||
default: return status;
|
default: return status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function billStatusClass(status: string) {
|
function billStatusClass(status: string) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'AWAITING_CONFIRM': return 'partner-settlement-status--pending';
|
|
||||||
case 'UNPAID': return 'partner-settlement-status--reviewing';
|
case 'UNPAID': return 'partner-settlement-status--reviewing';
|
||||||
case 'PAID': return 'partner-settlement-status--settled';
|
case 'PAID': return 'partner-settlement-status--settled';
|
||||||
case 'REJECTED': return 'partner-settlement-status--rejected';
|
|
||||||
default: return '';
|
default: return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,10 +53,10 @@ export default function SettlementPage() {
|
|||||||
}, [navigate, loadBills]);
|
}, [navigate, loadBills]);
|
||||||
|
|
||||||
const summary = useMemo(() => {
|
const summary = useMemo(() => {
|
||||||
const pending = bills.filter((b) => b.status === 'AWAITING_CONFIRM');
|
const unpaid = bills.filter((b) => b.status === 'UNPAID');
|
||||||
const settled = bills.filter((b) => b.status === 'PAID');
|
const settled = bills.filter((b) => b.status === 'PAID');
|
||||||
return {
|
return {
|
||||||
pendingTotal: pending.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
pendingTotal: unpaid.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
||||||
settledTotal: settled.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
settledTotal: settled.reduce((s, b) => s + Number(b.totalAmount || 0), 0),
|
||||||
};
|
};
|
||||||
}, [bills]);
|
}, [bills]);
|
||||||
@@ -68,10 +64,8 @@ export default function SettlementPage() {
|
|||||||
const filteredBills = useMemo(() => {
|
const filteredBills = useMemo(() => {
|
||||||
return bills.filter((b) => {
|
return bills.filter((b) => {
|
||||||
if (statusFilter === 'all') return true;
|
if (statusFilter === 'all') return true;
|
||||||
if (statusFilter === 'pending') return b.status === 'AWAITING_CONFIRM';
|
if (statusFilter === 'unpaid') return b.status === 'UNPAID';
|
||||||
if (statusFilter === 'settled') return b.status === 'PAID';
|
if (statusFilter === 'paid') return b.status === 'PAID';
|
||||||
if (statusFilter === 'reviewing') return b.status === 'UNPAID';
|
|
||||||
if (statusFilter === 'rejected') return b.status === 'REJECTED';
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}, [bills, statusFilter]);
|
}, [bills, statusFilter]);
|
||||||
@@ -82,10 +76,8 @@ export default function SettlementPage() {
|
|||||||
|
|
||||||
const statusTabs: Array<{ key: StatusFilter; label: string }> = [
|
const statusTabs: Array<{ key: StatusFilter; label: string }> = [
|
||||||
{ key: 'all', label: '全部' },
|
{ key: 'all', label: '全部' },
|
||||||
{ key: 'pending', label: '待确认' },
|
{ key: 'unpaid', label: '未打款' },
|
||||||
{ key: 'reviewing', label: '未打款' },
|
{ key: 'paid', label: '已打款' },
|
||||||
{ key: 'rejected', label: '已驳回' },
|
|
||||||
{ key: 'settled', label: '已打款' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -164,12 +156,7 @@ export default function SettlementPage() {
|
|||||||
<span className="partner-settlement-dot" />
|
<span className="partner-settlement-dot" />
|
||||||
订单分佣 ¥{fmtMoney(Number(bill.orderCommission || 0))}
|
订单分佣 ¥{fmtMoney(Number(bill.orderCommission || 0))}
|
||||||
</p>
|
</p>
|
||||||
{bill.status === 'REJECTED' && bill.rejectReason ? (
|
<p className={`partner-settlement-item-amount${bill.status === 'UNPAID' ? ' text-primary' : ''}`}>
|
||||||
<p className="label-md text-primary" style={{ marginTop: 6 }}>
|
|
||||||
驳回:{bill.rejectReason}
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
<p className={`partner-settlement-item-amount${bill.status === 'AWAITING_CONFIRM' || bill.status === 'REJECTED' ? ' text-primary' : ''}`}>
|
|
||||||
¥{fmtMoney(Number(bill.totalAmount || 0))}
|
¥{fmtMoney(Number(bill.totalAmount || 0))}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-2
@@ -72,14 +72,14 @@ C 端购酒核销 · 门店扫码核销+打款 · 合伙人拓店履约 · WebAd
|
|||||||
|
|
||||||
**推广码渠道负责人 / 关联合伙人(v4.0.20)**:规则见 v4-PRD §2.1 与 [`v4.0.20 开发文档`](./杜康好客-v4.0.20-开发文档.md)。表 `promo_code_channel_owner`(多对多主合伙人)+ `common_promo_code.assoc_partner_account_id`。HQ `POST/PUT /admin/promo-codes` 字段 `channelOwnerPartnerIds`、`assocPartnerAccountId`(须主账号 ACTIVE)。`POST /promo/touch` 登录后对关联合伙人 `PartnerCityService.tryBindIfUnbound`(已绑他人静默跳过,不回刷历史;Promo 不 import StoreModule)。合伙人主账号 `GET /partner/promo-codes` 仅返回 `scanCount` / `attributionCount` / `orderCount`(已完成订单),禁止用户/事件/订单明细。H5 入口:合伙人中心 → 运营管理 → 推广码数据。验收:只配渠道负责人不绑用户;只配关联合伙人会进「关联用户」且能看三项汇总;已关联他人扫码流程不中断。
|
**推广码渠道负责人 / 关联合伙人(v4.0.20)**:规则见 v4-PRD §2.1 与 [`v4.0.20 开发文档`](./杜康好客-v4.0.20-开发文档.md)。表 `promo_code_channel_owner`(多对多主合伙人)+ `common_promo_code.assoc_partner_account_id`。HQ `POST/PUT /admin/promo-codes` 字段 `channelOwnerPartnerIds`、`assocPartnerAccountId`(须主账号 ACTIVE)。`POST /promo/touch` 登录后对关联合伙人 `PartnerCityService.tryBindIfUnbound`(已绑他人静默跳过,不回刷历史;Promo 不 import StoreModule)。合伙人主账号 `GET /partner/promo-codes` 仅返回 `scanCount` / `attributionCount` / `orderCount`(已完成订单),禁止用户/事件/订单明细。H5 入口:合伙人中心 → 运营管理 → 推广码数据。验收:只配渠道负责人不绑用户;只配关联合伙人会进「关联用户」且能看三项汇总;已关联他人扫码流程不中断。
|
||||||
|
|
||||||
**合伙人周结算(v4.0.9)**:每周一 08:00 生成上一自然周账单。`GET /partner/settlement/cycle` 账期与出账日;`GET /partner/settlement/preview` 本周一至今预付款预估。零元账单 HQ 可见待审核、不可发送、合伙人端不可见。历史月账不回刷。
|
**合伙人周结算(v4.0.9,同步规则已改)**:每周一 08:00 生成上一自然周账单。应付大于 0 即「未打款」并同步合伙人端,无发送/确认/驳回。财务 `POST /admin/partner-bills/:id/mark-paid`(及 batch)标记已打款,body 可带 `paymentRef`、`paymentProofUrls`。已打款不可重算。`GET /partner/settlement/cycle` 账期与出账日;`GET /partner/settlement/preview` 本周一至今预付款预估。零元账单 HQ 可见待审核、合伙人端不可见。历史月账不回刷。
|
||||||
|
|
||||||
## 5. 验收用例(必过)
|
## 5. 验收用例(必过)
|
||||||
|
|
||||||
**主链路 15 项**:登录、4 SKU、起购、支付+权益、双通道核销、payout、关店不可见、拓店审核、配送完成、退款、T+1/T+30…
|
**主链路 15 项**:登录、4 SKU、起购、支付+权益、双通道核销、payout、关店不可见、拓店审核、配送完成、退款、T+1/T+30…
|
||||||
**后台 8 项**:商品/门店/订单/权益/核销/工单/日志/财务。
|
**后台 8 项**:商品/门店/订单/权益/核销/工单/日志/财务。
|
||||||
**开发计划任务**:状态 `TODO` 待开发 / `IN_PROGRESS` 开发中 / `DEVELOPED` 已开发 / `RELEASED` 已上线 / `STOPPED` 已停止;版本状态含 `STOPPED` 已停止,关联任务随版本同步(仅当全部关联版本已停止时任务才为已停止)。任务列表「来源工单」链到 `/tickets/support?id=`。
|
**开发计划任务**:状态 `TODO` 待开发 / `IN_PROGRESS` 开发中 / `DEVELOPED` 已开发 / `RELEASED` 已上线 / `STOPPED` 已停止;版本状态含 `STOPPED` 已停止,关联任务随版本同步(仅当全部关联版本已停止时任务才为已停止)。任务列表「来源工单」链到 `/tickets/support?id=`。
|
||||||
**财务账单日**:出账当天北京日历日(门店/酒厂 `billDate`;物流账期按北京自然月;合伙人周账按北京自然周,周一 08:00 出上周)。禁止 `toISOString().slice(0,10)` 或服务器本地 `Date` 午夜当账单日。酒厂应付为 0 仍出账(展示无需打款)。合伙人零元账单不同步给合伙人端。
|
**财务账单日**:出账当天北京日历日(门店/酒厂 `billDate`;物流账期按北京自然月;合伙人周账按北京自然周,周一 08:00 出上周)。禁止 `toISOString().slice(0,10)` 或服务器本地 `Date` 午夜当账单日。酒厂应付为 0 仍出账(展示无需打款)。合伙人应付大于 0 的账单生成即同步;零元账单不同步给合伙人端。财务打款上传凭证后合伙人可见状态与凭证。
|
||||||
|
|
||||||
## 6. 技术债(摘要)
|
## 6. 技术债(摘要)
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -84,9 +84,11 @@
|
|||||||
| 酒订单 | 账期内已付且 `partner_account_id_at_pay` = 本主合伙人 | 实付 × 支付快照费率 |
|
| 酒订单 | 账期内已付且 `partner_account_id_at_pay` = 本主合伙人 | 实付 × 支付快照费率 |
|
||||||
| 核销订单 | 账期内名下门店核销 | 核销额 × 出账时核销费率 |
|
| 核销订单 | 账期内名下门店核销 | 核销额 × 出账时核销费率 |
|
||||||
|
|
||||||
HQ 财务详情与合伙人确认页均展示两段列表。不再「无快照则全城已付单 × 当前费率」。
|
HQ 财务详情与合伙人账单页均展示两段列表。不再「无快照则全城已付单 × 当前费率」。
|
||||||
|
|
||||||
- **零元账单**:佣金合计为 0 时 HQ 仍可生成并留在待审核,**不得发送**给合伙人;合伙人端列表/详情不可见。
|
- **同步**:应付大于 0 的账单生成后即为「未打款」,直接出现在合伙人端。无总部发送、无合伙人确认、无驳回。
|
||||||
|
- **打款**:财务打款后在总部标记已打款,可填凭证号并上传打款凭证照片。合伙人可查看状态与凭证。已打款不可重算;未打款可重算。
|
||||||
|
- **零元账单**:佣金合计为 0 时 HQ 仍可生成并留在待审核,不同步给合伙人;合伙人端列表/详情不可见。
|
||||||
- 合伙人 H5 展示当前账期(本周一~本周日)与下次出账时间(下周一 08:00,若尚未过本周一 08:00 则为当日 08:00)。
|
- 合伙人 H5 展示当前账期(本周一~本周日)与下次出账时间(下周一 08:00,若尚未过本周一 08:00 则为当日 08:00)。
|
||||||
- **预付款**:当前未出账周期(本周一 00:00 至今)酒单 + 核销按佣金比例算出的预估合计。展示在首页与合伙人中心。与实际打款无关,实际以账单为准。
|
- **预付款**:当前未出账周期(本周一 00:00 至今)酒单 + 核销按佣金比例算出的预估合计。展示在首页与合伙人中心。与实际打款无关,实际以账单为准。
|
||||||
- 上线前已生成的月账保留,不回刷。
|
- 上线前已生成的月账保留,不回刷。
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|------|------|
|
|------|------|
|
||||||
| 版本线 | **v4.0.21** 接口访问(含 v4.0.20 推广码渠道负责人 + 关联合伙人) |
|
| 版本线 | **v4.0.21** 接口访问(含 v4.0.20 推广码渠道负责人 + 关联合伙人) |
|
||||||
| 订单佣金 | 区县归属已删除;只认关联 / 代下单选择 |
|
| 订单佣金 | 区县归属已删除;只认关联 / 代下单选择 |
|
||||||
| 账单 | 酒订单 / 核销订单分列;合伙人改为周账(周一 08:00);零元不同步合伙人;酒厂含现场提货,零应付仍出账(无需打款) |
|
| 账单 | 酒订单 / 核销订单分列;合伙人周账(周一 08:00)生成后应付大于 0 直接同步(未打款),财务打款并上传凭证;零元不同步;酒厂含现场提货,零应付仍出账(无需打款) |
|
||||||
| 活动图 | HQ 上传底图/码栏/文案;**v4.0.15 上传超限自动压缩并提示尺寸**;合伙人选择写入库;HQ 可指定一张图为勾选主合伙人合成下载;子账号不可看活动图 |
|
| 活动图 | HQ 上传底图/码栏/文案;**v4.0.15 上传超限自动压缩并提示尺寸**;合伙人选择写入库;HQ 可指定一张图为勾选主合伙人合成下载;子账号不可看活动图 |
|
||||||
|
|
||||||
## 1. 版本交付
|
## 1. 版本交付
|
||||||
@@ -47,3 +47,4 @@
|
|||||||
| 2026-09-16 | v4.0.20:推广码渠道负责人改为可多选主合伙人(H5 只看扫码/归因/订单数);关联合伙人扫码 first-lock,不回刷、已关联他人静默跳过 |
|
| 2026-09-16 | v4.0.20:推广码渠道负责人改为可多选主合伙人(H5 只看扫码/归因/订单数);关联合伙人扫码 first-lock,不回刷、已关联他人静默跳过 |
|
||||||
| 2026-09-17 | v4.0.20:PromoModule 改走 CityScope 避免 Nest 循环依赖;本地 Vite 代理默认 `127.0.0.1:3010` |
|
| 2026-09-17 | v4.0.20:PromoModule 改走 CityScope 避免 Nest 循环依赖;本地 Vite 代理默认 `127.0.0.1:3010` |
|
||||||
| 2026-09-26 | v4.0.21:HQ 接口访问。登录/商品加载/门店加载/门店提交按成功百分比放行;审核通知按百分比丢弃企微;订单/核销/账单/门店审核/套餐修改只做通知总开关。总部登录不参与 |
|
| 2026-09-26 | v4.0.21:HQ 接口访问。登录/商品加载/门店加载/门店提交按成功百分比放行;审核通知按百分比丢弃企微;订单/核销/账单/门店审核/套餐修改只做通知总开关。总部登录不参与 |
|
||||||
|
| 2026-09-26 | 合伙人账单:应付大于 0 生成即同步合伙人端;去掉总部发送、合伙人确认与驳回;财务标记已打款并上传凭证,合伙人可见状态与凭证 |
|
||||||
|
|||||||
@@ -146,9 +146,11 @@ export type PartnerBillStatus =
|
|||||||
|
|
||||||
export const PARTNER_BILL_STATUS_LABELS: Record<PartnerBillStatus, string> = {
|
export const PARTNER_BILL_STATUS_LABELS: Record<PartnerBillStatus, string> = {
|
||||||
PENDING_REVIEW: '待审核',
|
PENDING_REVIEW: '待审核',
|
||||||
|
/** 历史状态:新账单不再进入 */
|
||||||
AWAITING_CONFIRM: '待合伙人确认',
|
AWAITING_CONFIRM: '待合伙人确认',
|
||||||
UNPAID: '未打款',
|
UNPAID: '未打款',
|
||||||
PAID: '已打款',
|
PAID: '已打款',
|
||||||
|
/** 历史状态:新账单不再进入 */
|
||||||
REJECTED: '已驳回',
|
REJECTED: '已驳回',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -184,6 +186,8 @@ export interface PartnerBillDto {
|
|||||||
confirmedAt?: string | null;
|
confirmedAt?: string | null;
|
||||||
sentAt?: string | null;
|
sentAt?: string | null;
|
||||||
paidAt?: string | null;
|
paidAt?: string | null;
|
||||||
|
paymentRef?: string | null;
|
||||||
|
paymentProofUrls?: string[];
|
||||||
rejectReason?: string | null;
|
rejectReason?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1387,6 +1387,7 @@ model PartnerBill {
|
|||||||
totalAmount Decimal @map("total_amount") @db.Decimal(10, 2)
|
totalAmount Decimal @map("total_amount") @db.Decimal(10, 2)
|
||||||
status PartnerBillStatus @default(PENDING_REVIEW)
|
status PartnerBillStatus @default(PENDING_REVIEW)
|
||||||
paymentRef String? @map("payment_ref") @db.VarChar(128)
|
paymentRef String? @map("payment_ref") @db.VarChar(128)
|
||||||
|
paymentProofUrls Json? @map("payment_proof_urls")
|
||||||
confirmedAt DateTime? @map("confirmed_at") @db.DateTime(3)
|
confirmedAt DateTime? @map("confirmed_at") @db.DateTime(3)
|
||||||
sentAt DateTime? @map("sent_at") @db.DateTime(3)
|
sentAt DateTime? @map("sent_at") @db.DateTime(3)
|
||||||
paidAt DateTime? @map("paid_at") @db.DateTime(3)
|
paidAt DateTime? @map("paid_at") @db.DateTime(3)
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ async function main() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── 2. 合伙人月账单(上月待确认 + 本月已打款样例)───
|
// ─── 2. 合伙人月账单(上月未打款 + 更早已打款样例)───
|
||||||
const partnerBillAwait = await prisma.partnerBill.upsert({
|
const partnerBillAwait = await prisma.partnerBill.upsert({
|
||||||
where: {
|
where: {
|
||||||
partnerAccountId_periodStart: {
|
partnerAccountId_periodStart: {
|
||||||
@@ -134,7 +134,7 @@ async function main() {
|
|||||||
orderCommission: 860,
|
orderCommission: 860,
|
||||||
redeemCommission: 420,
|
redeemCommission: 420,
|
||||||
totalAmount: 1280,
|
totalAmount: 1280,
|
||||||
status: 'AWAITING_CONFIRM',
|
status: 'UNPAID',
|
||||||
sentAt: yesterday,
|
sentAt: yesterday,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
@@ -142,7 +142,7 @@ async function main() {
|
|||||||
orderCommission: 860,
|
orderCommission: 860,
|
||||||
redeemCommission: 420,
|
redeemCommission: 420,
|
||||||
totalAmount: 1280,
|
totalAmount: 1280,
|
||||||
status: 'AWAITING_CONFIRM',
|
status: 'UNPAID',
|
||||||
sentAt: yesterday,
|
sentAt: yesterday,
|
||||||
paidAt: null,
|
paidAt: null,
|
||||||
rejectReason: null,
|
rejectReason: null,
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- 应付大于 0 的合伙人账单直接同步为未打款(去掉发送 / 合伙人确认 / 驳回)。
|
||||||
|
-- 零元账单保持 PENDING_REVIEW,不同步给合伙人。
|
||||||
|
-- 打款凭证照片列与门店账单一致。
|
||||||
|
|
||||||
|
ALTER TABLE `partner_bill`
|
||||||
|
ADD COLUMN `payment_proof_urls` JSON NULL AFTER `payment_ref`;
|
||||||
|
|
||||||
|
UPDATE `partner_bill`
|
||||||
|
SET `status` = 'UNPAID',
|
||||||
|
`reject_reason` = NULL,
|
||||||
|
`sent_at` = COALESCE(`sent_at`, CURRENT_TIMESTAMP(3))
|
||||||
|
WHERE `total_amount` > 0
|
||||||
|
AND `status` IN ('PENDING_REVIEW', 'AWAITING_CONFIRM', 'REJECTED');
|
||||||
@@ -185,6 +185,7 @@ export class AdminDashboardService {
|
|||||||
? this.prisma.partnerBill.count({
|
? this.prisma.partnerBill.count({
|
||||||
where: {
|
where: {
|
||||||
status: 'PENDING_REVIEW',
|
status: 'PENDING_REVIEW',
|
||||||
|
totalAmount: { lte: 0 },
|
||||||
...(cityFilter ? { partnerAccount: { cityId: cityFilter } } : {}),
|
...(cityFilter ? { partnerAccount: { cityId: cityFilter } } : {}),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -97,16 +97,6 @@ export class SettlementController {
|
|||||||
billDetail(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
billDetail(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
||||||
return this.settlementService.getPartnerBill(user.actorId, BigInt(id));
|
return this.settlementService.getPartnerBill(user.actorId, BigInt(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('bills/batch-confirm')
|
|
||||||
batchConfirm(@CurrentUser() user: AuthUser, @Body() body: { ids: string[] }) {
|
|
||||||
return this.settlementService.batchPartnerConfirmBills(user.actorId, body.ids ?? []);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('bills/:id/confirm')
|
|
||||||
confirm(@CurrentUser() user: AuthUser, @Param('id') id: string) {
|
|
||||||
return this.settlementService.partnerConfirmBill(user.actorId, BigInt(id));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Controller('shop/payouts')
|
@Controller('shop/payouts')
|
||||||
@@ -432,17 +422,6 @@ export class AdminPartnerBillController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('batch-send')
|
|
||||||
@HqOperation({
|
|
||||||
action: HqOperationAction.PARTNER_BILL_BATCH_SEND,
|
|
||||||
refType: 'PARTNER_BILL',
|
|
||||||
batch: true,
|
|
||||||
includeBody: true,
|
|
||||||
})
|
|
||||||
batchSend(@Body() body: { ids: string[] }) {
|
|
||||||
return this.settlementService.batchSendPartnerBills(body.ids ?? []);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('batch-mark-paid')
|
@Post('batch-mark-paid')
|
||||||
@HqOperation({
|
@HqOperation({
|
||||||
action: HqOperationAction.PARTNER_BILL_BATCH_MARK_PAID,
|
action: HqOperationAction.PARTNER_BILL_BATCH_MARK_PAID,
|
||||||
@@ -450,8 +429,8 @@ export class AdminPartnerBillController {
|
|||||||
batch: true,
|
batch: true,
|
||||||
includeBody: true,
|
includeBody: true,
|
||||||
})
|
})
|
||||||
batchMarkPaid(@Body() body: { ids: string[] }) {
|
batchMarkPaid(@Body() body: { ids: string[]; paymentRef?: string; paymentProofUrls?: string[] }) {
|
||||||
return this.settlementService.batchMarkPartnerBillsPaid(body.ids ?? []);
|
return this.settlementService.batchMarkPartnerBillsPaid(body.ids ?? [], body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@@ -459,26 +438,6 @@ export class AdminPartnerBillController {
|
|||||||
return this.settlementService.getAdminPartnerBill(BigInt(id));
|
return this.settlementService.getAdminPartnerBill(BigInt(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/send')
|
|
||||||
@HqOperation({
|
|
||||||
action: HqOperationAction.PARTNER_BILL_SEND,
|
|
||||||
refType: 'PARTNER_BILL',
|
|
||||||
refIdParam: 'id',
|
|
||||||
})
|
|
||||||
send(@Param('id') id: string) {
|
|
||||||
return this.settlementService.sendPartnerBill(BigInt(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post(':id/confirm')
|
|
||||||
@HqOperation({
|
|
||||||
action: HqOperationAction.PARTNER_BILL_CONFIRM,
|
|
||||||
refType: 'PARTNER_BILL',
|
|
||||||
refIdParam: 'id',
|
|
||||||
})
|
|
||||||
confirm(@Param('id') id: string) {
|
|
||||||
return this.settlementService.confirmPartnerBill(BigInt(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post(':id/mark-paid')
|
@Post(':id/mark-paid')
|
||||||
@HqOperation({
|
@HqOperation({
|
||||||
action: HqOperationAction.PARTNER_BILL_MARK_PAID,
|
action: HqOperationAction.PARTNER_BILL_MARK_PAID,
|
||||||
@@ -486,23 +445,12 @@ export class AdminPartnerBillController {
|
|||||||
refIdParam: 'id',
|
refIdParam: 'id',
|
||||||
includeBody: true,
|
includeBody: true,
|
||||||
})
|
})
|
||||||
markPaid(@Param('id') id: string, @Body() body: { paymentRef?: string }) {
|
markPaid(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() body: { paymentRef?: string; paymentProofUrls?: string[] },
|
||||||
|
) {
|
||||||
return this.settlementService.markPartnerBillPaid(BigInt(id), body);
|
return this.settlementService.markPartnerBillPaid(BigInt(id), body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/reject')
|
|
||||||
@HqOperation({
|
|
||||||
action: HqOperationAction.PARTNER_BILL_REJECT,
|
|
||||||
refType: 'PARTNER_BILL',
|
|
||||||
refIdParam: 'id',
|
|
||||||
includeBody: true,
|
|
||||||
})
|
|
||||||
reject(@Param('id') id: string, @Body() body: { reason?: string }) {
|
|
||||||
if (!body?.reason?.trim()) {
|
|
||||||
throw new BadRequestException('请填写驳回理由');
|
|
||||||
}
|
|
||||||
return this.settlementService.rejectPartnerBill(BigInt(id), body.reason);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Controller('admin/winery-bills')
|
@Controller('admin/winery-bills')
|
||||||
|
|||||||
@@ -119,6 +119,16 @@ function withPartnerBillDates<T extends { periodStart: Date; periodEnd: Date }>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapPartnerBillView<T extends { periodStart: Date; periodEnd: Date; paymentProofUrls?: unknown }>(row: T) {
|
||||||
|
const { paymentProofUrls, ...rest } = row;
|
||||||
|
return {
|
||||||
|
...withPartnerBillDates(rest),
|
||||||
|
paymentProofUrls: parsePaymentProofUrls(paymentProofUrls),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const PARTNER_BILL_PUBLISH_STATUSES = ['PENDING_REVIEW', 'AWAITING_CONFIRM', 'REJECTED'] as const;
|
||||||
|
|
||||||
function resolvePartnerWeekPeriod(weekStartYmd: string): {
|
function resolvePartnerWeekPeriod(weekStartYmd: string): {
|
||||||
periodStart: Date;
|
periodStart: Date;
|
||||||
periodEnd: Date;
|
periodEnd: Date;
|
||||||
@@ -284,6 +294,12 @@ export class SettlementService implements OnModuleInit {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.warn(`Winery bill date align skipped: ${e instanceof Error ? e.message : e}`);
|
this.logger.warn(`Winery bill date align skipped: ${e instanceof Error ? e.message : e}`);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
const published = await this.publishOutstandingPartnerBills();
|
||||||
|
if (published > 0) this.logger.log(`Partner bills published to partners: ${published}`);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn(`Partner bill publish skipped: ${e instanceof Error ? e.message : e}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1722,12 +1738,33 @@ export class SettlementService implements OnModuleInit {
|
|||||||
|
|
||||||
// ─── Partner bills ───────────────────────────────────
|
// ─── Partner bills ───────────────────────────────────
|
||||||
|
|
||||||
|
/** 应付大于 0 且仍停在审核/待确认/驳回的账单,直接改为未打款并同步合伙人。 */
|
||||||
|
private async publishOutstandingPartnerBills(): Promise<number> {
|
||||||
|
const alreadySent = await this.prisma.partnerBill.updateMany({
|
||||||
|
where: {
|
||||||
|
totalAmount: { gt: 0 },
|
||||||
|
status: { in: [...PARTNER_BILL_PUBLISH_STATUSES] },
|
||||||
|
sentAt: { not: null },
|
||||||
|
},
|
||||||
|
data: { status: 'UNPAID', rejectReason: null },
|
||||||
|
});
|
||||||
|
const fresh = await this.prisma.partnerBill.updateMany({
|
||||||
|
where: {
|
||||||
|
totalAmount: { gt: 0 },
|
||||||
|
status: { in: [...PARTNER_BILL_PUBLISH_STATUSES] },
|
||||||
|
},
|
||||||
|
data: { status: 'UNPAID', sentAt: new Date(), rejectReason: null },
|
||||||
|
});
|
||||||
|
return alreadySent.count + fresh.count;
|
||||||
|
}
|
||||||
|
|
||||||
async listPartnerBills(partnerAccountId: bigint) {
|
async listPartnerBills(partnerAccountId: bigint) {
|
||||||
|
await this.publishOutstandingPartnerBills();
|
||||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||||
const bills = await this.prisma.partnerBill.findMany({
|
const bills = await this.prisma.partnerBill.findMany({
|
||||||
where: {
|
where: {
|
||||||
partnerAccountId: primary.id,
|
partnerAccountId: primary.id,
|
||||||
status: { in: ['AWAITING_CONFIRM', 'UNPAID', 'PAID', 'REJECTED'] },
|
status: { in: ['UNPAID', 'PAID'] },
|
||||||
totalAmount: { gt: 0 },
|
totalAmount: { gt: 0 },
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
@@ -1737,7 +1774,7 @@ export class SettlementService implements OnModuleInit {
|
|||||||
eventName: 'partner_bill_view',
|
eventName: 'partner_bill_view',
|
||||||
extraJson: { count: bills.length },
|
extraJson: { count: bills.length },
|
||||||
});
|
});
|
||||||
return serializeBigInt(bills.map(withPartnerBillDates));
|
return serializeBigInt(bills.map(mapPartnerBillView));
|
||||||
}
|
}
|
||||||
|
|
||||||
getPartnerSettlementCycle(anchor = new Date()) {
|
getPartnerSettlementCycle(anchor = new Date()) {
|
||||||
@@ -1793,6 +1830,7 @@ export class SettlementService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getPartnerBill(partnerAccountId: bigint, billId: bigint) {
|
async getPartnerBill(partnerAccountId: bigint, billId: bigint) {
|
||||||
|
await this.publishOutstandingPartnerBills();
|
||||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
||||||
const bill = await this.prisma.partnerBill.findFirst({
|
const bill = await this.prisma.partnerBill.findFirst({
|
||||||
where: { id: billId, partnerAccountId: primary.id },
|
where: { id: billId, partnerAccountId: primary.id },
|
||||||
@@ -1800,6 +1838,9 @@ export class SettlementService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
if (!bill) throw new NotFoundException('账单不存在');
|
if (!bill) throw new NotFoundException('账单不存在');
|
||||||
if (Number(bill.totalAmount) <= 0) throw new NotFoundException('账单不存在');
|
if (Number(bill.totalAmount) <= 0) throw new NotFoundException('账单不存在');
|
||||||
|
if (bill.status !== 'UNPAID' && bill.status !== 'PAID') {
|
||||||
|
throw new NotFoundException('账单不存在');
|
||||||
|
}
|
||||||
this.analyticsService.trackPartnerOneSafe(partnerAccountId, 'PARTNER_H5', {
|
this.analyticsService.trackPartnerOneSafe(partnerAccountId, 'PARTNER_H5', {
|
||||||
partnerAccountId: primary.id,
|
partnerAccountId: primary.id,
|
||||||
eventName: 'partner_bill_detail_view',
|
eventName: 'partner_bill_detail_view',
|
||||||
@@ -1809,7 +1850,7 @@ export class SettlementService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
const { items, ...header } = bill;
|
const { items, ...header } = bill;
|
||||||
return serializeBigInt({
|
return serializeBigInt({
|
||||||
...withPartnerBillDates(header),
|
...mapPartnerBillView(header),
|
||||||
partnerId: header.partnerAccountId.toString(),
|
partnerId: header.partnerAccountId.toString(),
|
||||||
...splitPartnerBillItems(items),
|
...splitPartnerBillItems(items),
|
||||||
});
|
});
|
||||||
@@ -1824,6 +1865,7 @@ export class SettlementService implements OnModuleInit {
|
|||||||
month?: number;
|
month?: number;
|
||||||
weekStartYmd?: string;
|
weekStartYmd?: string;
|
||||||
}) {
|
}) {
|
||||||
|
await this.publishOutstandingPartnerBills();
|
||||||
const page = query.page ?? 1;
|
const page = query.page ?? 1;
|
||||||
const pageSize = query.pageSize ?? 20;
|
const pageSize = query.pageSize ?? 20;
|
||||||
const where = this.buildPartnerBillWhere(query);
|
const where = this.buildPartnerBillWhere(query);
|
||||||
@@ -1857,7 +1899,7 @@ export class SettlementService implements OnModuleInit {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const items = rawItems.map((b) => ({
|
const items = rawItems.map((b) => ({
|
||||||
...withPartnerBillDates(b),
|
...mapPartnerBillView(b),
|
||||||
partner: b.partnerAccount,
|
partner: b.partnerAccount,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -1973,6 +2015,7 @@ export class SettlementService implements OnModuleInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAdminPartnerBill(id: bigint) {
|
async getAdminPartnerBill(id: bigint) {
|
||||||
|
await this.publishOutstandingPartnerBills();
|
||||||
const bill = await this.prisma.partnerBill.findUnique({
|
const bill = await this.prisma.partnerBill.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: { partnerAccount: true, items: { orderBy: { occurredAt: 'asc' } } },
|
include: { partnerAccount: true, items: { orderBy: { occurredAt: 'asc' } } },
|
||||||
@@ -1980,7 +2023,7 @@ export class SettlementService implements OnModuleInit {
|
|||||||
if (!bill) throw new NotFoundException('账单不存在');
|
if (!bill) throw new NotFoundException('账单不存在');
|
||||||
const { items, ...header } = bill;
|
const { items, ...header } = bill;
|
||||||
return serializeBigInt({
|
return serializeBigInt({
|
||||||
...withPartnerBillDates(header),
|
...mapPartnerBillView(header),
|
||||||
partnerId: header.partnerAccountId.toString(),
|
partnerId: header.partnerAccountId.toString(),
|
||||||
...splitPartnerBillItems(items),
|
...splitPartnerBillItems(items),
|
||||||
});
|
});
|
||||||
@@ -2000,8 +2043,8 @@ export class SettlementService implements OnModuleInit {
|
|||||||
periodStart: { gte: periodStart, lt: endExclusive },
|
periodStart: { gte: periodStart, lt: endExclusive },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (existing && existing.status !== 'PENDING_REVIEW') {
|
if (existing?.status === 'PAID') {
|
||||||
throw new BadRequestException('该账期账单已进入审核流程,不可重复生成');
|
throw new BadRequestException('该账期账单已打款,不可重复生成');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!primary.cityId) {
|
if (!primary.cityId) {
|
||||||
@@ -2070,6 +2113,8 @@ export class SettlementService implements OnModuleInit {
|
|||||||
|
|
||||||
const totalAmount = round2(orderCommission + redeemCommission);
|
const totalAmount = round2(orderCommission + redeemCommission);
|
||||||
const itemRows = [...orderRows, ...redeemRows];
|
const itemRows = [...orderRows, ...redeemRows];
|
||||||
|
const publish = totalAmount > 0;
|
||||||
|
const sentAt = publish ? (existing?.sentAt ?? new Date()) : null;
|
||||||
|
|
||||||
const bill = await this.prisma.$transaction(async (tx) => {
|
const bill = await this.prisma.$transaction(async (tx) => {
|
||||||
const header = existing
|
const header = existing
|
||||||
@@ -2081,7 +2126,9 @@ export class SettlementService implements OnModuleInit {
|
|||||||
totalAmount,
|
totalAmount,
|
||||||
periodStart,
|
periodStart,
|
||||||
periodEnd,
|
periodEnd,
|
||||||
status: 'PENDING_REVIEW',
|
status: publish ? 'UNPAID' : 'PENDING_REVIEW',
|
||||||
|
sentAt,
|
||||||
|
rejectReason: null,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
: await tx.partnerBill.create({
|
: await tx.partnerBill.create({
|
||||||
@@ -2093,7 +2140,8 @@ export class SettlementService implements OnModuleInit {
|
|||||||
orderCommission: round2(orderCommission),
|
orderCommission: round2(orderCommission),
|
||||||
redeemCommission: round2(redeemCommission),
|
redeemCommission: round2(redeemCommission),
|
||||||
totalAmount,
|
totalAmount,
|
||||||
status: 'PENDING_REVIEW',
|
status: publish ? 'UNPAID' : 'PENDING_REVIEW',
|
||||||
|
sentAt,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2155,128 +2203,14 @@ export class SettlementService implements OnModuleInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** HQ:发送给合伙人(待审核 → 待合伙人确认) */
|
async markPartnerBillPaid(
|
||||||
async sendPartnerBill(id: bigint) {
|
id: bigint,
|
||||||
const bill = await this.prisma.partnerBill.findUnique({ where: { id } });
|
dto: { paymentRef?: string; paymentProofUrls?: string[] } = {},
|
||||||
if (!bill) throw new NotFoundException('账单不存在');
|
) {
|
||||||
if (Number(bill.totalAmount) <= 0) {
|
await this.publishOutstandingPartnerBills();
|
||||||
throw new BadRequestException('零元账单不同步给合伙人');
|
|
||||||
}
|
|
||||||
if (bill.status !== 'PENDING_REVIEW' && bill.status !== 'REJECTED') {
|
|
||||||
throw new BadRequestException('仅待审核或已驳回账单可发送');
|
|
||||||
}
|
|
||||||
const updated = await this.prisma.partnerBill.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
status: 'AWAITING_CONFIRM',
|
|
||||||
sentAt: new Date(),
|
|
||||||
rejectReason: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return serializeBigInt(updated);
|
|
||||||
}
|
|
||||||
|
|
||||||
async batchSendPartnerBills(ids: string[]) {
|
|
||||||
const results: Array<{ id: string; ok: boolean; message?: string }> = [];
|
|
||||||
for (const id of ids) {
|
|
||||||
try {
|
|
||||||
await this.sendPartnerBill(BigInt(id));
|
|
||||||
results.push({ id, ok: true });
|
|
||||||
} catch (e) {
|
|
||||||
results.push({ id, ok: false, message: e instanceof Error ? e.message : '失败' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @deprecated HQ 代确认:待审核/待确认 → 未打款 */
|
|
||||||
async confirmPartnerBill(id: bigint) {
|
|
||||||
const bill = await this.prisma.partnerBill.findUnique({ where: { id } });
|
|
||||||
if (!bill) throw new NotFoundException('账单不存在');
|
|
||||||
if (bill.status !== 'PENDING_REVIEW' && bill.status !== 'AWAITING_CONFIRM' && bill.status !== 'REJECTED') {
|
|
||||||
throw new BadRequestException('当前状态不可确认');
|
|
||||||
}
|
|
||||||
const updated = await this.prisma.partnerBill.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
status: 'UNPAID',
|
|
||||||
confirmedAt: new Date(),
|
|
||||||
sentAt: bill.sentAt ?? new Date(),
|
|
||||||
rejectReason: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return serializeBigInt(updated);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 合伙人确认:待合伙人确认 → 未打款 */
|
|
||||||
async partnerConfirmBill(partnerAccountId: bigint, billId: bigint) {
|
|
||||||
const primary = await this.partnerCityService.resolvePrimaryAccount(partnerAccountId);
|
|
||||||
const bill = await this.prisma.partnerBill.findUnique({ where: { id: billId } });
|
|
||||||
if (!bill || bill.partnerAccountId !== primary.id) {
|
|
||||||
throw new NotFoundException('账单不存在');
|
|
||||||
}
|
|
||||||
if (bill.status !== 'AWAITING_CONFIRM') {
|
|
||||||
throw new BadRequestException('当前账单不可确认');
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated = await this.prisma.partnerBill.update({
|
|
||||||
where: { id: billId },
|
|
||||||
data: {
|
|
||||||
status: 'UNPAID',
|
|
||||||
confirmedAt: new Date(),
|
|
||||||
rejectReason: null,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.analyticsService.trackPartnerOneSafe(partnerAccountId, 'PARTNER_H5', {
|
|
||||||
partnerAccountId: primary.id,
|
|
||||||
eventName: 'partner_bill_confirm',
|
|
||||||
refType: 'PARTNER_BILL',
|
|
||||||
refId: billId,
|
|
||||||
extraJson: { billNo: bill.billNo },
|
|
||||||
});
|
|
||||||
|
|
||||||
return serializeBigInt(updated);
|
|
||||||
}
|
|
||||||
|
|
||||||
async batchPartnerConfirmBills(partnerAccountId: bigint, ids: string[]) {
|
|
||||||
const results: Array<{ id: string; ok: boolean; message?: string }> = [];
|
|
||||||
for (const id of ids) {
|
|
||||||
try {
|
|
||||||
await this.partnerConfirmBill(partnerAccountId, BigInt(id));
|
|
||||||
results.push({ id, ok: true });
|
|
||||||
} catch (e) {
|
|
||||||
results.push({ id, ok: false, message: e instanceof Error ? e.message : '失败' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
async rejectPartnerBill(id: bigint, reason: string) {
|
|
||||||
const rejectReason = reason.trim();
|
|
||||||
if (!rejectReason) throw new BadRequestException('请填写驳回理由');
|
|
||||||
if (rejectReason.length > 500) throw new BadRequestException('驳回理由不能超过 500 字');
|
|
||||||
|
|
||||||
const bill = await this.prisma.partnerBill.findUnique({ where: { id } });
|
|
||||||
if (!bill) throw new NotFoundException('账单不存在');
|
|
||||||
if (bill.status !== 'AWAITING_CONFIRM' && bill.status !== 'UNPAID') {
|
|
||||||
throw new BadRequestException('仅待合伙人确认或未打款账单可驳回');
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated = await this.prisma.partnerBill.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
status: 'REJECTED',
|
|
||||||
rejectReason,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return serializeBigInt(updated);
|
|
||||||
}
|
|
||||||
|
|
||||||
async markPartnerBillPaid(id: bigint, dto: { paymentRef?: string } = {}) {
|
|
||||||
const bill = await this.prisma.partnerBill.findUnique({ where: { id } });
|
const bill = await this.prisma.partnerBill.findUnique({ where: { id } });
|
||||||
if (!bill) throw new NotFoundException('账单不存在');
|
if (!bill) throw new NotFoundException('账单不存在');
|
||||||
|
if (Number(bill.totalAmount) <= 0) throw new BadRequestException('零元账单无需打款');
|
||||||
if (bill.status !== 'UNPAID') throw new BadRequestException('仅未打款账单可标记打款');
|
if (bill.status !== 'UNPAID') throw new BadRequestException('仅未打款账单可标记打款');
|
||||||
|
|
||||||
const updated = await this.prisma.partnerBill.update({
|
const updated = await this.prisma.partnerBill.update({
|
||||||
@@ -2286,17 +2220,21 @@ export class SettlementService implements OnModuleInit {
|
|||||||
paidAt: new Date(),
|
paidAt: new Date(),
|
||||||
rejectReason: null,
|
rejectReason: null,
|
||||||
paymentRef: dto.paymentRef?.trim() || null,
|
paymentRef: dto.paymentRef?.trim() || null,
|
||||||
|
paymentProofUrls: paymentProofUrlsInput(dto.paymentProofUrls),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return serializeBigInt(updated);
|
return serializeBigInt(mapPartnerBillView(updated));
|
||||||
}
|
}
|
||||||
|
|
||||||
async batchMarkPartnerBillsPaid(ids: string[]) {
|
async batchMarkPartnerBillsPaid(
|
||||||
|
ids: string[],
|
||||||
|
dto: { paymentRef?: string; paymentProofUrls?: string[] } = {},
|
||||||
|
) {
|
||||||
const results: Array<{ id: string; ok: boolean; message?: string }> = [];
|
const results: Array<{ id: string; ok: boolean; message?: string }> = [];
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
try {
|
try {
|
||||||
await this.markPartnerBillPaid(BigInt(id));
|
await this.markPartnerBillPaid(BigInt(id), dto);
|
||||||
results.push({ id, ok: true });
|
results.push({ id, ok: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
results.push({ id, ok: false, message: e instanceof Error ? e.message : '失败' });
|
results.push({ id, ok: false, message: e instanceof Error ? e.message : '失败' });
|
||||||
@@ -2363,7 +2301,18 @@ export class SettlementService implements OnModuleInit {
|
|||||||
{ key: '合计应付', header: '合计应付', value: (r) => Number(r.totalAmount) },
|
{ key: '合计应付', header: '合计应付', value: (r) => Number(r.totalAmount) },
|
||||||
{ key: '收款户名', header: '收款户名', value: (r) => r.partnerAccount.bankAccountName ?? '' },
|
{ key: '收款户名', header: '收款户名', value: (r) => r.partnerAccount.bankAccountName ?? '' },
|
||||||
{ key: '收款账号', header: '收款账号', value: (r) => r.partnerAccount.bankAccountNo ?? '' },
|
{ key: '收款账号', header: '收款账号', value: (r) => r.partnerAccount.bankAccountNo ?? '' },
|
||||||
{ key: '状态', header: '状态', value: (r) => r.status },
|
{
|
||||||
|
key: '状态',
|
||||||
|
header: '状态',
|
||||||
|
value: (r) =>
|
||||||
|
({
|
||||||
|
PENDING_REVIEW: '待审核',
|
||||||
|
AWAITING_CONFIRM: '待合伙人确认',
|
||||||
|
UNPAID: '未打款',
|
||||||
|
PAID: '已打款',
|
||||||
|
REJECTED: '已驳回',
|
||||||
|
})[r.status] ?? r.status,
|
||||||
|
},
|
||||||
{ key: '登录手机', header: '登录手机', value: (r) => r.partnerAccount.phone ?? '' },
|
{ key: '登录手机', header: '登录手机', value: (r) => r.partnerAccount.phone ?? '' },
|
||||||
{ key: '开户行', header: '开户行', value: (r) => r.partnerAccount.bankBranch ?? '' },
|
{ key: '开户行', header: '开户行', value: (r) => r.partnerAccount.bankBranch ?? '' },
|
||||||
{
|
{
|
||||||
@@ -2382,6 +2331,11 @@ export class SettlementService implements OnModuleInit {
|
|||||||
value: (r) => (r.paidAt ? shanghaiYmd(r.paidAt) : ''),
|
value: (r) => (r.paidAt ? shanghaiYmd(r.paidAt) : ''),
|
||||||
},
|
},
|
||||||
{ key: '打款凭证', header: '打款凭证', value: (r) => r.paymentRef ?? '' },
|
{ key: '打款凭证', header: '打款凭证', value: (r) => r.paymentRef ?? '' },
|
||||||
|
{
|
||||||
|
key: '打款凭证照片',
|
||||||
|
header: '打款凭证照片',
|
||||||
|
value: (r) => parsePaymentProofUrls(r.paymentProofUrls).join(' '),
|
||||||
|
},
|
||||||
{ key: '驳回理由', header: '驳回理由', value: (r) => r.rejectReason ?? '' },
|
{ key: '驳回理由', header: '驳回理由', value: (r) => r.rejectReason ?? '' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user