Files
SMATEC-FRONTEND/src/pages/Manager/Group/index.tsx
2026-01-21 11:48:57 +07:00

452 lines
15 KiB
TypeScript

import IconFont from '@/components/IconFont';
import TreeGroup from '@/components/shared/TreeGroup';
import { HTTPSTATUS } from '@/constants';
import {
apiDeleteGroup,
apiUpdateGroup,
} from '@/services/master/GroupController';
import {
DeleteOutlined,
EditOutlined,
ExclamationCircleOutlined,
PlusOutlined,
} from '@ant-design/icons';
import { ProCard, ProDescriptions } from '@ant-design/pro-components';
import { FormattedMessage, useIntl, useModel } from '@umijs/max';
import { Button, Grid, message, Modal } from 'antd';
import { Dropdown, MenuProps, Tooltip } from 'antd/lib';
import { useState } from 'react';
import CreateOrUpdateGroup from './components/CreateOrUpdateGroup';
type MenuItem = Required<MenuProps>['items'][number];
const ManagerGroupPage = () => {
const { useBreakpoint } = Grid;
const { initialState } = useModel('@@initialState');
const { currentUserProfile } = initialState || {};
const intl = useIntl();
const screens = useBreakpoint();
const [selectedItem, setSelectedItem] = useState<
MasterModel.GroupNode | undefined
>();
const [selectedGroup, setSelectedGroup] = useState<
MasterModel.GroupNode | undefined
>();
const { groups, getGroups } = useModel('master.useGroups');
const [handleChildModal, setHandleChildModal] = useState<boolean>(false);
const [type, setType] = useState<'create-root' | 'create-child' | 'update'>(
'create-root',
);
const [messageApi, contextHolder] = message.useMessage();
const [modalKey, setModalKey] = useState(0);
const findGroupById = (
groups: MasterModel.GroupNode[],
id: string,
): MasterModel.GroupNode | undefined => {
for (const group of groups) {
if (group.id === id) return group;
if (group.children) {
const found = findGroupById(group.children, id);
if (found) return found;
}
}
return undefined;
};
const handleUpdate = async (group: Partial<MasterModel.GroupNode>) => {
const key = 'update_group';
const { name, description, parent_id, code, short_name } = group;
const editGroup = parent_id
? {
...selectedItem,
name: name,
parent_id: parent_id,
metadata: {
code: code,
short_name: short_name,
},
description: description,
}
: {
...selectedItem,
name: name,
metadata: {
code: code,
short_name: short_name,
},
description: description,
};
try {
messageApi.open({
type: 'loading',
content: intl.formatMessage({
id: 'common.updating',
defaultMessage: 'updating...',
}),
key,
});
await apiUpdateGroup({ ...editGroup });
messageApi.open({
type: 'success',
content: intl.formatMessage({
id: 'master.groups.update.success',
defaultMessage: 'Updated successfully',
}),
key,
});
await getGroups();
return true;
} catch (error) {
messageApi.open({
type: 'error',
content: intl.formatMessage({
id: 'master.groups.update.failed',
defaultMessage: 'Updating failed, please try again!',
}),
key,
});
return false;
}
};
const showDeleteConfirm = (group: MasterModel.GroupNode) => {
Modal.confirm({
title: intl.formatMessage({
id: 'master.groups.delete.confirm',
defaultMessage: 'Are you sure delete this group',
}),
content: selectedItem?.name,
icon: <ExclamationCircleOutlined />,
okText: intl.formatMessage({ id: 'common.yes', defaultMessage: 'Yes' }),
okType: 'danger',
cancelText: intl.formatMessage({ id: 'common.no', defaultMessage: 'No' }),
async onOk() {
try {
const resp = await apiDeleteGroup(group.id);
if (resp.status === HTTPSTATUS.HTTP_NOCONTENT) {
messageApi.success(
intl.formatMessage({
id: 'master.groups.delete.success',
defaultMessage: 'Deleted group successfully',
}),
);
setSelectedItem(undefined);
await getGroups();
} else if (resp.status === HTTPSTATUS.HTTP_SERVERERROR) {
messageApi.warning(
intl.formatMessage({
id: 'master.groups.delete.failed_internal',
defaultMessage:
'The group contains devices or users and cannot be deleted',
}),
);
} else {
throw new Error('Delete group failed');
}
} catch (error) {
console.error('Error when delete group ', error);
messageApi.error(
intl.formatMessage({
id: 'master.groups.delete.failed',
defaultMessage: 'Delete group failed',
}),
);
}
},
});
};
const onItemClick = ({ key }: { key: string }) => {
const splitIndex = key.indexOf('-');
const action = key.substring(0, splitIndex);
const groupId = key.substring(splitIndex + 1);
const groupNode = findGroupById(groups || [], groupId);
console.log('GroupName', groupNode?.name);
setSelectedItem(groupNode);
setSelectedGroup(groupNode);
if (action === '1') {
setType('create-child');
} else if (action === '2') {
setType('update');
} else if (action === '3') {
showDeleteConfirm(groupNode!);
return;
}
// Increment modalKey to force remount component
setModalKey((prev) => prev + 1);
setHandleChildModal(true);
};
return (
<>
{contextHolder}
<CreateOrUpdateGroup
type={type}
key={modalKey}
isOpen={handleChildModal}
setIsOpen={setHandleChildModal}
group={selectedGroup}
message={messageApi}
/>
<ProCard split={screens.md ? 'vertical' : 'horizontal'}>
<ProCard
colSpan={{ xs: 24, sm: 24, md: 10, lg: 10, xl: 10 }}
extra={[
currentUserProfile?.metadata?.user_type === 'sysadmin' && (
<Button
type="primary"
key="primary"
icon={<PlusOutlined />}
onClick={() => {
setType('create-root');
setSelectedItem(undefined);
setModalKey((prev) => prev + 1);
setHandleChildModal(true);
}}
>
<FormattedMessage
id="master.groups.root"
defaultMessage="Add root"
/>
</Button>
),
]}
>
<TreeGroup
titleRender={(item) => {
const groupNode = findGroupById(groups || [], item.key as string);
const menus: MenuItem[] = [
{
label: (
<Tooltip
title={
groupNode?.metadata?.has_thing
? intl.formatMessage({
id: 'master.groups.cannot-add-group',
defaultMessage:
'Cannot add child to a group that has things',
})
: undefined
}
>
{intl.formatMessage({
id: 'master.groups.add',
defaultMessage: 'Add child',
})}
</Tooltip>
),
key: `1-${groupNode?.id}`,
icon: <IconFont type="icon-leaf" />,
disabled: groupNode?.metadata?.has_thing === true,
},
{
label: intl.formatMessage({
id: 'common.edit',
defaultMessage: 'Update',
}),
key: `2-${groupNode?.id}`,
icon: <EditOutlined />,
},
{
label: (
<Tooltip
title={
groupNode?.metadata?.has_thing
? intl.formatMessage({
id: 'master.groups.delete.failed_internal',
defaultMessage:
'The group contains devices or users and cannot be deleted',
})
: undefined
}
>
{intl.formatMessage({
id: 'common.delete',
defaultMessage: 'Delete',
})}
</Tooltip>
),
key: `3-${groupNode?.id}`,
icon: <DeleteOutlined />,
disabled: groupNode?.metadata?.has_thing === true,
},
];
return (
<Dropdown
menu={{
items: menus,
onClick: onItemClick,
}}
trigger={['contextMenu']}
placement="bottomRight"
arrow
>
<span>
{typeof item.title === 'function'
? item.title(item)
: item.title}
</span>
</Dropdown>
);
}}
multiple={false}
onSelected={(value: string | string[] | null) => {
if (!groups) {
setSelectedItem(undefined);
return;
}
// Find the selected group from model
if (value && !Array.isArray(value)) {
const found = findGroupById(groups, value);
setSelectedItem(found);
} else {
setSelectedItem(undefined);
}
}}
/>
</ProCard>
<ProCard colSpan={{ xs: 24, sm: 24, md: 14, lg: 14, xl: 14 }}>
{selectedItem?.name && (
<>
<ProDescriptions<Partial<MasterModel.GroupNode>>
column={1}
bordered
title={selectedItem.name}
extra={[
<Tooltip
key="add"
title={
selectedItem?.metadata?.has_thing
? intl.formatMessage({
id: 'master.groups.cannot-add-group',
defaultMessage:
'Cannot add child to a group that has things',
})
: undefined
}
>
<Button
key="add"
type="primary"
icon={<PlusOutlined />}
onClick={() => {
setType('create-child');
setModalKey((prev) => prev + 1);
setHandleChildModal(true);
}}
disabled={
selectedItem.metadata?.has_thing === true ? true : false
}
>
<FormattedMessage
id="master.groups.add"
defaultMessage="Add Group"
/>
</Button>
</Tooltip>,
<Tooltip
key="add-fail"
title={
selectedItem?.metadata?.has_thing
? intl.formatMessage({
id: 'master.groups.delete.failed_internal',
defaultMessage:
'Cannot add child to a group that has things',
})
: undefined
}
>
<Button
danger
key="delete"
title={intl.formatMessage({
id: 'master.groups.delete.confirm',
defaultMessage: 'Delete this group?',
})}
onClick={() => {
showDeleteConfirm(selectedItem);
}}
icon={<DeleteOutlined />}
disabled={
selectedItem.metadata?.has_thing === true ? true : false
}
>
<FormattedMessage
id="common.delete"
defaultMessage="Delete"
/>
</Button>
,
</Tooltip>,
]}
dataSource={{
name: selectedItem.name,
code: selectedItem.metadata?.code || '',
short_name: selectedItem.metadata?.short_name || '',
description: selectedItem.description || '',
}}
columns={[
{
title: intl.formatMessage({
id: 'common.name',
defaultMessage: 'Name',
}),
dataIndex: 'name',
},
{
title: intl.formatMessage({
id: 'master.groups.code',
defaultMessage: 'Code',
}),
dataIndex: 'code',
},
{
title: intl.formatMessage({
id: 'master.groups.short_name',
defaultMessage: 'Short name',
}),
dataIndex: 'short_name',
},
{
title: intl.formatMessage({
id: 'common.description',
defaultMessage: 'Description',
}),
dataIndex: 'description',
},
]}
editable={{
onSave: async (_, record) => {
const updatedData: MasterModel.GroupNode = {
...selectedItem,
name: record.name || '',
description: record.description || '',
metadata: {
...selectedItem.metadata,
code: record.code,
short_name: record.short_name,
},
};
const success = await handleUpdate(updatedData);
if (success) {
setSelectedItem(updatedData);
return true;
}
return false;
},
}}
/>
</>
)}
</ProCard>
</ProCard>
</>
);
};
export default ManagerGroupPage;