341 lines
9.6 KiB
TypeScript
341 lines
9.6 KiB
TypeScript
import React from 'react';
|
|
import {
|
|
Button,
|
|
Empty,
|
|
Popconfirm,
|
|
Space,
|
|
Table,
|
|
Tag,
|
|
Tooltip,
|
|
Upload,
|
|
} from 'antd';
|
|
import {
|
|
CheckOutlined,
|
|
FileTextOutlined,
|
|
StopOutlined,
|
|
UploadOutlined,
|
|
} from '@ant-design/icons';
|
|
import dayjs from 'dayjs';
|
|
import PermissionButton from '../../components/PermissionButton';
|
|
import EditableCell from '../../components/EditableCell';
|
|
import { message } from '../../ui/app-message';
|
|
|
|
const RENTAL_FIELDS = {
|
|
classroomId: 'classroomId',
|
|
lesseeOrganizationId: 'lesseeOrganizationId',
|
|
startDate: 'startDate',
|
|
endDate: 'endDate',
|
|
dailyRate: 'dailyRate',
|
|
totalAmount: 'totalAmount',
|
|
} as const;
|
|
|
|
export interface RentalTableProps {
|
|
data: any[];
|
|
loading: boolean;
|
|
classrooms: any[];
|
|
organizations: any[];
|
|
canPurgeRental: boolean;
|
|
hasPermission: (permission: string) => boolean;
|
|
onSaveCell: (record: any, field: string, value: unknown) => Promise<void> | void;
|
|
onEdit: (record: any) => void;
|
|
onAction: (id: number, action: 'cancel' | 'end') => void;
|
|
onArchive: (id: number) => void;
|
|
onPurge: (id: number, name: string) => void;
|
|
onDownloadContract: (id: number, filename?: string) => void;
|
|
onDeleteContract: (id: number) => void;
|
|
onUploadContract: (id: number, formData: FormData) => Promise<unknown>;
|
|
}
|
|
|
|
export const RentalTable: React.FC<RentalTableProps> = ({
|
|
data,
|
|
loading,
|
|
classrooms,
|
|
organizations,
|
|
canPurgeRental,
|
|
hasPermission,
|
|
onSaveCell,
|
|
onEdit,
|
|
onAction,
|
|
onArchive,
|
|
onPurge,
|
|
onDownloadContract,
|
|
onDeleteContract,
|
|
onUploadContract,
|
|
}) => {
|
|
const EditableRentalCell = <R extends { id: number; effectiveStatus?: string }>({
|
|
value,
|
|
field,
|
|
record,
|
|
editor,
|
|
min,
|
|
required,
|
|
options,
|
|
children,
|
|
}: {
|
|
value: unknown;
|
|
field: string;
|
|
record: R;
|
|
editor?: React.ComponentProps<typeof EditableCell>['editor'];
|
|
min?: number;
|
|
required?: boolean;
|
|
options?: Array<{ value: string | number; label: string }>;
|
|
children?: React.ReactNode;
|
|
}) => (
|
|
<EditableCell
|
|
value={value}
|
|
editor={editor}
|
|
min={min}
|
|
required={required}
|
|
options={options}
|
|
permission="rental:edit"
|
|
disabled={record.effectiveStatus !== 'active'}
|
|
onSave={async (next) => {
|
|
await onSaveCell(record, field, next);
|
|
}}
|
|
>
|
|
{children ?? String(value ?? '-')}
|
|
</EditableCell>
|
|
);
|
|
|
|
const columns = [
|
|
{
|
|
title: '教室',
|
|
width: 120,
|
|
dataIndex: 'classroom',
|
|
render: (c: any, r: any) => (
|
|
<EditableRentalCell
|
|
value={r.classroomId}
|
|
field={RENTAL_FIELDS.classroomId}
|
|
record={r}
|
|
editor="select"
|
|
options={classrooms
|
|
.filter((item) => item.status !== 'archived')
|
|
.map((item) => ({
|
|
value: item.id,
|
|
label: item.building ? `${item.building} · ${item.name}` : item.name,
|
|
}))}
|
|
required
|
|
>
|
|
{c ? (
|
|
<span>
|
|
{c.building ? `${c.building} · ` : ''}
|
|
{c.name}
|
|
</span>
|
|
) : (
|
|
'-'
|
|
)}
|
|
</EditableRentalCell>
|
|
),
|
|
},
|
|
{
|
|
title: '承租机构',
|
|
width: 100,
|
|
dataIndex: 'lesseeOrganization',
|
|
render: (t: any, r: any) => (
|
|
<EditableRentalCell
|
|
value={r.lesseeOrganizationId}
|
|
field={RENTAL_FIELDS.lesseeOrganizationId}
|
|
record={r}
|
|
editor="select"
|
|
options={organizations
|
|
.filter((item) => item.status !== 'archived')
|
|
.map((item) => ({ value: item.id, label: item.name }))}
|
|
required
|
|
>
|
|
{t ? (
|
|
<Tag
|
|
color={t.color}
|
|
style={{ background: t.color, color: '#fff', borderColor: t.color }}
|
|
>
|
|
{t.name}
|
|
</Tag>
|
|
) : (
|
|
'-'
|
|
)}
|
|
</EditableRentalCell>
|
|
),
|
|
},
|
|
{
|
|
title: '开始日期',
|
|
dataIndex: 'startDate',
|
|
width: 110,
|
|
render: (v: string, r: any) => (
|
|
<EditableRentalCell value={v} field={RENTAL_FIELDS.startDate} record={r} editor="date" required>
|
|
{v}
|
|
</EditableRentalCell>
|
|
),
|
|
},
|
|
{
|
|
title: '结束日期',
|
|
dataIndex: 'endDate',
|
|
width: 110,
|
|
render: (v: string, r: any) => (
|
|
<EditableRentalCell value={v} field={RENTAL_FIELDS.endDate} record={r} editor="date" required>
|
|
{v}
|
|
</EditableRentalCell>
|
|
),
|
|
},
|
|
{
|
|
title: '时长',
|
|
width: 80,
|
|
render: (_: any, r: any) => {
|
|
const d = dayjs(r.endDate).diff(dayjs(r.startDate), 'day') + 1;
|
|
return `${d}天`;
|
|
},
|
|
},
|
|
{
|
|
title: '日租金',
|
|
dataIndex: 'dailyRate',
|
|
width: 100,
|
|
render: (v: any, r: any) => (
|
|
<EditableRentalCell value={v} field={RENTAL_FIELDS.dailyRate} record={r} editor="money" min={0.01}>
|
|
{v ? `¥${v}` : '-'}
|
|
</EditableRentalCell>
|
|
),
|
|
},
|
|
{
|
|
title: '总额',
|
|
dataIndex: 'totalAmount',
|
|
width: 100,
|
|
render: (v: any, r: any) => (
|
|
<EditableRentalCell value={v} field={RENTAL_FIELDS.totalAmount} record={r} editor="money" min={0.01}>
|
|
{v ? `¥${v}` : '-'}
|
|
</EditableRentalCell>
|
|
),
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'effectiveStatus',
|
|
width: 90,
|
|
render: (status: string) => {
|
|
const config: Record<string, { text: string; color: string }> = {
|
|
active: { text: '进行中', color: 'green' },
|
|
ended: { text: '已结束', color: 'default' },
|
|
cancelled: { text: '已取消', color: 'red' },
|
|
};
|
|
return <Tag color={config[status]?.color}>{config[status]?.text || status}</Tag>;
|
|
},
|
|
},
|
|
{
|
|
title: '合同',
|
|
width: 120,
|
|
dataIndex: 'contractPath',
|
|
render: (v: string, r: any) =>
|
|
v ? (
|
|
<Space>
|
|
<Tooltip title={r.contractOriginalName}>
|
|
<Button
|
|
size="small"
|
|
icon={<FileTextOutlined />}
|
|
onClick={() => onDownloadContract(r.id, r.contractOriginalName)}
|
|
>
|
|
下载
|
|
</Button>
|
|
</Tooltip>
|
|
{hasPermission('rental:edit') ? (
|
|
<Popconfirm title="移除合同文件?" onConfirm={() => onDeleteContract(r.id)}>
|
|
<Button size="small" danger icon={<StopOutlined />} aria-label="移除合同文件" />
|
|
</Popconfirm>
|
|
) : null}
|
|
</Space>
|
|
) : hasPermission('rental:edit') ? (
|
|
<Upload
|
|
accept="application/pdf"
|
|
showUploadList={false}
|
|
customRequest={async ({ file, onSuccess, onError }: any) => {
|
|
if (file.size > 10 * 1024 * 1024) {
|
|
message.error('文件不能超过 10MB');
|
|
onError?.(new Error('size'));
|
|
return;
|
|
}
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
try {
|
|
await onUploadContract(r.id, formData);
|
|
message.success('合同已上传');
|
|
onSuccess?.({});
|
|
} catch (e) {
|
|
onError?.(e as Error);
|
|
}
|
|
}}
|
|
>
|
|
<Button size="small" icon={<UploadOutlined />}>
|
|
上传PDF
|
|
</Button>
|
|
</Upload>
|
|
) : (
|
|
'-'
|
|
),
|
|
},
|
|
{
|
|
title: '操作',
|
|
width: 150,
|
|
render: (_: any, record: any) => (
|
|
<Space>
|
|
{record.effectiveStatus === 'active' && (
|
|
<>
|
|
<PermissionButton permission="rental:edit" size="small" onClick={() => onEdit(record)}>
|
|
编辑
|
|
</PermissionButton>
|
|
<Popconfirm title="确定取消该租赁?" onConfirm={() => onAction(record.id, 'cancel')}>
|
|
<PermissionButton
|
|
permission="rental:edit"
|
|
size="small"
|
|
danger
|
|
icon={<StopOutlined />}
|
|
>
|
|
取消
|
|
</PermissionButton>
|
|
</Popconfirm>
|
|
{!dayjs(record.startDate).isAfter(dayjs(), 'day') && (
|
|
<Popconfirm title="确定今天结束该租赁?" onConfirm={() => onAction(record.id, 'end')}>
|
|
<PermissionButton permission="rental:edit" size="small" icon={<CheckOutlined />}>
|
|
结束
|
|
</PermissionButton>
|
|
</Popconfirm>
|
|
)}
|
|
</>
|
|
)}
|
|
{record.effectiveStatus !== 'active' && (
|
|
<Popconfirm
|
|
title="确定归档该租赁订单?合同文件会保留。"
|
|
onConfirm={() => onArchive(record.id)}
|
|
>
|
|
<PermissionButton permission="rental:delete" size="small" danger>
|
|
归档
|
|
</PermissionButton>
|
|
</Popconfirm>
|
|
)}
|
|
{record.status === 'cancelled' && canPurgeRental ? (
|
|
<Button
|
|
size="small"
|
|
danger
|
|
type="link"
|
|
onClick={() => onPurge(record.id, record.lesseeOrganization?.name || `订单${record.id}`)}
|
|
>
|
|
删除
|
|
</Button>
|
|
) : null}
|
|
</Space>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<Table
|
|
columns={columns}
|
|
dataSource={data}
|
|
rowKey="id"
|
|
loading={loading}
|
|
locale={{ emptyText: <Empty description="暂无数据" /> }}
|
|
pagination={{
|
|
defaultPageSize: 15,
|
|
showSizeChanger: true,
|
|
pageSizeOptions: [15, 30, 50, 100],
|
|
showTotal: (total) => `共 ${total} 条`,
|
|
}}
|
|
scroll={{ x: 1200 }}
|
|
/>
|
|
);
|
|
};
|