240 lines
7.6 KiB
TypeScript
240 lines
7.6 KiB
TypeScript
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;
|