1197 lines
44 KiB
TypeScript
1197 lines
44 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 {AccountBookOutlined, DownloadOutlined, EditOutlined, UnorderedListOutlined} 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 {
|
||
IBill,
|
||
IBillPrepare,
|
||
IPurchaseCell,
|
||
IPurchaseCellItem,
|
||
IPurchaseDetail,
|
||
IPurchaseDetailRow,
|
||
IPurchaseStoreItem,
|
||
IPurchaseStoreSummary,
|
||
} 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 {
|
||
exportPurchase,
|
||
generateBill,
|
||
getBillPrepare,
|
||
getPurchaseCell,
|
||
getPurchaseDetail,
|
||
getPurchaseStoreSummary,
|
||
type BillGenerateStoreParams,
|
||
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);
|
||
|
||
// 生成账单(已完成采购单按门店生成:填写配送费/周转筐/托盘数量,金额只读)
|
||
const [billOpen, setBillOpen] = useState(false);
|
||
const [billPrepare, setBillPrepare] = useState<IBillPrepare | null>(null);
|
||
const [billLoading, setBillLoading] = useState(false);
|
||
const [billSaving, setBillSaving] = useState(false);
|
||
const [billForm] = Form.useForm<{ stores: BillGenerateStoreParams[] }>();
|
||
|
||
// 弹窗内实时预览:附加金额 = 筐×筐单价 + 托盘×托盘单价;总额 = 商品 + 配送费 + 附加
|
||
const watchBillStores = Form.useWatch('stores', billForm) ?? [];
|
||
const billPreview = (billPrepare?.stores ?? []).map((row, index) => {
|
||
const input = watchBillStores[index] ?? {};
|
||
const deliveryFee = Number(input.delivery_fee ?? 0);
|
||
const added =
|
||
Number(input.box_num ?? 0) * Number(row.box_price) +
|
||
Number(input.tray_num ?? 0) * Number(row.tray_price);
|
||
return {
|
||
added,
|
||
total: Number(row.product_amount) + deliveryFee + added,
|
||
};
|
||
});
|
||
const billAllGenerated =
|
||
(billPrepare?.stores ?? []).length > 0 && billPrepare!.stores.every((row) => row.bill !== null);
|
||
|
||
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 openBillGenerate = async (id: number) => {
|
||
setBillOpen(true);
|
||
setBillLoading(true);
|
||
setBillPrepare(null);
|
||
try {
|
||
const res = await getBillPrepare(id);
|
||
const data = res.data.data ?? null;
|
||
setBillPrepare(data);
|
||
billForm.setFieldsValue({
|
||
stores: (data?.stores ?? []).map((row) => ({
|
||
store_id: row.store_id,
|
||
delivery_fee: row.bill ? Number(row.bill.delivery_fee) : 0,
|
||
box_num: row.bill ? row.bill.box_num : 0,
|
||
tray_num: row.bill ? row.bill.tray_num : 0,
|
||
})),
|
||
});
|
||
} finally {
|
||
setBillLoading(false);
|
||
}
|
||
};
|
||
|
||
/** 提交生成账单:按门店各生成一张并关联采购单全部订单,金额由系统汇总不可修改 */
|
||
const handleBillSave = async (values: { stores: BillGenerateStoreParams[] }) => {
|
||
if (!billPrepare) {
|
||
return;
|
||
}
|
||
setBillSaving(true);
|
||
try {
|
||
const res = await generateBill(billPrepare.purchase.id, values.stores);
|
||
message.success(`已生成 ${res.data.data?.count ?? 0} 张门店账单,采购单订单已关联`);
|
||
setBillOpen(false);
|
||
setBillPrepare(null);
|
||
await tableRef.current?.reload();
|
||
if (detail && detail.purchase.id === billPrepare.purchase.id) {
|
||
await loadDetail(detail.purchase.id!);
|
||
}
|
||
} finally {
|
||
setBillSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleComplete = async (id: number) => {
|
||
setCompleting(true);
|
||
try {
|
||
await Update(`/purchase/order/${id}/finish`, { status: 3 });
|
||
message.success('采购单已标记完成');
|
||
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) => (
|
||
<>
|
||
{ detail?.purchase.status === 0 ? (
|
||
<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 billColumns: TableProps<IBill>['columns'] = [
|
||
{ title: '门店', dataIndex: 'store_name', width: 140, align: 'center' },
|
||
{
|
||
title: '账单号',
|
||
dataIndex: 'bill_no',
|
||
width: 180,
|
||
align: 'center',
|
||
render: (v) => <Text copyable={{ text: v }}>{v}</Text>,
|
||
},
|
||
{ title: '账单日期', dataIndex: 'bill_date', width: 110, align: 'center' },
|
||
{
|
||
title: '商品金额',
|
||
dataIndex: 'product_amount',
|
||
width: 110,
|
||
align: 'center',
|
||
render: (v) => `¥${Number(v).toFixed(2)}`,
|
||
},
|
||
{
|
||
title: '配送费',
|
||
dataIndex: 'delivery_fee',
|
||
width: 100,
|
||
align: 'center',
|
||
render: (v) => `¥${Number(v).toFixed(2)}`,
|
||
},
|
||
{
|
||
title: '周转筐',
|
||
key: 'box',
|
||
width: 110,
|
||
align: 'center',
|
||
render: (_, row) => `${row.box_num} × ¥${Number(row.box_price).toFixed(2)}`,
|
||
},
|
||
{
|
||
title: '周转托盘',
|
||
key: 'tray',
|
||
width: 110,
|
||
align: 'center',
|
||
render: (_, row) => `${row.tray_num} × ¥${Number(row.tray_price).toFixed(2)}`,
|
||
},
|
||
{
|
||
title: '附加金额',
|
||
dataIndex: 'added_amount',
|
||
width: 100,
|
||
align: 'center',
|
||
render: (v) => `¥${Number(v).toFixed(2)}`,
|
||
},
|
||
{
|
||
title: '账单总金额',
|
||
dataIndex: 'total_amount',
|
||
width: 120,
|
||
align: 'center',
|
||
render: (v) => <Text strong type="danger">¥{Number(v).toFixed(2)}</Text>,
|
||
},
|
||
{
|
||
title: '支付状态',
|
||
dataIndex: 'status',
|
||
width: 100,
|
||
align: 'center',
|
||
render: (v) => {
|
||
const item = BILL_STATUS_MAP[Number(v ?? 0)];
|
||
return <Tag color={item?.color}>{item?.text}</Tag>;
|
||
},
|
||
},
|
||
];
|
||
|
||
/** 门店账单合计行 */
|
||
const renderBillSummary = () => {
|
||
const bills = detail?.bills ?? [];
|
||
const totalProduct = bills.reduce((sum, row) => sum + Number(row.product_amount), 0);
|
||
const totalDelivery = bills.reduce((sum, row) => sum + Number(row.delivery_fee), 0);
|
||
const totalAdded = bills.reduce((sum, row) => sum + Number(row.added_amount), 0);
|
||
const totalAmount = bills.reduce((sum, row) => sum + Number(row.total_amount), 0);
|
||
return (
|
||
<Table.Summary.Row>
|
||
<Table.Summary.Cell index={0} colSpan={3} align="center">
|
||
<Text strong>合计</Text>
|
||
</Table.Summary.Cell>
|
||
<Table.Summary.Cell index={3} align="center">
|
||
<Text strong>¥{totalProduct.toFixed(2)}</Text>
|
||
</Table.Summary.Cell>
|
||
<Table.Summary.Cell index={4} align="center">
|
||
<Text strong>¥{totalDelivery.toFixed(2)}</Text>
|
||
</Table.Summary.Cell>
|
||
<Table.Summary.Cell index={5} colSpan={2} align="center">
|
||
<Text strong>-</Text>
|
||
</Table.Summary.Cell>
|
||
<Table.Summary.Cell index={7} align="center">
|
||
<Text strong>¥{totalAdded.toFixed(2)}</Text>
|
||
</Table.Summary.Cell>
|
||
<Table.Summary.Cell index={8} align="center">
|
||
<Text strong type="danger">¥{totalAmount.toFixed(2)}</Text>
|
||
</Table.Summary.Cell>
|
||
<Table.Summary.Cell index={9} align="center">
|
||
<Text strong>-</Text>
|
||
</Table.Summary.Cell>
|
||
</Table.Summary.Row>
|
||
);
|
||
};
|
||
|
||
const columns: XinTableColumn<IPurchaseOrder>[] = [
|
||
{
|
||
title: '采购单号',
|
||
dataIndex: 'purchase_no',
|
||
valueType: 'text',
|
||
hideInForm: true,
|
||
align: 'center',
|
||
width: 260,
|
||
render: (_, record) => <Text copyable={{ text: record.purchase_no }}>{record.purchase_no}</Text>,
|
||
},
|
||
{
|
||
title: '采购日期',
|
||
dataIndex: 'purchase_date',
|
||
valueType: 'date',
|
||
align: 'center',
|
||
hideInForm: true,
|
||
render: (_, record) => record.purchase_date,
|
||
},
|
||
{
|
||
title: '预估成本',
|
||
dataIndex: 'estimate_amount',
|
||
hideInForm: true,
|
||
hideInSearch: true,
|
||
align: 'center',
|
||
render: (_, record) => `¥${record.estimate_amount}`,
|
||
},
|
||
{
|
||
title: '实际成本',
|
||
dataIndex: 'actual_amount',
|
||
hideInSearch: true,
|
||
valueType: "digit",
|
||
fieldProps: {
|
||
min: 0,
|
||
precision: 3,
|
||
placeholder: "请输入单价"
|
||
},
|
||
align: 'center',
|
||
render: (_, record) =>
|
||
Number(record.actual_amount) > 0 ? (
|
||
<Text strong>¥{record.actual_amount}</Text>
|
||
) : (
|
||
<Text type="secondary">未录入</Text>
|
||
),
|
||
},
|
||
{
|
||
title: '总件数',
|
||
dataIndex: 'total_quantity',
|
||
hideInForm: true,
|
||
hideInSearch: true,
|
||
align: 'center',
|
||
},
|
||
{
|
||
title: '备注',
|
||
dataIndex: 'remark',
|
||
valueType: "textarea",
|
||
hideInTable: true,
|
||
hideInSearch: true
|
||
},
|
||
{
|
||
title: '总重量',
|
||
dataIndex: 'total_weight',
|
||
hideInForm: true,
|
||
hideInSearch: true,
|
||
align: 'center',
|
||
render: (_, record) => `${record.total_weight}斤`,
|
||
},
|
||
{
|
||
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, dom) => [
|
||
record.status === 0 ? dom.edit : null,
|
||
<Button key="detail" size="small" icon={<UnorderedListOutlined />} type={'primary'} onClick={() => openDetail(record.id!)} />,
|
||
record.status === 0 ? (
|
||
<AuthButton auth="purchase.order.update">
|
||
<Popconfirm title="确认该采购单已完成?" description="已完成采购单将锁定,不能再修改订单信息" onConfirm={() => handleComplete(record.id!)}>
|
||
<Button type={'primary'} size="small" variant={'solid'} color={'green'} loading={completing}>
|
||
完成采购
|
||
</Button>
|
||
</Popconfirm>
|
||
</AuthButton>
|
||
) : (
|
||
<AuthButton auth="purchase.order.bill">
|
||
<Button
|
||
type={'primary'}
|
||
size="small"
|
||
variant={'solid'}
|
||
color={'orange'}
|
||
icon={<AccountBookOutlined />}
|
||
onClick={() => openBillGenerate(record.id!)}
|
||
>
|
||
生成账单
|
||
</Button>
|
||
</AuthButton>
|
||
)
|
||
];
|
||
|
||
const tableProps: XinTableProps<IPurchaseOrder> = {
|
||
api: '/purchase/order',
|
||
columns,
|
||
rowKey: 'id',
|
||
accessName: 'purchase.order',
|
||
tableRef,
|
||
operateRender,
|
||
operateProps: {
|
||
width: 300
|
||
},
|
||
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}
|
||
>
|
||
{detail && (
|
||
<>
|
||
<Title level={5} className="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_quantity}</Descriptions.Item>
|
||
<Descriptions.Item label="总重量">{detail.purchase.total_weight}</Descriptions.Item>
|
||
<Descriptions.Item label="创建时间" span={2}>{detail.purchase.created_at}</Descriptions.Item>
|
||
<Descriptions.Item label="备注" span={3}>{detail.purchase.remark}</Descriptions.Item>
|
||
</Descriptions>
|
||
|
||
<Tabs
|
||
activeKey={detailTab}
|
||
onChange={setDetailTab}
|
||
className="mt-3!"
|
||
items={[
|
||
{ key: 'items', label: '商品明细' },
|
||
{ key: 'stores', label: '门店购买详情' },
|
||
{ key: 'bills', label: '门店账单' },
|
||
]}
|
||
/>
|
||
|
||
{ detailTab === 'stores' ? (
|
||
<>
|
||
<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>
|
||
</>
|
||
) : detailTab === 'bills' ? (
|
||
detail.bills.length > 0 ? (
|
||
<Table<IBill>
|
||
rowKey="id"
|
||
size="small"
|
||
bordered
|
||
columns={billColumns}
|
||
dataSource={detail.bills}
|
||
pagination={false}
|
||
summary={renderBillSummary}
|
||
/>
|
||
) : (
|
||
<Empty
|
||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||
description={detail.purchase.status === 0 ? '采购单完成后可在列表操作列生成账单' : '该采购单暂未生成账单'}
|
||
className="py-8!"
|
||
/>
|
||
)
|
||
) : (
|
||
<>
|
||
<Space style={{ marginBottom: 20 }}>
|
||
<AuthButton key="export" auth="purchase.order.export">
|
||
<Button
|
||
type="primary"
|
||
ghost
|
||
icon={<DownloadOutlined />}
|
||
onClick={() => detail && exportPurchase(detail.purchase.id!)}
|
||
>
|
||
导出
|
||
</Button>
|
||
</AuthButton>
|
||
</Space>
|
||
<Table<IPurchaseDetailRow>
|
||
rowKey="product_id"
|
||
size="small"
|
||
bordered
|
||
columns={buildItemColumns()}
|
||
dataSource={detail.items}
|
||
pagination={false}
|
||
scroll={{ x: 'max-content' }}
|
||
summary={renderSummary}
|
||
/>
|
||
</>
|
||
) }
|
||
</>
|
||
)}
|
||
</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">
|
||
{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}` : '生成账单'}
|
||
open={billOpen}
|
||
onCancel={() => {
|
||
setBillOpen(false);
|
||
setBillPrepare(null);
|
||
}}
|
||
onOk={() => billForm.submit()}
|
||
confirmLoading={billSaving}
|
||
okText="确认生成"
|
||
okButtonProps={{ disabled: billAllGenerated }}
|
||
width={960}
|
||
destroyOnHidden
|
||
>
|
||
<Spin spinning={billLoading}>
|
||
{billPrepare && (
|
||
<>
|
||
<div className="py-2 text-gray-500">
|
||
每个门店单独生成一张账单;商品金额由订单汇总不可修改,请填写各门店的配送费与周转筐/托盘数量。
|
||
{billAllGenerated ? '该采购单已全部生成账单,仅可查看。' : '生成后采购单中的全部订单将关联到对应门店账单。'}
|
||
</div>
|
||
<Form form={billForm} layout="vertical" onFinish={handleBillSave}>
|
||
<Form.List name="stores">
|
||
{(fields) => (
|
||
<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="w-40 shrink-0">门店</div>
|
||
<div className="w-28 shrink-0 text-center">商品金额</div>
|
||
<div className="w-32 shrink-0 text-center">配送费(元)</div>
|
||
<div className="w-32 shrink-0 text-center">周转筐(¥{billPrepare.stores[0]?.box_price ?? '0.00'})</div>
|
||
<div className="w-32 shrink-0 text-center">周转托盘(¥{billPrepare.stores[0]?.tray_price ?? '0.00'})</div>
|
||
<div className="w-28 shrink-0 text-center">附加金额</div>
|
||
<div className="flex-1 text-center">账单总金额</div>
|
||
</div>
|
||
{fields.map((field) => {
|
||
const row = billPrepare.stores[field.name];
|
||
const billed = row?.bill != null;
|
||
return (
|
||
<div key={field.key} className="flex items-center border-t border-gray-100 px-4 py-2">
|
||
<div className="w-40 shrink-0 pr-2 text-sm">
|
||
<div>{row?.store_name ?? '-'}</div>
|
||
<div className="text-xs text-gray-400">
|
||
{row?.order_count ?? 0} 笔订单
|
||
{billed && <Tag className="ml-1!" color="success">已生成</Tag>}
|
||
</div>
|
||
</div>
|
||
<div className="w-28 shrink-0 text-center">
|
||
<Text strong>¥{row?.product_amount ?? '0.00'}</Text>
|
||
</div>
|
||
<Form.Item name={[field.name, 'store_id']} hidden>
|
||
<Input />
|
||
</Form.Item>
|
||
<Form.Item
|
||
className="m-0! w-32 shrink-0 px-1!"
|
||
name={[field.name, 'delivery_fee']}
|
||
rules={[{ required: true, message: '请输入配送费' }]}
|
||
>
|
||
<InputNumber
|
||
className="w-full"
|
||
min={0}
|
||
precision={2}
|
||
disabled={billed}
|
||
placeholder="配送费"
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item
|
||
className="m-0! w-32 shrink-0 px-1!"
|
||
name={[field.name, 'box_num']}
|
||
rules={[{ required: true, message: '请输入周转筐数量' }]}
|
||
>
|
||
<InputNumber
|
||
className="w-full"
|
||
min={0}
|
||
precision={0}
|
||
disabled={billed}
|
||
placeholder="周转筐数量"
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item
|
||
className="m-0! w-32 shrink-0 px-1!"
|
||
name={[field.name, 'tray_num']}
|
||
rules={[{ required: true, message: '请输入周转托盘数量' }]}
|
||
>
|
||
<InputNumber
|
||
className="w-full"
|
||
min={0}
|
||
precision={0}
|
||
disabled={billed}
|
||
placeholder="周转托盘数量"
|
||
/>
|
||
</Form.Item>
|
||
<div className="w-28 shrink-0 text-center">
|
||
¥{(billed ? Number(row.bill!.added_amount) : billPreview[field.name]?.added ?? 0).toFixed(2)}
|
||
</div>
|
||
<div className="flex-1 text-center">
|
||
<Text strong type="danger">
|
||
¥{(billed ? Number(row.bill!.total_amount) : billPreview[field.name]?.total ?? 0).toFixed(2)}
|
||
</Text>
|
||
{billed && (
|
||
<div className="text-xs text-gray-400">{row.bill!.bill_no}</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</Form.List>
|
||
</Form>
|
||
</>
|
||
)}
|
||
</Spin>
|
||
</Modal>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default PurchaseOrderPage;
|