import React, { useEffect, useRef, useState } from 'react'; import { Button, Descriptions, Drawer, Input, InputNumber, message, Modal, Popconfirm, Space, Switch, Table, Tabs, Tag, Typography, } from 'antd'; import { CheckSquareOutlined, FileDoneOutlined, ToolOutlined, } 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 IReconciliation from '@/domain/iReconciliation.ts'; import type { IReconDiff, IReconciliationItem } from '@/domain/iReconciliation.ts'; import { RECON_STATUS_MAP } from '@/domain/iReconciliation.ts'; import type IProductCategory from '@/domain/iProductCategory.ts'; import type ISupplier from '@/domain/iSupplier.ts'; import { getCategoryTree } from '@/api/product/category.ts'; import { getSupplierOptions } from '@/api/customer/supplier.ts'; import { buildRecon, getReconDiff, remarkReconItem, settleRecon, toggleReconItem, updateReconItem, } from '@/api/recon/list.ts'; import { Get } from '@/api/common/table.ts'; import AuthButton from '@/components/AuthButton'; const { Title, Text } = Typography; interface EditingItem { product_name: string; quantity: number; weight: number; publish_amount: number; actual_amount: number; } /** * 财务对账(D1/D2 筛选建单、D4 明细修改、D5 差额对比、D6 备注、D8 标记、D9 结算) */ const ReconListPage: React.FC = () => { const tableRef = useRef>(null); const [categoryTree, setCategoryTree] = useState([]); const [suppliers, setSuppliers] = useState([]); // 工作台抽屉 const [workOpen, setWorkOpen] = useState(false); const [workLoading, setWorkLoading] = useState(false); const [recon, setRecon] = useState(null); const [editing, setEditing] = useState>({}); const [savingItemId, setSavingItemId] = useState(null); const [diff, setDiff] = useState(null); // 备注弹窗 const [remarkOpen, setRemarkOpen] = useState(false); const [remarkTarget, setRemarkTarget] = useState(null); const [remarkValue, setRemarkValue] = useState(''); useEffect(() => { getCategoryTree().then((res) => setCategoryTree(res.data.data ?? [])); getSupplierOptions().then((res) => setSuppliers(res.data.data ?? [])); }, []); const loadRecon = async (id: number) => { const res = await Get('/recon/list', id); const data = res.data.data ?? null; setRecon(data); const editingMap: Record = {}; data?.items?.forEach((item) => { if (item.id !== undefined) { editingMap[item.id] = { product_name: item.product_name ?? '', quantity: Number(item.quantity ?? 0), weight: Number(item.weight ?? 0), publish_amount: Number(item.publish_amount ?? 0), actual_amount: Number(item.actual_amount ?? 0), }; } }); setEditing(editingMap); return data; }; const openWorkbench = async (id: number) => { setWorkOpen(true); setWorkLoading(true); setDiff(null); try { await loadRecon(id); } finally { setWorkLoading(false); } }; const loadDiff = async (id: number) => { const res = await getReconDiff(id); setDiff(res.data.data ?? null); }; const handleBuild = async (record: IReconciliation) => { const res = await buildRecon(record.id!); message.success(`已生成 ${res.data.data?.count} 条对账明细`); await tableRef.current?.reload(); }; const handleSettle = async (record: IReconciliation) => { const res = await settleRecon(record.id!); message.success(`已生成 ${res.data.data?.count} 张结算表`); await tableRef.current?.reload(); }; const isItemDirty = (item: IReconciliationItem): boolean => { const edit = editing[item.id!]; if (!edit) { return false; } return ( edit.product_name !== (item.product_name ?? '') || edit.quantity !== Number(item.quantity ?? 0) || edit.weight !== Number(item.weight ?? 0) || edit.publish_amount !== Number(item.publish_amount ?? 0) || edit.actual_amount !== Number(item.actual_amount ?? 0) ); }; const saveItem = async (item: IReconciliationItem) => { const edit = editing[item.id!]; if (!edit || !isItemDirty(item)) { return; } setSavingItemId(item.id!); try { const res = await updateReconItem(item.id!, { product_name: edit.product_name, quantity: edit.quantity, weight: edit.weight, publish_amount: edit.publish_amount, actual_amount: edit.actual_amount, }); message.success(`已保存,差额 ¥${res.data.data?.diff_amount}`); await loadRecon(recon!.id!); await loadDiff(recon!.id!); } finally { setSavingItemId(null); } }; const handleToggle = async (item: IReconciliationItem) => { await toggleReconItem(item.id!); await loadRecon(recon!.id!); }; const openRemark = (item: IReconciliationItem) => { setRemarkTarget(item); setRemarkValue(item.store_remark ?? ''); setRemarkOpen(true); }; const saveRemark = async () => { await remarkReconItem(remarkTarget!.id!, remarkValue); message.success('备注已保存'); setRemarkOpen(false); await loadRecon(recon!.id!); }; const readonly = recon?.status === 2; const itemColumns: TableProps['columns'] = [ { title: '品名', dataIndex: 'product_name', width: 160, render: (_, record) => readonly ? ( record.product_name ) : ( setEditing((prev) => ({ ...prev, [record.id!]: { ...prev[record.id!], product_name: e.target.value }, })) } /> ), }, { title: '门店', dataIndex: 'store', width: 130, render: (_, record) => record.store?.name ?? `门店#${record.store_id}`, }, { title: '订货量', dataIndex: 'quantity', width: 110, render: (_, record) => readonly ? ( record.quantity ) : ( setEditing((prev) => ({ ...prev, [record.id!]: { ...prev[record.id!], quantity: v ?? 0 }, })) } className="!w-20" /> ), }, { title: '称重', dataIndex: 'weight', width: 110, render: (_, record) => readonly ? ( record.weight ) : ( setEditing((prev) => ({ ...prev, [record.id!]: { ...prev[record.id!], weight: v ?? 0 }, })) } className="!w-20" /> ), }, { title: '公布金额', dataIndex: 'publish_amount', width: 120, render: (_, record) => readonly ? ( `¥${record.publish_amount}` ) : ( setEditing((prev) => ({ ...prev, [record.id!]: { ...prev[record.id!], publish_amount: v ?? 0 }, })) } className="!w-24" /> ), }, { title: '实际金额', dataIndex: 'actual_amount', width: 120, render: (_, record) => readonly ? ( `¥${record.actual_amount}` ) : ( setEditing((prev) => ({ ...prev, [record.id!]: { ...prev[record.id!], actual_amount: v ?? 0 }, })) } className="!w-24" /> ), }, { title: '差额', dataIndex: 'diff_amount', width: 100, align: 'right', render: (v) => { const num = Number(v ?? 0); return ( ¥{String(v)} ); }, }, { title: '对账', dataIndex: 'is_reconciled', width: 80, align: 'center', render: (_, record) => ( handleToggle(record)} /> ), }, { title: '门店备注', dataIndex: 'store_remark', width: 120, ellipsis: true, render: (_, record) => record.store_remark || , }, { title: '操作', key: 'action', width: 130, fixed: 'right', render: (_, record) => readonly ? null : ( ), }, ]; const diffColumns = (nameTitle: string, nameKey: 'store_name' | 'product_name') => [ { title: nameTitle, dataIndex: nameKey, render: (v: string) => v || '-' }, { title: '公布金额', dataIndex: 'publish', align: 'right' as const, render: (v: number) => `¥${v}` }, { title: '实际金额', dataIndex: 'actual', align: 'right' as const, render: (v: number) => `¥${v}` }, { title: '差额', dataIndex: 'diff', align: 'right' as const, render: (v: number) => ( ¥{v} ), }, ]; const columns: XinTableColumn[] = [ { title: '对账单号', dataIndex: 'recon_no', valueType: 'text', hideInForm: true, }, { title: '标题', dataIndex: 'title', valueType: 'text', required: true, rules: [{ required: true, message: '请输入对账标题' }], }, { title: '对账周期', dataIndex: 'period', hideInForm: true, hideInSearch: true, render: (_, record) => `${record.period_start} ~ ${record.period_end}`, }, { title: '开始日期', dataIndex: 'period_start', valueType: 'date', hideInTable: true, required: true, rules: [{ required: true, message: '请选择开始日期' }], }, { title: '结束日期', dataIndex: 'period_end', valueType: 'date', hideInTable: true, required: true, rules: [{ required: true, message: '请选择结束日期' }], }, { title: '商品分类', dataIndex: 'category_id', valueType: 'treeSelect', hideInTable: true, initialValue: 0, fieldProps: { treeData: [{ id: 0, name: '全部分类', children: categoryTree }], fieldNames: { label: 'name', value: 'id', children: 'children' }, treeDefaultExpandAll: true, }, }, { title: '供应商', dataIndex: 'supplier_id', valueType: 'select', hideInTable: true, initialValue: 0, fieldProps: { options: [ { label: '全部供应商', value: 0 }, ...suppliers.map((s) => ({ label: s.name, value: s.id })), ], }, }, { title: '公布金额', dataIndex: 'publish_amount', hideInForm: true, hideInSearch: true, align: 'right', render: (_, record) => `¥${record.publish_amount}`, }, { title: '实际金额', dataIndex: 'actual_amount', hideInForm: true, hideInSearch: true, align: 'right', render: (_, record) => `¥${record.actual_amount}`, }, { title: '差额', dataIndex: 'diff_amount', hideInForm: true, hideInSearch: true, align: 'right', render: (_, record) => { const num = Number(record.diff_amount ?? 0); return ( ¥{record.diff_amount} ); }, }, { title: '状态', dataIndex: 'status', valueType: 'select', hideInForm: true, fieldProps: { options: Object.entries(RECON_STATUS_MAP).map(([value, item]) => ({ value: Number(value), label: item.text, })), }, render: (_, record) => { const item = RECON_STATUS_MAP[record.status ?? 0]; return {item?.text}; }, align: 'center', }, { title: '备注', dataIndex: 'remark', valueType: 'textarea', hideInTable: true, hideInSearch: true, fieldProps: { rows: 2 }, }, ]; const operateRender: XinTableProps['operateRender'] = (record, dom) => [ handleBuild(record)} > , , handleSettle(record)} > , // 编辑/删除由 XinTable 默认提供;删除仅草稿可用,由后端校验拦截 dom.edit, dom.del, ]; const tableProps: XinTableProps = { api: '/recon/list', columns, rowKey: 'id', accessName: 'recon.list', tableRef, operateRender, scroll: { x: 1300 }, formProps: { grid: true, colProps: { span: 12 }, layout: 'vertical', }, modalProps: { width: 720 }, }; return ( <>
财务对账 按周期/品类/供应商建立对账单 → 生成明细(采购分摊数据)→ 核对修改 → 差额对比 → 生成结算表。
{...tableProps} /> {/* 对账工作台 */} setWorkOpen(false)} width={1200} loading={workLoading} > {recon ? ( <> {recon.title} {recon.period_start} ~ {recon.period_end} {RECON_STATUS_MAP[recon.status ?? 0]?.text} ¥{recon.diff_amount} 明细核对({recon.items?.length ?? 0}) ), children: ( <> {!readonly ? (
可修改订货量/称重/公布金额/实际金额,保存后自动重算差额与对账单汇总;开关标记单品对账状态。
) : null} rowKey="id" size="small" columns={itemColumns} dataSource={recon.items ?? []} pagination={{ pageSize: 15, showSizeChanger: false }} scroll={{ x: 1250 }} /> ), }, { key: 'diff', label: '差额对比', children: ( <> {diff ? ( 合计:公布 ¥{diff.total.publish} / 实际 ¥{diff.total.actual} /{' '} 差额 ¥{diff.total.diff} ) : null} {diff ? (
按门店 String(row.store_id)} size="small" columns={diffColumns('门店', 'store_name')} dataSource={diff.by_store} pagination={false} />
按商品
String(row.product_id)} size="small" columns={diffColumns('商品', 'product_name')} dataSource={diff.by_product} pagination={false} /> ) : ( )} ), }, ]} /> ) : null} {/* 门店备注弹窗 */} setRemarkOpen(false)} onOk={saveRemark} okText="保存备注" destroyOnHidden >
{remarkTarget?.product_name} {remarkTarget?.store ? ` · ${remarkTarget.store.name}` : ''}
setRemarkValue(e.target.value)} placeholder="填写该单品针对该门店的备注(如质量异常、补货说明等)" />
); }; export default ReconListPage;