Files
SMATEC-FRONTEND/src/pages/Slave/SGW/Manager/Area/index.tsx

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;