采购单修改
This commit is contained in:
@@ -48,7 +48,7 @@ class ProductController extends BaseMiniController
|
||||
}
|
||||
|
||||
$pageSize = (int) $request->input('pageSize', 10);
|
||||
$paginator = $query->orderBy('sort')
|
||||
$paginator = $query->orderBy('sort', 'desc')
|
||||
->orderBy('id')
|
||||
->paginate($pageSize);
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class ProductController extends BaseController
|
||||
$params,
|
||||
ProductModel::query()->with(['category:id,name', 'supplier:id,name'])
|
||||
)
|
||||
->orderBy('sort')
|
||||
->orderBy('sort', 'desc')
|
||||
->orderBy('id', 'desc')
|
||||
->paginate($pageSize);
|
||||
$data->getCollection()->makeVisible('cost_price');
|
||||
|
||||
+54
-284
@@ -5,7 +5,6 @@ import {
|
||||
Drawer,
|
||||
Empty,
|
||||
Form,
|
||||
Image,
|
||||
Input,
|
||||
InputNumber,
|
||||
message,
|
||||
@@ -32,8 +31,6 @@ import type IPurchaseOrder from '@/domain/iPurchaseOrder.ts';
|
||||
import type {
|
||||
IBill,
|
||||
IBillPrepare,
|
||||
IPurchaseCell,
|
||||
IPurchaseCellItem,
|
||||
IPurchaseDetail,
|
||||
IPurchaseDetailRow,
|
||||
IPurchaseStoreItem,
|
||||
@@ -43,7 +40,6 @@ import type {
|
||||
PurchaseStoreItemUpdateParams,
|
||||
} from '@/domain/iPurchaseOrder.ts';
|
||||
import { PURCHASE_STATUS_MAP, BILL_STATUS_MAP } from '@/domain/iPurchaseOrder.ts';
|
||||
import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts';
|
||||
import {
|
||||
addPurchaseStoreItem,
|
||||
exportPurchase,
|
||||
@@ -51,14 +47,11 @@ import {
|
||||
exportPurchaseSuppliers,
|
||||
generateBill,
|
||||
getBillPrepare,
|
||||
getPurchaseCell,
|
||||
getPurchaseDetail,
|
||||
getPurchaseStoreSummary,
|
||||
removePurchaseStoreItem,
|
||||
type BillGenerateStoreParams,
|
||||
type PurchaseCellUpdateParams,
|
||||
type PurchaseRowUpdateParams,
|
||||
updatePurchaseCellItem,
|
||||
updatePurchaseRow,
|
||||
updatePurchaseStoreItem,
|
||||
} from '@/api/purchase/order.ts';
|
||||
@@ -105,22 +98,9 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [completing, setCompleting] = useState(false);
|
||||
|
||||
// 行内编辑:正在编辑的单元格(点击 品名/供应商/包规/单位/成本 进入编辑态)
|
||||
const [editingCell, setEditingCell] = useState<{ productId: number; dataIndex: InlineEditField } | null>(null);
|
||||
// 行内编辑:供应商选项(品名/供应商/包规/单位/成本与各门店订货量直接在表格中编辑)
|
||||
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>();
|
||||
|
||||
// 门店购买详情(按商品聚合的门店采购汇总)
|
||||
const [detailTab, setDetailTab] = useState('items');
|
||||
const [storeId, setStoreId] = useState<number>(0);
|
||||
@@ -243,13 +223,6 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
getSupplierOptions().then((res) => setSuppliers(res.data.data ?? []));
|
||||
}, []);
|
||||
|
||||
// 单元格下钻弹窗打开时加载明细
|
||||
useEffect(() => {
|
||||
if (cellOpen && cellQuery) {
|
||||
loadCell();
|
||||
}
|
||||
}, [cellOpen, cellQuery]);
|
||||
|
||||
// 详情加载后默认选中第一个门店(当前选中门店仍在采购单内则保留)
|
||||
useEffect(() => {
|
||||
if (detail && detail.stores.length > 0) {
|
||||
@@ -375,75 +348,17 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
await refreshAfterStoreItemChange();
|
||||
};
|
||||
|
||||
/** 打开单元格下钻:门店 + 商品 → 该采购单下全部订货明细 */
|
||||
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 canUpdateRow = detail?.purchase.status === 0 && auth('purchase.order.update');
|
||||
|
||||
/**
|
||||
* 行内编辑保存:合并该行当前值整行提交(后端要求全字段);未变更 / 校验失败时不请求。
|
||||
* Enter 或失焦触发保存,保存后关闭编辑态并刷新详情与列表。
|
||||
* Enter 或失焦触发保存,保存后刷新详情与列表。
|
||||
*/
|
||||
const handleInlineSave = async (row: IPurchaseDetailRow, dataIndex: InlineEditField, rawValue: string | number) => {
|
||||
if (!detail) {
|
||||
return;
|
||||
}
|
||||
setEditingCell(null);
|
||||
|
||||
const params: PurchaseRowUpdateParams = {
|
||||
product_name: row.product_name,
|
||||
@@ -492,7 +407,29 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
await tableRef.current?.reload();
|
||||
};
|
||||
|
||||
/** 可编辑单元格:点击进入编辑态(文本 Input / 供应商 Select / 成本 InputNumber),Esc 取消 */
|
||||
/**
|
||||
* 门店订货量保存:复用门店单品修改接口(仅提交订货量,单价/称重不改动),
|
||||
* 同商品多笔订单明细合并到最早一条并级联重算;未变更 / 校验失败时不请求。
|
||||
*/
|
||||
const handleStoreCellSave = async (row: IPurchaseDetailRow, storeId: number, rawValue: string) => {
|
||||
if (!detail) {
|
||||
return;
|
||||
}
|
||||
const quantity = Number(rawValue);
|
||||
if (rawValue.trim() === '' || !Number.isInteger(quantity) || quantity < 0) {
|
||||
message.warning('订货量无效,已取消修改');
|
||||
return;
|
||||
}
|
||||
if (quantity === row.cells[storeId]) {
|
||||
return;
|
||||
}
|
||||
await updatePurchaseStoreItem(detail.purchase.id!, storeId, row.product_id, { quantity });
|
||||
message.success('订货量已更新,订货单与采购单汇总已重算');
|
||||
await loadDetail(detail.purchase.id!);
|
||||
await tableRef.current?.reload();
|
||||
};
|
||||
|
||||
/** 行内编辑单元格:直接渲染编辑组件(文本 Input / 供应商 Select / 成本 InputNumber),Enter 或失焦保存 */
|
||||
const renderEditableCell = (
|
||||
row: IPurchaseDetailRow,
|
||||
dataIndex: InlineEditField,
|
||||
@@ -501,41 +438,17 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
if (!canUpdateRow) {
|
||||
return display;
|
||||
}
|
||||
const editing = editingCell?.productId === row.product_id && editingCell.dataIndex === dataIndex;
|
||||
|
||||
if (!editing) {
|
||||
return (
|
||||
<div
|
||||
className="-mx-1 cursor-pointer rounded px-1 transition-colors hover:bg-blue-50"
|
||||
title="点击修改"
|
||||
onClick={() => setEditingCell({ productId: row.product_id, dataIndex })}
|
||||
>
|
||||
{display}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const cancelOnEscape = {
|
||||
onKeyDown: (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
setEditingCell(null);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (dataIndex === 'supplier_id') {
|
||||
return (
|
||||
<Select
|
||||
size="small"
|
||||
autoFocus
|
||||
defaultOpen
|
||||
className="w-full"
|
||||
defaultValue={row.supplier_id > 0 ? row.supplier_id : undefined}
|
||||
value={row.supplier_id > 0 ? row.supplier_id : undefined}
|
||||
placeholder="选择供应商"
|
||||
showSearch={{ optionFilterProp: 'label' }}
|
||||
options={suppliers.map((s) => ({ value: s.id, label: s.name }))}
|
||||
onChange={(value) => void handleInlineSave(row, dataIndex, value)}
|
||||
onBlur={() => setEditingCell(null)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -543,8 +456,8 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
if (dataIndex === 'cost_price') {
|
||||
return (
|
||||
<InputNumber
|
||||
key={`${row.product_id}-cost-${row.cost_price}`}
|
||||
size="small"
|
||||
autoFocus
|
||||
min={0}
|
||||
precision={2}
|
||||
prefix="¥"
|
||||
@@ -552,20 +465,19 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
defaultValue={Number(row.cost_price)}
|
||||
onPressEnter={(e) => void handleInlineSave(row, dataIndex, (e.target as HTMLInputElement).value)}
|
||||
onBlur={(e) => void handleInlineSave(row, dataIndex, e.target.value)}
|
||||
{...cancelOnEscape}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
key={`${row.product_id}-${dataIndex}-${row[dataIndex]}`}
|
||||
size="small"
|
||||
autoFocus
|
||||
style={{ textAlign: 'center' }}
|
||||
defaultValue={row[dataIndex]}
|
||||
maxLength={dataIndex === 'unit' ? 20 : 100}
|
||||
onPressEnter={(e) => void handleInlineSave(row, dataIndex, (e.target as HTMLInputElement).value)}
|
||||
onBlur={(e) => void handleInlineSave(row, dataIndex, e.target.value)}
|
||||
{...cancelOnEscape}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -653,7 +565,7 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
title: '市场',
|
||||
fixed: 'left',
|
||||
dataIndex: 'market',
|
||||
width: 90,
|
||||
width: 110,
|
||||
align: 'center',
|
||||
filters: itemMarketOptions.map((m) => ({ text: m, value: m })),
|
||||
filteredValue: itemColumnFilters.market ?? null,
|
||||
@@ -663,7 +575,7 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
{
|
||||
title: '单价',
|
||||
key: 'retail_price',
|
||||
width: 100,
|
||||
width: 110,
|
||||
align: 'center',
|
||||
render: (_, row) => {
|
||||
// 加权平均售价 ÷ 包规数值(无订货数量时无参考价)
|
||||
@@ -677,27 +589,27 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
title: '包规',
|
||||
dataIndex: 'product_spec',
|
||||
align: 'center',
|
||||
width: 90,
|
||||
width: 110,
|
||||
render: (v, row) => renderEditableCell(row, 'product_spec', v || '-'),
|
||||
},
|
||||
{
|
||||
title: '单位',
|
||||
dataIndex: 'unit',
|
||||
width: 70,
|
||||
width: 110,
|
||||
align: 'center',
|
||||
render: (v, row) => renderEditableCell(row, 'unit', v || '-'),
|
||||
},
|
||||
{
|
||||
title: '成本',
|
||||
dataIndex: 'cost_price',
|
||||
width: 100,
|
||||
width: 110,
|
||||
align: 'center',
|
||||
render: (v, row) => renderEditableCell(row, 'cost_price', `¥${Number(v).toFixed(2)}`),
|
||||
},
|
||||
{
|
||||
title: '合计数量',
|
||||
key: 'total_quantity',
|
||||
width: 90,
|
||||
width: 110,
|
||||
align: 'center',
|
||||
render: (_, row) => <Text strong>{row.quantity}</Text>,
|
||||
},
|
||||
@@ -706,14 +618,27 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
const storeColumns = (detail?.stores ?? []).map((store) => ({
|
||||
title: <span className={'text-[red]'}>{store.name}</span>,
|
||||
key: `store-${store.id}`,
|
||||
width: 100,
|
||||
align: 'center' as const,
|
||||
render: (_: unknown, row: IPurchaseDetailRow) => {
|
||||
const quantity = row.cells[store.id];
|
||||
return quantity !== undefined ? (
|
||||
<Typography.Link onClick={() => openCell(row, store)}>{quantity}</Typography.Link>
|
||||
) : (
|
||||
<Text type="secondary">-</Text>
|
||||
if (quantity === undefined) {
|
||||
return <Text type="secondary">-</Text>;
|
||||
}
|
||||
if (!canUpdateRow) {
|
||||
return quantity;
|
||||
}
|
||||
// 订货量直接在单元格编辑:Enter 或失焦保存(单价/称重不改动)
|
||||
return (
|
||||
<InputNumber
|
||||
key={`${row.product_id}-${store.id}-${quantity}`}
|
||||
size="small"
|
||||
min={0}
|
||||
precision={0}
|
||||
className="w-full"
|
||||
defaultValue={quantity}
|
||||
onPressEnter={(e) => void handleStoreCellSave(row, store.id, (e.target as HTMLInputElement).value)}
|
||||
onBlur={(e) => void handleStoreCellSave(row, store.id, e.target.value)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}));
|
||||
@@ -1352,7 +1277,7 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
</AuthButton>
|
||||
{canUpdateRow && (
|
||||
<Text type="secondary" className="text-xs">
|
||||
点击 品名/供应商/包规/单位/成本 单元格可直接修改;保存将同步该商品全部订货明细与商品档案,成本变化时各门店单价按等级上浮自动重算
|
||||
修改保存将同步该商品全部订货明细与商品档案,成本变化时各门店单价按等级上浮自动重算
|
||||
</Text>
|
||||
)}
|
||||
</Space>
|
||||
@@ -1373,161 +1298,6 @@ const PurchaseOrderPage: React.FC = () => {
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
{/* 单元格下钻 */}
|
||||
<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">
|
||||
{detail?.purchase.status === 0 ? (
|
||||
<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>
|
||||
|
||||
{/* 生成账单:按门店填写配送费/周转筐/托盘数量与售后金额(商品金额只读,由订单汇总) */}
|
||||
<Modal
|
||||
title={billPrepare ? `生成账单 · ${billPrepare.purchase.purchase_no}` : '生成账单'}
|
||||
|
||||
Reference in New Issue
Block a user