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

240 lines
7.6 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,
Table,
Tag,
Typography,
} from 'antd';
import type { TableProps } from 'antd';
import XinTable from '@/components/XinTable';
import type {
XinTableColumn,
XinTableInstance,
XinTableProps,
} from '@/components/XinTable/typings.ts';
import type IStatement from '@/domain/iStatement.ts';
import type { IStatementItem } from '@/domain/iStatement.ts';
import { STATEMENT_STATUS_MAP } from '@/domain/iStatement.ts';
import { RECONCILED_MAP } from '@/domain/iReconciliation.ts';
import { getStoreOptions } from '@/api/customer/store.ts';
import type IStore from '@/domain/iStore.ts';
import { Get } from '@/api/common/table.ts';
const { Title, Text } = Typography;
/**
* 门店对账单(后台只读视角;生成/导出在小程序端)
*/
const StatementPage: React.FC = () => {
const tableRef = useRef<XinTableInstance<IStatement>>(null);
const [stores, setStores] = useState<IStore[]>([]);
const [detailOpen, setDetailOpen] = useState(false);
const [detail, setDetail] = useState<IStatement | 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<IStatement>('/recon/statement', id);
setDetail(res.data.data ?? null);
} finally {
setDetailLoading(false);
}
};
const itemColumns: TableProps<IStatementItem>['columns'] = [
{ title: '品名', dataIndex: 'product_name' },
{ title: '单价', dataIndex: 'price', align: 'right', render: (v) => ${v}` },
{ title: '数量', dataIndex: 'quantity', align: 'right' },
{ title: '重量', dataIndex: 'weight', align: 'right' },
{ title: '金额', dataIndex: 'amount', align: 'right', render: (v) => ${v}` },
{
title: '对账状态',
dataIndex: 'is_reconciled',
align: 'center',
render: (v) => {
const item = RECONCILED_MAP[Number(v ?? 0)];
return <Tag color={item?.color}>{item?.text}</Tag>;
},
},
{ title: '备注', dataIndex: 'store_remark', render: (v) => v || '-' },
];
const columns: XinTableColumn<IStatement>[] = [
{
title: '对账单号',
dataIndex: 'statement_no',
valueType: 'text',
hideInForm: true,
render: (_, record) => <Text copyable={{ text: record.statement_no }}>{record.statement_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: 'period_start',
valueType: 'dateRange',
hideInForm: true,
render: (_, record) => `${record.period_start} ~ ${record.period_end}`,
},
{
title: '总金额',
dataIndex: 'total_amount',
hideInForm: true,
hideInSearch: true,
align: 'right',
render: (_, record) => <Text strong>¥{record.total_amount}</Text>,
},
{
title: '回款周期',
dataIndex: 'payment_cycle_days',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => `${record.payment_cycle_days} 天`,
},
{
title: '应结算日期',
dataIndex: 'settlement_date',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => {
const overdue =
record.status !== 2 && record.settlement_date
? new Date(record.settlement_date).getTime() < Date.now()
: false;
return (
<Text type={overdue ? 'danger' : undefined} strong={overdue}>
{record.settlement_date}
{overdue ? '(逾期)' : ''}
</Text>
);
},
},
{
title: '状态',
dataIndex: 'status',
valueType: 'select',
hideInForm: true,
fieldProps: {
options: Object.entries(STATEMENT_STATUS_MAP).map(([value, item]) => ({
value: Number(value),
label: item.text,
})),
},
render: (_, record) => {
const item = STATEMENT_STATUS_MAP[record.status ?? 0];
return <Tag color={item?.color}>{item?.text}</Tag>;
},
align: 'center',
},
];
const operateRender: XinTableProps<IStatement>['operateRender'] = (record) => [
<Button key="detail" size="small" onClick={() => openDetail(record.id!)}>
详情
</Button>,
];
const tableProps: XinTableProps<IStatement> = {
api: '/recon/statement',
columns,
rowKey: 'id',
accessName: 'recon.statement',
tableRef,
operateRender,
formProps: false,
actionBarRender: (dom) => [dom.search, dom.keywordSearch],
};
return (
<>
<div className="mb-5">
<Title level={3}>门店对账单</Title>
<Text type="secondary">
门店在小程序端自助生成;应结算日期 = 对账周期结束日 + 门店回款周期(生成时快照),逾期高亮提示。
</Text>
</div>
<XinTable<IStatement> {...tableProps} />
<Drawer
title={detail ? `对账单 ${detail.statement_no}` : '对账单详情'}
open={detailOpen}
onClose={() => setDetailOpen(false)}
width={860}
loading={detailLoading}
>
{detail ? (
<>
<Descriptions column={2} size="small" bordered>
<Descriptions.Item label="门店">{detail.store?.name}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={STATEMENT_STATUS_MAP[detail.status ?? 0]?.color}>
{STATEMENT_STATUS_MAP[detail.status ?? 0]?.text}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="对账周期">
{detail.period_start} ~ {detail.period_end}
</Descriptions.Item>
<Descriptions.Item label="回款周期(快照)">
{detail.payment_cycle_days}
</Descriptions.Item>
<Descriptions.Item label="应结算日期">
{detail.settlement_date}
</Descriptions.Item>
<Descriptions.Item label="总金额">¥{detail.total_amount}</Descriptions.Item>
{detail.remark ? (
<Descriptions.Item label="备注" span={2}>
{detail.remark}
</Descriptions.Item>
) : null}
</Descriptions>
<Title level={5} className="!mt-6 !mb-3">
对账明细({detail.items?.length ?? 0}
</Title>
<Table<IStatementItem>
rowKey="id"
size="small"
columns={itemColumns}
dataSource={detail.items ?? []}
pagination={false}
summary={() => (
<Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={4} align="right">
合计
</Table.Summary.Cell>
<Table.Summary.Cell index={1} align="right">
<Text strong>¥{detail.total_amount}</Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={2} colSpan={2} />
</Table.Summary.Row>
)}
/>
</>
) : null}
</Drawer>
</>
);
};
export default StatementPage;