Files
xin-procurement/web/pages/recon/settlement.tsx
T
2026-07-23 20:41:25 +08:00

237 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useRef, useState } from 'react';
import {
Button,
Descriptions,
Drawer,
Dropdown,
Tag,
Typography,
} from 'antd';
import { DownloadOutlined } from '@ant-design/icons';
import XinTable from '@/components/XinTable';
import type {
XinTableColumn,
XinTableInstance,
XinTableProps,
} from '@/components/XinTable/typings.ts';
import type ISettlement from '@/domain/iSettlement.ts';
import { SETTLEMENT_STATUS_MAP } from '@/domain/iSettlement.ts';
import { getStoreOptions } from '@/api/customer/store.ts';
import type IStore from '@/domain/iStore.ts';
import { downloadSettlement } from '@/api/recon/settlement.ts';
import { Get } from '@/api/common/table.ts';
import AuthButton from '@/components/AuthButton';
const { Title, Text } = Typography;
/**
* 结算表(D9 生成于对账结算,D10 导出存档)
*/
const SettlementPage: React.FC = () => {
const tableRef = useRef<XinTableInstance<ISettlement>>(null);
const [stores, setStores] = useState<IStore[]>([]);
const [detailOpen, setDetailOpen] = useState(false);
const [detail, setDetail] = useState<ISettlement | null>(null);
const [detailLoading, setDetailLoading] = useState(false);
useEffect(() => {
getStoreOptions().then((res) => setStores(res.data.data ?? []));
}, []);
const openDetail = async (id: number) => {
setDetailOpen(true);
setDetailLoading(true);
try {
const res = await Get<ISettlement>('/recon/settlement', id);
setDetail(res.data.data ?? null);
} finally {
setDetailLoading(false);
}
};
const columns: XinTableColumn<ISettlement>[] = [
{
title: '结算单号',
dataIndex: 'settlement_no',
valueType: 'text',
hideInForm: true,
render: (_, record) => <Text copyable={{ text: record.settlement_no }}>{record.settlement_no}</Text>,
},
{
title: '门店',
dataIndex: 'store_id',
valueType: 'select',
hideInForm: true,
fieldProps: {
options: stores.map((s) => ({ label: s.name, value: s.id })),
showSearch: true,
optionFilterProp: 'label',
},
render: (_, record) => record.store?.name ?? '-',
},
{
title: '来源对账单',
dataIndex: 'recon',
hideInForm: true,
hideInSearch: true,
render: (_, record) =>
record.recon ? (
<span>
{record.recon.recon_no}
<Text type="secondary">{record.recon.title}</Text>
</span>
) : (
'-'
),
},
{
title: '结算周期',
dataIndex: 'period',
hideInForm: true,
hideInSearch: true,
render: (_, record) => `${record.period_start} ~ ${record.period_end}`,
},
{
title: '公布金额',
dataIndex: 'total_amount',
hideInForm: true,
hideInSearch: true,
align: 'right',
render: (_, record) => ${record.total_amount}`,
},
{
title: '实际金额',
dataIndex: 'actual_amount',
hideInForm: true,
hideInSearch: true,
align: 'right',
render: (_, record) => <Text strong>¥{record.actual_amount}</Text>,
},
{
title: '差额',
dataIndex: 'diff_amount',
hideInForm: true,
hideInSearch: true,
align: 'right',
render: (_, record) => {
const num = Number(record.diff_amount ?? 0);
return (
<Text type={num === 0 ? 'secondary' : 'danger'} strong={num !== 0}>
¥{record.diff_amount}
</Text>
);
},
},
{
title: '状态',
dataIndex: 'status',
valueType: 'select',
hideInForm: true,
fieldProps: {
options: Object.entries(SETTLEMENT_STATUS_MAP).map(([value, item]) => ({
value: Number(value),
label: item.text,
})),
},
render: (_, record) => {
const item = SETTLEMENT_STATUS_MAP[record.status ?? 0];
return <Tag color={item?.color}>{item?.text}</Tag>;
},
align: 'center',
},
{
title: '结算时间',
dataIndex: 'settled_at',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => record.settled_at ?? '-',
},
];
const operateRender: XinTableProps<ISettlement>['operateRender'] = (record) => [
<Button key="detail" size="small" onClick={() => openDetail(record.id!)}>
详情
</Button>,
<AuthButton key="download" auth="recon.settlement.download">
<Dropdown
menu={{
items: [
{ key: 'xlsx', label: '下载 Excel', onClick: () => downloadSettlement(record.id!, 'xlsx') },
{ key: 'pdf', label: '下载 PDF', onClick: () => downloadSettlement(record.id!, 'pdf') },
],
}}
>
<Button size="small" type="primary" ghost icon={<DownloadOutlined />}>
下载
</Button>
</Dropdown>
</AuthButton>,
];
const tableProps: XinTableProps<ISettlement> = {
api: '/recon/settlement',
columns,
rowKey: 'id',
accessName: 'recon.settlement',
tableRef,
operateRender,
formProps: false,
scroll: { x: 1200 },
};
return (
<>
<div className="mb-5">
<Title level={3}>结算表</Title>
<Text type="secondary">
由财务对账结算按门店聚合生成;支持 Excel / PDF 导出存档(回框统计表规则待业务确认后补充)。
</Text>
</div>
<XinTable<ISettlement> {...tableProps} />
<Drawer
title={detail ? `结算表 ${detail.settlement_no}` : '结算表详情'}
open={detailOpen}
onClose={() => setDetailOpen(false)}
width={640}
loading={detailLoading}
>
{detail ? (
<Descriptions column={2} size="small" bordered>
<Descriptions.Item label="门店">{detail.store?.name}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={SETTLEMENT_STATUS_MAP[detail.status ?? 0]?.color}>
{SETTLEMENT_STATUS_MAP[detail.status ?? 0]?.text}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="来源对账单">
{detail.recon?.recon_no ?? '-'}
</Descriptions.Item>
<Descriptions.Item label="结算周期">
{detail.period_start} ~ {detail.period_end}
</Descriptions.Item>
<Descriptions.Item label="公布金额">¥{detail.total_amount}</Descriptions.Item>
<Descriptions.Item label="实际金额">¥{detail.actual_amount}</Descriptions.Item>
<Descriptions.Item label="差额">¥{detail.diff_amount}</Descriptions.Item>
<Descriptions.Item label="结算时间">
{detail.settled_at ?? '-'}
</Descriptions.Item>
<Descriptions.Item label="存档文件" span={2}>
{detail.file_path ?? <Text type="secondary">未导出</Text>}
</Descriptions.Item>
{detail.remark ? (
<Descriptions.Item label="备注" span={2}>
{detail.remark}
</Descriptions.Item>
) : null}
</Descriptions>
) : null}
</Drawer>
</>
);
};
export default SettlementPage;