import React, { useRef, useState } from 'react'; import { Button, Descriptions, Drawer, Dropdown, InputNumber, message, Popconfirm, Table, Tag, Typography, } from 'antd'; import { CheckOutlined, DownloadOutlined } 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 { IPurchaseDetail, IPurchaseDetailRow, } from '@/domain/iPurchaseOrder.ts'; import { PURCHASE_STATUS_MAP } from '@/domain/iPurchaseOrder.ts'; import { exportPurchase, getPurchaseDetail, } from '@/api/purchase/order.ts'; import { Update } from '@/api/common/table.ts'; import AuthButton from '@/components/AuthButton'; const { Title, Text } = Typography; /** 行草稿:成本/称重 + 各门店单元格数量 */ interface RowDraft { cost_price?: number; weight?: number; cells: Record; } /** * 采购单管理(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 [drafts, setDrafts] = useState>({}); const [savingProductId, setSavingProductId] = useState(null); const [completing, setCompleting] = useState(false); const loadDetail = async (id: number) => { setDetailLoading(true); try { const res = await getPurchaseDetail(id); setDetail(res.data.data ?? null); setDrafts({}); } finally { setDetailLoading(false); } }; const openDetail = async (id: number) => { setDetailOpen(true); await loadDetail(id); }; const getDraft = (productId: number): RowDraft => drafts[productId] ?? { cells: {} }; const patchDraft = (productId: number, patch: Partial) => { setDrafts((prev) => { const current = prev[productId] ?? { cells: {} }; return { ...prev, [productId]: { ...current, ...patch, cells: { ...current.cells, ...patch.cells } }, }; }); }; /** 草稿成本(未修改取原值) */ const draftCost = (row: IPurchaseDetailRow): number => getDraft(row.product_id).cost_price ?? row.cost_price; const isRowDirty = (row: IPurchaseDetailRow): boolean => { // const draft = getDraft(row.product_id); // if (draft.cost_price !== undefined && draft.cost_price !== row.cost_price) { // return true; // } // if (draft.weight !== undefined && draft.weight !== row.weight) { // return true; // } // return row.cells.some( // (cell) => draft.cells[cell.order_item_id] !== undefined // && draft.cells[cell.order_item_id] !== cell.quantity, // ); }; /** 保存一行:先落各门店单元格数量,再落行级成本/称重,最后刷新 */ const saveRow = async (row: IPurchaseDetailRow) => { if (!detail || !isRowDirty(row)) { return; } const draft = getDraft(row.product_id); setSavingProductId(row.product_id); try { // for (const cell of row.cells) { // const qty = draft.cells[cell.order_item_id]; // if (qty !== undefined && qty !== cell.quantity) { // await updatePurchaseCell(cell.order_item_id, { quantity: qty }); // } // } // const rowPatch: { cost_price?: number; weight?: number } = {}; // if (draft.cost_price !== undefined && draft.cost_price !== row.cost_price) { // rowPatch.cost_price = draft.cost_price; // } // if (draft.weight !== undefined && draft.weight !== row.weight) { // rowPatch.weight = draft.weight; // } // if (Object.keys(rowPatch).length > 0) { // await updatePurchaseRow(detail.purchase.id!, row.product_id, rowPatch); // } message.success('已保存并同步订货明细'); await loadDetail(detail.purchase.id!); await tableRef.current?.reload(); } finally { setSavingProductId(null); } }; const handleComplete = async () => { if (!detail) { return; } setCompleting(true); try { await Update(`/purchase/order/${detail.purchase.id}`, { status: 3 }); message.success('采购单已标记完成'); await loadDetail(detail.purchase.id!); await tableRef.current?.reload(); } finally { setCompleting(false); } }; /** 明细矩阵列:品名/供应商/包规/单位/成本/单价 + 每门店一组(数量/金额)+ 合计 + 操作 */ const buildItemColumns = (): TableProps['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: 80, align: 'center', render: (_, row) => `¥${(Number(row.cost_price) / Number(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: 110, align: 'center', render: (v) => v || '-' } ]; const storeColumns = (detail?.stores ?? []).map((store) => ({ title: store.name, key: `store-${store.id}`, align: 'center' as const, render: (_: unknown, row: IPurchaseDetailRow) => { const cell = row.cells[store.id]; if (!cell) { return -; } return ( { row.cells[store.id] } ); }, })); const tail: NonNullable['columns']> = [ { title: '合计数量', key: 'total_quantity', width: 90, align: 'center', render: (_, row) => Object.values(row.cells).reduce((a, b) => a + b, 0), }, { title: '操作', key: 'action', width: 80, fixed: 'right', render: (_, row) => ( ), }, ]; return [...fixed, ...storeColumns, ...tail]; }; const columns: XinTableColumn[] = [ { title: '采购单号', dataIndex: 'purchase_no', valueType: 'text', hideInForm: true, render: (_, record) => {record.purchase_no}, }, { title: '采购日期', dataIndex: 'purchase_date', valueType: 'dateRange', hideInForm: true, align: 'center', render: (_, record) => record.purchase_date, }, { title: '预估金额', dataIndex: 'estimate_amount', hideInForm: true, hideInSearch: true, align: 'right', render: (_, record) => `¥${record.estimate_amount}`, }, { title: '实际金额', dataIndex: 'actual_amount', hideInForm: true, hideInSearch: true, align: 'right', render: (_, record) => Number(record.actual_amount) > 0 ? ( ¥{record.actual_amount} ) : ( 未录入 ), }, { 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) => [ , exportPurchase(record.id!, 'all', 'xlsx') }, { key: 'all-pdf', label: '全品类 PDF', onClick: () => exportPurchase(record.id!, 'all', 'pdf') }, { key: 'category-xlsx', label: '蔬果分类 Excel', onClick: () => exportPurchase(record.id!, 'category', 'xlsx') }, { key: 'category-pdf', label: '蔬果分类 PDF', onClick: () => exportPurchase(record.id!, 'category', 'pdf') }, ], }} > ) : undefined } > {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_weight}
单价 = 成本 ÷ 包规;门店金额 = 数量 × 单价;行级「实际称重」保存时按各店数量比例分摊到订货明细(称重>0 时金额按称重×单价计)。
rowKey="product_id" size="small" bordered columns={buildItemColumns()} dataSource={detail.items} pagination={false} scroll={{ x: 'max-content' }} /> ) : null} ); }; export default PurchaseOrderPage;