diff --git a/.eslintrc.js b/.eslintrc.js index 85ba500..0ed1446 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,6 @@ module.exports = { extends: require.resolve('@umijs/max/eslint'), + rules: { + '@typescript-eslint/no-unused-vars': 'off', + }, }; diff --git a/README.md b/README.md new file mode 100644 index 0000000..194d2a4 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +Source web của các dự án SMATEC diff --git a/config/routes.ts b/config/routes.ts index df6edfb..48fa513 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -61,6 +61,10 @@ export const commonManagerRoutes = [ path: '/manager/users', component: './Manager/User', }, + { + path: '/manager/users/:userId/permissions', + component: './Manager/User/Permission/Assign', + }, ], }, { diff --git a/src/components/Avatar/AvatarDropdown.tsx b/src/components/Avatar/AvatarDropdown.tsx index e262f70..6e96a60 100644 --- a/src/components/Avatar/AvatarDropdown.tsx +++ b/src/components/Avatar/AvatarDropdown.tsx @@ -1,6 +1,10 @@ import { ROUTE_LOGIN, ROUTE_PROFILE } from '@/constants/routes'; import { clearAllData, clearSessionData, removeToken } from '@/utils/storage'; -import { LogoutOutlined, ProfileOutlined } from '@ant-design/icons'; +import { + LogoutOutlined, + SettingOutlined, + UserOutlined, +} from '@ant-design/icons'; import { history, useIntl } from '@umijs/max'; import { Dropdown } from 'antd'; @@ -18,7 +22,7 @@ export const AvatarDropdown = ({ items: [ { key: ROUTE_PROFILE, - icon: , + icon: , label: intl.formatMessage({ id: 'menu.profile' }), onClick: () => { history.push(ROUTE_PROFILE); @@ -42,17 +46,8 @@ export const AvatarDropdown = ({ }} >
- avatar - + + {currentUserProfile?.metadata?.full_name || ''}
diff --git a/src/components/Theme/ThemeSwitcher.tsx b/src/components/Theme/ThemeSwitcher.tsx index 55cd6e0..439facb 100644 --- a/src/components/Theme/ThemeSwitcher.tsx +++ b/src/components/Theme/ThemeSwitcher.tsx @@ -1,7 +1,7 @@ import { THEME_KEY } from '@/constants'; import { MoonOutlined, SunOutlined } from '@ant-design/icons'; -import { useIntl, useModel } from '@umijs/max'; -import { Button, Dropdown } from 'antd'; +import { useModel } from '@umijs/max'; +import { Segmented } from 'antd'; import React, { useEffect, useState } from 'react'; export interface ThemeSwitcherProps { @@ -10,7 +10,6 @@ export interface ThemeSwitcherProps { const ThemeSwitcher: React.FC = ({ className }) => { const { initialState, setInitialState } = useModel('@@initialState'); - const intl = useIntl(); const [isDark, setIsDark] = useState( (initialState?.theme as 'light' | 'dark') === 'dark', ); @@ -30,46 +29,80 @@ const ThemeSwitcher: React.FC = ({ className }) => { }, []); const handleThemeChange = (newTheme: 'light' | 'dark') => { - localStorage.setItem(THEME_KEY, newTheme); - // Update global state để trigger layout re-render - setInitialState({ - ...initialState, - theme: newTheme, - } as any).then(() => { - // Dispatch event để notify ThemeProvider + // Check if browser supports View Transitions API + const supportsViewTransition = 'startViewTransition' in document; + + if (!supportsViewTransition) { + // Fallback: just change theme without animation + localStorage.setItem(THEME_KEY, newTheme); + setIsDark(newTheme === 'dark'); + window.dispatchEvent( + new CustomEvent('theme-change', { detail: { theme: newTheme } }), + ); + setInitialState({ + ...initialState, + theme: newTheme, + } as any); + return; + } + + // Set origin to top-right corner + const x = window.innerWidth; + const y = 0; + + // Calculate end radius to cover the entire screen + const endRadius = Math.hypot( + Math.max(x, window.innerWidth - x), + Math.max(y, window.innerHeight - y), + ); + + // Start the view transition + const transition = (document as any).startViewTransition(() => { + localStorage.setItem(THEME_KEY, newTheme); + setIsDark(newTheme === 'dark'); window.dispatchEvent( new CustomEvent('theme-change', { detail: { theme: newTheme } }), ); }); + + // Animate the ripple effect + transition.ready.then(() => { + requestAnimationFrame(() => { + document.documentElement.animate( + { + clipPath: [ + `circle(0px at ${x}px ${y}px)`, + `circle(${endRadius}px at ${x}px ${y}px)`, + ], + }, + { + duration: 800, + easing: 'ease-in-out', + pseudoElement: '::view-transition-new(root)', + }, + ); + }); + }); + + // Update initialState after transition + setInitialState({ + ...initialState, + theme: newTheme, + } as any); }; - const items = [ - { - key: 'light', - label: intl.formatMessage({ - id: 'common.theme.light', - defaultMessage: 'Light Theme', - }), - icon: , - onClick: () => handleThemeChange('light'), - }, - { - key: 'dark', - label: intl.formatMessage({ - id: 'common.theme.dark', - defaultMessage: 'Dark Theme', - }), - icon: , - onClick: () => handleThemeChange('dark'), - }, - ]; - return ( - - - + handleThemeChange(value as 'light' | 'dark')} + options={[ + { value: 'light', icon: }, + { value: 'dark', icon: }, + ]} + /> ); }; diff --git a/src/components/Theme/style.less b/src/components/Theme/style.less index 249955c..58bb5af 100644 --- a/src/components/Theme/style.less +++ b/src/components/Theme/style.less @@ -1,3 +1,12 @@ +/* View Transitions API - Disable default animation for custom ripple effect */ +/* stylelint-disable selector-pseudo-element-no-unknown */ +::view-transition-old(root), +::view-transition-new(root) { + animation: none; + mix-blend-mode: normal; +} +/* stylelint-enable selector-pseudo-element-no-unknown */ + /* From Uiverse.io by Galahhad */ .theme-switch { --toggle-size: 20px; diff --git a/src/constants/api.ts b/src/constants/api.ts index 97f9da7..33ffafa 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -8,6 +8,7 @@ export const API_ALARMS_CONFIRM = '/api/alarms/confirm'; // Thing API Constants export const API_THINGS_SEARCH = '/api/things/search'; +export const API_THINGS_POLICY = '/api/things/policy'; // Group API Constants export const API_GROUPS = '/api/groups'; diff --git a/src/constants/routes.ts b/src/constants/routes.ts index 7e5524d..f039582 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -1,3 +1,5 @@ export const ROUTE_LOGIN = '/login'; export const ROUTER_HOME = '/'; export const ROUTE_PROFILE = '/profile'; +export const ROUTE_MANAGER_USERS = '/manager/users'; +export const ROUTE_MANAGER_USERS_PERMISSIONS = 'permissions'; diff --git a/src/locales/en-US/master/master-en.ts b/src/locales/en-US/master/master-en.ts index ec560d9..379ef04 100644 --- a/src/locales/en-US/master/master-en.ts +++ b/src/locales/en-US/master/master-en.ts @@ -7,6 +7,7 @@ import masterMenuProfileEn from './master-profile-en'; import masterThingEn from './master-thing-en'; import masterUserEn from './master-user-en'; export default { + 'master.footer.chosen': 'Chosen', ...masterAuthEn, ...masterMenuEn, ...masterMenuProfileEn, diff --git a/src/locales/en-US/master/master-user-en.ts b/src/locales/en-US/master/master-user-en.ts index 9a2422e..ea81138 100644 --- a/src/locales/en-US/master/master-user-en.ts +++ b/src/locales/en-US/master/master-user-en.ts @@ -32,4 +32,23 @@ export default { 'master.users.role.sgw.end_user': 'Ship Owner', 'master.users.create.error': 'User creation failed', 'master.users.create.success': 'User created successfully', + 'master.users.change_role.confirm.title': 'Confirm role change', + 'master.users.change_role.admin.content': + 'Are you sure you want to change the role to Unit Manager?', + 'master.users.change_role.user.content': + 'Are you sure you want to change the role to Unit Supervisor?', + 'master.users.change_role.user.success': 'Role change successful', + 'master.users.change_role.user.fail': 'Role change failed', + 'master.users.group_assign.title': 'Assigned Groups', + 'master.users.group_assign.button.title': 'Assign Groups', + 'master.users.group_assign.select.title': 'Select Group', + 'master.users.unassign.content': + 'Are you sure you want to unassign this group?', + 'master.users.unassign.success': 'Unassign successful', + 'master.users.unassign.fail': 'Unassign failed', + 'master.users.assign.success': 'Assign group successful', + 'master.users.assign.fail': 'Assign group failed', + 'master.users.deletion.title': 'Are you sure to delete this selected items?', + 'master.users.delete.success': 'User deleted successfully', + 'master.users.delete.fail': 'User deletion failed', }; diff --git a/src/locales/vi-VN/master/master-group-vi.ts b/src/locales/vi-VN/master/master-group-vi.ts index 017ad8a..ebf0453 100644 --- a/src/locales/vi-VN/master/master-group-vi.ts +++ b/src/locales/vi-VN/master/master-group-vi.ts @@ -4,7 +4,7 @@ export default { 'Không thể tạo đơn vị con khi gốc đã có thiết bị', 'master.groups.add': 'Tạo đơn vị cấp dưới', 'master.groups.delete.confirm': 'Bạn có chắc muốn xóa nhóm này không?', - 'master.groups.code': 'Mã', + 'master.groups.code': 'Mã đỡ vị', 'master.groups.code.exists': 'Mã đã tồn tại', 'master.groups.short_name': 'Tên viết tắt', 'master.groups.short_name.exists': 'Tên viết tắt đã tồn tại', diff --git a/src/locales/vi-VN/master/master-user-vi.ts b/src/locales/vi-VN/master/master-user-vi.ts index c147763..0ed2ae4 100644 --- a/src/locales/vi-VN/master/master-user-vi.ts +++ b/src/locales/vi-VN/master/master-user-vi.ts @@ -32,4 +32,22 @@ export default { 'master.users.role.sgw.end_user': 'Chủ tàu', 'master.users.create.error': 'Tạo người dùng lỗi', 'master.users.create.success': 'Tạo người dùng thành công', + 'master.users.change_role.confirm.title': 'Xác nhận thay đổi vai trò', + 'master.users.change_role.admin.content': + 'Bạn có chắc muốn thay đổi vai trò thành quản lý đơn vị không?', + 'master.users.change_role.user.content': + 'Bạn có chắc muốn thay đổi vai trò thành giám sát đơn vị không?', + 'master.users.change_role.user.success': 'Thay đổi vai trò thành công', + 'master.users.change_role.user.fail': 'Thay đổi vai trò thất bại', + 'master.users.group_assign.title': 'Đơn vị được phân quyền', + 'master.users.group_assign.button.title': 'Phân quyền đơn vị', + 'master.users.group_assign.select.title': 'Chọn đơn vị', + 'master.users.unassign.content': 'Bạn chắc chắn ngừng phân quyền khỏi đơn vị', + 'master.users.unassign.success': 'Ngừng phân quyền thành công', + 'master.users.unassign.fail': 'Ngừng phân quyền thất bại', + 'master.users.assign.success': 'Phân quyền đơn vị thành công', + 'master.users.assign.fail': 'Phân quyền đơn vị thất bại', + 'master.users.deletion.title': 'Chắc chắn xoá các tài khoản đã chọn?', + 'master.users.delete.success': 'Xoá người dùng thành công', + 'master.users.delete.fail': 'Xoá người dùng thất bại', }; diff --git a/src/locales/vi-VN/master/master-vi.ts b/src/locales/vi-VN/master/master-vi.ts index 8dbf4cc..fc9a44c 100644 --- a/src/locales/vi-VN/master/master-vi.ts +++ b/src/locales/vi-VN/master/master-vi.ts @@ -7,6 +7,7 @@ import masterProfileVi from './master-profile-vi'; import masterThingVi from './master-thing-vi'; import masterUserVi from './master-user-vi'; export default { + 'master.footer.chosen': 'Đã chọn', ...masterAuthVi, ...masterMenuVi, ...masterProfileVi, diff --git a/src/pages/Alarm/components/AlarmFormConfirm.tsx b/src/pages/Alarm/components/AlarmFormConfirm.tsx index 50df39f..b00aee0 100644 --- a/src/pages/Alarm/components/AlarmFormConfirm.tsx +++ b/src/pages/Alarm/components/AlarmFormConfirm.tsx @@ -38,7 +38,7 @@ const AlarmFormConfirm = ({ formRef={formRef} title={ - + } width="40%" @@ -66,7 +66,7 @@ const AlarmFormConfirm = ({ if (resp.status === HTTPSTATUS.HTTP_ACCEPTED) { message.success({ content: intl.formatMessage({ - id: 'alarms.confirm.success', + id: 'master.alarms.confirm.success', defaultMessage: 'Confirm alarm successfully', }), }); @@ -74,7 +74,7 @@ const AlarmFormConfirm = ({ } else if (resp.status === HTTPSTATUS.HTTP_NOTFOUND) { message.warning({ content: intl.formatMessage({ - id: 'alarms.not_found', + id: 'master.alarms.not_found', defaultMessage: 'Alarm has expired or does not exist', }), }); @@ -86,7 +86,7 @@ const AlarmFormConfirm = ({ console.error('Error when confirm alarm: ', error); message.error({ content: intl.formatMessage({ - id: 'alarms.confirm.fail', + id: 'master.alarms.confirm.fail', defaultMessage: 'Confirm alarm failed', }), }); @@ -120,7 +120,7 @@ const AlarmFormConfirm = ({ { + const { userId } = useParams<{ userId: string }>(); + const [userProfile, setUserProfile] = + useState(null); + const [loading, setLoading] = useState(false); + const [tabSelected, setTabSelected] = useState( + AssignTabsKey.group, + ); + useEffect(() => { + const fetchUserProfile = async () => { + try { + setLoading(true); + const profile = await apiQueryUserById(userId || ''); + setUserProfile(profile); + } catch (error) { + console.error('Failed to fetch user profile:', error); + } finally { + setLoading(false); + } + }; + fetchUserProfile(); + }, [userId]); + return ( + history.push(ROUTE_MANAGER_USERS), + }} + loading={loading} + tabList={[ + { + tab: 'Phân quyền đơn vị', + key: AssignTabsKey.group, + }, + { + tab: 'Chia sẻ thiết bị', + key: AssignTabsKey.device, + }, + ]} + onTabChange={(key) => { + setTabSelected(key as AssignTabsKey); + }} + > + {tabSelected === AssignTabsKey.group && ( + + )} + {tabSelected === AssignTabsKey.device && ( + + )} + + ); +}; + +export default AssignUserPage; diff --git a/src/pages/Manager/User/Permission/components/AssignGroup.tsx b/src/pages/Manager/User/Permission/components/AssignGroup.tsx new file mode 100644 index 0000000..592da1d --- /dev/null +++ b/src/pages/Manager/User/Permission/components/AssignGroup.tsx @@ -0,0 +1,318 @@ +import TreeSelectedGroup from '@/components/shared/TreeSelectedGroup'; +import { HTTPSTATUS } from '@/constants'; +import { + apiAssignToGroup, + apiQueryMembers, + apiUnassignToGroup, +} from '@/services/master/GroupController'; +import { apiChangeRoleUser } from '@/services/master/UserController'; +import { DeleteOutlined } from '@ant-design/icons'; +import { ActionType, ProList, ProListMetas } from '@ant-design/pro-components'; +import { useIntl } from '@umijs/max'; +import { + Button, + Flex, + message, + Modal, + Popconfirm, + Segmented, + Space, + Tag, + Tooltip, + Typography, +} from 'antd'; +import { useRef, useState } from 'react'; +const { Text } = Typography; +type AssignGroupProps = { + user: MasterModel.ProfileResponse | null; +}; +const AssignGroup = ({ user }: AssignGroupProps) => { + const groupActionRef = useRef(); + const intl = useIntl(); + const [assignedGroups, setAssignedGroups] = useState( + [], + ); + const [messageApi, contextHolder] = message.useMessage(); + const [userRole, setUserRole] = useState( + user?.metadata?.user_type || 'users', + ); + const [open, setOpen] = useState(false); + const [confirmLoading, setConfirmLoading] = useState(false); + const [groupSelected, setGroupSelected] = useState( + null, + ); + const columns: ProListMetas = { + title: { + dataIndex: 'name', + + render: (_, item: MasterModel.GroupNode) => ( + + {item?.name} + + ), + }, + subTitle: { + render: (_, entity) => { + return ( + + + {entity?.metadata?.code || '-'} + + + {entity?.metadata?.short_name || '-'} + + + ); + }, + }, + actions: { + render: (_, item: MasterModel.GroupNode) => { + return ( + { + try { + const body: MasterModel.AssignMemberRequest = { + group_id: item.id, + type: 'users', + members: [user?.id || ''], + }; + const success = await apiUnassignToGroup(body); + if (success.status === HTTPSTATUS.HTTP_NOCONTENT) { + messageApi.success( + intl.formatMessage({ + id: 'master.users.unassign.success', + defaultMessage: 'Unassign group successfully', + }), + ); + groupActionRef.current?.reload(); + } else { + throw new Error('Unassign group failed'); + } + } catch (error) { + console.error('Error when unassign group: ', error); + messageApi.error( + intl.formatMessage({ + id: 'master.users.unassign.fail', + defaultMessage: 'Unassign group failed', + }), + ); + } + }} + > + , + ]} + request={async () => { + if (user?.id) { + const resp = await apiQueryMembers(user.id); + if (resp?.groups) { + setAssignedGroups(resp.groups); + return Promise.resolve({ + success: true, + data: resp?.groups, + total: resp?.groups?.length || 0, + }); + } + } + return Promise.resolve({ + success: false, + data: [], + total: 0, + }); + }} + rowKey="id" + search={false} + /> + + ); +}; + +export default AssignGroup; diff --git a/src/pages/Manager/User/Permission/components/ShareThing.tsx b/src/pages/Manager/User/Permission/components/ShareThing.tsx new file mode 100644 index 0000000..f6ced78 --- /dev/null +++ b/src/pages/Manager/User/Permission/components/ShareThing.tsx @@ -0,0 +1,100 @@ +import { ActionType, ProList } from '@ant-design/pro-components'; +import { useIntl } from '@umijs/max'; +import { message } from 'antd'; +import { useRef } from 'react'; + +type ShareThingProps = { + user: MasterModel.ProfileResponse | null; +}; +const ShareThing = ({ user }: ShareThingProps) => { + const listActionRef = useRef(); + const intl = useIntl(); + const [messageAPI, contextHolder] = message.useMessage(); + return ( + <> + {contextHolder} + [ + // , + ]} + metas={columns} + request={async () => { + const query = { + type: 'sub', + id: user?.id || '', + }; + if (user?.id) { + const resp = (await apiQueryThingsByPolicy( + query, + )) as PolicyResponse; + const { relations } = resp; + if (relations) { + const queries = relations.map(async (rel: PolicyRelation) => { + const thg = await apiQueryThing(rel.id); + return { + ...thg, + relations: rel?.actions, + }; + }); + const policies = await Promise.all(queries); + return Promise.resolve({ + success: true, + data: policies, + total: policies.length, + }); + } + } + return Promise.resolve({ + success: false, + data: [], + total: 0, + }); + }} + rowKey="id" + search={false} + // rowSelection={{ + // selectedRowKeys: selectedRowsState.map((row) => row.id).filter((id): id is string => id !== undefined), + // onChange: (_: React.Key[], selectedRows: API.Thing[]) => { + // setSelectedRows(selectedRows); + // }, + // }} + /> + {/* { + console.log(values); + const success = await handleShare(values); + if (success) { + handleShareModalVisible(false); + onReload(); + if (actionRef.current) { + //await delay(1000); + actionRef.current.reload(); + } + } + }} + /> */} + + ); +}; + +export default ShareThing; diff --git a/src/pages/Manager/User/components/CreateUser.tsx b/src/pages/Manager/User/components/CreateUser.tsx index 1c5f91c..31090af 100644 --- a/src/pages/Manager/User/components/CreateUser.tsx +++ b/src/pages/Manager/User/components/CreateUser.tsx @@ -232,24 +232,28 @@ const CreateUser = ({ message, onSuccess }: CreateUserProps) => { { 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', - }), - ), - ); + try { + 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', + }), + ), + ); + } + } catch (error) { + return Promise.resolve(); } } return Promise.resolve(); diff --git a/src/pages/Manager/User/index.tsx b/src/pages/Manager/User/index.tsx index 87d3211..31e7031 100644 --- a/src/pages/Manager/User/index.tsx +++ b/src/pages/Manager/User/index.tsx @@ -2,17 +2,24 @@ import IconFont from '@/components/IconFont'; import TreeGroup from '@/components/shared/TreeGroup'; import { DEFAULT_PAGE_SIZE } from '@/constants'; import { + ROUTE_MANAGER_USERS, + ROUTE_MANAGER_USERS_PERMISSIONS, +} from '@/constants/routes'; +import { + apiDeleteUser, apiQueryUsers, apiQueryUsersByGroup, } from '@/services/master/UserController'; +import { DeleteOutlined } from '@ant-design/icons'; import { ActionType, + FooterToolbar, ProCard, ProColumns, ProTable, } from '@ant-design/pro-components'; -import { FormattedMessage, useIntl } from '@umijs/max'; -import { Button, Grid } from 'antd'; +import { FormattedMessage, history, useIntl } from '@umijs/max'; +import { Button, Grid, Popconfirm, theme } from 'antd'; import message from 'antd/es/message'; import Paragraph from 'antd/lib/typography/Paragraph'; import { useRef, useState } from 'react'; @@ -22,18 +29,21 @@ const ManagerUserPage = () => { const { useBreakpoint } = Grid; const intl = useIntl(); const screens = useBreakpoint(); + const { token } = theme.useToken(); const actionRef = useRef(null); const [isLoading, setIsLoading] = useState(false); const [messageApi, contextHolder] = message.useMessage(); const [selectedRowsState, setSelectedRowsState] = useState< MasterModel.ProfileResponse[] >([]); + const [groupCheckedKeys, setGroupCheckedKeys] = useState< string | string[] | null >(null); const handleClickAssign = (user: MasterModel.ProfileResponse) => { - console.log('User ', user); + const path = `${ROUTE_MANAGER_USERS}/${user.id}/${ROUTE_MANAGER_USERS_PERMISSIONS}`; + history.push(path); }; const columns: ProColumns[] = [ @@ -53,6 +63,7 @@ const ManagerUserPage = () => { marginBottom: 0, verticalAlign: 'middle', display: 'inline-block', + color: token.colorText, }} copyable > @@ -82,6 +93,7 @@ const ManagerUserPage = () => { marginBottom: 0, verticalAlign: 'middle', display: 'inline-block', + color: token.colorText, }} copyable > @@ -124,6 +136,48 @@ const ManagerUserPage = () => { }, ]; + const handleRemove = async (selectedRows: MasterModel.ProfileResponse[]) => { + const key = 'remove_user'; + if (!selectedRows) return true; + + try { + messageApi.open({ + type: 'loading', + content: intl.formatMessage({ + id: 'common.deleting', + defaultMessage: 'deleting...', + }), + duration: 0, + key, + }); + const allDelete = selectedRows.map( + async (row: MasterModel.ProfileResponse) => { + await apiDeleteUser(row?.id || ''); + }, + ); + await Promise.all(allDelete); + messageApi.open({ + type: 'success', + content: intl.formatMessage({ + id: 'master.users.delete.success', + defaultMessage: 'Deleted successfully and will refresh soon', + }), + key, + }); + return true; + } catch (error) { + messageApi.open({ + type: 'error', + content: intl.formatMessage({ + id: 'master.users.delete.fail', + defaultMessage: 'Delete failed, please try again!', + }), + key, + }); + return false; + } + }; + return ( <> {contextHolder} @@ -182,56 +236,66 @@ const ManagerUserPage = () => { 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; + try { + 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), + const userByGroupResponses = await apiQueryUsersByGroup( + groupIdsArray, ); - } - if (phone_number) { - users = users.filter((user: MasterModel.ProfileResponse) => - user.metadata?.phone_number?.includes(phone_number), - ); - } + 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); + 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 = {}; + 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, + }; + } + } catch (error) { setIsLoading(false); return { - data: paginatedUsers, - success: true, - total: total, - }; - } else { - // Use regular queryUsers API - const metadata: Partial = {}; - 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, + data: [], + success: false, + total: 0, }; } }} @@ -255,6 +319,53 @@ const ManagerUserPage = () => { /> + {selectedRowsState?.length > 0 && ( + + {' '} + + {selectedRowsState.length} + {' '} + + + } + > + { + const success = await handleRemove(selectedRowsState); + if (success) { + setSelectedRowsState([]); + if (actionRef.current) { + actionRef.current.reload(); + } + } + }} + > + + + + )} ); }; diff --git a/src/services/master/GroupController.ts b/src/services/master/GroupController.ts index 5325c85..6623194 100644 --- a/src/services/master/GroupController.ts +++ b/src/services/master/GroupController.ts @@ -55,3 +55,20 @@ export async function apiDeleteGroup(group_id: string) { }, ); } + +export async function apiUnassignToGroup( + body: MasterModel.AssignMemberRequest, +) { + return request(`${API_GROUPS}/${body.group_id}/members`, { + method: 'DELETE', + data: body, + getResponse: true, + }); +} +export async function apiAssignToGroup(body: MasterModel.AssignMemberRequest) { + return request(`${API_GROUPS}/${body.group_id}/members`, { + method: 'POST', + data: body, + getResponse: true, + }); +} diff --git a/src/services/master/UserController.ts b/src/services/master/UserController.ts index c0947ec..ff10be3 100644 --- a/src/services/master/UserController.ts +++ b/src/services/master/UserController.ts @@ -1,4 +1,4 @@ -import { API_USERS, API_USERS_BY_GROUP } from '@/constants/api'; +import { API_GROUPS, API_USERS, API_USERS_BY_GROUP } from '@/constants/api'; import { request } from '@umijs/max'; export async function apiQueryUsers( @@ -9,6 +9,10 @@ export async function apiQueryUsers( }); } +export async function apiQueryUserById(userId: string) { + return request(`${API_USERS}/${userId}`); +} + export async function apiQueryUsersByGroup( group_id: string, ): Promise { @@ -22,3 +26,19 @@ export async function apiCreateUsers(body: MasterModel.CreateUserBodyRequest) { getResponse: true, }); } +export async function apiChangeRoleUser( + userId: string, + role: 'users' | 'admin', +) { + return request(`${API_GROUPS}/${userId}/${role}`, { + method: 'PUT', + getResponse: true, + }); +} + +export async function apiDeleteUser(userId: string) { + return request(`${API_USERS}/${userId}`, { + method: 'DELETE', + getResponse: true, + }); +} diff --git a/src/services/master/typings.d.ts b/src/services/master/typings.d.ts index 68528a0..dc9fdbf 100644 --- a/src/services/master/typings.d.ts +++ b/src/services/master/typings.d.ts @@ -36,6 +36,7 @@ declare namespace MasterModel { subtopic?: string; } + // Auth interface LoginRequestBody { guid: string; email: string; @@ -66,6 +67,7 @@ declare namespace MasterModel { user_type?: 'admin' | 'enduser' | 'sysadmin' | 'users'; } + // User interface CreateUserMetadata extends ProfileMetadata { group_id?: string; } @@ -96,7 +98,7 @@ declare namespace MasterModel { thing_id?: string; time?: number; } - + // Alarm interface Alarm { name?: string; time?: number; @@ -110,6 +112,8 @@ declare namespace MasterModel { thing_name?: string; thing_type?: string; } + + // Thing interface ThingMetadata { address?: string; alarm_list?: string; @@ -155,6 +159,25 @@ declare namespace MasterModel { metadata?: T; } + // Thing Policy + interface ThingPolicy { + next_page_token?: string; + relations?: Relation[]; + } + + interface Relation { + id?: string; + actions?: Action[]; + } + + enum Action { + Delete = 'delete', + Read = 'read', + Write = 'write', + } + + // Group + interface GroupBodyRequest { id?: string; name: string; @@ -200,6 +223,8 @@ declare namespace MasterModel { [key: string]: any; } + // Log + type LogTypeRequest = 'user_logs' | undefined; interface LogResponse { @@ -222,4 +247,12 @@ declare namespace MasterModel { time?: number; string_value?: string; } + + // User + + interface AssignMemberRequest { + group_id: string; + type: 'users' | 'admin' | 'things' | undefined; + members: string[]; + } }