250 lines
7.7 KiB
TypeScript
250 lines
7.7 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { Button, DatePicker, Form, Modal, Select, Typography } from 'antd';
|
|
import { DownloadOutlined } from '@ant-design/icons';
|
|
import dayjs from 'dayjs';
|
|
import XinTable from '@/components/XinTable';
|
|
import type { XinTableColumn, XinTableProps } from '@/components/XinTable/typings.ts';
|
|
import type IContainerReturn from '@/domain/iContainerReturn.ts';
|
|
import { getStoreOptions } from '@/api/customer/store.ts';
|
|
import { exportContainerReturns } from '@/api/recon/containerReturn.ts';
|
|
import type IStore from '@/domain/iStore.ts';
|
|
import AuthButton from '@/components/AuthButton';
|
|
|
|
const { Title, Text } = Typography;
|
|
const { RangePicker } = DatePicker;
|
|
|
|
/** 汇总导出表单值 */
|
|
interface ExportFormValues {
|
|
date_range: [dayjs.Dayjs, dayjs.Dayjs];
|
|
store_ids?: number[];
|
|
}
|
|
|
|
/** 有符号数量展示:正数=压筐(橙色 +N),负数=回筐(绿色 N),0 置灰 */
|
|
const SignedNum: React.FC<{ value?: number }> = ({ value }) => {
|
|
const num = Number(value ?? 0);
|
|
if (num > 0) {
|
|
return <Text strong type="warning">+{num}</Text>;
|
|
}
|
|
if (num < 0) {
|
|
return <Text strong type="success">{num}</Text>;
|
|
}
|
|
return <Text type="secondary">0</Text>;
|
|
};
|
|
|
|
/** 有符号金额展示:正=压筐附加(橙),负=回筐抵扣(绿) */
|
|
const SignedAmount: React.FC<{ value?: string }> = ({ value }) => {
|
|
const num = Number(value ?? 0);
|
|
if (num > 0) {
|
|
return <Text strong type="warning">+¥{num.toFixed(2)}</Text>;
|
|
}
|
|
if (num < 0) {
|
|
return <Text strong type="success">-¥{Math.abs(num).toFixed(2)}</Text>;
|
|
}
|
|
return <Text type="secondary">¥0.00</Text>;
|
|
};
|
|
|
|
/**
|
|
* 压回筐记录(周转筐/托盘跟账单走:生成账单时自动写入完整快照,只读)
|
|
* 数量正数=压筐(附加金额),负数=回筐(抵扣金额)
|
|
*/
|
|
const ContainerReturnPage: React.FC = () => {
|
|
const [stores, setStores] = useState<IStore[]>([]);
|
|
|
|
// 汇总导出(日期区间 + 门店:行=日期,列=门店,单元格为抵扣/附加金额)
|
|
const [exportOpen, setExportOpen] = useState(false);
|
|
const [exporting, setExporting] = useState(false);
|
|
const [exportForm] = Form.useForm<ExportFormValues>();
|
|
|
|
useEffect(() => {
|
|
getStoreOptions().then((res) => setStores(res.data.data ?? []));
|
|
}, []);
|
|
|
|
/** 提交汇总导出:下载失败时 downloadBlob 内部已提示 */
|
|
const handleExport = async (values: ExportFormValues) => {
|
|
const [start, end] = values.date_range;
|
|
setExporting(true);
|
|
try {
|
|
await exportContainerReturns(
|
|
start.format('YYYY-MM-DD'),
|
|
end.format('YYYY-MM-DD'),
|
|
values.store_ids ?? []
|
|
);
|
|
setExportOpen(false);
|
|
} finally {
|
|
setExporting(false);
|
|
}
|
|
};
|
|
|
|
const columns: XinTableColumn<IContainerReturn>[] = [
|
|
{
|
|
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 ?? `门店#${record.store_id}`,
|
|
},
|
|
{
|
|
title: '关联账单',
|
|
dataIndex: 'bill_id',
|
|
hideInForm: true,
|
|
hideInSearch: true,
|
|
render: (_, record) => record.bill ? (
|
|
<>
|
|
<Text copyable={{ text: record.bill.bill_no }}>{record.bill.bill_no}</Text>
|
|
<div className={'text-[12px] text-[#999]'}>{record.bill.bill_date}</div>
|
|
</>
|
|
) : '-',
|
|
},
|
|
{
|
|
title: '压(回)筐数量',
|
|
dataIndex: 'box_num',
|
|
hideInForm: true,
|
|
hideInSearch: true,
|
|
align: 'center',
|
|
render: (_, record) => <SignedNum value={record.box_num} />,
|
|
},
|
|
{
|
|
title: '压(回)托盘数量',
|
|
dataIndex: 'tray_num',
|
|
hideInForm: true,
|
|
hideInSearch: true,
|
|
align: 'center',
|
|
render: (_, record) => <SignedNum value={record.tray_num} />,
|
|
},
|
|
{
|
|
title: '筐单价',
|
|
dataIndex: 'box_price',
|
|
hideInForm: true,
|
|
hideInSearch: true,
|
|
align: 'center',
|
|
render: (_, record) => `¥${Number(record.box_price ?? 0).toFixed(2)}`,
|
|
},
|
|
{
|
|
title: '托盘单价',
|
|
dataIndex: 'tray_price',
|
|
hideInForm: true,
|
|
hideInSearch: true,
|
|
align: 'center',
|
|
render: (_, record) => `¥${Number(record.tray_price ?? 0).toFixed(2)}`,
|
|
},
|
|
{
|
|
title: '抵扣(附加)金额',
|
|
dataIndex: 'amount',
|
|
hideInForm: true,
|
|
hideInSearch: true,
|
|
align: 'center',
|
|
render: (_, record) => <SignedAmount value={record.amount} />,
|
|
},
|
|
{
|
|
title: '操作人',
|
|
dataIndex: 'operator',
|
|
hideInForm: true,
|
|
hideInSearch: true,
|
|
align: 'center',
|
|
render: (_, record) => record.operator?.nickname ?? '-',
|
|
},
|
|
{
|
|
title: '记录时间',
|
|
dataIndex: 'created_at',
|
|
valueType: 'dateRange',
|
|
hideInForm: true,
|
|
hideInTable: true,
|
|
align: 'center',
|
|
},
|
|
{
|
|
title: '记录时间',
|
|
dataIndex: 'created_at',
|
|
hideInForm: true,
|
|
hideInSearch: true,
|
|
align: 'center',
|
|
},
|
|
];
|
|
|
|
const tableProps: XinTableProps<IContainerReturn> = {
|
|
api: '/recon/container-return',
|
|
columns,
|
|
rowKey: 'id',
|
|
accessName: 'recon.containerReturn',
|
|
// 记录随账单生成,只读:不新增、不编辑、不删除
|
|
addShow: false,
|
|
editShow: false,
|
|
deleteShow: false,
|
|
formProps: false,
|
|
toolBarRender: (dom) => [
|
|
<AuthButton key="export" auth="recon.containerReturn.export">
|
|
<Button icon={<DownloadOutlined />} onClick={() => setExportOpen(true)}>
|
|
导出
|
|
</Button>
|
|
</AuthButton>,
|
|
dom.columnSetting,
|
|
dom.hideBorder,
|
|
dom.reload,
|
|
dom.columnHeight,
|
|
],
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="mb-5">
|
|
<Title level={3}>回筐记录</Title>
|
|
<Text type="secondary">
|
|
周转筐/托盘跟账单走:生成账单时按填写数量自动写入;正数=压筐(附加金额),负数=回筐(抵扣金额),数量为 0 不生成记录;
|
|
可按日期区间与门店汇总导出(行=日期,列=门店,含行列合计)。
|
|
</Text>
|
|
</div>
|
|
<XinTable<IContainerReturn> {...tableProps} />
|
|
|
|
{/* 汇总导出:日期区间 + 门店,同日记录合并,无记录填 0 */}
|
|
<Modal
|
|
title="导出回筐记录"
|
|
open={exportOpen}
|
|
onCancel={() => setExportOpen(false)}
|
|
onOk={() => exportForm.submit()}
|
|
confirmLoading={exporting}
|
|
okText="导出"
|
|
destroyOnHidden
|
|
>
|
|
<div className="py-2 text-gray-500">
|
|
按日期区间与门店导出抵扣(附加)金额汇总表:行=日期(同日记录合并),列=门店,
|
|
当天门店无记录填 0,含行合计与列合计。门店不选默认导出全部门店。
|
|
</div>
|
|
<Form
|
|
form={exportForm}
|
|
layout="vertical"
|
|
onFinish={handleExport}
|
|
initialValues={{
|
|
date_range: [dayjs().startOf('month'), dayjs()],
|
|
store_ids: [],
|
|
}}
|
|
>
|
|
<Form.Item
|
|
label="日期区间"
|
|
name="date_range"
|
|
rules={[{ required: true, message: '请选择日期区间' }]}
|
|
>
|
|
<RangePicker className="w-full" allowClear={false} />
|
|
</Form.Item>
|
|
<Form.Item label="门店" name="store_ids">
|
|
<Select
|
|
mode="multiple"
|
|
allowClear
|
|
maxTagCount="responsive"
|
|
placeholder="全部门店"
|
|
showSearch
|
|
optionFilterProp="label"
|
|
options={stores.map((s) => ({ label: s.name, value: s.id }))}
|
|
/>
|
|
</Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default ContainerReturnPage;
|