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 = ({
}}
>
-

-
+
+
{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',
+ }),
+ );
+ }
+ }}
+ >
+ } />
+
+ );
+ },
+ },
+ };
+
+ const showModal = () => {
+ setOpen(true);
+ };
+
+ const handleAssignUserToGroup = async () => {
+ setConfirmLoading(true);
+ try {
+ const body: MasterModel.AssignMemberRequest = {
+ group_id: groupSelected as string,
+ type: userRole === 'admin' ? 'admin' : 'users',
+ members: [user?.id || ''],
+ };
+ const resp = await apiAssignToGroup(body);
+ if (resp.status === HTTPSTATUS.HTTP_SUCCESS) {
+ messageApi.success(
+ intl.formatMessage({
+ id: 'master.users.assign.success',
+ defaultMessage: 'Assign group successfully',
+ }),
+ );
+ groupActionRef.current?.reload();
+ setOpen(false);
+ } else {
+ throw new Error('Assign group failed');
+ }
+ } catch (error) {
+ console.error('Error when assign group: ', error);
+ messageApi.error(
+ intl.formatMessage({
+ id: 'master.users.assign.fail',
+ defaultMessage: 'Assign group failed',
+ }),
+ );
+ } finally {
+ setConfirmLoading(false);
+ }
+ };
+
+ const handleCancel = () => {
+ setOpen(false);
+ };
+ return (
+ <>
+ {contextHolder}
+
+
+
+ {intl.formatMessage({
+ id: 'master.users.group_assign.select.title',
+ defaultMessage: 'Select Group',
+ })}
+ :
+
+ group.id)}
+ onSelected={(value) => {
+ setGroupSelected(value);
+ }}
+ />
+
+
+
+ headerTitle={intl.formatMessage({
+ id: 'master.users.group_assign.title',
+ defaultMessage: 'Assigned list',
+ })}
+ actionRef={groupActionRef}
+ metas={columns}
+ toolBarRender={() => [
+ user?.metadata?.user_type !== 'enduser' && (
+ {
+ Modal.confirm({
+ title: intl.formatMessage({
+ id: 'master.users.change_role.confirm.title',
+ defaultMessage: 'Xác nhận thay đổi vai trò',
+ }),
+ content:
+ userRole === 'admin'
+ ? intl.formatMessage({
+ id: 'master.users.change_role.user.content',
+ defaultMessage:
+ 'Bạn có chắc muốn thay đổi vai trò thành giám sát đơn vị không?',
+ })
+ : intl.formatMessage({
+ id: 'master.users.change_role.admin.content',
+ defaultMessage:
+ 'Bạn có chắc muốn thay đổi vai trò thành quản lý đơn vị không?',
+ }),
+ onOk: async () => {
+ try {
+ const resp = await apiChangeRoleUser(
+ user?.id || '',
+ value as 'users' | 'admin',
+ );
+ if (resp.status === HTTPSTATUS.HTTP_SUCCESS) {
+ messageApi.success(
+ intl.formatMessage({
+ id: 'master.users.change_role.user.success',
+ defaultMessage: 'Role change successful',
+ }),
+ );
+ groupActionRef.current?.reload();
+ setUserRole(value as string);
+ } else {
+ throw new Error('Change role failed');
+ }
+ } catch (error) {
+ console.error('Error when change role user: ', error);
+ messageApi.error(
+ intl.formatMessage({
+ id: 'master.users.change_role.user.fail',
+ defaultMessage: 'Role change failed',
+ }),
+ );
+ }
+ },
+ });
+ }}
+ options={[
+ {
+ label: intl.formatMessage({
+ id: 'master.users.role.user',
+ defaultMessage: 'Giám sát',
+ }),
+ value: 'users',
+ },
+ {
+ label: intl.formatMessage({
+ id: 'master.users.role.admin',
+ defaultMessage: 'Quản lý',
+ }),
+ value: 'admin',
+ },
+ ]}
+ />
+ ),
+ ,
+ ]}
+ 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();
+ }
+ }
+ }}
+ >
+ } type="primary" danger>
+
+
+
+
+ )}
>
);
};
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[];
+ }
}