采购单优化
This commit is contained in:
+170
-18
@@ -15,6 +15,7 @@ import {
|
||||
Space,
|
||||
Spin,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
@@ -32,12 +33,16 @@ import type {
|
||||
IPurchaseCellItem,
|
||||
IPurchaseDetail,
|
||||
IPurchaseDetailRow,
|
||||
IPurchaseStoreItem,
|
||||
IPurchaseStoreSummary,
|
||||
} from '@/domain/iPurchaseOrder.ts';
|
||||
import { PURCHASE_STATUS_MAP } from '@/domain/iPurchaseOrder.ts';
|
||||
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
|
||||
import {
|
||||
getPurchaseCell,
|
||||
getPurchaseDetail, type PurchaseCellUpdateParams, type PurchaseRowUpdateParams,
|
||||
getPurchaseDetail,
|
||||
getPurchaseStoreSummary,
|
||||
type PurchaseCellUpdateParams, type PurchaseRowUpdateParams,
|
||||
updatePurchaseCellItem,
|
||||
updatePurchaseRow,
|
||||
} from '@/api/purchase/order.ts';
|
||||
@@ -48,10 +53,10 @@ import AuthButton from '@/components/AuthButton';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
/** 参考单价 = 成本 / 包规数值(包规解析不出正数时按 1 处理,与后端同口径;仅展示参考,不参与金额计算) */
|
||||
const calcUnitCost = (cost: number, spec: string): number => {
|
||||
/** 每单位参考价 = 整单价(成本/售价) ÷ 包规数值(包规解析不出正数时按 1 处理,与后端同口径;仅展示参考,不参与金额计算) */
|
||||
const calcUnitRefPrice = (total: number, spec: string): number => {
|
||||
const pack = parseFloat(spec);
|
||||
return Number.isFinite(pack) && pack > 0 ? cost / pack : cost;
|
||||
return Number.isFinite(pack) && pack > 0 ? total / pack : total;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -85,6 +90,12 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
const [cellItemSaving, setCellItemSaving] = useState(false);
|
||||
const [cellItemForm] = Form.useForm<PurchaseCellUpdateParams>();
|
||||
|
||||
// 门店购买详情(按商品聚合的门店采购汇总)
|
||||
const [detailTab, setDetailTab] = useState('items');
|
||||
const [storeId, setStoreId] = useState<number>(0);
|
||||
const [storeSummary, setStoreSummary] = useState<IPurchaseStoreSummary | null>(null);
|
||||
const [storeLoading, setStoreLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getSupplierOptions().then((res) => setSuppliers(res.data.data ?? []));
|
||||
}, []);
|
||||
@@ -96,6 +107,20 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
}
|
||||
}, [cellOpen, cellQuery]);
|
||||
|
||||
// 详情加载后默认选中第一个门店(当前选中门店仍在采购单内则保留)
|
||||
useEffect(() => {
|
||||
if (detail && detail.stores.length > 0) {
|
||||
setStoreId((prev) => (detail.stores.some((s) => s.id === prev) ? prev : detail.stores[0].id));
|
||||
}
|
||||
}, [detail]);
|
||||
|
||||
// 切到「门店购买详情」页签或切换门店时加载汇总
|
||||
useEffect(() => {
|
||||
if (detailOpen && detailTab === 'stores' && detail && storeId > 0) {
|
||||
void loadStoreSummary();
|
||||
}
|
||||
}, [detailOpen, detailTab, storeId, detail?.purchase.id]);
|
||||
|
||||
const loadDetail = async (id: number) => {
|
||||
setDetailLoading(true);
|
||||
try {
|
||||
@@ -107,10 +132,26 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
};
|
||||
|
||||
const openDetail = async (id: number) => {
|
||||
setDetailTab('items');
|
||||
setStoreSummary(null);
|
||||
setDetailOpen(true);
|
||||
await loadDetail(id);
|
||||
};
|
||||
|
||||
/** 加载门店购买详情(门店采购汇总) */
|
||||
const loadStoreSummary = async () => {
|
||||
if (!detail || storeId <= 0) {
|
||||
return;
|
||||
}
|
||||
setStoreLoading(true);
|
||||
try {
|
||||
const res = await getPurchaseStoreSummary(detail.purchase.id!, storeId);
|
||||
setStoreSummary(res.data.data ?? null);
|
||||
} finally {
|
||||
setStoreLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** 打开单元格下钻:门店 + 商品 → 该采购单下全部订货明细 */
|
||||
const openCell = (row: IPurchaseDetailRow, store: { id: number; name: string }) => {
|
||||
setCellQuery({ productId: row.product_id, storeId: store.id });
|
||||
@@ -226,11 +267,11 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
render: (_, row) => row.supplier?.name ?? '-',
|
||||
},
|
||||
{
|
||||
title: '参考单价',
|
||||
title: '参考成本单价',
|
||||
key: 'unit_cost',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
render: (_, row) => `¥${calcUnitCost(Number(row.cost_price), row.product_spec ?? '').toFixed(2)}`,
|
||||
render: (_, row) => `¥${calcUnitRefPrice(Number(row.cost_price), row.product_spec ?? '').toFixed(2)}`,
|
||||
},
|
||||
{ title: '包规', dataIndex: 'product_spec', align: 'center', width: 90, render: (v) => v || '-' },
|
||||
{ title: '单位', dataIndex: 'unit', width: 70, align: 'center', render: (v) => v || '-' },
|
||||
@@ -317,6 +358,66 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
/** 门店购买详情列:商品/购买参考零售价(单价÷包规)/包规/单位/单价/数量/预计金额 */
|
||||
const storeColumns: TableProps<IPurchaseStoreItem>['columns'] = [
|
||||
{ title: '商品', dataIndex: 'product_name', width: 160, align: 'center' },
|
||||
{
|
||||
title: '参考零售价',
|
||||
key: 'retail_price',
|
||||
width: 130,
|
||||
align: 'center',
|
||||
render: (_, row) => `¥${calcUnitRefPrice(Number(row.price), row.product_spec).toFixed(2)}`,
|
||||
},
|
||||
{ title: '包规', dataIndex: 'product_spec', width: 90, align: 'center', render: (v) => v || '-' },
|
||||
{ title: '单位', dataIndex: 'unit', width: 90, align: 'center', render: (v) => v || '-' },
|
||||
{
|
||||
title: '单价',
|
||||
dataIndex: 'price',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
render: (v) => `¥${Number(v).toFixed(2)}`,
|
||||
},
|
||||
{
|
||||
title: '数量',
|
||||
dataIndex: 'quantity',
|
||||
width: 90,
|
||||
align: 'center',
|
||||
render: (v) => <Text strong>{v}</Text>,
|
||||
},
|
||||
{ title: '重量', dataIndex: 'weight', width: 90, align: 'center', render: (v) => `${v || '-'}斤` },
|
||||
{
|
||||
title: '预计金额',
|
||||
dataIndex: 'amount',
|
||||
width: 110,
|
||||
align: 'center',
|
||||
render: (v) => <Text strong>¥{Number(v).toFixed(2)}</Text>,
|
||||
},
|
||||
];
|
||||
|
||||
/** 门店购买详情合计行:总数量 + 总预计金额 */
|
||||
const renderStoreTotal = () => {
|
||||
const items = storeSummary?.items ?? [];
|
||||
const totalQuantity = items.reduce((sum, row) => sum + Number(row.quantity), 0);
|
||||
const totalAmount = items.reduce((sum, row) => sum + Number(row.amount), 0);
|
||||
const totalWeight = items.reduce((sum, row) => sum + Number(row.weight), 0);
|
||||
return (
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell index={0} colSpan={5} align="center">
|
||||
<Text strong>合计</Text>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={5} align="center">
|
||||
<Text strong>{totalQuantity}</Text>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={6} align="center">
|
||||
<Text strong>{totalWeight.toFixed(3)}斤</Text>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={7} align="center">
|
||||
<Text strong type="danger">¥{totalAmount.toFixed(2)}</Text>
|
||||
</Table.Summary.Cell>
|
||||
</Table.Summary.Row>
|
||||
);
|
||||
};
|
||||
|
||||
const columns: XinTableColumn<IPurchaseOrder>[] = [
|
||||
{
|
||||
title: '采购单号',
|
||||
@@ -444,18 +545,69 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
<Descriptions.Item label="总重量">{detail.purchase.total_weight}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Title level={5} className="mt-5! mb-3!">
|
||||
商品明细
|
||||
</Title>
|
||||
<Table<IPurchaseDetailRow>
|
||||
rowKey="product_id"
|
||||
size="small"
|
||||
bordered
|
||||
columns={buildItemColumns()}
|
||||
dataSource={detail.items}
|
||||
pagination={false}
|
||||
scroll={{ x: 'max-content' }}
|
||||
summary={renderSummary}
|
||||
<Tabs
|
||||
activeKey={detailTab}
|
||||
onChange={setDetailTab}
|
||||
className="mt-3!"
|
||||
items={[
|
||||
{
|
||||
key: 'items',
|
||||
label: '商品明细',
|
||||
children: (
|
||||
<Table<IPurchaseDetailRow>
|
||||
rowKey="product_id"
|
||||
size="small"
|
||||
bordered
|
||||
columns={buildItemColumns()}
|
||||
dataSource={detail.items}
|
||||
pagination={false}
|
||||
scroll={{ x: 'max-content' }}
|
||||
summary={renderSummary}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'stores',
|
||||
label: '门店购买详情',
|
||||
children: (
|
||||
<>
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<Text>选择门店:</Text>
|
||||
<Select
|
||||
value={storeId || undefined}
|
||||
onChange={(value) => setStoreId(value)}
|
||||
placeholder="选择门店"
|
||||
className="w-60!"
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
options={detail.stores.map((s) => ({ value: s.id, label: s.name }))}
|
||||
/>
|
||||
</div>
|
||||
<Spin spinning={storeLoading}>
|
||||
{storeSummary && storeSummary.items.length > 0 ? (
|
||||
<Table<IPurchaseStoreItem>
|
||||
rowKey="product_id"
|
||||
size="small"
|
||||
bordered
|
||||
columns={storeColumns}
|
||||
dataSource={storeSummary.items}
|
||||
pagination={false}
|
||||
summary={renderStoreTotal}
|
||||
/>
|
||||
) : (
|
||||
!storeLoading && (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="该门店在此采购单中无采购商品"
|
||||
className="py-8!"
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Spin>
|
||||
</>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user