feat(project): base smatec's frontend
This commit is contained in:
5
src/pages/Manager/Device/index.tsx
Normal file
5
src/pages/Manager/Device/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
const ManagerDevicePage = () => {
|
||||
return <div>ManagerDevicePage</div>;
|
||||
};
|
||||
|
||||
export default ManagerDevicePage;
|
||||
300
src/pages/Manager/Group/components/CreateOrUpdateGroup.tsx
Normal file
300
src/pages/Manager/Group/components/CreateOrUpdateGroup.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
import {
|
||||
apiCreateGroup,
|
||||
apiUpdateGroup,
|
||||
} from '@/services/master/GroupController';
|
||||
import {
|
||||
ModalForm,
|
||||
ProFormInstance,
|
||||
ProFormText,
|
||||
} from '@ant-design/pro-components';
|
||||
import { FormattedMessage, useIntl, useModel } from '@umijs/max';
|
||||
import { MessageInstance } from 'antd/es/message/interface';
|
||||
import { useEffect, useRef } from 'react';
|
||||
type CreateOrUpdateGroupProps = {
|
||||
type: 'create-root' | 'create-child' | 'update';
|
||||
isOpen?: boolean;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
group?: MasterModel.GroupNode | null;
|
||||
message: MessageInstance;
|
||||
};
|
||||
type HandleGroupForm = {
|
||||
parent_id?: string;
|
||||
parent_name?: string;
|
||||
name: string;
|
||||
code: string;
|
||||
short_name: string;
|
||||
description?: string;
|
||||
};
|
||||
const CreateOrUpdateGroup = ({
|
||||
type,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
group,
|
||||
message,
|
||||
}: CreateOrUpdateGroupProps) => {
|
||||
const formRef = useRef<ProFormInstance<HandleGroupForm>>();
|
||||
const intl = useIntl();
|
||||
const { groups, getGroups } = useModel('master.useGroups');
|
||||
|
||||
// Sync form values when modal opens or group/type changes
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
'useEffect trigger - isOpen:',
|
||||
isOpen,
|
||||
'group:',
|
||||
group,
|
||||
'type:',
|
||||
type,
|
||||
);
|
||||
if (isOpen && formRef.current) {
|
||||
if (type === 'update' && group) {
|
||||
formRef.current.setFieldsValue({
|
||||
parent_id: group?.id || '',
|
||||
parent_name: '',
|
||||
name: group?.name || '',
|
||||
code: group?.metadata?.code || '',
|
||||
short_name: group?.metadata?.short_name || '',
|
||||
description: group?.description || '',
|
||||
});
|
||||
} else if (type === 'create-child' && group) {
|
||||
formRef.current.setFieldsValue({
|
||||
parent_id: group?.id,
|
||||
parent_name: group?.name || '',
|
||||
name: '',
|
||||
code: '',
|
||||
short_name: '',
|
||||
description: '',
|
||||
});
|
||||
} else if (type === 'create-root') {
|
||||
formRef.current.setFieldsValue({
|
||||
parent_id: '',
|
||||
parent_name: '',
|
||||
name: '',
|
||||
code: '',
|
||||
short_name: '',
|
||||
description: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [isOpen, group, type]);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setIsOpen(open);
|
||||
};
|
||||
return (
|
||||
<ModalForm<HandleGroupForm>
|
||||
open={isOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
layout="vertical"
|
||||
formRef={formRef}
|
||||
title={
|
||||
group === undefined ? (
|
||||
<FormattedMessage id="master.groups.root" />
|
||||
) : (
|
||||
<FormattedMessage id="master.groups.add" />
|
||||
)
|
||||
}
|
||||
onFinish={async (values) => {
|
||||
const body: Partial<MasterModel.GroupBodyRequest> = {
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
metadata: {
|
||||
code: values.code,
|
||||
short_name: values.short_name,
|
||||
},
|
||||
};
|
||||
switch (type) {
|
||||
case 'update': {
|
||||
body.id = group?.id;
|
||||
try {
|
||||
const response = await apiUpdateGroup(body);
|
||||
if (response.id) {
|
||||
message?.success(
|
||||
intl.formatMessage({
|
||||
id: 'master.groups.update.success',
|
||||
defaultMessage: 'Updated successfully',
|
||||
}),
|
||||
);
|
||||
getGroups();
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error when update group: ', error);
|
||||
message?.error(
|
||||
intl.formatMessage({
|
||||
id: 'master.groups.update.failed',
|
||||
defaultMessage: 'Update failed, please try again!',
|
||||
}),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'create-child': {
|
||||
body.parent_id = group?.id;
|
||||
try {
|
||||
const response = await apiCreateGroup(body);
|
||||
if (response.id) {
|
||||
message?.success(
|
||||
intl.formatMessage({
|
||||
id: 'master.groups.create.success',
|
||||
defaultMessage: 'Created group successfully',
|
||||
}),
|
||||
);
|
||||
setIsOpen(false);
|
||||
getGroups();
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error when create group: ', error);
|
||||
message?.error(
|
||||
intl.formatMessage({
|
||||
id: 'master.groups.create.failed',
|
||||
defaultMessage: 'Create group failed, please try again!',
|
||||
}),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'create-root': {
|
||||
try {
|
||||
const response = await apiCreateGroup(body);
|
||||
if (response.id) {
|
||||
message?.success(
|
||||
intl.formatMessage({
|
||||
id: 'master.groups.create.success',
|
||||
defaultMessage: 'Created group successfully',
|
||||
}),
|
||||
);
|
||||
setIsOpen(false);
|
||||
getGroups();
|
||||
return true;
|
||||
} else {
|
||||
throw new Error('Create group failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error when create group: ', error);
|
||||
message?.error(
|
||||
intl.formatMessage({
|
||||
id: 'master.groups.create.failed',
|
||||
defaultMessage: 'Create group failed, please try again!',
|
||||
}),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ProFormText name="parent_id" hidden />
|
||||
|
||||
{type !== 'update' && (
|
||||
<ProFormText
|
||||
name="parent_name"
|
||||
disabled
|
||||
label={intl.formatMessage({
|
||||
id: 'master.groups.parent',
|
||||
defaultMessage: 'Parent',
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ProFormText
|
||||
name="name"
|
||||
label={intl.formatMessage({
|
||||
id: 'common.name',
|
||||
defaultMessage: 'Name',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id="common.name.required"
|
||||
defaultMessage="The name is required"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<ProFormText
|
||||
name="code"
|
||||
label={intl.formatMessage({
|
||||
id: 'master.groups.code',
|
||||
defaultMessage: 'Code',
|
||||
})}
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
validator: async (_, value) => {
|
||||
if (
|
||||
value &&
|
||||
groups!.some(
|
||||
(g) => g?.metadata?.code === value && type !== 'update',
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
intl.formatMessage({
|
||||
id: 'master.groups.code.exists',
|
||||
defaultMessage: 'The code already exists',
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<ProFormText
|
||||
name="short_name"
|
||||
label={intl.formatMessage({
|
||||
id: 'master.groups.short_name',
|
||||
defaultMessage: 'Short name',
|
||||
})}
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
validator: async (_, value) => {
|
||||
if (
|
||||
value &&
|
||||
groups!.some(
|
||||
(g) => g?.metadata?.short_name === value && type !== 'update',
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
intl.formatMessage({
|
||||
id: 'master.groups.short_name.exists',
|
||||
defaultMessage: 'The short name already exists',
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<ProFormText
|
||||
name="description"
|
||||
label={intl.formatMessage({
|
||||
id: 'common.description',
|
||||
defaultMessage: 'Description',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id="common.description.required"
|
||||
defaultMessage="The description is required"
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateOrUpdateGroup;
|
||||
451
src/pages/Manager/Group/index.tsx
Normal file
451
src/pages/Manager/Group/index.tsx
Normal file
@@ -0,0 +1,451 @@
|
||||
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;
|
||||
484
src/pages/Manager/Log/index.tsx
Normal file
484
src/pages/Manager/Log/index.tsx
Normal file
@@ -0,0 +1,484 @@
|
||||
import { DATE_TIME_FORMAT, DEFAULT_PAGE_SIZE } from '@/constants';
|
||||
import { apiQueryLogs } from '@/services/master/LogController';
|
||||
import { apiQueryUsers } from '@/services/master/UserController';
|
||||
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
|
||||
import { useIntl } from '@umijs/max';
|
||||
import { DatePicker } from 'antd/lib';
|
||||
import dayjs from 'dayjs';
|
||||
import { useRef } from 'react';
|
||||
|
||||
const SystemLogs = () => {
|
||||
const intl = useIntl();
|
||||
const tableRef = useRef<ActionType>();
|
||||
const actions = [
|
||||
//Alarm
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.things.alarm.confirm',
|
||||
defaultMessage: 'Alarm confirm',
|
||||
}),
|
||||
value: '0-0',
|
||||
selectable: false,
|
||||
children: [
|
||||
{
|
||||
value: 'things.alarm_confirm',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.things.confirm',
|
||||
defaultMessage: 'Confirm',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'things.alarm_unconfirm',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.things.unconfirm',
|
||||
defaultMessage: 'Unconfirm',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
//Things
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.things',
|
||||
defaultMessage: 'Things',
|
||||
}),
|
||||
value: '0-1',
|
||||
selectable: false,
|
||||
children: [
|
||||
{
|
||||
value: 'things.create',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.things.create',
|
||||
defaultMessage: 'Create new thing',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'things.update',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.things.update',
|
||||
defaultMessage: 'Update thing',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'things.remove',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.things.remove',
|
||||
defaultMessage: 'Remove thing',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'things.share',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.things.share',
|
||||
defaultMessage: 'Share thing',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'things.unshare',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.things.unshare',
|
||||
defaultMessage: 'Unshare thing',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'things.update_key',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.things.update_key',
|
||||
defaultMessage: 'Update key thing',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
// Users
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.users',
|
||||
defaultMessage: 'Users',
|
||||
}),
|
||||
value: '0-2',
|
||||
selectable: false,
|
||||
children: [
|
||||
{
|
||||
value: 'users.create',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.users.create',
|
||||
defaultMessage: 'Register user',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'users.update',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.users.update',
|
||||
defaultMessage: 'Update user',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'users.remove',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.users.remove',
|
||||
defaultMessage: 'Remove user',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'users.login',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.users.login',
|
||||
defaultMessage: 'User login',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
// Groups
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.groups',
|
||||
defaultMessage: 'Groups',
|
||||
}),
|
||||
value: '0-3',
|
||||
selectable: false,
|
||||
children: [
|
||||
{
|
||||
value: 'group.create',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.groups.create',
|
||||
defaultMessage: 'Create new group',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'group.update',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.groups.update',
|
||||
defaultMessage: 'Update group',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'group.remove',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.groups.remove',
|
||||
defaultMessage: 'Remove group',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'group.assign_thing',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.groups.assign_thing',
|
||||
defaultMessage: 'Assign thing to group',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'group.assign_user',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.groups.assign_user',
|
||||
defaultMessage: 'Assign user to group',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'group.unassign_thing',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.groups.unassign_thing',
|
||||
defaultMessage: 'Remove thing from group',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'group.unassign_user',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.groups.unassign_user',
|
||||
defaultMessage: 'Remove user from group',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
// Ships
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.ships',
|
||||
defaultMessage: 'Ships',
|
||||
}),
|
||||
value: '0-4',
|
||||
selectable: false,
|
||||
children: [
|
||||
{
|
||||
value: 'ships.create',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.ships.create',
|
||||
defaultMessage: 'Create new ship',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'ships.update',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.ships.update',
|
||||
defaultMessage: 'Update ship',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'ships.remove',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.ships.remove',
|
||||
defaultMessage: 'Remove ship',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'ships.assign_thing',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.ships.assign_thing',
|
||||
defaultMessage: 'Assign thing to ship',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'ships.assign_user',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.ships.assign_user',
|
||||
defaultMessage: 'Assign user to ship',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'ships.unassign_thing',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.ships.unassign_thing',
|
||||
defaultMessage: 'Remove thing from ship',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'ships.unassign_user',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.ships.unassign_user',
|
||||
defaultMessage: 'Remove user from ship',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
// Trips
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.trips',
|
||||
defaultMessage: 'Trips',
|
||||
}),
|
||||
value: '0-5',
|
||||
selectable: false,
|
||||
children: [
|
||||
{
|
||||
value: 'trips.create',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.trips.create',
|
||||
defaultMessage: 'Create new ship',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'trips.update',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.trips.update',
|
||||
defaultMessage: 'Update ship',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'trips.remove',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.trips.remove',
|
||||
defaultMessage: 'Remove ship',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'trips.approve',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.trips.approve',
|
||||
defaultMessage: 'Approve trip',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'trips.request_approve',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.trips.request_approve',
|
||||
defaultMessage: 'Request approval for trip',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'trips.unassign_thing',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.trips.unassign_thing',
|
||||
defaultMessage: 'Remove thing from ship',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'trips.unassign_user',
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.trips.unassign_user',
|
||||
defaultMessage: 'Remove user from ship',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const queryUserSource = async (): Promise<MasterModel.ProfileResponse[]> => {
|
||||
try {
|
||||
const body: MasterModel.SearchUserPaginationBody = {
|
||||
offset: 0,
|
||||
limit: 100,
|
||||
order: 'email',
|
||||
dir: 'asc',
|
||||
};
|
||||
const resp = await apiQueryUsers(body);
|
||||
return resp?.users ?? [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
const columns: ProColumns<MasterModel.Message>[] = [
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.date.text',
|
||||
defaultMessage: 'Date',
|
||||
}),
|
||||
key: 'dateTimeRange',
|
||||
dataIndex: 'time',
|
||||
width: '20%',
|
||||
valueType: 'dateTimeRange',
|
||||
initialValue: [dayjs().add(-1, 'day'), dayjs()],
|
||||
search: {
|
||||
transform: (value) => ({ startTime: value[0], endTime: value[1] }),
|
||||
},
|
||||
render: (_, row) => {
|
||||
return dayjs.unix(row?.time || 0).format(DATE_TIME_FORMAT);
|
||||
},
|
||||
renderFormItem: (_, config, form) => {
|
||||
return (
|
||||
<DatePicker.RangePicker
|
||||
width="50%"
|
||||
presets={[
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'master.logs.search.yesterday',
|
||||
defaultMessage: 'Yesterday',
|
||||
}),
|
||||
value: [dayjs().add(-1, 'day'), dayjs()],
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'master.logs.search.last_week',
|
||||
defaultMessage: 'Last Week',
|
||||
}),
|
||||
value: [dayjs().add(-7, 'day'), dayjs()],
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'master.logs.search.last_month',
|
||||
defaultMessage: 'Last Month',
|
||||
}),
|
||||
value: [dayjs().add(-30, 'day'), dayjs()],
|
||||
},
|
||||
]}
|
||||
onChange={(dates) => {
|
||||
form.setFieldsValue({ dateTimeRange: dates });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.email.text',
|
||||
defaultMessage: 'Email',
|
||||
}),
|
||||
dataIndex: 'publisher',
|
||||
valueType: 'select',
|
||||
fieldProps: { mode: 'multiple' },
|
||||
width: '25%',
|
||||
request: async () => {
|
||||
const users = await queryUserSource();
|
||||
return users.map((u) => ({
|
||||
label: u.email,
|
||||
value: u.email,
|
||||
}));
|
||||
},
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
id: 'master.logs.action.text',
|
||||
defaultMessage: 'Action',
|
||||
}),
|
||||
dataIndex: 'subtopic',
|
||||
valueType: 'treeSelect',
|
||||
width: '25%',
|
||||
fieldProps: {
|
||||
treeCheckable: true,
|
||||
multiple: true,
|
||||
},
|
||||
request: async () => actions,
|
||||
render: (_, item) => {
|
||||
const childs = actions.flatMap((a) => a.children || []);
|
||||
const action = childs.find((a) => a?.value === item.subtopic);
|
||||
return action?.title ?? '...';
|
||||
},
|
||||
},
|
||||
];
|
||||
return (
|
||||
<ProTable<MasterModel.Message>
|
||||
actionRef={tableRef}
|
||||
columns={columns}
|
||||
rowKey={(item) =>
|
||||
`${item.subtopic}:${item.time}:${item.publisher}:${item.name}`
|
||||
}
|
||||
size="large"
|
||||
options={{
|
||||
search: false,
|
||||
setting: false,
|
||||
density: false,
|
||||
reload: true,
|
||||
}}
|
||||
pagination={{
|
||||
defaultPageSize: DEFAULT_PAGE_SIZE * 2,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '15', '20'],
|
||||
showTotal: (total, range) =>
|
||||
`${range[0]}-${range[1]}
|
||||
${intl.formatMessage({
|
||||
id: 'common.paginations.of',
|
||||
defaultMessage: 'of',
|
||||
})}
|
||||
${total} ${intl.formatMessage({
|
||||
id: 'master.logs.table.pagination',
|
||||
defaultMessage: 'activities',
|
||||
})}`,
|
||||
}}
|
||||
request={async (params) => {
|
||||
try {
|
||||
const { current, pageSize, subtopic, publisher, startTime, endTime } =
|
||||
params;
|
||||
const offset = current === 1 ? 0 : (current! - 1) * pageSize!;
|
||||
const body: MasterModel.SearchLogPaginationBody = {
|
||||
limit: pageSize,
|
||||
offset: offset,
|
||||
from: startTime
|
||||
? typeof startTime === 'string'
|
||||
? dayjs(startTime).unix()
|
||||
: startTime.unix()
|
||||
: undefined,
|
||||
to: endTime
|
||||
? typeof endTime === 'string'
|
||||
? dayjs(endTime).unix()
|
||||
: endTime.unix()
|
||||
: undefined,
|
||||
publisher: publisher?.length ? publisher.join(',') : undefined,
|
||||
subtopic: subtopic?.length ? subtopic.join(',') : undefined,
|
||||
};
|
||||
const resp = await apiQueryLogs(body, 'user_logs');
|
||||
return {
|
||||
data: resp.messages || [],
|
||||
success: true,
|
||||
total: resp.total || 0,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error when get logs: ', error);
|
||||
return {
|
||||
data: [],
|
||||
success: false,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemLogs;
|
||||
368
src/pages/Manager/User/components/CreateUser.tsx
Normal file
368
src/pages/Manager/User/components/CreateUser.tsx
Normal file
@@ -0,0 +1,368 @@
|
||||
import TreeSelectedGroup from '@/components/shared/TreeSelectedGroup';
|
||||
import { HTTPSTATUS } from '@/constants';
|
||||
import {
|
||||
apiCreateUsers,
|
||||
apiQueryUsers,
|
||||
} from '@/services/master/UserController';
|
||||
import { LockOutlined, PlusOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
ModalForm,
|
||||
ProForm,
|
||||
ProFormInstance,
|
||||
ProFormSelect,
|
||||
ProFormText,
|
||||
} from '@ant-design/pro-components';
|
||||
import { FormattedMessage, useIntl } from '@umijs/max';
|
||||
import { Button } from 'antd';
|
||||
import { MessageInstance } from 'antd/es/message/interface';
|
||||
import { useRef, useState } from 'react';
|
||||
type CreateUserProps = {
|
||||
message: MessageInstance;
|
||||
onSuccess?: (isSuccess: boolean) => void;
|
||||
};
|
||||
|
||||
type CreateUserFormValues = {
|
||||
full_name: string;
|
||||
phone_number: string;
|
||||
user_type: 'admin' | 'enduser' | 'sysadmin' | 'users';
|
||||
email: string;
|
||||
password: string;
|
||||
group_id?: string;
|
||||
};
|
||||
|
||||
const RoleSelectForm = () => {
|
||||
const domain = process.env.DOMAIN_ENV;
|
||||
const intl = useIntl();
|
||||
switch (domain) {
|
||||
case 'sgw':
|
||||
return (
|
||||
<ProFormSelect
|
||||
name="user_type"
|
||||
label={intl.formatMessage({ id: 'master.users.role' })}
|
||||
options={[
|
||||
{
|
||||
label: intl.formatMessage({
|
||||
id: 'master.users.role.sgw.end_user',
|
||||
}),
|
||||
value: 'endusers',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'master.users.role.user' }),
|
||||
value: 'users',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'master.users.role.admin' }),
|
||||
value: 'admin',
|
||||
},
|
||||
]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'master.users.role.placeholder',
|
||||
defaultMessage: 'Please select a role',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case 'spole':
|
||||
return (
|
||||
<ProFormSelect
|
||||
name="user_type"
|
||||
label={intl.formatMessage({ id: 'master.users.role' })}
|
||||
options={[
|
||||
{
|
||||
label: intl.formatMessage({ id: 'master.users.role.user' }),
|
||||
value: 'users',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'master.users.role.admin' }),
|
||||
value: 'admin',
|
||||
},
|
||||
]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'master.users.role.placeholder',
|
||||
defaultMessage: 'Please select a role',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
case 'gms':
|
||||
default:
|
||||
return (
|
||||
<ProFormSelect
|
||||
name="user_type"
|
||||
label={intl.formatMessage({ id: 'master.users.role' })}
|
||||
options={[
|
||||
{
|
||||
label: intl.formatMessage({ id: 'master.users.role.user' }),
|
||||
value: 'users',
|
||||
},
|
||||
{
|
||||
label: intl.formatMessage({ id: 'master.users.role.admin' }),
|
||||
value: 'admin',
|
||||
},
|
||||
]}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'master.users.role.placeholder',
|
||||
defaultMessage: 'Please select a role',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const CreateUser = ({ message, onSuccess }: CreateUserProps) => {
|
||||
const formRef = useRef<ProFormInstance<CreateUserFormValues>>();
|
||||
const intl = useIntl();
|
||||
const [group_id, setGroupId] = useState<string | string[] | null>(null);
|
||||
const handleGroupSelect = (group: string | string[] | null) => {
|
||||
setGroupId(group);
|
||||
// Đồng bộ giá trị vào form để validation hoạt động
|
||||
formRef.current?.setFieldsValue({
|
||||
group_id: Array.isArray(group) ? group.join(',') : group || undefined,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<ModalForm<CreateUserFormValues>
|
||||
title={intl.formatMessage({
|
||||
id: 'master.users.register.title',
|
||||
defaultMessage: 'Register New User',
|
||||
})}
|
||||
formRef={formRef}
|
||||
trigger={
|
||||
<Button type="primary" key="primary" icon={<PlusOutlined />}>
|
||||
<FormattedMessage
|
||||
id="master.users.register"
|
||||
defaultMessage="Register"
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
autoFocusFirstInput
|
||||
onFinish={async (values: CreateUserFormValues) => {
|
||||
const body: MasterModel.CreateUserBodyRequest = {
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
metadata: {
|
||||
full_name: values.full_name,
|
||||
phone_number: values.phone_number,
|
||||
user_type: values.user_type || 'enduser',
|
||||
group_id: values.group_id,
|
||||
},
|
||||
};
|
||||
try {
|
||||
const resp = await apiCreateUsers(body);
|
||||
if (resp.status === HTTPSTATUS.HTTP_CREATED) {
|
||||
message.success(
|
||||
intl.formatMessage({
|
||||
id: 'master.users.create.success',
|
||||
defaultMessage: 'Create user successfully',
|
||||
}),
|
||||
);
|
||||
formRef.current?.resetFields();
|
||||
onSuccess?.(true);
|
||||
return true;
|
||||
} else {
|
||||
throw new Error('Create user failed');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(
|
||||
intl.formatMessage({
|
||||
id: 'master.users.create.error',
|
||||
defaultMessage: 'Failed to create user',
|
||||
}),
|
||||
);
|
||||
onSuccess?.(false);
|
||||
return false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ProFormText
|
||||
name={'full_name'}
|
||||
label={intl.formatMessage({
|
||||
id: 'master.users.full_name',
|
||||
defaultMessage: 'Full name',
|
||||
})}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'master.users.full_name.placeholder',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'master.users.full_name.required',
|
||||
defaultMessage: 'The full name is required',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<RoleSelectForm />
|
||||
|
||||
<ProFormText
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id="master.users.email.required"
|
||||
defaultMessage="The email is required"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
type: 'email',
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id="master.users.email.invalid"
|
||||
defaultMessage="Invalid email address"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
validator: async (rule, value) => {
|
||||
if (value) {
|
||||
const query: MasterModel.SearchUserPaginationBody = {
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
order: 'email',
|
||||
email: value,
|
||||
dir: 'asc',
|
||||
};
|
||||
const resp = await apiQueryUsers(query);
|
||||
const { total } = resp;
|
||||
if (total && total > 0) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
intl.formatMessage({
|
||||
id: 'master.users.email.exists',
|
||||
defaultMessage: 'The email is exists',
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
name="email"
|
||||
label={intl.formatMessage({
|
||||
id: 'master.users.email',
|
||||
defaultMessage: 'Email',
|
||||
})}
|
||||
fieldProps={{
|
||||
prefix: <UserOutlined />,
|
||||
}}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'master.users.email.placeholder',
|
||||
defaultMessage: 'Email',
|
||||
})}
|
||||
validateTrigger={['onBlur']}
|
||||
/>
|
||||
<ProFormText
|
||||
name="phone_number"
|
||||
label={intl.formatMessage({
|
||||
id: 'master.users.phone_number',
|
||||
defaultMessage: 'Phone number',
|
||||
})}
|
||||
fieldProps={{
|
||||
autoComplete: 'phone-number',
|
||||
}}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'master.users.phone_number.required',
|
||||
defaultMessage: 'The phone number is required',
|
||||
}),
|
||||
},
|
||||
{
|
||||
pattern: /((09|03|07|08|05)+([0-9]{8})\b)/g,
|
||||
message: intl.formatMessage({
|
||||
id: 'master.users.phone_number.notvalid',
|
||||
defaultMessage: 'Invalid phone number',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ProFormText.Password
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: (
|
||||
<FormattedMessage
|
||||
id="master.users.password"
|
||||
defaultMessage="Password is required"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
pattern: /^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*.]{6,16}$/g,
|
||||
message: intl.formatMessage({
|
||||
id: 'master.profile.change-password.password.strong',
|
||||
defaultMessage:
|
||||
'A password contains at least 8 characters, including at least one number and includes both lower and uppercase letters and special characters, for example #, ?, !',
|
||||
}),
|
||||
},
|
||||
{
|
||||
validator: async (rule, value) => {
|
||||
if (value && value.length < 8) {
|
||||
return Promise.reject(
|
||||
intl.formatMessage({
|
||||
id: 'master.users.password.minimum',
|
||||
defaultMessage: 'Minimum password length is 8',
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
name="password"
|
||||
label={intl.formatMessage({
|
||||
id: 'master.users.password',
|
||||
defaultMessage: 'Password',
|
||||
})}
|
||||
fieldProps={{
|
||||
prefix: <LockOutlined />,
|
||||
autoComplete: 'current-password',
|
||||
}}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'master.users.password.placeholder',
|
||||
defaultMessage: 'Password',
|
||||
})}
|
||||
/>
|
||||
<ProForm.Item
|
||||
name="group_id"
|
||||
label={intl.formatMessage({
|
||||
id: 'master.users.groups',
|
||||
defaultMessage: 'Groups',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'master.users.groups.required',
|
||||
defaultMessage: 'Please select groups!',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TreeSelectedGroup groupIds={group_id} onSelected={handleGroupSelect} />
|
||||
</ProForm.Item>
|
||||
</ModalForm>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateUser;
|
||||
258
src/pages/Manager/User/index.tsx
Normal file
258
src/pages/Manager/User/index.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
import TreeGroup from '@/components/shared/TreeGroup';
|
||||
import { DEFAULT_PAGE_SIZE } from '@/constants';
|
||||
import {
|
||||
apiQueryUsers,
|
||||
apiQueryUsersByGroup,
|
||||
} from '@/services/master/UserController';
|
||||
import {
|
||||
ActionType,
|
||||
ProCard,
|
||||
ProColumns,
|
||||
ProTable,
|
||||
} from '@ant-design/pro-components';
|
||||
import { FormattedMessage, useIntl } from '@umijs/max';
|
||||
import { Grid } from 'antd';
|
||||
import message from 'antd/es/message';
|
||||
import Paragraph from 'antd/lib/typography/Paragraph';
|
||||
import { useRef, useState } from 'react';
|
||||
import CreateUser from './components/CreateUser';
|
||||
|
||||
const ManagerUserPage = () => {
|
||||
const { useBreakpoint } = Grid;
|
||||
const intl = useIntl();
|
||||
const screens = useBreakpoint();
|
||||
const actionRef = useRef<ActionType | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [selectedRowsState, setSelectedRowsState] = useState<
|
||||
MasterModel.ProfileResponse[]
|
||||
>([]);
|
||||
const [groupCheckedKeys, setGroupCheckedKeys] = useState<
|
||||
string | string[] | null
|
||||
>(null);
|
||||
|
||||
const columns: ProColumns<MasterModel.ProfileResponse>[] = [
|
||||
{
|
||||
key: 'email',
|
||||
title: (
|
||||
<FormattedMessage id="master.users.email" defaultMessage="Email" />
|
||||
),
|
||||
tip: intl.formatMessage({
|
||||
id: 'master.users.email.tip',
|
||||
defaultMessage: 'The email is the unique key',
|
||||
}),
|
||||
dataIndex: 'email',
|
||||
render: (_, record) => (
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 0,
|
||||
verticalAlign: 'middle',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
copyable
|
||||
>
|
||||
{record?.email}
|
||||
</Paragraph>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
key: 'phone_number',
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id="master.users.phone_number"
|
||||
defaultMessage="Phone number"
|
||||
/>
|
||||
),
|
||||
tip: intl.formatMessage({
|
||||
id: 'master.users.phone_number.tip',
|
||||
defaultMessage: 'The phone number is the unique key',
|
||||
}),
|
||||
responsive: ['lg', 'md'],
|
||||
dataIndex: ['metadata', 'phone_number'],
|
||||
render: (_, record) =>
|
||||
record?.metadata?.phone_number ? (
|
||||
<Paragraph
|
||||
style={{
|
||||
marginBottom: 0,
|
||||
verticalAlign: 'middle',
|
||||
display: 'inline-block',
|
||||
}}
|
||||
copyable
|
||||
>
|
||||
{record?.metadata?.phone_number}
|
||||
</Paragraph>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
key: 'user_type',
|
||||
hideInSearch: true,
|
||||
title: <FormattedMessage id="master.users.role" defaultMessage="Role" />,
|
||||
tip: intl.formatMessage({
|
||||
id: 'master.users.role.tip',
|
||||
defaultMessage: 'The role is the unique key',
|
||||
}),
|
||||
dataIndex: ['metadata', 'user_type'],
|
||||
render: (_, record) => record?.metadata?.user_type || '...',
|
||||
},
|
||||
|
||||
{
|
||||
title: (
|
||||
<FormattedMessage id="common.actions" defaultMessage="Operating" />
|
||||
),
|
||||
hideInSearch: true,
|
||||
render: () => {
|
||||
return (
|
||||
<>
|
||||
{/* <PermissionButton
|
||||
user={record}
|
||||
title={intl.formatMessage({
|
||||
id: 'master.users.assign',
|
||||
defaultMessage: 'Assgin',
|
||||
})}
|
||||
/> */}
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
<ProCard split={screens.md ? 'vertical' : 'horizontal'}>
|
||||
<ProCard colSpan={{ xs: 24, sm: 24, md: 6, lg: 6, xl: 6 }}>
|
||||
<TreeGroup
|
||||
disable={isLoading}
|
||||
multiple={true}
|
||||
groupIds={groupCheckedKeys}
|
||||
onSelected={(value: string | string[] | null) => {
|
||||
setGroupCheckedKeys(value);
|
||||
if (actionRef.current) {
|
||||
actionRef.current.reload();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ProCard>
|
||||
<ProCard colSpan={{ xs: 24, sm: 24, md: 18, lg: 18, xl: 18 }}>
|
||||
<ProTable<MasterModel.ProfileResponse>
|
||||
columns={columns}
|
||||
tableLayout="auto"
|
||||
actionRef={actionRef}
|
||||
rowKey="id"
|
||||
search={{
|
||||
layout: 'vertical',
|
||||
defaultCollapsed: false,
|
||||
}}
|
||||
dateFormatter="string"
|
||||
rowSelection={{
|
||||
selectedRowKeys: selectedRowsState.map((row) => row.id!),
|
||||
onChange: (
|
||||
_: React.Key[],
|
||||
selectedRows: MasterModel.ProfileResponse[],
|
||||
) => {
|
||||
setSelectedRowsState(selectedRows);
|
||||
},
|
||||
}}
|
||||
pagination={{
|
||||
defaultPageSize: DEFAULT_PAGE_SIZE * 2,
|
||||
showSizeChanger: true,
|
||||
pageSizeOptions: ['10', '15', '20'],
|
||||
showTotal: (total, range) =>
|
||||
`${range[0]}-${range[1]}
|
||||
${intl.formatMessage({
|
||||
id: 'common.paginations.of',
|
||||
defaultMessage: 'of',
|
||||
})}
|
||||
${total} ${intl.formatMessage({
|
||||
id: 'master.users.table.pagination',
|
||||
defaultMessage: 'users',
|
||||
})}`,
|
||||
}}
|
||||
request={async (params = {}) => {
|
||||
const { current = 1, pageSize, email, phone_number } = params;
|
||||
const size = pageSize || DEFAULT_PAGE_SIZE * 2;
|
||||
const offset = current === 1 ? 0 : (current - 1) * size;
|
||||
setIsLoading(true);
|
||||
// If groups are checked, use queryUsersByGroup
|
||||
if (groupCheckedKeys && groupCheckedKeys.length > 0) {
|
||||
// Ensure groupCheckedKeys is an array
|
||||
const groupIdsArray = Array.isArray(groupCheckedKeys)
|
||||
? groupCheckedKeys.join(',')
|
||||
: groupCheckedKeys;
|
||||
|
||||
const userByGroupResponses = await apiQueryUsersByGroup(
|
||||
groupIdsArray,
|
||||
);
|
||||
let users = userByGroupResponses.users || [];
|
||||
// Apply filters
|
||||
if (email) {
|
||||
users = users.filter((user: MasterModel.ProfileResponse) =>
|
||||
user.email?.includes(email),
|
||||
);
|
||||
}
|
||||
if (phone_number) {
|
||||
users = users.filter((user: MasterModel.ProfileResponse) =>
|
||||
user.metadata?.phone_number?.includes(phone_number),
|
||||
);
|
||||
}
|
||||
|
||||
const total = users.length;
|
||||
const paginatedUsers = users.slice(offset, offset + size);
|
||||
setIsLoading(false);
|
||||
return {
|
||||
data: paginatedUsers,
|
||||
success: true,
|
||||
total: total,
|
||||
};
|
||||
} else {
|
||||
// Use regular queryUsers API
|
||||
const metadata: Partial<MasterModel.ProfileMetadata> = {};
|
||||
if (phone_number) metadata.phone_number = phone_number;
|
||||
|
||||
const query: MasterModel.SearchUserPaginationBody = {
|
||||
offset: offset,
|
||||
limit: size,
|
||||
order: 'name',
|
||||
dir: 'asc',
|
||||
};
|
||||
if (email) query.email = email;
|
||||
if (Object.keys(metadata).length > 0) query.metadata = metadata;
|
||||
|
||||
const response = await apiQueryUsers(query);
|
||||
setIsLoading(false);
|
||||
return {
|
||||
data: response.users,
|
||||
success: true,
|
||||
total: response.total,
|
||||
};
|
||||
}
|
||||
}}
|
||||
options={{
|
||||
search: false,
|
||||
setting: false,
|
||||
density: false,
|
||||
reload: true,
|
||||
}}
|
||||
toolBarRender={() => [
|
||||
<CreateUser
|
||||
message={messageApi}
|
||||
onSuccess={(isSuccess) => {
|
||||
if (isSuccess) {
|
||||
actionRef.current?.reload();
|
||||
}
|
||||
}}
|
||||
key="create-user"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</ProCard>
|
||||
</ProCard>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManagerUserPage;
|
||||
Reference in New Issue
Block a user