645 lines
19 KiB
TypeScript
645 lines
19 KiB
TypeScript
import IconFont from '@/components/IconFont';
|
|
import TreeGroup from '@/components/shared/TreeGroup';
|
|
import { DEFAULT_PAGE_SIZE } from '@/const';
|
|
import {
|
|
SGW_ROUTE_BANZONES,
|
|
SGW_ROUTE_BANZONES_LIST,
|
|
} from '@/constants/slave/sgw/routes';
|
|
|
|
import {
|
|
apiGetAllBanzones,
|
|
apiRemoveBanzone,
|
|
} from '@/services/slave/sgw/ZoneController';
|
|
import { flattenGroupNodes } from '@/utils/slave/sgw/groupUtils';
|
|
import { formatDate } from '@/utils/slave/sgw/timeUtils';
|
|
import { DeleteOutlined, DownOutlined, EditOutlined } from '@ant-design/icons';
|
|
import {
|
|
ActionType,
|
|
ProCard,
|
|
ProColumns,
|
|
ProTable,
|
|
} from '@ant-design/pro-components';
|
|
import { FormattedMessage, history, useIntl, useModel } from '@umijs/max';
|
|
import {
|
|
Button,
|
|
Dropdown,
|
|
Flex,
|
|
Grid,
|
|
message,
|
|
Popconfirm,
|
|
Space,
|
|
Tag,
|
|
Tooltip,
|
|
Typography,
|
|
} from 'antd';
|
|
import { MenuProps } from 'antd/lib';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
const { Paragraph, Text } = Typography;
|
|
|
|
const BanZoneList = () => {
|
|
const { useBreakpoint } = Grid;
|
|
const intl = useIntl();
|
|
const screens = useBreakpoint();
|
|
const tableRef = useRef<ActionType>();
|
|
const [groupCheckedKeys, setGroupCheckedKeys] = useState<string>('');
|
|
const { groups, getGroups } = useModel('master.useGroups');
|
|
const groupFlattened = flattenGroupNodes(groups || []);
|
|
const [messageApi, contextHolder] = message.useMessage();
|
|
const [selectedRowsState, setSelectedRows] = useState<SgwModel.Banzone[]>([]);
|
|
useEffect(() => {
|
|
if (groups === null) {
|
|
getGroups();
|
|
}
|
|
}, [groups]);
|
|
|
|
// Reload table khi groups được load
|
|
useEffect(() => {
|
|
if (groups && groups.length > 0 && tableRef.current) {
|
|
tableRef.current.reload();
|
|
}
|
|
}, [groups]);
|
|
|
|
const handleEdit = (record: SgwModel.Banzone) => {
|
|
console.log('record: ', record);
|
|
let geomType = 1; // Default: Polygon
|
|
try {
|
|
if (record.geometry) {
|
|
const geometry: SgwModel.Geom = JSON.parse(record.geometry);
|
|
geomType = geometry.geom_type || 1;
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to parse geometry:', e);
|
|
}
|
|
history.push(`${SGW_ROUTE_BANZONES_LIST}/${record.id}`, {
|
|
type: 'update',
|
|
shape: geomType,
|
|
});
|
|
};
|
|
const handleDelete = async (record: SgwModel.Banzone) => {
|
|
try {
|
|
const groupID = groupFlattened.find(
|
|
(m) => m.metadata.code === record.province_code,
|
|
)?.id;
|
|
await apiRemoveBanzone(record.id || '', groupID || '');
|
|
messageApi.success(
|
|
intl.formatMessage({
|
|
id: 'banzone.notify.delete_zone_success',
|
|
defaultMessage: 'Zone deleted successfully',
|
|
}),
|
|
);
|
|
// Reload lại bảng
|
|
if (tableRef.current) {
|
|
tableRef.current.reload();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting area:', error);
|
|
messageApi.error(
|
|
intl.formatMessage({
|
|
id: 'banzone.notify.fail',
|
|
defaultMessage: 'Delete zone failed!',
|
|
}),
|
|
);
|
|
}
|
|
};
|
|
const columns: ProColumns<SgwModel.Banzone>[] = [
|
|
{
|
|
key: 'name',
|
|
title: <FormattedMessage id="banzones.name" defaultMessage="Name" />,
|
|
dataIndex: 'name',
|
|
render: (_, record) => (
|
|
<div
|
|
style={{
|
|
whiteSpace: 'normal',
|
|
wordBreak: 'break-word',
|
|
}}
|
|
>
|
|
<Paragraph
|
|
copyable
|
|
style={{ margin: 0 }}
|
|
ellipsis={{ rows: 999, tooltip: record?.name }}
|
|
>
|
|
{record?.name}
|
|
</Paragraph>
|
|
</div>
|
|
),
|
|
width: '15%',
|
|
},
|
|
{
|
|
key: 'group',
|
|
title: <FormattedMessage id="banzones.area" defaultMessage="Province" />,
|
|
dataIndex: 'province_code',
|
|
hideInSearch: true,
|
|
responsive: ['lg', 'md'],
|
|
ellipsis: true,
|
|
render: (_, record) => {
|
|
const matchedMember =
|
|
groupFlattened.find(
|
|
(group) => group.metadata.code === record.province_code,
|
|
) ?? null;
|
|
return (
|
|
<Text ellipsis={{ tooltip: matchedMember?.name || '-' }}>
|
|
{matchedMember?.name || '-'}
|
|
</Text>
|
|
);
|
|
},
|
|
width: '15%',
|
|
},
|
|
{
|
|
key: 'description',
|
|
title: (
|
|
<FormattedMessage id="banzones.description" defaultMessage="Mô tả" />
|
|
),
|
|
dataIndex: 'description',
|
|
hideInSearch: true,
|
|
render: (_, record) => (
|
|
<Paragraph
|
|
ellipsis={{ rows: 2, tooltip: record?.description }}
|
|
style={{
|
|
margin: 0,
|
|
whiteSpace: 'pre-wrap',
|
|
wordBreak: 'break-word',
|
|
}}
|
|
>
|
|
{record?.description || '-'}
|
|
</Paragraph>
|
|
),
|
|
width: '15%',
|
|
},
|
|
{
|
|
key: 'type',
|
|
title: <FormattedMessage id="banzones.type" defaultMessage="Loại" />,
|
|
dataIndex: 'type',
|
|
valueType: 'select',
|
|
fieldProps: {
|
|
options: [
|
|
{
|
|
label: intl.formatMessage({
|
|
id: 'banzone.area.fishing_ban',
|
|
defaultMessage: 'Fishing Ban',
|
|
}),
|
|
value: 1,
|
|
},
|
|
{
|
|
label: intl.formatMessage({
|
|
id: 'banzone.area.move_ban',
|
|
defaultMessage: 'Movement Ban',
|
|
}),
|
|
value: 2,
|
|
},
|
|
{
|
|
label: intl.formatMessage({
|
|
id: 'banzone.area.safe',
|
|
defaultMessage: 'Safe Area',
|
|
}),
|
|
value: 3,
|
|
},
|
|
],
|
|
},
|
|
render: (_, record) => (
|
|
<Tag color={record.type === 1 ? '#f50' : 'orange'}>
|
|
{record.type === 1
|
|
? intl.formatMessage({
|
|
id: 'banzone.area.fishing_ban',
|
|
defaultMessage: 'Fishing Ban',
|
|
})
|
|
: intl.formatMessage({
|
|
id: 'banzone.area.move_ban',
|
|
defaultMessage: 'Movement Ban',
|
|
})}
|
|
</Tag>
|
|
),
|
|
width: 120,
|
|
},
|
|
{
|
|
key: 'conditions',
|
|
title: (
|
|
<FormattedMessage id="banzones.conditions" defaultMessage="Điều kiện" />
|
|
),
|
|
dataIndex: 'conditions',
|
|
hideInSearch: true,
|
|
render: (conditions) => {
|
|
if (!Array.isArray(conditions)) return null;
|
|
return (
|
|
<Space direction="vertical" size={4}>
|
|
{conditions.map((cond, index) => {
|
|
switch (cond.type) {
|
|
case 'month_range':
|
|
return (
|
|
<Tooltip
|
|
key={index}
|
|
title={`Áp dụng từ tháng ${cond.from} đến tháng ${cond.to} hàng năm`}
|
|
>
|
|
<Tag
|
|
color="geekblue"
|
|
style={{ borderRadius: 8, margin: 0 }}
|
|
>
|
|
Th.{cond.from} - Th.{cond.to}
|
|
</Tag>
|
|
</Tooltip>
|
|
);
|
|
case 'date_range':
|
|
return (
|
|
<Tooltip
|
|
key={index}
|
|
title={`Áp dụng từ ${formatDate(
|
|
cond.from,
|
|
)} đến ${formatDate(cond.to)}`}
|
|
>
|
|
<Tag color="green" style={{ borderRadius: 8, margin: 0 }}>
|
|
{formatDate(cond.from)} → {formatDate(cond.to)}
|
|
</Tag>
|
|
</Tooltip>
|
|
);
|
|
case 'length_limit':
|
|
return (
|
|
<Tooltip
|
|
key={index}
|
|
title={`Tàu từ ${cond.min} đến ${cond.max} mét`}
|
|
>
|
|
<Tag color="cyan" style={{ borderRadius: 8, margin: 0 }}>
|
|
{cond.min}-{cond.max}m
|
|
</Tag>
|
|
</Tooltip>
|
|
);
|
|
default:
|
|
return null;
|
|
}
|
|
})}
|
|
</Space>
|
|
);
|
|
},
|
|
width: 180,
|
|
},
|
|
{
|
|
key: 'enabled',
|
|
title: (
|
|
<FormattedMessage id="banzones.state" defaultMessage="Trạng thái" />
|
|
),
|
|
dataIndex: 'enabled',
|
|
valueType: 'select',
|
|
fieldProps: {
|
|
options: [
|
|
{
|
|
label: intl.formatMessage({
|
|
id: 'banzone.is_enable',
|
|
defaultMessage: 'Enabled',
|
|
}),
|
|
value: true,
|
|
},
|
|
{
|
|
label: intl.formatMessage({
|
|
id: 'banzone.is_unenabled',
|
|
defaultMessage: 'Disabled',
|
|
}),
|
|
value: false,
|
|
},
|
|
],
|
|
},
|
|
hideInSearch: false,
|
|
responsive: ['lg', 'md'],
|
|
render: (_, record) => {
|
|
return (
|
|
<Tag color={record.enabled === true ? '#08CB00' : '#DCDCDC'}>
|
|
{record.enabled === true
|
|
? intl.formatMessage({
|
|
id: 'banzone.is_enable',
|
|
defaultMessage: 'Enabled',
|
|
})
|
|
: intl.formatMessage({
|
|
id: 'banzone.is_unenabled',
|
|
defaultMessage: 'Disabled',
|
|
})}
|
|
</Tag>
|
|
);
|
|
},
|
|
width: 120,
|
|
},
|
|
{
|
|
title: <FormattedMessage id="banzones.action" defaultMessage="Action" />,
|
|
hideInSearch: true,
|
|
width: 120,
|
|
fixed: 'right',
|
|
render: (_, record) => [
|
|
<Space key="actions">
|
|
<Button
|
|
key="edit"
|
|
type="primary"
|
|
icon={<EditOutlined />}
|
|
size="small"
|
|
onClick={() => handleEdit(record)}
|
|
></Button>
|
|
<Popconfirm
|
|
key="delete"
|
|
title={intl.formatMessage({
|
|
id: 'common.delete_confirm',
|
|
defaultMessage: 'Confirm delete?',
|
|
})}
|
|
description={`${intl.formatMessage({
|
|
id: 'banzone.notify.delete_zone_confirm',
|
|
defaultMessage: 'Are you sure you want to delete this zone',
|
|
})} "${record.name}"?`}
|
|
onConfirm={() => handleDelete(record)}
|
|
okText={intl.formatMessage({
|
|
id: 'common.delete',
|
|
defaultMessage: 'Delete',
|
|
})}
|
|
cancelText={intl.formatMessage({
|
|
id: 'common.cancel',
|
|
defaultMessage: 'Cancel',
|
|
})}
|
|
okType="danger"
|
|
>
|
|
<Button
|
|
type="primary"
|
|
danger
|
|
icon={<DeleteOutlined />}
|
|
size="small"
|
|
></Button>
|
|
</Popconfirm>
|
|
</Space>,
|
|
],
|
|
},
|
|
];
|
|
|
|
const items: Required<MenuProps>['items'] = [
|
|
{
|
|
label: intl.formatMessage({
|
|
id: 'banzone.polygon',
|
|
defaultMessage: 'Polygon',
|
|
}),
|
|
onClick: () => {
|
|
history.push(SGW_ROUTE_BANZONES, {
|
|
shape: 1,
|
|
type: 'create',
|
|
});
|
|
},
|
|
key: '0',
|
|
icon: <IconFont type="icon-polygon" />,
|
|
},
|
|
{
|
|
label: intl.formatMessage({
|
|
id: 'banzone.polyline',
|
|
defaultMessage: 'Polyline',
|
|
}),
|
|
key: '1',
|
|
onClick: () => {
|
|
history.push(SGW_ROUTE_BANZONES, {
|
|
shape: 2,
|
|
type: 'create',
|
|
});
|
|
},
|
|
icon: <IconFont type="icon-polyline" />,
|
|
},
|
|
{
|
|
label: intl.formatMessage({
|
|
id: 'banzone.circle',
|
|
defaultMessage: 'Circle',
|
|
}),
|
|
key: '3',
|
|
onClick: () => {
|
|
history.push(SGW_ROUTE_BANZONES, {
|
|
shape: 3,
|
|
type: 'create',
|
|
});
|
|
},
|
|
icon: <IconFont type="icon-circle" />,
|
|
},
|
|
];
|
|
|
|
const deleteMultipleBanzones = async (records: SgwModel.Banzone[]) => {
|
|
const key = 'deleteMultiple';
|
|
messageApi.open({
|
|
key,
|
|
type: 'loading',
|
|
content: intl.formatMessage({
|
|
id: 'common.deleting',
|
|
defaultMessage: 'Deleting...',
|
|
}),
|
|
duration: 0,
|
|
});
|
|
try {
|
|
for (const record of records) {
|
|
const groupID = groupFlattened.find(
|
|
(m) => m.metadata.code === record.province_code,
|
|
)?.id;
|
|
|
|
await apiRemoveBanzone(record.id || '', groupID || '');
|
|
}
|
|
messageApi.open({
|
|
key,
|
|
type: 'success',
|
|
content: `Đã xoá thành công ${records.length} khu vực`,
|
|
duration: 2,
|
|
});
|
|
tableRef.current?.reload();
|
|
} catch (error) {
|
|
console.error('Error deleting area:', error);
|
|
messageApi.open({
|
|
key,
|
|
type: 'error',
|
|
content: intl.formatMessage({
|
|
id: 'banzone.notify.fail',
|
|
defaultMessage: 'Delete zone failed!',
|
|
}),
|
|
duration: 2,
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{contextHolder}
|
|
<ProCard split={screens.md ? 'vertical' : 'horizontal'}>
|
|
<ProCard colSpan={{ xs: 24, sm: 24, md: 4, lg: 4, xl: 4 }}>
|
|
<TreeGroup
|
|
multiple
|
|
onSelected={(value) => {
|
|
// Convert group IDs to province codes string
|
|
const selectedIds = Array.isArray(value)
|
|
? value
|
|
: value
|
|
? [value]
|
|
: [];
|
|
const provinceCodes =
|
|
selectedIds.length > 0
|
|
? selectedIds
|
|
.reduce((codes: string[], id) => {
|
|
const group = groupFlattened.find((g) => g.id === id);
|
|
if (group?.metadata?.code) {
|
|
codes.push(group.metadata.code);
|
|
}
|
|
return codes;
|
|
}, [])
|
|
.join(',')
|
|
: '';
|
|
|
|
setGroupCheckedKeys(provinceCodes);
|
|
tableRef.current?.reload();
|
|
}}
|
|
/>
|
|
</ProCard>
|
|
<ProCard colSpan={{ xs: 24, sm: 24, md: 20, lg: 20, xl: 20 }}>
|
|
<ProTable<SgwModel.Banzone>
|
|
tableLayout="fixed"
|
|
scroll={{ x: 1000 }}
|
|
actionRef={tableRef}
|
|
columns={columns}
|
|
pagination={{
|
|
defaultPageSize: DEFAULT_PAGE_SIZE,
|
|
showSizeChanger: true,
|
|
pageSizeOptions: ['5', '10', '15', '20'],
|
|
showTotal: (total, range) =>
|
|
`${range[0]}-${range[1]}
|
|
${intl.formatMessage({
|
|
id: 'common.of',
|
|
defaultMessage: 'of',
|
|
})}
|
|
${total} ${intl.formatMessage({
|
|
id: 'banzones.title',
|
|
defaultMessage: 'zones',
|
|
})}`,
|
|
}}
|
|
request={async (params) => {
|
|
const { current, pageSize, name, type, enabled } = params;
|
|
|
|
// Nếu chưa có groups, đợi
|
|
if (!groups || groups.length === 0) {
|
|
return {
|
|
success: true,
|
|
data: [],
|
|
total: 0,
|
|
};
|
|
}
|
|
|
|
const offset = current === 1 ? 0 : (current! - 1) * pageSize!;
|
|
|
|
const groupFalttened = flattenGroupNodes(groups || []);
|
|
const groupId =
|
|
groupCheckedKeys ||
|
|
groupFalttened
|
|
.map((group) => group.metadata.code)
|
|
.filter(Boolean)
|
|
.join(',') + ',';
|
|
|
|
if (!groupId || groupId === ',') {
|
|
return {
|
|
success: true,
|
|
data: [],
|
|
total: 0,
|
|
};
|
|
}
|
|
|
|
const body: SgwModel.SearchZonePaginationBody = {
|
|
name: name || '',
|
|
order: 'name',
|
|
dir: 'asc',
|
|
limit: pageSize,
|
|
offset: offset,
|
|
metadata: {
|
|
province_code: groupId,
|
|
...(type ? { type: Number(type) } : {}), // nếu có type thì thêm vào
|
|
...(enabled !== undefined ? { enabled } : {}),
|
|
},
|
|
};
|
|
|
|
try {
|
|
const resp = await apiGetAllBanzones(body);
|
|
return {
|
|
success: true,
|
|
data: resp?.banzones || [],
|
|
total: resp?.total || 0,
|
|
};
|
|
} catch (error) {
|
|
console.error('Query banzones failed:', error);
|
|
return {
|
|
success: true,
|
|
data: [],
|
|
total: 0,
|
|
};
|
|
}
|
|
}}
|
|
rowKey="id"
|
|
options={{
|
|
search: false,
|
|
setting: false,
|
|
density: false,
|
|
reload: true,
|
|
}}
|
|
search={{
|
|
layout: 'vertical',
|
|
defaultCollapsed: false,
|
|
}}
|
|
dateFormatter="string"
|
|
rowSelection={{
|
|
selectedRowKeys: selectedRowsState?.map((row) => row?.id ?? ''),
|
|
onChange: (_, selectedRows) => {
|
|
setSelectedRows(selectedRows);
|
|
},
|
|
}}
|
|
tableAlertRender={({ selectedRowKeys }) => (
|
|
<div>Đã chọn {selectedRowKeys.length} mục</div>
|
|
)}
|
|
tableAlertOptionRender={({ selectedRows, onCleanSelected }) => {
|
|
return (
|
|
<Flex gap={5}>
|
|
<Popconfirm
|
|
title={intl.formatMessage({
|
|
id: 'common.notification',
|
|
defaultMessage: 'Thông báo',
|
|
})}
|
|
description={`Bạn muốn xoá hết ${selectedRows.length} khu vực này?`}
|
|
onConfirm={() => {
|
|
deleteMultipleBanzones(selectedRows);
|
|
}}
|
|
okText={intl.formatMessage({
|
|
id: 'common.sure',
|
|
defaultMessage: 'Chắc chắn',
|
|
})}
|
|
cancelText={intl.formatMessage({
|
|
id: 'common.no',
|
|
defaultMessage: 'Không',
|
|
})}
|
|
>
|
|
<Button type="primary" danger>
|
|
{intl.formatMessage({
|
|
id: 'common.delete',
|
|
defaultMessage: 'Xóa',
|
|
})}
|
|
</Button>
|
|
</Popconfirm>
|
|
|
|
<Button color="cyan" variant="text" onClick={onCleanSelected}>
|
|
{intl.formatMessage({
|
|
id: 'common.cancel',
|
|
defaultMessage: 'Bỏ chọn',
|
|
})}
|
|
</Button>
|
|
</Flex>
|
|
);
|
|
}}
|
|
toolBarRender={() => [
|
|
<Dropdown
|
|
menu={{ items }}
|
|
trigger={['click']}
|
|
key="toolbar-dropdown"
|
|
>
|
|
<Button type="primary">
|
|
<Space>
|
|
{intl.formatMessage({
|
|
id: 'banzones.create',
|
|
defaultMessage: 'Tạo khu vực',
|
|
})}
|
|
<DownOutlined />
|
|
</Space>
|
|
</Button>
|
|
</Dropdown>,
|
|
]}
|
|
/>
|
|
</ProCard>
|
|
</ProCard>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default BanZoneList;
|