采购单优化
This commit is contained in:
+271
-48
@@ -3,13 +3,17 @@ import {
|
||||
Button,
|
||||
Descriptions,
|
||||
Drawer,
|
||||
Empty,
|
||||
Form,
|
||||
Image,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Select,
|
||||
Space,
|
||||
Spin,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
@@ -24,12 +28,17 @@ import type {
|
||||
} from '@/components/XinTable/typings.ts';
|
||||
import type IPurchaseOrder from '@/domain/iPurchaseOrder.ts';
|
||||
import type {
|
||||
IPurchaseCell,
|
||||
IPurchaseCellItem,
|
||||
IPurchaseDetail,
|
||||
IPurchaseDetailRow,
|
||||
} from '@/domain/iPurchaseOrder.ts';
|
||||
import { PURCHASE_STATUS_MAP } from '@/domain/iPurchaseOrder.ts';
|
||||
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
|
||||
import {
|
||||
getPurchaseDetail,
|
||||
getPurchaseCell,
|
||||
getPurchaseDetail, type PurchaseCellUpdateParams, type PurchaseRowUpdateParams,
|
||||
updatePurchaseCellItem,
|
||||
updatePurchaseRow,
|
||||
} from '@/api/purchase/order.ts';
|
||||
import { Update } from '@/api/common/table.ts';
|
||||
@@ -39,16 +48,7 @@ import AuthButton from '@/components/AuthButton';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
/** 行修改表单:品名/供应商/包规/单位/成本 */
|
||||
interface RowEditForm {
|
||||
product_name: string;
|
||||
supplier_id?: number;
|
||||
product_spec: string;
|
||||
unit: string;
|
||||
cost_price: number;
|
||||
}
|
||||
|
||||
/** 单价 = 成本 / 包规数值(包规解析不出正数时按 1 处理,与后端同口径) */
|
||||
/** 参考单价 = 成本 / 包规数值(包规解析不出正数时按 1 处理,与后端同口径;仅展示参考,不参与金额计算) */
|
||||
const calcUnitCost = (cost: number, spec: string): number => {
|
||||
const pack = parseFloat(spec);
|
||||
return Number.isFinite(pack) && pack > 0 ? cost / pack : cost;
|
||||
@@ -70,14 +70,32 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
// 行修改弹窗
|
||||
const [editingRow, setEditingRow] = useState<IPurchaseDetailRow | null>(null);
|
||||
const [rowSaving, setRowSaving] = useState(false);
|
||||
const [syncTarget, setSyncTarget] = useState<'purchase' | 'product'>('purchase');
|
||||
const [editForm] = Form.useForm<RowEditForm>();
|
||||
const [editForm] = Form.useForm<PurchaseRowUpdateParams>();
|
||||
const [suppliers, setSuppliers] = useState<ISupplier[]>([]);
|
||||
|
||||
// 单元格下钻弹窗(门店 × 商品订货明细)
|
||||
const [cellOpen, setCellOpen] = useState(false);
|
||||
const [cellLoading, setCellLoading] = useState(false);
|
||||
const [cellData, setCellData] = useState<IPurchaseCell | null>(null);
|
||||
const [cellQuery, setCellQuery] = useState<{ productId: number; storeId: number } | null>(null);
|
||||
|
||||
// 单元格明细编辑(数量/称重)
|
||||
const [cellItemOpen, setCellItemOpen] = useState(false);
|
||||
const [cellItemTarget, setCellItemTarget] = useState<IPurchaseCellItem | null>(null);
|
||||
const [cellItemSaving, setCellItemSaving] = useState(false);
|
||||
const [cellItemForm] = Form.useForm<PurchaseCellUpdateParams>();
|
||||
|
||||
useEffect(() => {
|
||||
getSupplierOptions().then((res) => setSuppliers(res.data.data ?? []));
|
||||
}, []);
|
||||
|
||||
// 单元格下钻弹窗打开时加载明细
|
||||
useEffect(() => {
|
||||
if (cellOpen && cellQuery) {
|
||||
loadCell();
|
||||
}
|
||||
}, [cellOpen, cellQuery]);
|
||||
|
||||
const loadDetail = async (id: number) => {
|
||||
setDetailLoading(true);
|
||||
try {
|
||||
@@ -93,6 +111,63 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
await loadDetail(id);
|
||||
};
|
||||
|
||||
/** 打开单元格下钻:门店 + 商品 → 该采购单下全部订货明细 */
|
||||
const openCell = (row: IPurchaseDetailRow, store: { id: number; name: string }) => {
|
||||
setCellQuery({ productId: row.product_id, storeId: store.id });
|
||||
setCellData(null);
|
||||
setCellOpen(true);
|
||||
};
|
||||
|
||||
/** 加载单元格明细 */
|
||||
const loadCell = async () => {
|
||||
if (!detail || !cellQuery) {
|
||||
return;
|
||||
}
|
||||
setCellLoading(true);
|
||||
try {
|
||||
const res = await getPurchaseCell(detail.purchase.id!, cellQuery.productId, cellQuery.storeId);
|
||||
setCellData(res.data.data ?? null);
|
||||
} finally {
|
||||
setCellLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** 单元格明细修改/同步后:刷新弹窗、采购单详情与列表 */
|
||||
const refreshAfterCellChange = async () => {
|
||||
await loadCell();
|
||||
if (detail) {
|
||||
await loadDetail(detail.purchase.id!);
|
||||
}
|
||||
await tableRef.current?.reload();
|
||||
};
|
||||
|
||||
const openCellItemEdit = (item: IPurchaseCellItem) => {
|
||||
setCellItemTarget(item);
|
||||
cellItemForm.setFieldsValue({
|
||||
quantity: item.quantity,
|
||||
price: Number(item.price ?? 0),
|
||||
weight: Number(item.weight ?? 0),
|
||||
});
|
||||
setCellItemOpen(true);
|
||||
};
|
||||
|
||||
/** 提交单元格明细修改:级联重算明细金额、订货单与采购单汇总 */
|
||||
const handleCellItemSave = async (values: PurchaseCellUpdateParams) => {
|
||||
if (!cellItemTarget?.id) {
|
||||
return;
|
||||
}
|
||||
setCellItemSaving(true);
|
||||
try {
|
||||
await updatePurchaseCellItem(cellItemTarget.id, values);
|
||||
message.success('明细已更新,订货单与采购单汇总已重算');
|
||||
setCellItemOpen(false);
|
||||
setCellItemTarget(null);
|
||||
await refreshAfterCellChange();
|
||||
} finally {
|
||||
setCellItemSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openEdit = (row: IPurchaseDetailRow) => {
|
||||
setEditingRow(row);
|
||||
editForm.setFieldsValue({
|
||||
@@ -105,7 +180,7 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
};
|
||||
|
||||
/** 提交行修改:同步该商品全部订货明细;syncTarget=product 时追加同步商品档案 */
|
||||
const handleEditSave = async (values: RowEditForm) => {
|
||||
const handleEditSave = async (values: PurchaseRowUpdateParams) => {
|
||||
if (!detail || !editingRow) {
|
||||
return;
|
||||
}
|
||||
@@ -114,13 +189,8 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
const res = await updatePurchaseRow(detail.purchase.id!, editingRow.product_id, {
|
||||
...values,
|
||||
supplier_id: values.supplier_id ?? 0,
|
||||
sync_product: syncTarget === 'product',
|
||||
});
|
||||
message.success(
|
||||
syncTarget === 'product'
|
||||
? `已同步 ${res.data.data?.count ?? 0} 条订货明细,并更新商品档案`
|
||||
: `已同步 ${res.data.data?.count ?? 0} 条订货明细`,
|
||||
);
|
||||
message.success(`已更新 ${res.data.data?.count ?? 0} 条订货明细`);
|
||||
setEditingRow(null);
|
||||
await loadDetail(detail.purchase.id!);
|
||||
await tableRef.current?.reload();
|
||||
@@ -156,9 +226,9 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
render: (_, row) => row.supplier?.name ?? '-',
|
||||
},
|
||||
{
|
||||
title: '单价',
|
||||
title: '参考单价',
|
||||
key: 'unit_cost',
|
||||
width: 90,
|
||||
width: 100,
|
||||
align: 'center',
|
||||
render: (_, row) => `¥${calcUnitCost(Number(row.cost_price), row.product_spec ?? '').toFixed(2)}`,
|
||||
},
|
||||
@@ -174,7 +244,11 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
align: 'center' as const,
|
||||
render: (_: unknown, row: IPurchaseDetailRow) => {
|
||||
const quantity = row.cells[store.id];
|
||||
return quantity !== undefined ? <Text>{quantity}</Text> : <Text type="secondary">-</Text>;
|
||||
return quantity !== undefined ? (
|
||||
<Typography.Link onClick={() => openCell(row, store)}>{quantity}</Typography.Link>
|
||||
) : (
|
||||
<Text type="secondary">-</Text>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -387,7 +461,7 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
{/* 行修改:品名/供应商/包规/单位/成本,同步采购单明细 / 追加同步商品档案 */}
|
||||
{/* 行修改:品名/供应商/包规/单位/成本 */}
|
||||
<Modal
|
||||
title={editingRow ? `修改「${editingRow.product_name}」` : '修改明细行'}
|
||||
open={editingRow !== null}
|
||||
@@ -397,31 +471,13 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
<Button key="cancel" onClick={() => setEditingRow(null)}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="product"
|
||||
loading={rowSaving && syncTarget === 'product'}
|
||||
onClick={() => {
|
||||
setSyncTarget('product');
|
||||
editForm.submit();
|
||||
}}
|
||||
>
|
||||
保存并同步
|
||||
</Button>,
|
||||
<Button
|
||||
key="purchase"
|
||||
type="primary"
|
||||
loading={rowSaving && syncTarget === 'purchase'}
|
||||
onClick={() => {
|
||||
setSyncTarget('purchase');
|
||||
editForm.submit();
|
||||
}}
|
||||
>
|
||||
<Button key="purchase" type="primary" loading={rowSaving} onClick={() => editForm.submit()}>
|
||||
保存
|
||||
</Button>,
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<div className="py-2 text-gray-500">
|
||||
「保存」仅修改本采购单中该商品的所有订单项;「保存并同步」在此基础上同时更新商品档案(后续新订单生效)。
|
||||
「保存」仅修改本采购单中该商品的所有订单项
|
||||
</div>
|
||||
<Form form={editForm} layout="vertical" onFinish={handleEditSave}>
|
||||
<Form.Item
|
||||
@@ -431,7 +487,11 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
>
|
||||
<Input maxLength={100} />
|
||||
</Form.Item>
|
||||
<Form.Item label="供应商" name="supplier_id">
|
||||
<Form.Item
|
||||
label="供应商"
|
||||
name="supplier_id"
|
||||
rules={[{ required: true, message: '请选择供应商' }]}
|
||||
>
|
||||
<Select
|
||||
allowClear
|
||||
showSearch={{ optionFilterProp: 'label' }}
|
||||
@@ -439,10 +499,18 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
options={suppliers.map((s) => ({ value: s.id, label: s.name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="包规" name="product_spec" rules={[{ max: 100 }]}>
|
||||
<Form.Item
|
||||
label="包规"
|
||||
name="product_spec"
|
||||
rules={[{ required: true, message: '请输入包规' }, { max: 100 }]}
|
||||
>
|
||||
<Input maxLength={100} placeholder="如:10斤/箱" />
|
||||
</Form.Item>
|
||||
<Form.Item label="单位" name="unit" rules={[{ max: 20 }]}>
|
||||
<Form.Item
|
||||
label="单位"
|
||||
name="unit"
|
||||
rules={[{ required: true, message: '请输入单位' },{ max: 20 }]}
|
||||
>
|
||||
<Input maxLength={20} placeholder="如:斤" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -454,6 +522,161 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 单元格下钻 */}
|
||||
<Modal
|
||||
title={cellData ? `${cellData.store?.name ?? ''} · ${cellData.product?.name ?? ''}` : '门店商品明细'}
|
||||
open={cellOpen}
|
||||
onCancel={() => setCellOpen(false)}
|
||||
footer={null}
|
||||
width={1000}
|
||||
destroyOnHidden
|
||||
styles={{ body: {paddingTop: 16} }}
|
||||
>
|
||||
<Spin spinning={cellLoading}>
|
||||
{cellData && (
|
||||
<>
|
||||
{cellData.items.length === 0 ? (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="该门店无此商品明细"
|
||||
className="py-8!"
|
||||
/>
|
||||
) : (
|
||||
<div className="overflow-hidden rounded border border-gray-200">
|
||||
<div className="flex bg-gray-50 px-4 py-2 text-sm text-gray-500">
|
||||
<div className="flex-1">商品信息</div>
|
||||
<div className="w-30 shrink-0 text-center">单价</div>
|
||||
<div className="w-26 shrink-0 text-center">订货量</div>
|
||||
<div className="w-33 shrink-0 text-center">订货金额</div>
|
||||
<div className="w-33 shrink-0 text-center">重量</div>
|
||||
<div className="w-40 shrink-0 text-center">操作</div>
|
||||
</div>
|
||||
{cellData.items.map((item) => (
|
||||
<div key={item.id} className="flex items-center border-t border-gray-100 px-4 py-3">
|
||||
<div className="flex min-w-0 flex-1 items-center">
|
||||
<Image.PreviewGroup>
|
||||
{item.image ? (
|
||||
<Image
|
||||
src={item.image}
|
||||
width={48}
|
||||
height={48}
|
||||
style={{ objectFit: 'cover', borderRadius: 4 }}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded bg-gray-100 text-xs text-gray-400">
|
||||
暂无图片
|
||||
</div>
|
||||
)}
|
||||
</Image.PreviewGroup>
|
||||
<div className="ml-3 min-w-0">
|
||||
<div className="text-sm font-medium">
|
||||
{item.product_name}
|
||||
<Tag
|
||||
className="ml-2!"
|
||||
color={STORE_ORDER_STATUS_MAP[item.order_status]?.color}
|
||||
>
|
||||
{STORE_ORDER_STATUS_MAP[item.order_status]?.text}
|
||||
</Tag>
|
||||
</div>
|
||||
<div className="mt-0.5 text-xs text-gray-500">
|
||||
订单:{item.order_no}
|
||||
</div>
|
||||
{item.remark ? (
|
||||
<div className="mt-0.5 truncate text-xs text-gray-500">
|
||||
备注:{item.remark}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-30 shrink-0 text-center">¥{item.price}</div>
|
||||
<div className="w-26 shrink-0 text-center">{item.quantity}</div>
|
||||
<div className="w-33 shrink-0 text-center">
|
||||
<Text strong>¥{item.amount}</Text>
|
||||
</div>
|
||||
<div className="w-33 shrink-0 text-center">
|
||||
{item.weight ?? '-'} 斤
|
||||
</div>
|
||||
<div className="w-40 shrink-0 text-center">
|
||||
{item.editable ? (
|
||||
<Space size={0}>
|
||||
<AuthButton auth="purchase.order.update">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => openCellItemEdit(item)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
</AuthButton>
|
||||
</Space>
|
||||
) : (
|
||||
<Text type="secondary">-</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{cellData.items.length > 0 && (
|
||||
<div className="mt-3! flex justify-end gap-6 text-sm">
|
||||
<Text type="secondary">
|
||||
合计数量:
|
||||
<Text strong>{cellData.items.reduce((sum, item) => sum + item.quantity, 0)}</Text>
|
||||
</Text>
|
||||
<Text type="secondary">
|
||||
合计订货金额:
|
||||
<Text strong type="danger">
|
||||
¥
|
||||
{cellData.items
|
||||
.reduce((sum, item) => sum + Number(item.amount), 0)
|
||||
.toFixed(2)}
|
||||
</Text>
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Spin>
|
||||
</Modal>
|
||||
|
||||
{/* 单元格明细编辑 */}
|
||||
<Modal
|
||||
title={cellItemTarget ? `编辑「${cellItemTarget.product_name}」` : '编辑明细'}
|
||||
open={cellItemOpen}
|
||||
onCancel={() => {
|
||||
setCellItemOpen(false);
|
||||
setCellItemTarget(null);
|
||||
}}
|
||||
onOk={() => cellItemForm.submit()}
|
||||
confirmLoading={cellItemSaving}
|
||||
okText="保存"
|
||||
destroyOnHidden
|
||||
>
|
||||
<div className="py-2 text-gray-500">
|
||||
订单 {cellItemTarget?.order_no};修改保存后,系统将自动重算明细金额、订货单与采购单汇总。
|
||||
</div>
|
||||
<Form form={cellItemForm} layout="vertical" onFinish={handleCellItemSave}>
|
||||
<Form.Item
|
||||
label="单价(元)"
|
||||
name="price"
|
||||
rules={[{ required: true, message: '请输入单价' }]}
|
||||
>
|
||||
<InputNumber className="w-full" min={0} precision={2} placeholder="请输入单价" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="订货量"
|
||||
name="quantity"
|
||||
rules={[{ required: true, message: '请输入订货量' }]}
|
||||
>
|
||||
<InputNumber className="w-full" min={0} precision={0} placeholder="请输入订货量" />
|
||||
</Form.Item>
|
||||
<Form.Item label="称重" name="weight" rules={[{ required: true, message: '请输入称重' }]}>
|
||||
<InputNumber className="w-full" min={0} precision={3} placeholder="请输入称重" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user