import React, { useEffect, useRef, useState } from 'react'; import { Button, Descriptions, Drawer, Dropdown, Empty, Form, Image, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Spin, Table, Tabs, Tag, Typography, } from 'antd'; import {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 { IPurchaseCell, IPurchaseCellItem, IPurchaseContainerStore, IPurchaseDetail, IPurchaseDetailRow, IPurchaseStoreItem, IPurchaseStoreSummary, } from '@/domain/iPurchaseOrder.ts'; import { PURCHASE_STATUS_MAP } from '@/domain/iPurchaseOrder.ts'; import { STORE_ORDER_STATUS_MAP } from '@/domain/iStoreOrder.ts'; import { exportPurchase, getPurchaseCell, getPurchaseDetail, getPurchaseStoreSummary, type PurchaseCellUpdateParams, type PurchaseContainerOrderParams, type PurchaseRowUpdateParams, updatePurchaseCellItem, updatePurchaseContainer, 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>(null); // 详情抽屉 const [detailOpen, setDetailOpen] = useState(false); const [detail, setDetail] = useState(null); const [detailLoading, setDetailLoading] = useState(false); const [completing, setCompleting] = useState(false); // 行修改弹窗 const [editingRow, setEditingRow] = useState(null); const [rowSaving, setRowSaving] = useState(false); const [editForm] = Form.useForm(); const [suppliers, setSuppliers] = useState([]); // 单元格下钻弹窗(门店 × 商品订货明细) const [cellOpen, setCellOpen] = useState(false); const [cellLoading, setCellLoading] = useState(false); const [cellData, setCellData] = useState(null); const [cellQuery, setCellQuery] = useState<{ productId: number; storeId: number } | null>(null); // 单元格明细编辑(数量/称重) const [cellItemOpen, setCellItemOpen] = useState(false); const [cellItemTarget, setCellItemTarget] = useState(null); const [cellItemSaving, setCellItemSaving] = useState(false); const [cellItemForm] = Form.useForm(); // 门店购买详情(按商品聚合的门店采购汇总) const [detailTab, setDetailTab] = useState('items'); const [storeId, setStoreId] = useState(0); const [storeSummary, setStoreSummary] = useState(null); const [storeLoading, setStoreLoading] = useState(false); // 周转框/托盘合并记录修改(按门店覆盖全部订单逐笔修改) const [containerOpen, setContainerOpen] = useState(false); const [containerStore, setContainerStore] = useState(null); const [containerSaving, setContainerSaving] = useState(false); const [containerForm] = Form.useForm<{ orders: PurchaseContainerOrderParams[] }>(); // 弹窗内实时预览:合并合计 = Σ 各订单框/托盘数量,附加金额 = 合计 × 单价 const watchContainerOrders = Form.useWatch('orders', containerForm) ?? []; const previewContainerBox = watchContainerOrders.reduce( (sum, order) => sum + Number(order?.box_num ?? 0), 0, ); const previewContainerTray = watchContainerOrders.reduce( (sum, order) => sum + Number(order?.tray_num ?? 0), 0, ); const previewContainerAdded = previewContainerBox * Number(containerStore?.box_price ?? 0) + previewContainerTray * Number(containerStore?.tray_price ?? 0); 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 openContainerEdit = (store: IPurchaseContainerStore) => { setContainerStore(store); containerForm.setFieldsValue({ orders: store.orders.map((order) => ({ order_id: order.order_id, box_num: order.box_num, tray_num: order.tray_num, })), }); setContainerOpen(true); }; /** 提交合并记录修改:逐笔更新订单,自动重算附加金额与订单总金额 */ const handleContainerSave = async (values: { orders: PurchaseContainerOrderParams[] }) => { if (!detail || !containerStore) { return; } setContainerSaving(true); try { await updatePurchaseContainer(detail.purchase.id!, containerStore.store_id, values.orders); message.success('周转框/托盘已更新,订单金额已重算'); setContainerOpen(false); setContainerStore(null); await loadDetail(detail.purchase.id!); await tableRef.current?.reload(); } finally { setContainerSaving(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['columns'] => { const fixed: NonNullable['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: {store.name}, key: `store-${store.id}`, width: 100, align: 'center' as const, render: (_: unknown, row: IPurchaseDetailRow) => { const quantity = row.cells[store.id]; return quantity !== undefined ? ( openCell(row, store)}>{quantity} ) : ( - ); }, })); const tail: NonNullable['columns']> = [ { title: '合计数量', key: 'total_quantity', width: 90, align: 'center', render: (_, row) => {row.quantity}, }, { title: '操作', key: 'action', width: 90, fixed: 'right', align: 'center', render: (_, row) => ( <> { detail?.purchase.status === 0 ? ( ) : '-' } ), }, ]; 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 ( 成本统计(按门店) {storeTotals.map((amount, index) => ( ¥{amount.toFixed(2)} ))} {totalQuantity} ¥{totalAmount.toFixed(2)} ); }; /** 门店购买详情列:商品/购买参考零售价(单价÷包规)/包规/单位/单价/数量/预计金额 */ const storeColumns: TableProps['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) => {v}, }, { title: '重量', dataIndex: 'weight', width: 90, align: 'center', render: (v) => `${v || '-'}斤` }, { title: '预计金额', dataIndex: 'amount', width: 110, align: 'center', render: (v) => ¥{Number(v).toFixed(2)}, }, ]; /** 门店购买详情合计行:总数量 + 总预计金额 */ 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 ( 合计 {totalQuantity} {totalWeight.toFixed(3)}斤 ¥{totalAmount.toFixed(2)} ); }; /** 周转框/托盘合并记录列:门店/订单数/框合计/托盘合计/附加金额(合计可点击下钻逐订单修改) */ const containerColumns: TableProps['columns'] = [ { title: '门店', dataIndex: 'store_name', width: 180, align: 'center' }, { title: '订单数', dataIndex: 'order_count', width: 90, align: 'center' }, { title: '周转框合计', dataIndex: 'box_num', width: 110, align: 'center', render: (_, row) => ( openContainerEdit(row)}>{row.box_num} ), }, { title: '周转托盘合计', dataIndex: 'tray_num', width: 120, align: 'center', render: (_, row) => ( openContainerEdit(row)}>{row.tray_num} ), }, { title: '附加金额', dataIndex: 'added_amount', width: 110, align: 'center', render: (_, row) => ¥{row.added_amount}, }, ]; /** 周转框/托盘合并记录合计行 */ const renderContainerSummary = () => { const containers = detail?.containers ?? []; const totalBox = containers.reduce((sum, row) => sum + row.box_num, 0); const totalTray = containers.reduce((sum, row) => sum + row.tray_num, 0); const totalAdded = containers.reduce((sum, row) => sum + Number(row.added_amount), 0); return ( 合计 {totalBox} {totalTray} ¥{totalAdded.toFixed(2)} ); }; const columns: XinTableColumn[] = [ { title: '采购单号', dataIndex: 'purchase_no', valueType: 'text', hideInForm: true, align: 'center', width: 260, render: (_, record) => {record.purchase_no}, }, { 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 ? ( ¥{record.actual_amount} ) : ( 未录入 ), }, { 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 {item?.text}; }, align: 'center', }, { title: '制单人', dataIndex: 'operator', hideInForm: true, hideInSearch: true, render: (_, record) => record.operator?.nickname ?? '-', align: 'center', }, ]; const operateRender: XinTableProps['operateRender'] = (record, dom) => [ record.status === 0 ? dom.edit : null, ) : null ]; const tableProps: XinTableProps = { api: '/purchase/order', columns, rowKey: 'id', accessName: 'purchase.order', tableRef, operateRender, operateProps: { width: 300 }, formProps: false, }; return ( <>
采购单 在「门店订单」页勾选已接单订单合并生成采购单;采购单支持修改订单信息。
{...tableProps} /> {/* 采购单详情:商品行 × 门店列矩阵 */} setDetailOpen(false)} size={1200} loading={detailLoading} > {detail && ( <> 采购单信息 {detail.purchase.purchase_date} {PURCHASE_STATUS_MAP[detail.purchase.status ?? 0]?.text} {detail.purchase.operator?.nickname ?? '-'} ¥{detail.purchase.estimate_amount} ¥{detail.purchase.actual_amount} {detail.purchase.total_quantity} {detail.purchase.total_weight} {detail.purchase.created_at} {detail.purchase.remark} { detailTab === 'stores' ? ( <>
选择门店: {/* 单元格下钻 */} setCellOpen(false)} footer={null} width={1000} destroyOnHidden styles={{ body: {paddingTop: 16} }} > {cellData && ( <> {cellData.items.length === 0 ? ( ) : (
商品信息
单价
订货量
订货金额
重量
操作
{cellData.items.map((item) => (
{item.image ? ( ) : (
暂无图片
)}
{item.product_name} {STORE_ORDER_STATUS_MAP[item.order_status]?.text}
订单:{item.order_no}
{item.remark ? (
备注:{item.remark}
) : null}
¥{item.price}
{item.quantity}
¥{item.amount}
{item.weight ?? '-'} 斤
{detail?.purchase.status === 0 ? ( ) : ( - )}
))}
)} {cellData.items.length > 0 && (
合计数量: {cellData.items.reduce((sum, item) => sum + item.quantity, 0)} 合计订货金额: ¥ {cellData.items .reduce((sum, item) => sum + Number(item.amount), 0) .toFixed(2)}
)} )}
{/* 单元格明细编辑 */} { setCellItemOpen(false); setCellItemTarget(null); }} onOk={() => cellItemForm.submit()} confirmLoading={cellItemSaving} okText="保存" destroyOnHidden >
订单 {cellItemTarget?.order_no};修改保存后,系统将自动重算明细金额、订货单与采购单汇总。
{/* 周转框/托盘合并记录下钻:按门店展示全部订单逐笔修改(采购单已完成则只读) */} { setContainerOpen(false); setContainerStore(null); }} onOk={() => containerForm.submit()} confirmLoading={containerSaving} okText="保存" okButtonProps={{ disabled: detail?.purchase.status !== 0 }} width={760} destroyOnHidden >
该门店在本采购单下的全部订单({containerStore?.order_count ?? 0} 笔): {detail?.purchase.status === 0 ? '逐笔修改后保存,系统将自动重算每笔订单的附加金额与订单总金额。' : '采购单已完成,仅可查看。'}
{(fields) => (
订单号
周转框(单价 ¥{containerStore?.box_price ?? '0.00'})
周转托盘(单价 ¥{containerStore?.tray_price ?? '0.00'})
{fields.map((field) => { const order = containerStore?.orders[field.name]; return (
{order?.order_no ?? '-'}
); })}
)}
周转框合计: {previewContainerBox} 周转托盘合计: {previewContainerTray}
附加金额合计: ¥{previewContainerAdded.toFixed(2)}
); }; export default PurchaseOrderPage;