1835 lines
69 KiB
TypeScript
1835 lines
69 KiB
TypeScript
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||
import {
|
||
Button,
|
||
Descriptions,
|
||
Drawer,
|
||
Empty,
|
||
Form,
|
||
Image,
|
||
Input,
|
||
InputNumber,
|
||
message,
|
||
Modal,
|
||
Popconfirm,
|
||
Radio,
|
||
Select,
|
||
Space,
|
||
Spin,
|
||
Table,
|
||
Tabs,
|
||
Tag,
|
||
Typography,
|
||
} from 'antd';
|
||
import {AccountBookOutlined, DeleteOutlined, DownloadOutlined, EditOutlined, PlusOutlined, 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,
|
||
IPurchaseSupplierItem,
|
||
PurchaseStoreItemAddParams,
|
||
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,
|
||
exportPurchaseStores,
|
||
exportPurchaseSuppliers,
|
||
generateBill,
|
||
getBillPrepare,
|
||
getPurchaseCell,
|
||
getPurchaseDetail,
|
||
getPurchaseStoreSummary,
|
||
removePurchaseStoreItem,
|
||
type BillGenerateStoreParams,
|
||
type PurchaseCellUpdateParams,
|
||
type PurchaseRowUpdateParams,
|
||
updatePurchaseCellItem,
|
||
updatePurchaseRow,
|
||
updatePurchaseStoreItem,
|
||
} from '@/api/purchase/order.ts';
|
||
import { Update } from '@/api/common/table.ts';
|
||
import { getSupplierOptions } from '@/api/customer/supplier.ts';
|
||
import { getProductOptions } from '@/api/product/goods.ts';
|
||
import type ISupplier from '@/domain/iSupplier.ts';
|
||
import type IProduct from '@/domain/iProduct.ts';
|
||
import AuthButton from '@/components/AuthButton';
|
||
import useAuth from '@/hooks/useAuth.ts';
|
||
|
||
const { Title, Text } = Typography;
|
||
|
||
/** 商品明细可行内编辑的字段 */
|
||
type InlineEditField = 'product_name' | 'supplier_id' | 'product_spec' | 'unit' | 'cost_price';
|
||
|
||
/** 行内编辑字段中文名(校验提示用) */
|
||
const INLINE_FIELD_LABELS: Record<InlineEditField, string> = {
|
||
product_name: '品名',
|
||
supplier_id: '供应商',
|
||
product_spec: '包规',
|
||
unit: '单位',
|
||
cost_price: '成本',
|
||
};
|
||
|
||
/** 每单位参考价 = 整单价(成本/售价) ÷ 包规数值(包规解析不出正数时按 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 { auth } = useAuth();
|
||
|
||
// 详情抽屉
|
||
const [detailOpen, setDetailOpen] = useState(false);
|
||
const [detailSize, setDetailSize] = useState(1200);
|
||
const [detail, setDetail] = useState<IPurchaseDetail | null>(null);
|
||
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);
|
||
const [storeSummary, setStoreSummary] = useState<IPurchaseStoreSummary | null>(null);
|
||
const [storeLoading, setStoreLoading] = useState(false);
|
||
|
||
// 门店购买详情:新增单品弹窗
|
||
const [addItemOpen, setAddItemOpen] = useState(false);
|
||
const [addItemSaving, setAddItemSaving] = useState(false);
|
||
const [addItemForm] = Form.useForm<PurchaseStoreItemAddParams>();
|
||
const [productOptions, setProductOptions] = useState<IProduct[]>([]);
|
||
|
||
// 门店购买详情:单品编辑弹窗(数量/称重/单价)
|
||
const [storeItemTarget, setStoreItemTarget] = useState<IPurchaseStoreItem | null>(null);
|
||
const [storeItemSaving, setStoreItemSaving] = useState(false);
|
||
const [storeItemForm] = Form.useForm<PurchaseStoreItemUpdateParams>();
|
||
|
||
// 供应商采购明细页签
|
||
const [supplierId, setSupplierId] = useState<number>(0);
|
||
/** 市场筛选('' = 全部市场) */
|
||
const [marketFilter, setMarketFilter] = useState<string>('');
|
||
|
||
// 商品明细页签:供应商/市场列筛选值(antd Table 受控筛选,用于底部统计行联动)
|
||
const [itemColumnFilters, setItemColumnFilters] = useState<Record<string, (React.Key | boolean)[] | null>>({});
|
||
|
||
// 导出弹窗:商品明细(供应商筛选)/ 门店购买详情 / 供应商采购明细
|
||
const [itemExportOpen, setItemExportOpen] = useState(false);
|
||
const [itemExportSupplier, setItemExportSupplier] = useState<number>(0);
|
||
const [storeExportOpen, setStoreExportOpen] = useState(false);
|
||
const [storeExportScope, setStoreExportScope] = useState<'current' | 'all'>('current');
|
||
const [supplierExportOpen, setSupplierExportOpen] = useState(false);
|
||
const [supplierExportScope, setSupplierExportScope] = useState<'current' | 'all'>('current');
|
||
|
||
/** 采购单内出现的供应商(矩阵/供应商页签筛选与导出选项共用) */
|
||
const purchaseSuppliers = useMemo(() => {
|
||
const map = new Map<number, string>();
|
||
(detail?.items ?? []).forEach((row) => {
|
||
if (row.supplier_id > 0 && row.supplier?.name) {
|
||
map.set(row.supplier_id, row.supplier.name);
|
||
}
|
||
});
|
||
return Array.from(map, ([id, name]) => ({ id, name }));
|
||
}, [detail?.items]);
|
||
|
||
/** 当前供应商在采购单内出现的市场列表(市场筛选下拉选项) */
|
||
const supplierMarkets = useMemo(() => {
|
||
const markets = new Set<string>();
|
||
(detail?.items ?? []).forEach((row) => {
|
||
if (row.supplier_id === supplierId && row.market) {
|
||
markets.add(row.market);
|
||
}
|
||
});
|
||
return Array.from(markets);
|
||
}, [detail?.items, supplierId]);
|
||
|
||
/** 供应商采购明细行:按供应商+市场过滤矩阵行,金额=数量×成本价(成本口径) */
|
||
const supplierItems = useMemo<IPurchaseSupplierItem[]>(() => {
|
||
if (!detail || supplierId <= 0) return [];
|
||
return detail.items
|
||
.filter((row) => row.supplier_id === supplierId)
|
||
.filter((row) => marketFilter === '' || (row.market ?? '') === marketFilter)
|
||
.map((row) => ({
|
||
product_id: row.product_id,
|
||
product_name: row.product_name,
|
||
product_spec: row.product_spec,
|
||
unit: row.unit,
|
||
market: row.market,
|
||
cost_price: String(row.cost_price),
|
||
quantity: row.quantity,
|
||
weight: String(row.weight),
|
||
amount: (row.quantity * Number(row.cost_price)).toFixed(2),
|
||
}));
|
||
}, [detail, supplierId, marketFilter]);
|
||
|
||
/** 商品明细市场列筛选选项(采购单内出现的全部市场) */
|
||
const itemMarketOptions = useMemo(() => {
|
||
const markets = new Set<string>();
|
||
(detail?.items ?? []).forEach((row) => {
|
||
if (row.market) {
|
||
markets.add(row.market);
|
||
}
|
||
});
|
||
return Array.from(markets);
|
||
}, [detail?.items]);
|
||
|
||
/** 商品明细当前可见行:按列筛选值本地过滤,驱动底部成本统计与列筛选联动 */
|
||
const summaryItems = useMemo<IPurchaseDetailRow[]>(() => {
|
||
const supplierKeys = itemColumnFilters.supplier;
|
||
const marketKeys = itemColumnFilters.market;
|
||
return (detail?.items ?? [])
|
||
.filter((row) => !supplierKeys?.length || supplierKeys.includes(row.supplier_id))
|
||
.filter((row) => !marketKeys?.length || marketKeys.includes(row.market ?? ''));
|
||
}, [detail?.items, itemColumnFilters]);
|
||
|
||
// 生成账单(已完成采购单按门店生成:填写配送费/周转筐/托盘数量,金额只读)
|
||
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 afterSale = Number(input.after_sale ?? 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 + afterSale,
|
||
};
|
||
});
|
||
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 (purchaseSuppliers.length > 0) {
|
||
setSupplierId((prev) => (purchaseSuppliers.some((s) => s.id === prev) ? prev : purchaseSuppliers[0].id));
|
||
} else {
|
||
setSupplierId(0);
|
||
}
|
||
}, [purchaseSuppliers]);
|
||
|
||
// 切到「门店购买详情」页签或切换门店时加载汇总
|
||
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);
|
||
setItemColumnFilters({});
|
||
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 refreshAfterStoreItemChange = async () => {
|
||
await loadStoreSummary();
|
||
if (detail) {
|
||
await loadDetail(detail.purchase.id!);
|
||
}
|
||
await tableRef.current?.reload();
|
||
};
|
||
|
||
/** 打开新增单品弹窗(懒加载商品选项) */
|
||
const openAddItem = () => {
|
||
addItemForm.resetFields();
|
||
setAddItemOpen(true);
|
||
if (productOptions.length === 0) {
|
||
getProductOptions().then((res) => setProductOptions(res.data.data ?? []));
|
||
}
|
||
};
|
||
|
||
/** 提交新增单品 */
|
||
const handleAddItemSave = async (values: PurchaseStoreItemAddParams) => {
|
||
if (!detail || storeId <= 0) {
|
||
return;
|
||
}
|
||
setAddItemSaving(true);
|
||
try {
|
||
await addPurchaseStoreItem(detail.purchase.id!, storeId, values);
|
||
message.success('已添加单品,订货单与采购单汇总已重算');
|
||
setAddItemOpen(false);
|
||
await refreshAfterStoreItemChange();
|
||
} finally {
|
||
setAddItemSaving(false);
|
||
}
|
||
};
|
||
|
||
/** 打开单品编辑弹窗(回显加权平均单价/数量/称重) */
|
||
const openStoreItemEdit = (row: IPurchaseStoreItem) => {
|
||
setStoreItemTarget(row);
|
||
storeItemForm.setFieldsValue({
|
||
quantity: row.quantity,
|
||
price: Number(row.price ?? 0),
|
||
weight: Number(row.weight ?? 0),
|
||
});
|
||
};
|
||
|
||
/** 提交单品修改:同商品多笔订单明细合并到最早一条,级联重算 */
|
||
const handleStoreItemSave = async (values: PurchaseStoreItemUpdateParams) => {
|
||
if (!detail || !storeItemTarget || storeId <= 0) {
|
||
return;
|
||
}
|
||
setStoreItemSaving(true);
|
||
try {
|
||
await updatePurchaseStoreItem(detail.purchase.id!, storeId, storeItemTarget.product_id, values);
|
||
message.success('单品已更新,订货单与采购单汇总已重算');
|
||
setStoreItemTarget(null);
|
||
await refreshAfterStoreItemChange();
|
||
} finally {
|
||
setStoreItemSaving(false);
|
||
}
|
||
};
|
||
|
||
/** 移除单品 */
|
||
const handleStoreItemRemove = async (row: IPurchaseStoreItem) => {
|
||
if (!detail || storeId <= 0) {
|
||
return;
|
||
}
|
||
await removePurchaseStoreItem(detail.purchase.id!, storeId, row.product_id);
|
||
message.success('已移除单品,订货单与采购单汇总已重算');
|
||
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 或失焦触发保存,保存后关闭编辑态并刷新详情与列表。
|
||
*/
|
||
const handleInlineSave = async (row: IPurchaseDetailRow, dataIndex: InlineEditField, rawValue: string | number) => {
|
||
if (!detail) {
|
||
return;
|
||
}
|
||
setEditingCell(null);
|
||
|
||
const params: PurchaseRowUpdateParams = {
|
||
product_name: row.product_name,
|
||
supplier_id: row.supplier_id,
|
||
product_spec: row.product_spec,
|
||
unit: row.unit,
|
||
cost_price: Number(row.cost_price),
|
||
};
|
||
|
||
if (dataIndex === 'cost_price') {
|
||
const cost = Number(rawValue);
|
||
if (rawValue === '' || !Number.isFinite(cost) || cost < 0) {
|
||
message.warning('成本无效,已取消修改');
|
||
return;
|
||
}
|
||
if (cost === Number(row.cost_price)) {
|
||
return;
|
||
}
|
||
params.cost_price = cost;
|
||
} else if (dataIndex === 'supplier_id') {
|
||
const supplierId = Number(rawValue);
|
||
if (supplierId === row.supplier_id) {
|
||
return;
|
||
}
|
||
params.supplier_id = supplierId;
|
||
} else {
|
||
const text = String(rawValue).trim();
|
||
if (!text) {
|
||
message.warning(`${INLINE_FIELD_LABELS[dataIndex]}不能为空,已取消修改`);
|
||
return;
|
||
}
|
||
if (text === row[dataIndex]) {
|
||
return;
|
||
}
|
||
params[dataIndex] = text;
|
||
}
|
||
|
||
if (params.supplier_id <= 0) {
|
||
message.warning('请先通过供应商单元格为该商品设置供应商');
|
||
return;
|
||
}
|
||
|
||
const res = await updatePurchaseRow(detail.purchase.id!, row.product_id, params);
|
||
message.success(`已更新 ${res.data.data?.count ?? 0} 条订货明细`);
|
||
await loadDetail(detail.purchase.id!);
|
||
await tableRef.current?.reload();
|
||
};
|
||
|
||
/** 可编辑单元格:点击进入编辑态(文本 Input / 供应商 Select / 成本 InputNumber),Esc 取消 */
|
||
const renderEditableCell = (
|
||
row: IPurchaseDetailRow,
|
||
dataIndex: InlineEditField,
|
||
display: React.ReactNode,
|
||
): React.ReactNode => {
|
||
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}
|
||
placeholder="选择供应商"
|
||
showSearch={{ optionFilterProp: 'label' }}
|
||
options={suppliers.map((s) => ({ value: s.id, label: s.name }))}
|
||
onChange={(value) => void handleInlineSave(row, dataIndex, value)}
|
||
onBlur={() => setEditingCell(null)}
|
||
/>
|
||
);
|
||
}
|
||
|
||
if (dataIndex === 'cost_price') {
|
||
return (
|
||
<InputNumber
|
||
size="small"
|
||
autoFocus
|
||
min={0}
|
||
precision={2}
|
||
prefix="¥"
|
||
className="w-full"
|
||
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
|
||
size="small"
|
||
autoFocus
|
||
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}
|
||
/>
|
||
);
|
||
};
|
||
|
||
/** 打开生成账单弹窗:拉取按门店汇总的商品金额(只读),初始化配送费/周转筐/托盘数量 */
|
||
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,
|
||
after_sale: row.bill ? Number(row.bill.after_sale) : 0,
|
||
remark: row.bill?.remark ?? '',
|
||
})),
|
||
});
|
||
} 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: 160,
|
||
align: 'center',
|
||
fixed: 'left',
|
||
ellipsis: true,
|
||
render: (_, row) => renderEditableCell(row, 'product_name', row.product_name),
|
||
},
|
||
{
|
||
title: '供应商',
|
||
dataIndex: 'supplier',
|
||
width: 110,
|
||
align: 'center',
|
||
fixed: 'left',
|
||
filters: purchaseSuppliers.map((s) => ({ text: s.name, value: s.id })),
|
||
filteredValue: itemColumnFilters.supplier ?? null,
|
||
onFilter: (value, row) => row.supplier_id === Number(value),
|
||
render: (_, row) => renderEditableCell(row, 'supplier_id', row.supplier?.name ?? '-'),
|
||
},
|
||
{
|
||
title: '市场',
|
||
fixed: 'left',
|
||
dataIndex: 'market',
|
||
width: 90,
|
||
align: 'center',
|
||
filters: itemMarketOptions.map((m) => ({ text: m, value: m })),
|
||
filteredValue: itemColumnFilters.market ?? null,
|
||
onFilter: (value, row) => (row.market ?? '') === value,
|
||
render: (v) => v || '-',
|
||
},
|
||
{
|
||
title: '单价',
|
||
key: 'retail_price',
|
||
width: 100,
|
||
align: 'center',
|
||
render: (_, row) => {
|
||
// 加权平均售价 ÷ 包规数值(无订货数量时无参考价)
|
||
const quantity = Number(row.quantity ?? 0);
|
||
if (quantity <= 0) return <Text type="secondary">-</Text>;
|
||
const weightedPrice = Number(row.amount ?? 0) / quantity;
|
||
return `¥${calcUnitRefPrice(weightedPrice, row.product_spec ?? '').toFixed(2)}`;
|
||
},
|
||
},
|
||
{
|
||
title: '包规',
|
||
dataIndex: 'product_spec',
|
||
align: 'center',
|
||
width: 90,
|
||
render: (v, row) => renderEditableCell(row, 'product_spec', v || '-'),
|
||
},
|
||
{
|
||
title: '单位',
|
||
dataIndex: 'unit',
|
||
width: 70,
|
||
align: 'center',
|
||
render: (v, row) => renderEditableCell(row, 'unit', v || '-'),
|
||
},
|
||
{
|
||
title: '成本',
|
||
dataIndex: 'cost_price',
|
||
width: 100,
|
||
align: 'center',
|
||
render: (v, row) => renderEditableCell(row, 'cost_price', `¥${Number(v).toFixed(2)}`),
|
||
},
|
||
{
|
||
title: '合计数量',
|
||
key: 'total_quantity',
|
||
width: 90,
|
||
align: 'center',
|
||
render: (_, row) => <Text strong>{row.quantity}</Text>,
|
||
},
|
||
];
|
||
|
||
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>
|
||
);
|
||
},
|
||
}));
|
||
|
||
return [...fixed, ...storeColumns];
|
||
};
|
||
|
||
/** 底部统计行:按门店统计金额(Σ 门店数量 × 行单价)+ 合计(随供应商/市场列筛选联动) */
|
||
const renderSummary = () => {
|
||
const stores = detail?.stores ?? [];
|
||
const items = summaryItems;
|
||
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={7} align="center">
|
||
<Space size={16}>
|
||
<Text strong>成本统计(按门店)</Text>
|
||
<Text strong type="danger">总计 ¥{totalAmount.toFixed(2)}</Text>
|
||
</Space>
|
||
</Table.Summary.Cell>
|
||
<Table.Summary.Cell index={7} align="center">
|
||
<Text strong>{totalQuantity}</Text>
|
||
</Table.Summary.Cell>
|
||
{storeTotals.map((amount, index) => (
|
||
<Table.Summary.Cell key={stores[index].id} index={8 + index} align="center">
|
||
<Text strong>¥{amount.toFixed(2)}</Text>
|
||
</Table.Summary.Cell>
|
||
))}
|
||
</Table.Summary.Row>
|
||
);
|
||
};
|
||
|
||
/** 门店购买详情列:商品/市场/购买单价(单价÷包规)/包规/单位/单价/数量/预计金额 + 操作 */
|
||
const storeColumns: TableProps<IPurchaseStoreItem>['columns'] = [
|
||
{ title: '商品', dataIndex: 'product_name', width: 160, align: 'center' },
|
||
{ title: '市场', dataIndex: 'market', width: 90, align: 'center', render: (v) => v || '-' },
|
||
{
|
||
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>,
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
width: 130,
|
||
align: 'center',
|
||
render: (_, row) =>
|
||
detail?.purchase.status === 0 ? (
|
||
<Space size={0}>
|
||
<AuthButton auth="purchase.order.update">
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
icon={<EditOutlined />}
|
||
onClick={() => openStoreItemEdit(row)}
|
||
>
|
||
编辑
|
||
</Button>
|
||
</AuthButton>
|
||
<AuthButton auth="purchase.order.update">
|
||
<Popconfirm
|
||
title="移除该单品?"
|
||
description={`将从「${storeSummary?.store?.name ?? '该门店'}」采购明细中移除「${row.product_name}」`}
|
||
onConfirm={() => handleStoreItemRemove(row)}
|
||
>
|
||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
||
移除
|
||
</Button>
|
||
</Popconfirm>
|
||
</AuthButton>
|
||
</Space>
|
||
) : (
|
||
<Text type="secondary">-</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={6} align="center">
|
||
<Text strong>合计</Text>
|
||
</Table.Summary.Cell>
|
||
<Table.Summary.Cell index={6} align="center">
|
||
<Text strong>{totalQuantity}</Text>
|
||
</Table.Summary.Cell>
|
||
<Table.Summary.Cell index={7} align="center">
|
||
<Text strong>{totalWeight.toFixed(3)}斤</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 supplierColumns: TableProps<IPurchaseSupplierItem>['columns'] = [
|
||
{ title: '品名', dataIndex: 'product_name', width: 180, align: 'center' },
|
||
{ title: '市场', dataIndex: 'market', width: 100, align: 'center', render: (v) => v || '-' },
|
||
{ title: '包规', dataIndex: 'product_spec', width: 110, align: 'center', render: (v) => v || '-' },
|
||
{ title: '单位', dataIndex: 'unit', width: 90, align: 'center', render: (v) => v || '-' },
|
||
{
|
||
title: '成本价',
|
||
dataIndex: 'cost_price',
|
||
width: 110,
|
||
align: 'center',
|
||
render: (v) => `¥${Number(v).toFixed(2)}`,
|
||
},
|
||
{
|
||
title: '数量',
|
||
dataIndex: 'quantity',
|
||
width: 100,
|
||
align: 'center',
|
||
render: (v) => <Text strong>{v}</Text>,
|
||
},
|
||
{ title: '重量', dataIndex: 'weight', width: 110, align: 'center', render: (v) => `${Number(v).toFixed(3)}斤` },
|
||
{
|
||
title: '金额',
|
||
dataIndex: 'amount',
|
||
width: 120,
|
||
align: 'center',
|
||
render: (v) => <Text strong>¥{Number(v).toFixed(2)}</Text>,
|
||
},
|
||
];
|
||
|
||
/** 供应商采购明细合计行 */
|
||
const renderSupplierTotal = () => {
|
||
const totalQuantity = supplierItems.reduce((sum, row) => sum + Number(row.quantity), 0);
|
||
const totalWeight = supplierItems.reduce((sum, row) => sum + Number(row.weight), 0);
|
||
const totalAmount = supplierItems.reduce((sum, row) => sum + Number(row.amount), 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={detailSize}
|
||
resizable={{
|
||
onResize: (newSize) => setDetailSize(newSize),
|
||
}}
|
||
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: 'suppliers', label: '供应商采购明细' },
|
||
{ key: 'bills', label: '门店账单' },
|
||
]}
|
||
/>
|
||
|
||
{ detailTab === 'stores' ? (
|
||
<>
|
||
<div className="mb-3 flex items-center justify-between gap-2">
|
||
<div className="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>
|
||
<Space>
|
||
{detail.purchase.status === 0 && (
|
||
<AuthButton auth="purchase.order.update">
|
||
<Button icon={<PlusOutlined />} onClick={openAddItem}>
|
||
新增单品
|
||
</Button>
|
||
</AuthButton>
|
||
)}
|
||
<AuthButton auth="purchase.order.export">
|
||
<Button
|
||
type="primary"
|
||
ghost
|
||
icon={<DownloadOutlined />}
|
||
onClick={() => {
|
||
setStoreExportScope('current');
|
||
setStoreExportOpen(true);
|
||
}}
|
||
>
|
||
导出
|
||
</Button>
|
||
</AuthButton>
|
||
</Space>
|
||
</div>
|
||
<Spin spinning={storeLoading}>
|
||
{storeSummary && storeSummary.items.length > 0 ? (
|
||
<Table<IPurchaseStoreItem>
|
||
rowKey="product_id"
|
||
size="small"
|
||
bordered
|
||
columns={storeColumns}
|
||
dataSource={storeSummary.items}
|
||
pagination={false}
|
||
scroll={{ x: 'max-content' }}
|
||
summary={renderStoreTotal}
|
||
/>
|
||
) : (
|
||
!storeLoading && (
|
||
<Empty
|
||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||
description="该门店在此采购单中无采购商品"
|
||
className="py-8!"
|
||
/>
|
||
)
|
||
)}
|
||
</Spin>
|
||
</>
|
||
) : detailTab === 'suppliers' ? (
|
||
<>
|
||
<div className="mb-3 flex items-center justify-between gap-2">
|
||
<div className="flex items-center gap-2">
|
||
<Text>选择供应商:</Text>
|
||
<Select
|
||
value={supplierId || undefined}
|
||
onChange={(value) => {
|
||
setSupplierId(value);
|
||
setMarketFilter('');
|
||
}}
|
||
placeholder="选择供应商"
|
||
className="w-60!"
|
||
showSearch
|
||
optionFilterProp="label"
|
||
options={purchaseSuppliers.map((s) => ({ value: s.id, label: s.name }))}
|
||
/>
|
||
<Text>市场:</Text>
|
||
<Select
|
||
value={marketFilter}
|
||
onChange={setMarketFilter}
|
||
className="w-40!"
|
||
options={[
|
||
{ value: '', label: '全部市场' },
|
||
...supplierMarkets.map((m) => ({ value: m, label: m })),
|
||
]}
|
||
/>
|
||
</div>
|
||
<AuthButton auth="purchase.order.export">
|
||
<Button
|
||
type="primary"
|
||
ghost
|
||
icon={<DownloadOutlined />}
|
||
onClick={() => {
|
||
setSupplierExportScope('current');
|
||
setSupplierExportOpen(true);
|
||
}}
|
||
>
|
||
导出
|
||
</Button>
|
||
</AuthButton>
|
||
</div>
|
||
{supplierItems.length > 0 ? (
|
||
<Table<IPurchaseSupplierItem>
|
||
rowKey="product_id"
|
||
size="small"
|
||
bordered
|
||
columns={supplierColumns}
|
||
dataSource={supplierItems}
|
||
pagination={false}
|
||
scroll={{ x: 'max-content' }}
|
||
summary={renderSupplierTotal}
|
||
/>
|
||
) : (
|
||
<Empty
|
||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||
description="该供应商在此采购单中无采购明细"
|
||
className="py-8!"
|
||
/>
|
||
)}
|
||
</>
|
||
) : detailTab === 'bills' ? (
|
||
detail.bills.length > 0 ? (
|
||
<Table<IBill>
|
||
rowKey="id"
|
||
size="small"
|
||
bordered
|
||
columns={billColumns}
|
||
dataSource={detail.bills}
|
||
pagination={false}
|
||
scroll={{ x: 'max-content' }}
|
||
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={() => {
|
||
setItemExportSupplier(0);
|
||
setItemExportOpen(true);
|
||
}}
|
||
>
|
||
导出
|
||
</Button>
|
||
</AuthButton>
|
||
{canUpdateRow && (
|
||
<Text type="secondary" className="text-xs">
|
||
点击 品名/供应商/包规/单位/成本 单元格可直接修改;保存将同步该商品全部订货明细与商品档案,成本变化时各门店单价按等级上浮自动重算
|
||
</Text>
|
||
)}
|
||
</Space>
|
||
<Table<IPurchaseDetailRow>
|
||
rowKey="product_id"
|
||
size="small"
|
||
bordered
|
||
columns={buildItemColumns()}
|
||
dataSource={detail.items}
|
||
onChange={(_pagination, filters) => setItemColumnFilters(filters)}
|
||
pagination={false}
|
||
scroll={{ x: 1200, y: 800 }}
|
||
summary={renderSummary}
|
||
/>
|
||
</>
|
||
) }
|
||
</>
|
||
)}
|
||
</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}` : '生成账单'}
|
||
open={billOpen}
|
||
onCancel={() => {
|
||
setBillOpen(false);
|
||
setBillPrepare(null);
|
||
}}
|
||
onOk={() => billForm.submit()}
|
||
confirmLoading={billSaving}
|
||
okText="确认生成"
|
||
okButtonProps={{ disabled: billAllGenerated }}
|
||
width={1280}
|
||
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-36 shrink-0 text-center">售后金额(元)</div>
|
||
<div className="w-36 shrink-0 text-center">备注</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"
|
||
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"
|
||
precision={0}
|
||
disabled={billed}
|
||
placeholder="正压负回"
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item
|
||
className="m-0! w-36 shrink-0 px-1!"
|
||
name={[field.name, 'after_sale']}
|
||
>
|
||
<InputNumber
|
||
className="w-full"
|
||
precision={2}
|
||
disabled={billed}
|
||
placeholder="可正负,计入总额"
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item
|
||
className="m-0! w-36 shrink-0 px-1!"
|
||
name={[field.name, 'remark']}
|
||
>
|
||
<Input
|
||
maxLength={255}
|
||
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>
|
||
{/* 新增单品:选择商品 + 采购数量 + 称重 */}
|
||
<Modal
|
||
title={`新增单品 · ${storeSummary?.store?.name ?? ''}`}
|
||
open={addItemOpen}
|
||
onCancel={() => setAddItemOpen(false)}
|
||
onOk={() => addItemForm.submit()}
|
||
confirmLoading={addItemSaving}
|
||
okText="添加"
|
||
destroyOnHidden
|
||
>
|
||
<div className="py-2 text-gray-500">
|
||
单品将挂靠到该门店在此采购单中的最新一笔订单;单价按门店等级上浮比例自动换算。
|
||
</div>
|
||
<Form form={addItemForm} layout="vertical" onFinish={handleAddItemSave}>
|
||
<Form.Item
|
||
label="商品"
|
||
name="product_id"
|
||
rules={[{ required: true, message: '请选择商品' }]}
|
||
>
|
||
<Select
|
||
showSearch={{ optionFilterProp: 'label' }}
|
||
placeholder="搜索并选择商品"
|
||
options={productOptions.map((p) => ({
|
||
value: p.id!,
|
||
label: `${p.name}${p.spec ? `(${p.spec})` : ''}`,
|
||
}))}
|
||
/>
|
||
</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">
|
||
<InputNumber className="w-full" min={0} precision={3} placeholder="请输入称重" />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
|
||
{/* 单品编辑:数量/称重/单价(同商品多笔订单明细合并到最早一条) */}
|
||
<Modal
|
||
title={storeItemTarget ? `编辑「${storeItemTarget.product_name}」` : '编辑单品'}
|
||
open={storeItemTarget !== null}
|
||
onCancel={() => setStoreItemTarget(null)}
|
||
onOk={() => storeItemForm.submit()}
|
||
confirmLoading={storeItemSaving}
|
||
okText="保存"
|
||
destroyOnHidden
|
||
>
|
||
<div className="py-2 text-gray-500">
|
||
修改保存后,系统将自动重算明细金额、订货单与采购单汇总。
|
||
</div>
|
||
<Form form={storeItemForm} layout="vertical" onFinish={handleStoreItemSave}>
|
||
<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="导出商品明细"
|
||
open={itemExportOpen}
|
||
onCancel={() => setItemExportOpen(false)}
|
||
onOk={() => {
|
||
if (detail) {
|
||
void exportPurchase(detail.purchase.id!, itemExportSupplier > 0 ? itemExportSupplier : undefined);
|
||
}
|
||
setItemExportOpen(false);
|
||
}}
|
||
okText="导出"
|
||
destroyOnHidden
|
||
>
|
||
<div className="py-2 text-gray-500">
|
||
导出系统全部商品行(无订货的商品数量为 0),门店列附数量合计与突出颜色。
|
||
</div>
|
||
<div className="flex items-center gap-2 py-2">
|
||
<Text>供应商:</Text>
|
||
<Select
|
||
value={itemExportSupplier}
|
||
onChange={setItemExportSupplier}
|
||
className="flex-1"
|
||
options={[
|
||
{ value: 0, label: '全部供应商' },
|
||
...purchaseSuppliers.map((s) => ({ value: s.id, label: s.name })),
|
||
]}
|
||
/>
|
||
</div>
|
||
</Modal>
|
||
|
||
{/* 门店购买详情导出:当前门店 / 全部门店(多工作表) */}
|
||
<Modal
|
||
title="导出门店购买详情"
|
||
open={storeExportOpen}
|
||
onCancel={() => setStoreExportOpen(false)}
|
||
onOk={() => {
|
||
if (detail) {
|
||
void exportPurchaseStores(
|
||
detail.purchase.id!,
|
||
storeExportScope === 'current' && storeId > 0 ? storeId : undefined,
|
||
);
|
||
}
|
||
setStoreExportOpen(false);
|
||
}}
|
||
okText="导出"
|
||
destroyOnHidden
|
||
>
|
||
<Radio.Group
|
||
className="py-2"
|
||
value={storeExportScope}
|
||
onChange={(e) => setStoreExportScope(e.target.value)}
|
||
options={[
|
||
{ value: 'current', label: `当前门店(${detail?.stores.find((s) => s.id === storeId)?.name ?? '-'})` },
|
||
{ value: 'all', label: '全部门店(合并为一个 XLSX,每门店一个工作表)' },
|
||
]}
|
||
/>
|
||
</Modal>
|
||
|
||
{/* 供应商采购明细导出:当前供应商 / 全部供应商(按「供应商×市场」拆分工作表) */}
|
||
<Modal
|
||
title="导出供应商采购明细"
|
||
open={supplierExportOpen}
|
||
onCancel={() => setSupplierExportOpen(false)}
|
||
onOk={() => {
|
||
if (detail) {
|
||
void exportPurchaseSuppliers(
|
||
detail.purchase.id!,
|
||
supplierExportScope === 'current' && supplierId > 0 ? supplierId : undefined,
|
||
);
|
||
}
|
||
setSupplierExportOpen(false);
|
||
}}
|
||
okText="导出"
|
||
destroyOnHidden
|
||
>
|
||
<div className="py-2 text-gray-500">
|
||
导出 XLSX 按「供应商 × 市场」拆分工作表,列序:品名、汇总、市场、各门店明细,有数据的单元格突出显示。
|
||
</div>
|
||
<Radio.Group
|
||
className="py-2"
|
||
value={supplierExportScope}
|
||
onChange={(e) => setSupplierExportScope(e.target.value)}
|
||
options={[
|
||
{ value: 'current', label: `当前供应商(${purchaseSuppliers.find((s) => s.id === supplierId)?.name ?? '-'})` },
|
||
{ value: 'all', label: '全部供应商(合并为一个 XLSX,每供应商×市场一个工作表)' },
|
||
]}
|
||
/>
|
||
</Modal>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default PurchaseOrderPage;
|