837 lines
30 KiB
TypeScript
837 lines
30 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react';
|
|
import {
|
|
Button,
|
|
Descriptions,
|
|
Drawer,
|
|
Empty,
|
|
Form,
|
|
Image,
|
|
Input,
|
|
InputNumber,
|
|
message,
|
|
Modal,
|
|
Popconfirm,
|
|
Select,
|
|
Space,
|
|
Spin,
|
|
Table,
|
|
Tabs,
|
|
Tag,
|
|
Typography,
|
|
} from 'antd';
|
|
import { CheckOutlined, EditOutlined } from '@ant-design/icons';
|
|
import type { TableProps } from 'antd';
|
|
import XinTable from '@/components/XinTable';
|
|
import type {
|
|
XinTableColumn,
|
|
XinTableInstance,
|
|
XinTableProps,
|
|
} from '@/components/XinTable/typings.ts';
|
|
import type IPurchaseOrder from '@/domain/iPurchaseOrder.ts';
|
|
import type {
|
|
IPurchaseCell,
|
|
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,
|
|
getPurchaseStoreSummary,
|
|
type PurchaseCellUpdateParams, type PurchaseRowUpdateParams,
|
|
updatePurchaseCellItem,
|
|
updatePurchaseRow,
|
|
} from '@/api/purchase/order.ts';
|
|
import { Update } from '@/api/common/table.ts';
|
|
import { getSupplierOptions } from '@/api/customer/supplier.ts';
|
|
import type ISupplier from '@/domain/iSupplier.ts';
|
|
import AuthButton from '@/components/AuthButton';
|
|
|
|
const { Title, Text } = Typography;
|
|
|
|
/** 每单位参考价 = 整单价(成本/售价) ÷ 包规数值(包规解析不出正数时按 1 处理,与后端同口径;仅展示参考,不参与金额计算) */
|
|
const calcUnitRefPrice = (total: number, spec: string): number => {
|
|
const pack = parseFloat(spec);
|
|
return Number.isFinite(pack) && pack > 0 ? total / pack : total;
|
|
};
|
|
|
|
/**
|
|
* 采购单管理(C1 在门店订单页合并生成 / C2-C3 导出 / C4 明细矩阵修改)
|
|
* 采购单无独立明细表,明细直接溯源门店订货明细:商品行 × 门店列
|
|
*/
|
|
const PurchaseOrderPage: React.FC = () => {
|
|
const tableRef = useRef<XinTableInstance<IPurchaseOrder>>(null);
|
|
|
|
// 详情抽屉
|
|
const [detailOpen, setDetailOpen] = useState(false);
|
|
const [detail, setDetail] = useState<IPurchaseDetail | null>(null);
|
|
const [detailLoading, setDetailLoading] = useState(false);
|
|
const [completing, setCompleting] = useState(false);
|
|
|
|
// 行修改弹窗
|
|
const [editingRow, setEditingRow] = useState<IPurchaseDetailRow | null>(null);
|
|
const [rowSaving, setRowSaving] = useState(false);
|
|
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>();
|
|
|
|
// 门店购买详情(按商品聚合的门店采购汇总)
|
|
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 ?? []));
|
|
}, []);
|
|
|
|
// 单元格下钻弹窗打开时加载明细
|
|
useEffect(() => {
|
|
if (cellOpen && cellQuery) {
|
|
loadCell();
|
|
}
|
|
}, [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 {
|
|
const res = await getPurchaseDetail(id);
|
|
setDetail(res.data.data ?? null);
|
|
} finally {
|
|
setDetailLoading(false);
|
|
}
|
|
};
|
|
|
|
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 });
|
|
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({
|
|
product_name: row.product_name,
|
|
supplier_id: row.supplier_id > 0 ? row.supplier_id : undefined,
|
|
product_spec: row.product_spec,
|
|
unit: row.unit,
|
|
cost_price: Number(row.cost_price),
|
|
});
|
|
};
|
|
|
|
/** 提交行修改:同步该商品全部订货明细;syncTarget=product 时追加同步商品档案 */
|
|
const handleEditSave = async (values: PurchaseRowUpdateParams) => {
|
|
if (!detail || !editingRow) {
|
|
return;
|
|
}
|
|
setRowSaving(true);
|
|
try {
|
|
const res = await updatePurchaseRow(detail.purchase.id!, editingRow.product_id, {
|
|
...values,
|
|
supplier_id: values.supplier_id ?? 0,
|
|
});
|
|
message.success(`已更新 ${res.data.data?.count ?? 0} 条订货明细`);
|
|
setEditingRow(null);
|
|
await loadDetail(detail.purchase.id!);
|
|
await tableRef.current?.reload();
|
|
} finally {
|
|
setRowSaving(false);
|
|
}
|
|
};
|
|
|
|
const handleComplete = async () => {
|
|
if (!detail) {
|
|
return;
|
|
}
|
|
setCompleting(true);
|
|
try {
|
|
await Update(`/purchase/order/${detail.purchase.id}`, { status: 3 });
|
|
message.success('采购单已标记完成');
|
|
await loadDetail(detail.purchase.id!);
|
|
await tableRef.current?.reload();
|
|
} finally {
|
|
setCompleting(false);
|
|
}
|
|
};
|
|
|
|
/** 明细矩阵列:品名/供应商/单价/包规/单位/成本 + 每门店一列(数量)+ 合计 + 操作 */
|
|
const buildItemColumns = (): TableProps<IPurchaseDetailRow>['columns'] => {
|
|
const fixed: NonNullable<TableProps<IPurchaseDetailRow>['columns']> = [
|
|
{ title: '品名', dataIndex: 'product_name', width: 120, align: 'center' },
|
|
{
|
|
title: '供应商',
|
|
dataIndex: 'supplier',
|
|
width: 110,
|
|
align: 'center',
|
|
render: (_, row) => row.supplier?.name ?? '-',
|
|
},
|
|
{
|
|
title: '参考成本单价',
|
|
key: 'unit_cost',
|
|
width: 100,
|
|
align: 'center',
|
|
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 || '-' },
|
|
{ title: '成本', dataIndex: 'cost_price', width: 90, align: 'center', render: (v) => `¥${Number(v).toFixed(2)}` },
|
|
];
|
|
|
|
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>
|
|
);
|
|
},
|
|
}));
|
|
|
|
const tail: NonNullable<TableProps<IPurchaseDetailRow>['columns']> = [
|
|
{
|
|
title: '合计数量',
|
|
key: 'total_quantity',
|
|
width: 90,
|
|
align: 'center',
|
|
render: (_, row) => <Text strong>{row.quantity}</Text>,
|
|
},
|
|
{
|
|
title: '操作',
|
|
key: 'action',
|
|
width: 90,
|
|
fixed: 'right',
|
|
align: 'center',
|
|
render: (_, row) => (
|
|
<AuthButton auth="purchase.order.update">
|
|
<Button
|
|
size="small"
|
|
type="link"
|
|
icon={<EditOutlined />}
|
|
onClick={() => openEdit(row)}
|
|
>
|
|
修改
|
|
</Button>
|
|
</AuthButton>
|
|
),
|
|
},
|
|
];
|
|
|
|
return [...fixed, ...storeColumns, ...tail];
|
|
};
|
|
|
|
/** 底部统计行:按门店统计金额(Σ 门店数量 × 行单价)+ 合计 */
|
|
const renderSummary = () => {
|
|
const stores = detail?.stores ?? [];
|
|
const items = detail?.items ?? [];
|
|
const storeTotals = stores.map((store) =>
|
|
items.reduce(
|
|
(sum, row) => sum + (row.cells[store.id] ?? 0) * Number(row.cost_price),
|
|
0,
|
|
),
|
|
);
|
|
const totalQuantity = items.reduce((sum, row) => sum + Number(row.quantity), 0);
|
|
const totalAmount = storeTotals.reduce((sum, amount) => sum + amount, 0);
|
|
|
|
return (
|
|
<Table.Summary.Row>
|
|
<Table.Summary.Cell index={0} colSpan={6} align="center">
|
|
<Text strong>成本统计(按门店)</Text>
|
|
</Table.Summary.Cell>
|
|
{storeTotals.map((amount, index) => (
|
|
<Table.Summary.Cell key={stores[index].id} index={6 + index} align="center">
|
|
<Text strong>¥{amount.toFixed(2)}</Text>
|
|
</Table.Summary.Cell>
|
|
))}
|
|
<Table.Summary.Cell index={6 + stores.length} align="center">
|
|
<Text strong>{totalQuantity}</Text>
|
|
</Table.Summary.Cell>
|
|
<Table.Summary.Cell index={7 + stores.length} align="center">
|
|
<Text strong>¥{totalAmount.toFixed(2)}</Text>
|
|
</Table.Summary.Cell>
|
|
</Table.Summary.Row>
|
|
);
|
|
};
|
|
|
|
/** 门店购买详情列:商品/购买参考零售价(单价÷包规)/包规/单位/单价/数量/预计金额 */
|
|
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: '采购单号',
|
|
dataIndex: 'purchase_no',
|
|
valueType: 'text',
|
|
hideInForm: true,
|
|
render: (_, record) => <Text copyable={{ text: record.purchase_no }}>{record.purchase_no}</Text>,
|
|
},
|
|
{
|
|
title: '采购日期',
|
|
dataIndex: 'purchase_date',
|
|
valueType: 'dateRange',
|
|
hideInForm: true,
|
|
align: 'center',
|
|
render: (_, record) => record.purchase_date,
|
|
},
|
|
{
|
|
title: '预估成本',
|
|
dataIndex: 'estimate_amount',
|
|
hideInForm: true,
|
|
hideInSearch: true,
|
|
align: 'right',
|
|
render: (_, record) => `¥${record.estimate_amount}`,
|
|
},
|
|
{
|
|
title: '实际成本',
|
|
dataIndex: 'actual_amount',
|
|
hideInForm: true,
|
|
hideInSearch: true,
|
|
align: 'right',
|
|
render: (_, record) =>
|
|
Number(record.actual_amount) > 0 ? (
|
|
<Text strong>¥{record.actual_amount}</Text>
|
|
) : (
|
|
<Text type="secondary">未录入</Text>
|
|
),
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
valueType: 'select',
|
|
hideInForm: true,
|
|
fieldProps: {
|
|
options: Object.entries(PURCHASE_STATUS_MAP).map(([value, item]) => ({
|
|
value: Number(value),
|
|
label: item.text,
|
|
})),
|
|
},
|
|
render: (_, record) => {
|
|
const item = PURCHASE_STATUS_MAP[record.status ?? 0];
|
|
return <Tag color={item?.color}>{item?.text}</Tag>;
|
|
},
|
|
align: 'center',
|
|
},
|
|
{
|
|
title: '制单人',
|
|
dataIndex: 'operator',
|
|
hideInForm: true,
|
|
hideInSearch: true,
|
|
render: (_, record) => record.operator?.nickname ?? '-',
|
|
align: 'center',
|
|
},
|
|
];
|
|
|
|
const operateRender: XinTableProps<IPurchaseOrder>['operateRender'] = (record) => [
|
|
<Button key="detail" size="small" onClick={() => openDetail(record.id!)}>
|
|
详情
|
|
</Button>
|
|
];
|
|
|
|
const tableProps: XinTableProps<IPurchaseOrder> = {
|
|
api: '/purchase/order',
|
|
columns,
|
|
rowKey: 'id',
|
|
accessName: 'purchase.order',
|
|
tableRef,
|
|
operateRender,
|
|
formProps: false,
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="mb-5">
|
|
<Title level={3}>采购单</Title>
|
|
<Text type="secondary">
|
|
在「门店订单」页勾选已接单订单合并生成采购单;采购单支持修改订单信息。
|
|
</Text>
|
|
</div>
|
|
<XinTable<IPurchaseOrder> {...tableProps} />
|
|
|
|
{/* 采购单详情:商品行 × 门店列矩阵 */}
|
|
<Drawer
|
|
title={detail ? `采购单 ${detail.purchase.purchase_no}` : '采购单详情'}
|
|
open={detailOpen}
|
|
onClose={() => setDetailOpen(false)}
|
|
size={1200}
|
|
loading={detailLoading}
|
|
extra={detail?.purchase.status === 0 && (
|
|
<AuthButton auth="purchase.order.update">
|
|
<Popconfirm title="确认标记该采购单为已完成?" onConfirm={handleComplete}>
|
|
<Button size="small" icon={<CheckOutlined />} loading={completing}>
|
|
标记完成
|
|
</Button>
|
|
</Popconfirm>
|
|
</AuthButton>
|
|
)}
|
|
>
|
|
{detail && (
|
|
<>
|
|
<Title level={5} className="mt-5! mb-3!">
|
|
采购单信息
|
|
</Title>
|
|
<Descriptions column={3} size="small" bordered>
|
|
<Descriptions.Item label="采购日期">{detail.purchase.purchase_date}</Descriptions.Item>
|
|
<Descriptions.Item label="状态">
|
|
<Tag color={PURCHASE_STATUS_MAP[detail.purchase.status ?? 0]?.color}>
|
|
{PURCHASE_STATUS_MAP[detail.purchase.status ?? 0]?.text}
|
|
</Tag>
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="制单人">
|
|
{detail.purchase.operator?.nickname ?? '-'}
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="预估金额">¥{detail.purchase.estimate_amount}</Descriptions.Item>
|
|
<Descriptions.Item label="实际金额">¥{detail.purchase.actual_amount}</Descriptions.Item>
|
|
<Descriptions.Item label="总重量">{detail.purchase.total_weight}</Descriptions.Item>
|
|
</Descriptions>
|
|
|
|
<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>
|
|
</>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
</>
|
|
)}
|
|
</Drawer>
|
|
|
|
{/* 行修改:品名/供应商/包规/单位/成本 */}
|
|
<Modal
|
|
title={editingRow ? `修改「${editingRow.product_name}」` : '修改明细行'}
|
|
open={editingRow !== null}
|
|
onCancel={() => setEditingRow(null)}
|
|
destroyOnHidden
|
|
footer={[
|
|
<Button key="cancel" onClick={() => setEditingRow(null)}>
|
|
取消
|
|
</Button>,
|
|
<Button key="purchase" type="primary" loading={rowSaving} onClick={() => editForm.submit()}>
|
|
保存
|
|
</Button>
|
|
]}
|
|
>
|
|
<div className="py-2 text-gray-500">
|
|
「保存」仅修改本采购单中该商品的所有订单项
|
|
</div>
|
|
<Form form={editForm} layout="vertical" onFinish={handleEditSave}>
|
|
<Form.Item
|
|
label="品名"
|
|
name="product_name"
|
|
rules={[{ required: true, message: '请输入品名' }, { max: 100 }]}
|
|
>
|
|
<Input maxLength={100} />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="供应商"
|
|
name="supplier_id"
|
|
rules={[{ required: true, message: '请选择供应商' }]}
|
|
>
|
|
<Select
|
|
allowClear
|
|
showSearch={{ optionFilterProp: 'label' }}
|
|
placeholder="选择供应商"
|
|
options={suppliers.map((s) => ({ value: s.id, label: s.name }))}
|
|
/>
|
|
</Form.Item>
|
|
<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={[{ required: true, message: '请输入单位' },{ max: 20 }]}
|
|
>
|
|
<Input maxLength={20} placeholder="如:斤" />
|
|
</Form.Item>
|
|
<Form.Item
|
|
label="成本"
|
|
name="cost_price"
|
|
rules={[{ required: true, message: '请输入成本' }]}
|
|
>
|
|
<InputNumber min={0} precision={2} prefix="¥" className="w-full" />
|
|
</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>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default PurchaseOrderPage;
|