采购单优化
This commit is contained in:
+4
-30
@@ -1,8 +1,7 @@
|
||||
import createAxios from '@/utils/request';
|
||||
import type IStoreOrder from '@/domain/iStoreOrder.ts';
|
||||
import type { IOrderSummaryRow, IStoreOrderItem, IStoreOrderItemUpdate } from '@/domain/iStoreOrder.ts';
|
||||
|
||||
/** 订单详情(头 + 明细) */
|
||||
/** 订单详情 */
|
||||
export async function getStoreOrder(id: number) {
|
||||
return createAxios<IStoreOrder>({
|
||||
url: `/order/store/${id}`,
|
||||
@@ -10,7 +9,7 @@ export async function getStoreOrder(id: number) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 订单状态流转(1已接单 3配送中 4已完成 9取消;已接单→采购中由生成采购单完成) */
|
||||
/** 订单状态流转 */
|
||||
export async function updateOrderStatus(id: number, status: number) {
|
||||
return createAxios({
|
||||
url: `/order/store/${id}/status`,
|
||||
@@ -19,7 +18,7 @@ export async function updateOrderStatus(id: number, status: number) {
|
||||
});
|
||||
}
|
||||
|
||||
/** 批量订单状态流转(全量校验,任一订单不允许流转则整批中止) */
|
||||
/** 批量订单状态流转 */
|
||||
export async function batchUpdateOrderStatus(ids: number[], status: number) {
|
||||
return createAxios<{ success: number }>({
|
||||
url: '/order/store/batchStatus',
|
||||
@@ -37,32 +36,7 @@ export async function updateOrderContainer(id: number, data: { box_num: number;
|
||||
});
|
||||
}
|
||||
|
||||
/** 已接单预览(按商品聚合,生成采购单前确认) */
|
||||
export async function getOrderSummary() {
|
||||
return createAxios<IOrderSummaryRow[]>({
|
||||
url: '/order/store/summary',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
/** 修改订单明细(供应商/品名/规格/单位/单价/成本价/订货量/重量,后端重算订单总价) */
|
||||
export async function updateOrderItem(itemId: number, data: IStoreOrderItemUpdate) {
|
||||
return createAxios<IStoreOrderItem>({
|
||||
url: `/order/store/item/${itemId}`,
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 一键同步明细商品快照为最新商品档案信息(供应商/品名/规格/单位/单价/成本价) */
|
||||
export async function syncOrderItem(itemId: number) {
|
||||
return createAxios<IStoreOrderItem>({
|
||||
url: `/order/store/item/${itemId}/sync`,
|
||||
method: 'put',
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除订单(软删除,仅已取消订单允许) */
|
||||
/** 删除订单 */
|
||||
export async function deleteStoreOrder(id: number) {
|
||||
return createAxios({
|
||||
url: `/order/store/${id}`,
|
||||
|
||||
+40
-32
@@ -1,12 +1,27 @@
|
||||
import createAxios from '@/utils/request';
|
||||
import type {
|
||||
ExportFormat,
|
||||
IPurchaseCell,
|
||||
IPurchaseDetail,
|
||||
PurchaseExportType,
|
||||
} from '@/domain/iPurchaseOrder.ts';
|
||||
import { downloadBlob } from '@/api/common/download.ts';
|
||||
|
||||
/** C1 合并「已接单」门店订单生成采购单(order_ids 为空 = 全部已接单;生成后源订单转采购中) */
|
||||
/** 订单商品参数修改 */
|
||||
export interface PurchaseRowUpdateParams {
|
||||
product_name: string;
|
||||
supplier_id: number;
|
||||
product_spec: string;
|
||||
unit: string;
|
||||
cost_price: number;
|
||||
}
|
||||
|
||||
/** 修改门店订单明细参数 */
|
||||
export interface PurchaseCellUpdateParams {
|
||||
quantity: number;
|
||||
price: number;
|
||||
weight?: number;
|
||||
}
|
||||
|
||||
|
||||
/** 生成采购单 */
|
||||
export async function generatePurchase(purchase_date: string, order_ids?: number[]) {
|
||||
return createAxios<{ id: number; purchase_no: string }>({
|
||||
url: '/purchase/order/generate',
|
||||
@@ -15,7 +30,7 @@ export async function generatePurchase(purchase_date: string, order_ids?: number
|
||||
});
|
||||
}
|
||||
|
||||
/** 采购单详情(商品行 × 门店列矩阵) */
|
||||
/** 采购单详情 */
|
||||
export async function getPurchaseDetail(id: number) {
|
||||
return createAxios<IPurchaseDetail>({
|
||||
url: `/purchase/order/${id}`,
|
||||
@@ -23,36 +38,29 @@ export async function getPurchaseDetail(id: number) {
|
||||
});
|
||||
}
|
||||
|
||||
/** C2/C3 导出采购单(blob 下载) */
|
||||
export async function exportPurchase(id: number, type: PurchaseExportType, format: ExportFormat) {
|
||||
return downloadBlob(
|
||||
`/purchase/order/${id}/export`,
|
||||
{ type, format },
|
||||
`采购单_${id}.${format}`
|
||||
);
|
||||
}
|
||||
|
||||
/** C4 商品行修改参数(品名/供应商/包规/单位/成本/称重,一键同步该商品全部订货明细;sync_product 追加同步商品档案) */
|
||||
export interface PurchaseRowUpdateParams {
|
||||
product_name?: string;
|
||||
supplier_id?: number;
|
||||
product_spec?: string;
|
||||
unit?: string;
|
||||
cost_price?: number;
|
||||
weight?: number;
|
||||
/** true = 同时同步至商品档案(product 表) */
|
||||
sync_product?: boolean;
|
||||
}
|
||||
|
||||
/** C4 商品行修改 */
|
||||
export async function updatePurchaseRow(
|
||||
purchaseId: number,
|
||||
productId: number,
|
||||
data: PurchaseRowUpdateParams,
|
||||
) {
|
||||
/** 商品行修改 */
|
||||
export async function updatePurchaseRow(purchaseId: number, productId: number, data: PurchaseRowUpdateParams) {
|
||||
return createAxios<{ count: number }>({
|
||||
url: `/purchase/order/${purchaseId}/row/${productId}`,
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 单元格下钻:采购单中某门店某商品的全部订货明细 */
|
||||
export async function getPurchaseCell(purchaseId: number, productId: number, storeId: number) {
|
||||
return createAxios<IPurchaseCell>({
|
||||
url: `/purchase/order/${purchaseId}/cell`,
|
||||
method: 'get',
|
||||
params: { product_id: productId, store_id: storeId },
|
||||
});
|
||||
}
|
||||
|
||||
/** 门店订单明细修改(订货量、重量、单价),自动重算价格 */
|
||||
export async function updatePurchaseCellItem(itemId: number, data: PurchaseCellUpdateParams) {
|
||||
return createAxios({
|
||||
url: `/purchase/order/cell/${itemId}`,
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,6 +50,40 @@ export interface IPurchaseDetail {
|
||||
items: IPurchaseDetailRow[];
|
||||
}
|
||||
|
||||
/** 单元格下钻明细行(溯源订货单明细,附订单号/状态/可编辑标记) */
|
||||
export interface IPurchaseCellItem {
|
||||
id: number;
|
||||
order_id: number;
|
||||
order_no: string;
|
||||
order_date: string;
|
||||
/** 订货单状态:0待接单 1已接单 2采购中 3配送中 4已完成 9已取消 */
|
||||
order_status: number;
|
||||
product_name: string;
|
||||
product_spec: string;
|
||||
unit: string;
|
||||
supplier_id: number;
|
||||
supplier?: { id: number; name: string } | null;
|
||||
/** 等级单价 */
|
||||
price: string;
|
||||
cost_price: string;
|
||||
quantity: number;
|
||||
weight: string;
|
||||
/** 金额 = 数量×单价(整数金额返回 number) */
|
||||
amount: number | string;
|
||||
remark: string;
|
||||
/** 首图 */
|
||||
image?: string;
|
||||
/** 是否可编辑/同步(采购单进行中且订货单未锁定) */
|
||||
editable: boolean;
|
||||
}
|
||||
|
||||
/** 单元格下钻数据(门店 + 商品 + 全部订货明细) */
|
||||
export interface IPurchaseCell {
|
||||
store: { id: number; name: string } | null;
|
||||
product: { id: number; name: string } | null;
|
||||
items: IPurchaseCellItem[];
|
||||
}
|
||||
|
||||
export const PURCHASE_STATUS_MAP: Record<number, { text: string; color: string }> = {
|
||||
0: { text: '进行中', color: 'processing' },
|
||||
3: { text: '已完成', color: 'success' },
|
||||
|
||||
+10
-179
@@ -6,13 +6,11 @@ import {
|
||||
Drawer,
|
||||
Empty,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Radio,
|
||||
Select,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
@@ -26,33 +24,25 @@ import type {
|
||||
XinTableProps,
|
||||
} from '@/components/XinTable/typings.ts';
|
||||
import type IStoreOrder from '@/domain/iStoreOrder.ts';
|
||||
import type { IStoreOrderItem, IStoreOrderItemUpdate } from '@/domain/iStoreOrder.ts';
|
||||
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
|
||||
import {
|
||||
getStoreOrder,
|
||||
updateOrderContainer,
|
||||
updateOrderStatus,
|
||||
batchUpdateOrderStatus,
|
||||
updateOrderItem,
|
||||
syncOrderItem,
|
||||
deleteStoreOrder,
|
||||
} from '@/api/order/store.ts';
|
||||
import { generatePurchase } from '@/api/purchase/order.ts';
|
||||
import { getStoreOptions } from '@/api/customer/store.ts';
|
||||
import { getSupplierOptions } from '@/api/customer/supplier.ts';
|
||||
import type IStore from '@/domain/iStore.ts';
|
||||
import type ISupplier from '@/domain/iSupplier.ts';
|
||||
import AuthButton from '@/components/AuthButton';
|
||||
import { DeleteOutlined, EditOutlined, SettingOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons';
|
||||
import TextArea from "antd/es/input/TextArea";
|
||||
import { DeleteOutlined, SettingOutlined, UnorderedListOutlined } from '@ant-design/icons';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
/** 允许修改周转框/托盘数量的订单状态:已接单、采购中、配送中 */
|
||||
const CONTAINER_EDITABLE_STATUS = [1, 2, 3];
|
||||
|
||||
/** 允许修改/同步商品明细的订单状态:待接单、已接单、采购中、配送中(已完成/已取消锁定) */
|
||||
const ITEM_EDITABLE_STATUS = [0, 1, 2, 3];
|
||||
|
||||
/**
|
||||
* 状态流转合法路径:待接单→已接单/已取消;采购中→配送中/已完成;配送中→已完成
|
||||
@@ -99,18 +89,9 @@ const StoreOrderPage: React.FC = () => {
|
||||
const [generateLoading, setGenerateLoading] = useState(false);
|
||||
const [generateForm] = Form.useForm<{ purchase_date: dayjs.Dayjs }>();
|
||||
|
||||
/** 供应商选项(明细编辑用) */
|
||||
const [suppliers, setSuppliers] = useState<ISupplier[]>([]);
|
||||
|
||||
/** 商品明细编辑(弹窗表单,保存后后端重算订单总价) */
|
||||
const [itemEditOpen, setItemEditOpen] = useState(false);
|
||||
const [itemEditTarget, setItemEditTarget] = useState<IStoreOrderItem | null>(null);
|
||||
const [itemSaving, setItemSaving] = useState(false);
|
||||
const [itemForm] = Form.useForm<IStoreOrderItemUpdate>();
|
||||
|
||||
useEffect(() => {
|
||||
getStoreOptions().then((res) => setStores(res.data.data ?? []));
|
||||
getSupplierOptions().then((res) => setSuppliers(res.data.data ?? []));
|
||||
}, []);
|
||||
|
||||
const openDetail = async (id: number) => {
|
||||
@@ -184,47 +165,6 @@ const StoreOrderPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
/** 打开明细编辑弹窗(商品快照 + 订货量/重量) */
|
||||
const openItemEdit = (item: IStoreOrderItem) => {
|
||||
setItemEditTarget(item);
|
||||
itemForm.setFieldsValue({
|
||||
supplier_id: item.supplier_id ?? 0,
|
||||
product_name: item.product_name ?? '',
|
||||
product_spec: item.product_spec ?? '',
|
||||
unit: item.unit ?? '',
|
||||
price: Number(item.price ?? 0),
|
||||
cost_price: Number(item.cost_price ?? 0),
|
||||
quantity: Number(item.quantity ?? 0),
|
||||
weight: Number(item.weight ?? 0),
|
||||
remark: item.remark ?? '',
|
||||
});
|
||||
setItemEditOpen(true);
|
||||
};
|
||||
|
||||
/** 保存明细修改:后端重算单品金额与订单总价 */
|
||||
const handleItemSave = async (values: IStoreOrderItemUpdate) => {
|
||||
if (!itemEditTarget?.id || !detail?.id) return;
|
||||
setItemSaving(true);
|
||||
try {
|
||||
await updateOrderItem(itemEditTarget.id, values);
|
||||
message.success('明细已更新,订单总价已重算');
|
||||
setItemEditOpen(false);
|
||||
await openDetail(detail.id);
|
||||
await tableRef.current?.reload();
|
||||
} finally {
|
||||
setItemSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
/** 一键同步:按商品ID拉取最新商品档案(供应商/品名/规格/单位/单价/成本价) */
|
||||
const handleItemSync = async (item: IStoreOrderItem) => {
|
||||
if (!item.id || !detail?.id) return;
|
||||
await syncOrderItem(item.id);
|
||||
message.success('已同步最新商品信息,订单总价已重算');
|
||||
await openDetail(detail.id);
|
||||
await tableRef.current?.reload();
|
||||
};
|
||||
|
||||
/** 删除订单(软删除,仅已取消订单可删;删除后后台/小程序端均不可见) */
|
||||
const handleDelete = async (id: number) => {
|
||||
await deleteStoreOrder(id);
|
||||
@@ -240,9 +180,6 @@ const StoreOrderPage: React.FC = () => {
|
||||
+ watchTrayNum * Number(containerOrder?.tray_price ?? 0);
|
||||
const previewTotal = Number(containerOrder?.product_amount ?? 0) + previewAdded;
|
||||
|
||||
/** 明细行编辑/同步按钮(仅未完成、未取消订单可见) */
|
||||
const itemEditable = ITEM_EDITABLE_STATUS.includes(detail?.status ?? -1);
|
||||
|
||||
const columns: XinTableColumn<IStoreOrder>[] = [
|
||||
{
|
||||
title: '订单号',
|
||||
@@ -622,8 +559,8 @@ const StoreOrderPage: React.FC = () => {
|
||||
<div className="w-30 shrink-0 text-center">供应商</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>
|
||||
{itemEditable ? <div className="w-40 shrink-0 text-center">操作</div> : null}
|
||||
<div className="w-33 shrink-0 text-center">订货金额</div>
|
||||
<div className="w-33 shrink-0 text-center">重量</div>
|
||||
</div>
|
||||
{(detail.items ?? []).map((item) => (
|
||||
<div key={item.id} className="flex items-center border-t border-gray-100 px-4 py-3">
|
||||
@@ -645,9 +582,8 @@ const StoreOrderPage: React.FC = () => {
|
||||
<div className="ml-3 min-w-0">
|
||||
<div className="text-sm font-medium">{item.product_name}</div>
|
||||
<div className="mt-0.5 text-xs text-gray-500">
|
||||
{item.product_spec || '-'} / {item.unit || '-'} · 成本价 ¥
|
||||
{item.cost_price ?? '0.00'}
|
||||
{Number(item.weight ?? 0) > 0 ? ` · 重量:${item.weight}` : ''}
|
||||
{item.product_spec || '-'} {item.unit || '-'}
|
||||
<div>成本:¥{item.cost_price ?? '0.00'}</div>
|
||||
</div>
|
||||
{item.remark ? (
|
||||
<div className="mt-0.5 truncate text-xs text-gray-500">备注:{item.remark}</div>
|
||||
@@ -660,42 +596,18 @@ const StoreOrderPage: React.FC = () => {
|
||||
<div className="w-33 shrink-0 text-center">
|
||||
<Text strong>¥{item.amount}</Text>
|
||||
</div>
|
||||
{itemEditable ? (
|
||||
<div className="w-40 shrink-0 text-center">
|
||||
<Space size={0}>
|
||||
<AuthButton auth="order.store.update">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => openItemEdit(item)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
</AuthButton>
|
||||
<AuthButton auth="order.store.update">
|
||||
<Popconfirm
|
||||
title="确认同步最新商品信息?"
|
||||
description="将按商品ID同步供应商、品名、规格、单位、成本价,并按门店等级价重算单价"
|
||||
onConfirm={() => handleItemSync(item)}
|
||||
>
|
||||
<Button type="link" size="small" icon={<SyncOutlined />}>
|
||||
同步
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</AuthButton>
|
||||
</Space>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="w-33 shrink-0 text-center">
|
||||
{item.weight ?? '-'} 斤
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{(detail.items ?? []).length === 0 ? (
|
||||
{(detail.items ?? []).length === 0 && (
|
||||
<Empty
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
description="暂无商品明细"
|
||||
className="py-8!"
|
||||
/>
|
||||
) : null}
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 附加信息 */}
|
||||
@@ -828,87 +740,6 @@ const StoreOrderPage: React.FC = () => {
|
||||
</div>
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
{/* 编辑商品明细(保存后后端重算单品金额与订单总价) */}
|
||||
<Modal
|
||||
title="编辑商品明细"
|
||||
open={itemEditOpen}
|
||||
onCancel={() => setItemEditOpen(false)}
|
||||
onOk={() => itemForm.submit()}
|
||||
confirmLoading={itemSaving}
|
||||
okText="保存"
|
||||
width={640}
|
||||
destroyOnHidden
|
||||
>
|
||||
<div className="py-2 text-gray-500">
|
||||
订单 {detail?.order_no};修改保存后,系统将自动重算单品金额与订单总价。
|
||||
</div>
|
||||
<Form form={itemForm} layout="vertical" onFinish={handleItemSave}>
|
||||
<div className="grid grid-cols-2 gap-x-4">
|
||||
<Form.Item
|
||||
label="供应商"
|
||||
name="supplier_id"
|
||||
rules={[{ required: true, message: '请选择供应商' }]}
|
||||
>
|
||||
<Select
|
||||
showSearch={{
|
||||
optionFilterProp: 'label'
|
||||
}}
|
||||
placeholder="请选择供应商"
|
||||
options={suppliers.map((s) => ({ label: s.name, value: s.id }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="品名"
|
||||
name="product_name"
|
||||
rules={[{ required: true, message: '请输入品名' }]}
|
||||
>
|
||||
<Input placeholder="请输入品名" maxLength={100} />
|
||||
</Form.Item>
|
||||
<Form.Item label="规格/包规" name="product_spec">
|
||||
<Input placeholder="请输入规格/包规" maxLength={100} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="计价单位"
|
||||
name="unit"
|
||||
rules={[{ required: true, message: '请输入计价单位' }]}
|
||||
>
|
||||
<Input placeholder="斤/件/箱等" maxLength={20} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="单价(元)"
|
||||
name="price"
|
||||
rules={[{ required: true, message: '请输入单价' }]}
|
||||
>
|
||||
<InputNumber className="w-full" min={0} precision={2} placeholder="请输入单价" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="成本价(元)"
|
||||
name="cost_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={1} 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.Item label="备注" name="remark">
|
||||
<TextArea className="w-full" placeholder="请输入备注" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+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