fix(admin): contract preview, package audit detail, empty packages, winery status
Allow HQ to save stores with zero packages, preview contract images/PDFs, inspect package audit images/content, and show gray 无需打款 when winery payable is 0. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -59,7 +59,7 @@ const AdminStorePackagesSection = forwardRef<AdminStorePackagesHandle, { storeId
|
||||
sortOrder: i,
|
||||
};
|
||||
})
|
||||
: [emptyRow()],
|
||||
: [],
|
||||
);
|
||||
})
|
||||
.catch((e) => message.error(e instanceof Error ? e.message : '加载套餐失败'))
|
||||
@@ -124,7 +124,13 @@ const AdminStorePackagesSection = forwardRef<AdminStorePackagesHandle, { storeId
|
||||
sortOrder: index,
|
||||
};
|
||||
})
|
||||
.filter((item) => item.name || item.dishes || item.price || item.imageUrls.length > 0);
|
||||
// 允许整店无套餐:忽略空白占位行(默认 price=0 不算已填)
|
||||
.filter((item) => {
|
||||
const hasText = !!(item.name || item.dishes || item.usableTime || item.otherNotes);
|
||||
const hasImages = item.imageUrls.length > 0;
|
||||
const hasNonZeroPrice = item.price !== '' && Number(item.price) !== 0;
|
||||
return hasText || hasImages || hasNonZeroPrice;
|
||||
});
|
||||
|
||||
for (let i = 0; i < filled.length; i++) {
|
||||
const item = filled[i];
|
||||
@@ -168,7 +174,7 @@ const AdminStorePackagesSection = forwardRef<AdminStorePackagesHandle, { storeId
|
||||
sortOrder: i,
|
||||
};
|
||||
})
|
||||
: [emptyRow()],
|
||||
: [],
|
||||
);
|
||||
} catch (e) {
|
||||
if (!opts?.quiet) message.error(e instanceof Error ? e.message : '保存失败');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { Button, Image, Input, Space, Upload, message } from 'antd';
|
||||
import { UploadOutlined } from '@ant-design/icons';
|
||||
import { Button, Image, Input, Modal, Space, Upload, message } from 'antd';
|
||||
import { EyeOutlined, FilePdfOutlined, UploadOutlined } from '@ant-design/icons';
|
||||
import type { UploadProps } from 'antd';
|
||||
import { uploadFileToOss, type OssMediaType, type UploadFileResult } from '../lib/upload';
|
||||
|
||||
@@ -15,6 +15,14 @@ type OssUploadProps = {
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
function isImageUrl(url: string) {
|
||||
return /\.(png|jpe?g|gif|webp|bmp|svg)(\?|#|$)/i.test(url);
|
||||
}
|
||||
|
||||
function isPdfUrl(url: string) {
|
||||
return /\.pdf(\?|#|$)/i.test(url);
|
||||
}
|
||||
|
||||
export default function OssUpload({
|
||||
value,
|
||||
onChange,
|
||||
@@ -25,6 +33,7 @@ export default function OssUpload({
|
||||
placeholder = '上传后自动填入,或手动粘贴 URL',
|
||||
}: OssUploadProps) {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [pdfPreviewOpen, setPdfPreviewOpen] = useState(false);
|
||||
|
||||
const resolvedAccept =
|
||||
accept ?? (mediaType === 'VIDEO' ? 'video/*' : mediaType === 'FILE' ? undefined : 'image/*');
|
||||
@@ -47,6 +56,57 @@ export default function OssUpload({
|
||||
}
|
||||
};
|
||||
|
||||
const filePreview =
|
||||
value && mediaType === 'FILE' ? (
|
||||
isImageUrl(value) ? (
|
||||
<Image src={value} width={120} height={120} style={{ objectFit: 'cover', borderRadius: 4 }} />
|
||||
) : isPdfUrl(value) ? (
|
||||
<Space direction="vertical" size={8}>
|
||||
<div
|
||||
style={{
|
||||
width: 120,
|
||||
height: 120,
|
||||
borderRadius: 4,
|
||||
border: '1px solid #f0f0f0',
|
||||
background: '#fafafa',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 8,
|
||||
color: '#cf1322',
|
||||
}}
|
||||
>
|
||||
<FilePdfOutlined style={{ fontSize: 36 }} />
|
||||
<span style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)' }}>PDF 合同</span>
|
||||
</div>
|
||||
<Space wrap>
|
||||
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => setPdfPreviewOpen(true)}>
|
||||
预览
|
||||
</Button>
|
||||
<Button type="link" size="small" href={value} target="_blank" rel="noreferrer">
|
||||
新窗口打开
|
||||
</Button>
|
||||
</Space>
|
||||
<Modal
|
||||
title="签约合同预览"
|
||||
open={pdfPreviewOpen}
|
||||
onCancel={() => setPdfPreviewOpen(false)}
|
||||
footer={null}
|
||||
width="90vw"
|
||||
styles={{ body: { height: '75vh', padding: 0 } }}
|
||||
destroyOnClose
|
||||
>
|
||||
<iframe title="合同 PDF 预览" src={value} style={{ width: '100%', height: '100%', border: 0 }} />
|
||||
</Modal>
|
||||
</Space>
|
||||
) : (
|
||||
<Button type="link" href={value} target="_blank" rel="noreferrer" style={{ paddingLeft: 0 }}>
|
||||
打开已上传文件
|
||||
</Button>
|
||||
)
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="small">
|
||||
{value && mediaType === 'IMAGE' && (
|
||||
@@ -55,6 +115,7 @@ export default function OssUpload({
|
||||
{value && mediaType === 'VIDEO' && (
|
||||
<video src={value} controls style={{ maxWidth: '100%', maxHeight: 160, borderRadius: 4 }} />
|
||||
)}
|
||||
{filePreview}
|
||||
<Space wrap>
|
||||
<Upload
|
||||
accept={resolvedAccept}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
Image,
|
||||
Input,
|
||||
Modal,
|
||||
Space,
|
||||
@@ -17,7 +18,10 @@ import type {
|
||||
StorePackageItemDto,
|
||||
StorePackageViewDto,
|
||||
} from '@dukang/shared-types';
|
||||
import { STORE_PACKAGE_CHANGE_STATUS_LABELS } from '@dukang/shared-types';
|
||||
import {
|
||||
STORE_PACKAGE_CHANGE_STATUS_LABELS,
|
||||
normalizeStorePackageImageUrls,
|
||||
} from '@dukang/shared-types';
|
||||
import { request, type Paginated } from '../lib/api';
|
||||
import { fmtTime } from '../lib/constants';
|
||||
|
||||
@@ -26,6 +30,10 @@ function packageKey(pkg: StorePackageItemDto | StorePackageViewDto, index: numbe
|
||||
return name ? `name:${name}` : `idx:${index}`;
|
||||
}
|
||||
|
||||
function imageSignature(pkg: StorePackageItemDto | StorePackageViewDto) {
|
||||
return normalizeStorePackageImageUrls(pkg).join('|');
|
||||
}
|
||||
|
||||
function diffPackages(live: StorePackageViewDto[], proposed: StorePackageItemDto[]) {
|
||||
const liveMap = new Map(live.map((p, i) => [packageKey(p, i), p]));
|
||||
const proposedMap = new Map(proposed.map((p, i) => [packageKey(p, i), p]));
|
||||
@@ -49,7 +57,8 @@ function diffPackages(live: StorePackageViewDto[], proposed: StorePackageItemDto
|
||||
l.price !== p.price ||
|
||||
l.dishes !== p.dishes ||
|
||||
(l.usableTime ?? '') !== (p.usableTime ?? '') ||
|
||||
(l.otherNotes ?? '') !== (p.otherNotes ?? '');
|
||||
(l.otherNotes ?? '') !== (p.otherNotes ?? '') ||
|
||||
imageSignature(l) !== imageSignature(p);
|
||||
rows.push({ key, change: changed ? 'changed' : 'unchanged', live: l, proposed: p });
|
||||
}
|
||||
}
|
||||
@@ -63,6 +72,108 @@ const CHANGE_LABELS = {
|
||||
unchanged: { text: '未变', color: 'default' },
|
||||
} as const;
|
||||
|
||||
function PackageDetailCard({
|
||||
title,
|
||||
pkg,
|
||||
}: {
|
||||
title?: string;
|
||||
pkg: StorePackageItemDto | StorePackageViewDto;
|
||||
}) {
|
||||
const images = normalizeStorePackageImageUrls(pkg);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
marginBottom: 12,
|
||||
padding: 12,
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: 8,
|
||||
background: '#fafafa',
|
||||
}}
|
||||
>
|
||||
{title ? (
|
||||
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 8 }}>
|
||||
{title}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<strong>{pkg.name}</strong>
|
||||
<span style={{ marginLeft: 8 }}>¥{pkg.price}</span>
|
||||
</div>
|
||||
<Typography.Paragraph style={{ marginBottom: 8, whiteSpace: 'pre-wrap' }}>
|
||||
{pkg.dishes || '—'}
|
||||
</Typography.Paragraph>
|
||||
{pkg.usableTime ? (
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<Typography.Text type="secondary">可用时间:{pkg.usableTime}</Typography.Text>
|
||||
</div>
|
||||
) : null}
|
||||
{pkg.otherNotes ? (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Typography.Text type="secondary">其他说明:{pkg.otherNotes}</Typography.Text>
|
||||
</div>
|
||||
) : null}
|
||||
{images.length ? (
|
||||
<Image.PreviewGroup>
|
||||
<Space wrap size={8}>
|
||||
{images.map((url) => (
|
||||
<Image
|
||||
key={url}
|
||||
src={url}
|
||||
width={72}
|
||||
height={72}
|
||||
style={{ objectFit: 'cover', borderRadius: 4 }}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
</Image.PreviewGroup>
|
||||
) : (
|
||||
<Typography.Text type="secondary">无套餐图片</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PackageSummaryCell({ pkg }: { pkg?: StorePackageItemDto | StorePackageViewDto }) {
|
||||
if (!pkg) return <>—</>;
|
||||
const images = normalizeStorePackageImageUrls(pkg);
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<strong>{pkg.name}</strong> · ¥{pkg.price}
|
||||
</div>
|
||||
<Typography.Text type="secondary">{pkg.dishes}</Typography.Text>
|
||||
{pkg.usableTime ? (
|
||||
<div>
|
||||
<Typography.Text type="secondary">可用:{pkg.usableTime}</Typography.Text>
|
||||
</div>
|
||||
) : null}
|
||||
{pkg.otherNotes ? (
|
||||
<div>
|
||||
<Typography.Text type="secondary">备注:{pkg.otherNotes}</Typography.Text>
|
||||
</div>
|
||||
) : null}
|
||||
{images.length ? (
|
||||
<Image.PreviewGroup>
|
||||
<Space wrap size={4} style={{ marginTop: 8 }}>
|
||||
{images.slice(0, 4).map((url) => (
|
||||
<Image
|
||||
key={url}
|
||||
src={url}
|
||||
width={48}
|
||||
height={48}
|
||||
style={{ objectFit: 'cover', borderRadius: 4 }}
|
||||
/>
|
||||
))}
|
||||
{images.length > 4 ? (
|
||||
<Typography.Text type="secondary">+{images.length - 4}</Typography.Text>
|
||||
) : null}
|
||||
</Space>
|
||||
</Image.PreviewGroup>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function StorePackageAuditsPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [items, setItems] = useState<StorePackageChangeRequestDto[]>([]);
|
||||
@@ -75,6 +186,11 @@ export default function StorePackageAuditsPage() {
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [detail, setDetail] = useState<StorePackageAuditDetailDto | null>(null);
|
||||
const [pkgDetailOpen, setPkgDetailOpen] = useState(false);
|
||||
const [pkgDetailTitle, setPkgDetailTitle] = useState('');
|
||||
const [pkgDetailList, setPkgDetailList] = useState<Array<StorePackageItemDto | StorePackageViewDto>>(
|
||||
[],
|
||||
);
|
||||
|
||||
async function reload(nextPage = page, nextStatus = status) {
|
||||
setLoading(true);
|
||||
@@ -116,6 +232,15 @@ export default function StorePackageAuditsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
function openPackageDetails(
|
||||
title: string,
|
||||
list: Array<StorePackageItemDto | StorePackageViewDto>,
|
||||
) {
|
||||
setPkgDetailTitle(title);
|
||||
setPkgDetailList(list);
|
||||
setPkgDetailOpen(true);
|
||||
}
|
||||
|
||||
async function audit(id: string, action: 'APPROVE' | 'REJECT', reason?: string) {
|
||||
try {
|
||||
await request(`/admin/store-package-audits/${id}/audit`, {
|
||||
@@ -146,39 +271,11 @@ export default function StorePackageAuditsPage() {
|
||||
},
|
||||
{
|
||||
title: '当前线上',
|
||||
render: (_, row) =>
|
||||
row.live ? (
|
||||
<div>
|
||||
<div><strong>{row.live.name}</strong> · ¥{row.live.price}</div>
|
||||
<Typography.Text type="secondary">{row.live.dishes}</Typography.Text>
|
||||
{row.live.usableTime ? (
|
||||
<div><Typography.Text type="secondary">可用:{row.live.usableTime}</Typography.Text></div>
|
||||
) : null}
|
||||
{row.live.otherNotes ? (
|
||||
<div><Typography.Text type="secondary">备注:{row.live.otherNotes}</Typography.Text></div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
'—'
|
||||
),
|
||||
render: (_, row) => <PackageSummaryCell pkg={row.live} />,
|
||||
},
|
||||
{
|
||||
title: '申请变更',
|
||||
render: (_, row) =>
|
||||
row.proposed ? (
|
||||
<div>
|
||||
<div><strong>{row.proposed.name}</strong> · ¥{row.proposed.price}</div>
|
||||
<Typography.Text type="secondary">{row.proposed.dishes}</Typography.Text>
|
||||
{row.proposed.usableTime ? (
|
||||
<div><Typography.Text type="secondary">可用:{row.proposed.usableTime}</Typography.Text></div>
|
||||
) : null}
|
||||
{row.proposed.otherNotes ? (
|
||||
<div><Typography.Text type="secondary">备注:{row.proposed.otherNotes}</Typography.Text></div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
'—'
|
||||
),
|
||||
render: (_, row) => <PackageSummaryCell pkg={row.proposed} />,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -257,7 +354,7 @@ export default function StorePackageAuditsPage() {
|
||||
|
||||
<Drawer
|
||||
title={detail ? `${detail.storeName || detail.storeId} · 套餐变更` : '套餐变更详情'}
|
||||
width={720}
|
||||
width={880}
|
||||
open={detailOpen}
|
||||
onClose={() => setDetailOpen(false)}
|
||||
extra={
|
||||
@@ -282,7 +379,7 @@ export default function StorePackageAuditsPage() {
|
||||
<Typography.Text type="secondary">加载中…</Typography.Text>
|
||||
) : detail ? (
|
||||
<>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Space style={{ marginBottom: 16 }} wrap>
|
||||
<Tag>{STORE_PACKAGE_CHANGE_STATUS_LABELS[detail.status]}</Tag>
|
||||
<Typography.Text type="secondary">
|
||||
提交方:{detail.submitterType === 'PARTNER' ? '合伙人' : '门店'} · {fmtTime(detail.createdAt)}
|
||||
@@ -291,9 +388,27 @@ export default function StorePackageAuditsPage() {
|
||||
{detail.rejectReason ? (
|
||||
<Typography.Paragraph type="danger">驳回原因:{detail.rejectReason}</Typography.Paragraph>
|
||||
) : null}
|
||||
<Typography.Paragraph type="secondary">
|
||||
线上 {detail.livePackages?.length ?? 0} 条 → 申请 {detail.packages?.length ?? 0} 条
|
||||
</Typography.Paragraph>
|
||||
<Space style={{ marginBottom: 12 }} wrap>
|
||||
<Typography.Text type="secondary">
|
||||
线上 {detail.livePackages?.length ?? 0} 条 → 申请 {detail.packages?.length ?? 0} 条
|
||||
</Typography.Text>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() =>
|
||||
openPackageDetails('申请套餐详情', detail.packages ?? [])
|
||||
}
|
||||
>
|
||||
查看申请套餐详情
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() =>
|
||||
openPackageDetails('线上套餐详情', detail.livePackages ?? [])
|
||||
}
|
||||
>
|
||||
查看线上套餐详情
|
||||
</Button>
|
||||
</Space>
|
||||
<Table
|
||||
size="small"
|
||||
rowKey="key"
|
||||
@@ -305,6 +420,23 @@ export default function StorePackageAuditsPage() {
|
||||
) : null}
|
||||
</Drawer>
|
||||
|
||||
<Modal
|
||||
title={pkgDetailTitle || '套餐详情'}
|
||||
open={pkgDetailOpen}
|
||||
onCancel={() => setPkgDetailOpen(false)}
|
||||
footer={null}
|
||||
width={720}
|
||||
destroyOnClose
|
||||
>
|
||||
{pkgDetailList.length ? (
|
||||
pkgDetailList.map((pkg, index) => (
|
||||
<PackageDetailCard key={`${pkg.name}-${index}`} title={`套餐 ${index + 1}`} pkg={pkg} />
|
||||
))
|
||||
) : (
|
||||
<Typography.Text type="secondary">暂无套餐</Typography.Text>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="驳回套餐变更"
|
||||
open={rejectOpen}
|
||||
|
||||
@@ -51,13 +51,35 @@ type BillItem = {
|
||||
const STATUS_LABELS: Record<string, string> = {
|
||||
UNPAID: '未打款',
|
||||
PAID: '已打款',
|
||||
NO_PAYMENT_NEEDED: '无需打款',
|
||||
};
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
UNPAID: 'red',
|
||||
PAID: 'green',
|
||||
NO_PAYMENT_NEEDED: 'default',
|
||||
};
|
||||
|
||||
function isZeroPayable(amount: number | string | null | undefined) {
|
||||
return Number(amount ?? 0) === 0;
|
||||
}
|
||||
|
||||
/** 应付为 0 时展示「无需打款」(灰),否则按 DB 打款状态 */
|
||||
function displayWineryStatus(status: string, wineryAmount: number | string) {
|
||||
if (isZeroPayable(wineryAmount)) {
|
||||
return { key: 'NO_PAYMENT_NEEDED', label: STATUS_LABELS.NO_PAYMENT_NEEDED, color: STATUS_COLORS.NO_PAYMENT_NEEDED };
|
||||
}
|
||||
return {
|
||||
key: status,
|
||||
label: STATUS_LABELS[status] || status,
|
||||
color: STATUS_COLORS[status] || 'default',
|
||||
};
|
||||
}
|
||||
|
||||
function canConfirmWineryPay(row: { status: string; wineryAmount: number | string }) {
|
||||
return row.status === 'UNPAID' && !isZeroPayable(row.wineryAmount);
|
||||
}
|
||||
|
||||
const DELIVERY_LABELS: Record<string, string> = {
|
||||
LOCAL: '同城',
|
||||
CROSS_CITY: '跨城',
|
||||
@@ -212,7 +234,7 @@ export default function WineryBillsPage() {
|
||||
render: (v) => `${Math.round(Number(v) * 100)}%`,
|
||||
},
|
||||
{
|
||||
title: '酒厂应付',
|
||||
title: '应付',
|
||||
dataIndex: 'wineryAmount',
|
||||
width: 110,
|
||||
render: (v) => `¥${Number(v).toFixed(2)}`,
|
||||
@@ -220,8 +242,11 @@ export default function WineryBillsPage() {
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 90,
|
||||
render: (s) => <Tag color={STATUS_COLORS[s] || 'default'}>{STATUS_LABELS[s] || s}</Tag>,
|
||||
width: 100,
|
||||
render: (s, row) => {
|
||||
const d = displayWineryStatus(s, row.wineryAmount);
|
||||
return <Tag color={d.color}>{d.label}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
@@ -232,7 +257,7 @@ export default function WineryBillsPage() {
|
||||
<Button type="link" size="small" onClick={() => void openDetail(row.id)}>
|
||||
明细
|
||||
</Button>
|
||||
{row.status === 'UNPAID' && (
|
||||
{canConfirmWineryPay(row) && (
|
||||
<Button type="link" size="small" onClick={() => confirmPay([row.id], Number(row.wineryAmount))}>
|
||||
确认打款
|
||||
</Button>
|
||||
@@ -258,7 +283,8 @@ export default function WineryBillsPage() {
|
||||
酒厂对账单
|
||||
</Typography.Title>
|
||||
<Typography.Text type="secondary">
|
||||
T+3:每日 8:00 汇总 3 天前(自然日)已完成的同城/跨城订单(实付 × {ratePct}%);未打款红色、已打款绿色,可展开订单明细
|
||||
T+3:每日 8:00 汇总 3 天前(自然日)已完成的同城/跨城订单(实付 × {ratePct}%);未打款红色、已打款绿色、应付为 0
|
||||
无需打款(灰),可展开订单明细
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
{canEditWineryBank ? (
|
||||
@@ -273,7 +299,7 @@ export default function WineryBillsPage() {
|
||||
<Space size="large" wrap>
|
||||
<Statistic title="账单数" value={summary.count} />
|
||||
<Statistic title="酒单实付合计" value={summary.orderAmount ?? 0} prefix="¥" precision={2} />
|
||||
<Statistic title="酒厂应付合计" value={summary.wineryAmount ?? 0} prefix="¥" precision={2} />
|
||||
<Statistic title="应付合计" value={summary.wineryAmount ?? 0} prefix="¥" precision={2} />
|
||||
</Space>
|
||||
</Card>
|
||||
)}
|
||||
@@ -296,7 +322,7 @@ export default function WineryBillsPage() {
|
||||
<Form.Item name="status" label="状态">
|
||||
<Select
|
||||
allowClear
|
||||
style={{ width: 120 }}
|
||||
style={{ width: 130 }}
|
||||
options={Object.entries(STATUS_LABELS).map(([value, label]) => ({ value, label }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -347,7 +373,7 @@ export default function WineryBillsPage() {
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedKeys,
|
||||
onChange: setSelectedKeys,
|
||||
getCheckboxProps: (r) => ({ disabled: r.status !== 'UNPAID' }),
|
||||
getCheckboxProps: (r) => ({ disabled: !canConfirmWineryPay(r) }),
|
||||
}}
|
||||
scroll={{ x: 1100 }}
|
||||
pagination={{
|
||||
@@ -368,8 +394,10 @@ export default function WineryBillsPage() {
|
||||
<Descriptions column={1} size="small" bordered>
|
||||
<Descriptions.Item label="账单号">{detail.billNo}</Descriptions.Item>
|
||||
<Descriptions.Item label="账单日">{String(detail.billDate).slice(0, 10)}</Descriptions.Item>
|
||||
<Descriptions.Item label="酒厂应付">¥{Number(detail.wineryAmount).toFixed(2)}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">{STATUS_LABELS[detail.status] || detail.status}</Descriptions.Item>
|
||||
<Descriptions.Item label="应付">¥{Number(detail.wineryAmount).toFixed(2)}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
{displayWineryStatus(detail.status, detail.wineryAmount).label}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="打款时间">{detail.paidAt ? fmtTime(detail.paidAt) : '—'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<Typography.Title level={5} style={{ marginTop: 16 }}>
|
||||
@@ -395,7 +423,7 @@ export default function WineryBillsPage() {
|
||||
render: (v) => `¥${Number(v).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
title: '酒厂应付',
|
||||
title: '应付',
|
||||
dataIndex: 'wineryAmount',
|
||||
width: 90,
|
||||
render: (v) => `¥${Number(v).toFixed(2)}`,
|
||||
|
||||
@@ -1708,6 +1708,9 @@ export class SettlementService {
|
||||
const bill = await this.prisma.wineryBill.findUnique({ where: { id } });
|
||||
if (!bill) throw new NotFoundException('酒厂对账单不存在');
|
||||
if (bill.status !== 'UNPAID') throw new BadRequestException('仅未打款账单可确认打款');
|
||||
if (Number(bill.wineryAmount) === 0) {
|
||||
throw new BadRequestException('应付为 0,无需打款');
|
||||
}
|
||||
const updated = await this.prisma.wineryBill.update({
|
||||
where: { id },
|
||||
data: { status: 'PAID', paidAt: new Date() },
|
||||
@@ -1749,11 +1752,17 @@ export class SettlementService {
|
||||
'配送类型',
|
||||
'实付金额',
|
||||
'酒厂比例',
|
||||
'酒厂应付',
|
||||
'应付',
|
||||
'支付时间',
|
||||
'账单状态',
|
||||
].join(',');
|
||||
const rows: string[] = [];
|
||||
const statusLabel = (b: { status: string; wineryAmount: Prisma.Decimal | number }) => {
|
||||
if (Number(b.wineryAmount) === 0) return '无需打款';
|
||||
if (b.status === 'PAID') return '已打款';
|
||||
if (b.status === 'UNPAID') return '未打款';
|
||||
return b.status;
|
||||
};
|
||||
for (const b of bills) {
|
||||
if (b.items.length === 0) {
|
||||
rows.push(
|
||||
@@ -1766,7 +1775,7 @@ export class SettlementService {
|
||||
Number(b.wineryRate),
|
||||
Number(b.wineryAmount),
|
||||
'',
|
||||
b.status,
|
||||
statusLabel(b),
|
||||
].join(','),
|
||||
);
|
||||
continue;
|
||||
@@ -1782,7 +1791,7 @@ export class SettlementService {
|
||||
Number(b.wineryRate),
|
||||
Number(item.wineryAmount),
|
||||
item.paidAt.toISOString().slice(0, 19).replace('T', ' '),
|
||||
b.status,
|
||||
statusLabel(b),
|
||||
].join(','),
|
||||
);
|
||||
}
|
||||
@@ -1798,7 +1807,15 @@ export class SettlementService {
|
||||
month?: number;
|
||||
}): Prisma.WineryBillWhereInput {
|
||||
const where: Prisma.WineryBillWhereInput = {};
|
||||
if (query.status === 'UNPAID' || query.status === 'PAID') where.status = query.status;
|
||||
if (query.status === 'NO_PAYMENT_NEEDED') {
|
||||
where.wineryAmount = 0;
|
||||
} else if (query.status === 'UNPAID') {
|
||||
where.status = 'UNPAID';
|
||||
where.wineryAmount = { gt: 0 };
|
||||
} else if (query.status === 'PAID') {
|
||||
where.status = 'PAID';
|
||||
where.wineryAmount = { gt: 0 };
|
||||
}
|
||||
if (query.year && query.month) {
|
||||
const start = new Date(query.year, query.month - 1, 1);
|
||||
const end = new Date(query.year, query.month, 0, 23, 59, 59, 999);
|
||||
|
||||
Reference in New Issue
Block a user