Compare commits
2 Commits
httpsImple
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0286e6e217 | |||
| dd3fd1403a |
74
Users-API.md
Normal file
74
Users-API.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# User API
|
||||
|
||||
Tai lieu mo ta cac endpoint cap nhat role va thong tin nguoi dung.
|
||||
|
||||
----------------------------------------
|
||||
|
||||
## 1) Cap nhat thong tin nguoi dung
|
||||
- PUT /api/User/{id}
|
||||
- Permission: EDIT_USER_ROLE
|
||||
|
||||
### Request
|
||||
```json
|
||||
{
|
||||
"name": "Nguyen Van A",
|
||||
"userName": "nguyenvana",
|
||||
"accessRooms": [1, 2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
### Response (200)
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "User updated successfully",
|
||||
"data": {
|
||||
"userId": 12,
|
||||
"userName": "nguyenvana",
|
||||
"name": "Nguyen Van A",
|
||||
"roleId": 3,
|
||||
"accessRooms": [1, 2, 3],
|
||||
"updatedAt": "2026-04-03T10:20:30Z",
|
||||
"updatedBy": "admin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ghi chu
|
||||
- Neu khong truyen `accessRooms` thi giu nguyen danh sach phong.
|
||||
- Neu truyen `accessRooms` = [] thi xoa tat ca phong.
|
||||
- Neu `userName` bi trung hoac khong hop le thi tra ve 400.
|
||||
|
||||
----------------------------------------
|
||||
|
||||
## 2) Cap nhat role nguoi dung
|
||||
- PUT /api/User/{id}/role
|
||||
- Permission: EDIT_USER_ROLE
|
||||
|
||||
### Request
|
||||
```json
|
||||
{
|
||||
"roleId": 2
|
||||
}
|
||||
```
|
||||
|
||||
### Response (200)
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "User role updated",
|
||||
"data": {
|
||||
"userId": 12,
|
||||
"userName": "nguyenvana",
|
||||
"roleId": 2,
|
||||
"roleName": "Manager",
|
||||
"updatedAt": "2026-04-03T10:20:30Z",
|
||||
"updatedBy": "admin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Ghi chu
|
||||
- Chi System Admin moi duoc phep cap nhat role System Admin.
|
||||
|
||||
----------------------------------------
|
||||
|
|
@ -22,7 +22,7 @@ export function RoomManagementCard({
|
|||
<Card className="bg-white/80 backdrop-blur border-muted/60 shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Quản lý phòng</CardTitle>
|
||||
<CardDescription>Thông tin tổng quan và phòng cần chú ý</CardDescription>
|
||||
<CardDescription>Thông tin tổng quan và các phòng đang không sử dụng</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
|
|
@ -47,7 +47,7 @@ export function RoomManagementCard({
|
|||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="text-sm font-medium">Phòng cần chú ý</div>
|
||||
<div className="text-sm font-medium">Phòng không dùng</div>
|
||||
<div className="mt-2 space-y-2">
|
||||
{data?.roomsNeedAttention && data.roomsNeedAttention.length > 0 ? (
|
||||
data.roomsNeedAttention.map((r: RoomHealthStatus) => (
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
export interface SelectItem {
|
||||
label: string;
|
||||
|
|
@ -16,6 +16,7 @@ interface SelectDialogProps {
|
|||
description?: string;
|
||||
icon?: React.ReactNode;
|
||||
items: SelectItem[];
|
||||
selectedValues?: string[];
|
||||
onConfirm: (values: string[]) => Promise<void> | void;
|
||||
}
|
||||
|
||||
|
|
@ -26,11 +27,18 @@ export function SelectDialog({
|
|||
description,
|
||||
icon,
|
||||
items,
|
||||
selectedValues,
|
||||
onConfirm,
|
||||
}: SelectDialogProps) {
|
||||
const [selected, setSelected] = useState<string[]>([]);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (!selectedValues) return;
|
||||
setSelected(selectedValues);
|
||||
}, [open, selectedValues]);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
return items.filter((item) =>
|
||||
item.label.toLowerCase().includes(search.toLowerCase())
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ export const API_ENDPOINTS = {
|
|||
CREATE_ACCOUNT: `${BASE_URL}/auth/create-account`,
|
||||
GET_USERS_LIST: `${BASE_URL}/users-info`,
|
||||
},
|
||||
USER: {
|
||||
UPDATE_INFO: (id: number) => `${BASE_URL}/User/${id}`,
|
||||
UPDATE_ROLE: (id: number) => `${BASE_URL}/User/${id}/role`,
|
||||
},
|
||||
APP_VERSION: {
|
||||
//agent and app api
|
||||
GET_VERSION: `${BASE_URL}/AppVersion/version`,
|
||||
|
|
@ -38,7 +42,7 @@ export const API_ENDPOINTS = {
|
|||
GET_REQUIRED_FILES: `${BASE_URL}/AppVersion/requirefiles`,
|
||||
ADD_REQUIRED_FILE: `${BASE_URL}/AppVersion/requirefile/add`,
|
||||
DELETE_REQUIRED_FILE: `${BASE_URL}/AppVersion/requirefile/delete`,
|
||||
DELETE_FILES: (fileId: number) => `${BASE_URL}/AppVersion/delete/${fileId}`,
|
||||
DELETE_FILES: `${BASE_URL}/AppVersion/delete`,
|
||||
},
|
||||
DEVICE_COMM: {
|
||||
DOWNLOAD_FILES: (roomName: string) => `${BASE_URL}/DeviceComm/downloadfile/${roomName}`,
|
||||
|
|
|
|||
|
|
@ -176,7 +176,7 @@ export function useDeleteFile() {
|
|||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (fileId: number) => appVersionService.deleteFile(fileId),
|
||||
mutationFn: (data: { MsiFileIds: number[] }) => appVersionService.deleteFile(data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: APP_VERSION_QUERY_KEYS.softwareList(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import * as userService from "@/services/user.service";
|
||||
import type { UserProfile } from "@/types/user-profile";
|
||||
import type {
|
||||
UserProfile,
|
||||
UpdateUserInfoRequest,
|
||||
UpdateUserRoleRequest,
|
||||
} from "@/types/user-profile";
|
||||
|
||||
const USER_QUERY_KEYS = {
|
||||
all: ["users"] as const,
|
||||
|
|
@ -18,3 +22,47 @@ export function useGetUsersInfo(enabled = true) {
|
|||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook để cập nhật thông tin người dùng
|
||||
*/
|
||||
export function useUpdateUserInfo() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
id,
|
||||
data,
|
||||
}: {
|
||||
id: number;
|
||||
data: UpdateUserInfoRequest;
|
||||
}) => userService.updateUserInfo(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: USER_QUERY_KEYS.list(),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook để cập nhật role người dùng
|
||||
*/
|
||||
export function useUpdateUserRole() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
id,
|
||||
data,
|
||||
}: {
|
||||
id: number;
|
||||
data: UpdateUserRoleRequest;
|
||||
}) => userService.updateUserRole(id, data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: USER_QUERY_KEYS.list(),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { Route as AuthProfileChangePasswordIndexRouteImport } from './routes/_au
|
|||
import { Route as AuthProfileUserNameIndexRouteImport } from './routes/_auth/profile/$userName/index'
|
||||
import { Route as authSsoCallbackIndexRouteImport } from './routes/(auth)/sso/callback/index'
|
||||
import { Route as AuthUserRoleRoleIdIndexRouteImport } from './routes/_auth/user/role/$roleId/index'
|
||||
import { Route as AuthUserEditUserNameIndexRouteImport } from './routes/_auth/user/edit/$userName/index'
|
||||
import { Route as AuthUserChangePasswordUserNameIndexRouteImport } from './routes/_auth/user/change-password/$userName/index'
|
||||
import { Route as AuthRoomsRoomNameFolderStatusIndexRouteImport } from './routes/_auth/rooms/$roomName/folder-status/index'
|
||||
import { Route as AuthRoomsRoomNameConnectIndexRouteImport } from './routes/_auth/rooms/$roomName/connect/index'
|
||||
|
|
@ -141,6 +142,12 @@ const AuthUserRoleRoleIdIndexRoute = AuthUserRoleRoleIdIndexRouteImport.update({
|
|||
path: '/user/role/$roleId/',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const AuthUserEditUserNameIndexRoute =
|
||||
AuthUserEditUserNameIndexRouteImport.update({
|
||||
id: '/user/edit/$userName/',
|
||||
path: '/user/edit/$userName/',
|
||||
getParentRoute: () => AuthRoute,
|
||||
} as any)
|
||||
const AuthUserChangePasswordUserNameIndexRoute =
|
||||
AuthUserChangePasswordUserNameIndexRouteImport.update({
|
||||
id: '/user/change-password/$userName/',
|
||||
|
|
@ -189,6 +196,7 @@ export interface FileRoutesByFullPath {
|
|||
'/rooms/$roomName/connect': typeof AuthRoomsRoomNameConnectIndexRoute
|
||||
'/rooms/$roomName/folder-status': typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
||||
'/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute
|
||||
'/user/edit/$userName': typeof AuthUserEditUserNameIndexRoute
|
||||
'/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
|
|
@ -215,6 +223,7 @@ export interface FileRoutesByTo {
|
|||
'/rooms/$roomName/connect': typeof AuthRoomsRoomNameConnectIndexRoute
|
||||
'/rooms/$roomName/folder-status': typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
||||
'/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute
|
||||
'/user/edit/$userName': typeof AuthUserEditUserNameIndexRoute
|
||||
'/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
|
|
@ -243,6 +252,7 @@ export interface FileRoutesById {
|
|||
'/_auth/rooms/$roomName/connect/': typeof AuthRoomsRoomNameConnectIndexRoute
|
||||
'/_auth/rooms/$roomName/folder-status/': typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
||||
'/_auth/user/change-password/$userName/': typeof AuthUserChangePasswordUserNameIndexRoute
|
||||
'/_auth/user/edit/$userName/': typeof AuthUserEditUserNameIndexRoute
|
||||
'/_auth/user/role/$roleId/': typeof AuthUserRoleRoleIdIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
|
|
@ -271,6 +281,7 @@ export interface FileRouteTypes {
|
|||
| '/rooms/$roomName/connect'
|
||||
| '/rooms/$roomName/folder-status'
|
||||
| '/user/change-password/$userName'
|
||||
| '/user/edit/$userName'
|
||||
| '/user/role/$roleId'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
|
|
@ -297,6 +308,7 @@ export interface FileRouteTypes {
|
|||
| '/rooms/$roomName/connect'
|
||||
| '/rooms/$roomName/folder-status'
|
||||
| '/user/change-password/$userName'
|
||||
| '/user/edit/$userName'
|
||||
| '/user/role/$roleId'
|
||||
id:
|
||||
| '__root__'
|
||||
|
|
@ -324,6 +336,7 @@ export interface FileRouteTypes {
|
|||
| '/_auth/rooms/$roomName/connect/'
|
||||
| '/_auth/rooms/$roomName/folder-status/'
|
||||
| '/_auth/user/change-password/$userName/'
|
||||
| '/_auth/user/edit/$userName/'
|
||||
| '/_auth/user/role/$roleId/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
|
|
@ -483,6 +496,13 @@ declare module '@tanstack/react-router' {
|
|||
preLoaderRoute: typeof AuthUserRoleRoleIdIndexRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/_auth/user/edit/$userName/': {
|
||||
id: '/_auth/user/edit/$userName/'
|
||||
path: '/user/edit/$userName'
|
||||
fullPath: '/user/edit/$userName'
|
||||
preLoaderRoute: typeof AuthUserEditUserNameIndexRouteImport
|
||||
parentRoute: typeof AuthRoute
|
||||
}
|
||||
'/_auth/user/change-password/$userName/': {
|
||||
id: '/_auth/user/change-password/$userName/'
|
||||
path: '/user/change-password/$userName'
|
||||
|
|
@ -535,6 +555,7 @@ interface AuthRouteChildren {
|
|||
AuthRoomsRoomNameConnectIndexRoute: typeof AuthRoomsRoomNameConnectIndexRoute
|
||||
AuthRoomsRoomNameFolderStatusIndexRoute: typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
||||
AuthUserChangePasswordUserNameIndexRoute: typeof AuthUserChangePasswordUserNameIndexRoute
|
||||
AuthUserEditUserNameIndexRoute: typeof AuthUserEditUserNameIndexRoute
|
||||
AuthUserRoleRoleIdIndexRoute: typeof AuthUserRoleRoleIdIndexRoute
|
||||
}
|
||||
|
||||
|
|
@ -561,6 +582,7 @@ const AuthRouteChildren: AuthRouteChildren = {
|
|||
AuthRoomsRoomNameFolderStatusIndexRoute,
|
||||
AuthUserChangePasswordUserNameIndexRoute:
|
||||
AuthUserChangePasswordUserNameIndexRoute,
|
||||
AuthUserEditUserNameIndexRoute: AuthUserEditUserNameIndexRoute,
|
||||
AuthUserRoleRoleIdIndexRoute: AuthUserRoleRoleIdIndexRoute,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -137,11 +137,10 @@ function AppsComponent() {
|
|||
return;
|
||||
}
|
||||
|
||||
const MsiFileIds = selectedRows.map((row: any) => row.original.id);
|
||||
|
||||
try {
|
||||
for (const row of selectedRows) {
|
||||
const { id } = row.original;
|
||||
await deleteMutation.mutateAsync(id);
|
||||
}
|
||||
await deleteMutation.mutateAsync({ MsiFileIds });
|
||||
toast.success("Xóa phần mềm thành công!");
|
||||
} catch (e) {
|
||||
toast.error("Xóa phần mềm thất bại!");
|
||||
|
|
@ -175,12 +174,10 @@ function AppsComponent() {
|
|||
if (!table) return;
|
||||
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
const MsiFileIds = selectedRows.map((row: any) => row.original.id);
|
||||
|
||||
try {
|
||||
for (const row of selectedRows) {
|
||||
const { id } = row.original;
|
||||
await deleteMutation.mutateAsync(id);
|
||||
}
|
||||
await deleteMutation.mutateAsync({ MsiFileIds });
|
||||
toast.success("Xóa phần mềm từ server thành công!");
|
||||
if (table) {
|
||||
table.setRowSelection({});
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ import { LoaderCircle } from "lucide-react";
|
|||
import { useState } from "react";
|
||||
|
||||
export const Route = createFileRoute("/_auth/user/change-password/$userName/")({
|
||||
head: () => ({
|
||||
meta: [{ title: "Thay đổi mật khẩu" }],
|
||||
}),
|
||||
component: AdminChangePasswordComponent,
|
||||
loader: async ({ context, params }) => {
|
||||
context.breadcrumbs = [
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ import { UserPlus, ArrowLeft, Save, Loader2 } from "lucide-react";
|
|||
import { toast } from "sonner";
|
||||
|
||||
export const Route = createFileRoute("/_auth/user/create/")({
|
||||
head: () => ({
|
||||
meta: [{ title: "Tạo người dùng mới" }],
|
||||
}),
|
||||
component: CreateUserComponent,
|
||||
loader: async ({ context }) => {
|
||||
context.breadcrumbs = [
|
||||
|
|
@ -59,7 +62,8 @@ function CreateUserComponent() {
|
|||
if (!formData.userName) {
|
||||
newErrors.userName = "Tên đăng nhập không được để trống";
|
||||
} else if (!validateUserName(formData.userName)) {
|
||||
newErrors.userName = "Tên đăng nhập chỉ cho phép chữ cái, số, dấu chấm và gạch dưới (3-20 ký tự)";
|
||||
newErrors.userName =
|
||||
"Tên đăng nhập chỉ cho phép chữ cái, số, dấu chấm và gạch dưới (3-20 ký tự)";
|
||||
}
|
||||
|
||||
// Validate name
|
||||
|
|
@ -106,7 +110,8 @@ function CreateUserComponent() {
|
|||
toast.success("Tạo tài khoản thành công!");
|
||||
navigate({ to: "/dashboard" }); // TODO: Navigate to user list page when it exists
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.response?.data?.message || "Tạo tài khoản thất bại!";
|
||||
const errorMessage =
|
||||
error.response?.data?.message || "Tạo tài khoản thất bại!";
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
|
@ -128,15 +133,14 @@ function CreateUserComponent() {
|
|||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Tạo người dùng mới</h1>
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
Tạo người dùng mới
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Thêm tài khoản người dùng mới vào hệ thống
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => navigate({ to: "/user" })}
|
||||
>
|
||||
<Button variant="outline" onClick={() => navigate({ to: "/user" })}>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Quay lại
|
||||
</Button>
|
||||
|
|
@ -164,7 +168,9 @@ function CreateUserComponent() {
|
|||
<Input
|
||||
id="userName"
|
||||
value={formData.userName}
|
||||
onChange={(e) => handleInputChange("userName", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("userName", e.target.value)
|
||||
}
|
||||
placeholder="Nhập tên đăng nhập (3-20 ký tự, chỉ chữ, số, . và _)"
|
||||
disabled={createMutation.isPending}
|
||||
className="h-10"
|
||||
|
|
@ -202,7 +208,9 @@ function CreateUserComponent() {
|
|||
id="password"
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) => handleInputChange("password", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("password", e.target.value)
|
||||
}
|
||||
placeholder="Nhập mật khẩu (tối thiểu 6 ký tự)"
|
||||
disabled={createMutation.isPending}
|
||||
className="h-10"
|
||||
|
|
@ -220,13 +228,17 @@ function CreateUserComponent() {
|
|||
id="confirmPassword"
|
||||
type="password"
|
||||
value={formData.confirmPassword}
|
||||
onChange={(e) => handleInputChange("confirmPassword", e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleInputChange("confirmPassword", e.target.value)
|
||||
}
|
||||
placeholder="Nhập lại mật khẩu"
|
||||
disabled={createMutation.isPending}
|
||||
className="h-10"
|
||||
/>
|
||||
{errors.confirmPassword && (
|
||||
<p className="text-sm text-destructive">{errors.confirmPassword}</p>
|
||||
<p className="text-sm text-destructive">
|
||||
{errors.confirmPassword}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -288,8 +300,8 @@ function CreateUserComponent() {
|
|||
>
|
||||
Hủy
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={createMutation.isPending}
|
||||
className="min-w-[140px]"
|
||||
>
|
||||
|
|
|
|||
361
src/routes/_auth/user/edit/$userName/index.tsx
Normal file
361
src/routes/_auth/user/edit/$userName/index.tsx
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
useGetRoleList,
|
||||
useGetRoomList,
|
||||
useGetUsersInfo,
|
||||
useUpdateUserInfo,
|
||||
useUpdateUserRole,
|
||||
} from "@/hooks/queries";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { SelectDialog } from "@/components/dialogs/select-dialog";
|
||||
import { ArrowLeft, Save } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import type { UserProfile } from "@/types/user-profile";
|
||||
|
||||
export const Route = createFileRoute("/_auth/user/edit/$userName/")({
|
||||
head: () => ({
|
||||
meta: [{ title: "Chỉnh sửa người dùng" }],
|
||||
}),
|
||||
component: EditUserComponent,
|
||||
loader: async ({ context, params }) => {
|
||||
context.breadcrumbs = [
|
||||
{ title: "Quản lý người dùng", path: "/user" },
|
||||
{
|
||||
title: `Chỉnh sửa thông tin người dùng ${params.userName}`,
|
||||
path: `/user/edit/${params.userName}`,
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
function EditUserComponent() {
|
||||
const { userName } = Route.useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: users = [], isLoading } = useGetUsersInfo();
|
||||
const { data: roomData = [], isLoading: roomsLoading } = useGetRoomList();
|
||||
const { data: roles = [], isLoading: rolesLoading } = useGetRoleList();
|
||||
const updateUserInfoMutation = useUpdateUserInfo();
|
||||
const updateUserRoleMutation = useUpdateUserRole();
|
||||
|
||||
const user = useMemo(() => {
|
||||
return users.find((u) => u.userName === userName) as
|
||||
| UserProfile
|
||||
| undefined;
|
||||
}, [users, userName]);
|
||||
|
||||
const [editForm, setEditForm] = useState({
|
||||
userName: "",
|
||||
name: "",
|
||||
});
|
||||
const [selectedRoleId, setSelectedRoleId] = useState<string>("");
|
||||
const [selectedRoomValues, setSelectedRoomValues] = useState<string[]>([]);
|
||||
const [isRoomDialogOpen, setIsRoomDialogOpen] = useState(false);
|
||||
|
||||
const roomOptions = useMemo(() => {
|
||||
const list = Array.isArray(roomData) ? roomData : [];
|
||||
return list
|
||||
.map((room: any) => {
|
||||
const rawValue =
|
||||
room.id ??
|
||||
room.roomId ??
|
||||
room.roomID ??
|
||||
room.Id ??
|
||||
room.ID ??
|
||||
room.RoomId ??
|
||||
room.RoomID ??
|
||||
room.name ??
|
||||
room.roomName ??
|
||||
room.RoomName ??
|
||||
"";
|
||||
const label =
|
||||
room.name ?? room.roomName ?? room.RoomName ?? (rawValue ? String(rawValue) : "");
|
||||
if (!rawValue || !label) return null;
|
||||
return { label: String(label), value: String(rawValue) };
|
||||
})
|
||||
.filter((item): item is { label: string; value: string } => !!item);
|
||||
}, [roomData]);
|
||||
|
||||
const roomLabelMap = useMemo(() => {
|
||||
return new Map(roomOptions.map((room) => [room.value, room.label]));
|
||||
}, [roomOptions]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
setEditForm({
|
||||
userName: user.userName ?? "",
|
||||
name: user.name ?? "",
|
||||
});
|
||||
setSelectedRoleId(user.roleId ? String(user.roleId) : "");
|
||||
setSelectedRoomValues(
|
||||
Array.isArray(user.accessRooms)
|
||||
? user.accessRooms.map((roomId) => String(roomId))
|
||||
: []
|
||||
);
|
||||
}, [user]);
|
||||
|
||||
const handleUpdateUserInfo = async () => {
|
||||
if (!user?.userId) {
|
||||
toast.error("Không tìm thấy userId để cập nhật.");
|
||||
return;
|
||||
}
|
||||
|
||||
const nextUserName = editForm.userName.trim();
|
||||
const nextName = editForm.name.trim();
|
||||
|
||||
if (!nextUserName || !nextName) {
|
||||
toast.error("Vui lòng nhập đầy đủ tên đăng nhập và họ tên.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const accessRooms = selectedRoomValues
|
||||
.map((value) => Number(value))
|
||||
.filter((value) => Number.isFinite(value));
|
||||
|
||||
if (
|
||||
selectedRoomValues.length > 0 &&
|
||||
accessRooms.length !== selectedRoomValues.length
|
||||
) {
|
||||
toast.error("Danh sách phòng không hợp lệ, vui lòng chọn lại.");
|
||||
return;
|
||||
}
|
||||
|
||||
await updateUserInfoMutation.mutateAsync({
|
||||
id: user.userId,
|
||||
data: {
|
||||
userName: nextUserName,
|
||||
name: nextName,
|
||||
accessRooms,
|
||||
},
|
||||
});
|
||||
toast.success("Cập nhật thông tin người dùng thành công!");
|
||||
} catch (error: any) {
|
||||
const message = error?.response?.data?.message || "Cập nhật thất bại!";
|
||||
toast.error(message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateUserRole = async () => {
|
||||
if (!user?.userId) {
|
||||
toast.error("Không tìm thấy userId để cập nhật role.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedRoleId) {
|
||||
toast.error("Vui lòng chọn vai trò.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateUserRoleMutation.mutateAsync({
|
||||
id: user.userId,
|
||||
data: { roleId: Number(selectedRoleId) },
|
||||
});
|
||||
toast.success("Cập nhật vai trò thành công!");
|
||||
} catch (error: any) {
|
||||
const message =
|
||||
error?.response?.data?.message || "Cập nhật vai trò thất bại!";
|
||||
toast.error(message);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="w-full px-6 py-8">
|
||||
<div className="flex items-center justify-center min-h-[320px]">
|
||||
<div className="text-muted-foreground">
|
||||
Đang tải thông tin người dùng...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="w-full px-6 py-8 space-y-4">
|
||||
<Button variant="outline" onClick={() => navigate({ to: "/user" })}>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Quay lại
|
||||
</Button>
|
||||
<div className="text-muted-foreground">
|
||||
Không tìm thấy người dùng cần chỉnh sửa.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full px-6 py-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
Chỉnh sửa người dùng
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Tài khoản: {user.userName}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => navigate({ to: "/user" })}>
|
||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||
Quay lại
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Thông tin người dùng</CardTitle>
|
||||
<CardDescription>
|
||||
Cập nhật họ tên, username và danh sách phòng truy cập.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-userName">Tên đăng nhập</Label>
|
||||
<Input
|
||||
id="edit-userName"
|
||||
value={editForm.userName}
|
||||
onChange={(e) =>
|
||||
setEditForm((prev) => ({ ...prev, userName: e.target.value }))
|
||||
}
|
||||
disabled={updateUserInfoMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-name">Họ và tên</Label>
|
||||
<Input
|
||||
id="edit-name"
|
||||
value={editForm.name}
|
||||
onChange={(e) =>
|
||||
setEditForm((prev) => ({ ...prev, name: e.target.value }))
|
||||
}
|
||||
disabled={updateUserInfoMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Các phòng phụ trách</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedRoomValues.length > 0 ? (
|
||||
selectedRoomValues.map((value) => (
|
||||
<Badge key={value} variant="secondary">
|
||||
{roomLabelMap.get(value) ?? value}
|
||||
</Badge>
|
||||
))
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Chưa chọn phòng.
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsRoomDialogOpen(true)}
|
||||
disabled={roomsLoading || updateUserInfoMutation.isPending}
|
||||
>
|
||||
Chọn phòng
|
||||
</Button>
|
||||
{roomsLoading && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Đang tải danh sách phòng...
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleUpdateUserInfo}
|
||||
disabled={updateUserInfoMutation.isPending}
|
||||
className="gap-2"
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
{updateUserInfoMutation.isPending
|
||||
? "Đang lưu..."
|
||||
: "Lưu thông tin"}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle>Vai trò</CardTitle>
|
||||
<CardDescription>Cập nhật vai trò của người dùng.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2 max-w-md">
|
||||
<Label>Vai trò</Label>
|
||||
<Select
|
||||
value={selectedRoleId}
|
||||
onValueChange={setSelectedRoleId}
|
||||
disabled={rolesLoading || updateUserRoleMutation.isPending}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
rolesLoading ? "Đang tải vai trò..." : "Chọn vai trò"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{roles.map((role) => (
|
||||
<SelectItem key={role.id} value={String(role.id)}>
|
||||
{role.roleName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleUpdateUserRole}
|
||||
disabled={updateUserRoleMutation.isPending || rolesLoading}
|
||||
className="gap-2"
|
||||
>
|
||||
<Save className="h-4 w-4" />
|
||||
{updateUserRoleMutation.isPending ? "Đang lưu..." : "Lưu vai trò"}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<SelectDialog
|
||||
open={isRoomDialogOpen}
|
||||
onClose={() => setIsRoomDialogOpen(false)}
|
||||
title="Chọn phòng phụ trách"
|
||||
description="Chọn một hoặc nhiều phòng để gán quyền truy cập."
|
||||
items={roomOptions}
|
||||
selectedValues={selectedRoomValues}
|
||||
onConfirm={(values) => setSelectedRoomValues(values)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -10,10 +10,13 @@ import {
|
|||
} from "@/components/ui/tooltip";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { VersionTable } from "@/components/tables/version-table";
|
||||
import { Edit2, Trash2, Shield } from "lucide-react";
|
||||
import { Edit2, Settings, Shield, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export const Route = createFileRoute("/_auth/user/")({
|
||||
head: () => ({
|
||||
meta: [{ title: "Danh sách người dùng" }],
|
||||
}),
|
||||
component: RouteComponent,
|
||||
loader: async ({ context }) => {
|
||||
context.breadcrumbs = [
|
||||
|
|
@ -65,21 +68,6 @@ function RouteComponent() {
|
|||
<div className="flex justify-center">{Array.isArray(getValue()) ? (getValue() as number[]).join(", ") : "-"}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "select",
|
||||
header: () => <div className="text-center whitespace-normal max-w-xs">Chọn</div>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex justify-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={row.getIsSelected?.() ?? false}
|
||||
onChange={row.getToggleSelectedHandler?.()}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (
|
||||
|
|
@ -87,42 +75,78 @@ function RouteComponent() {
|
|||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-2 justify-center items-center">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate({
|
||||
to: "/user/change-password/$userName",
|
||||
params: { userName: row.original.userName },
|
||||
} as any);
|
||||
}}
|
||||
>
|
||||
<Edit2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate({ to: "/user/role/$roleId", params: { roleId: String(row.original.roleId) } } as any);
|
||||
}}
|
||||
>
|
||||
<Shield className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
if (!confirm("Bạn có chắc muốn xóa người dùng này?")) return;
|
||||
// Placeholder delete - implement API call as needed
|
||||
toast.success("Xóa người dùng (chưa thực thi API)");
|
||||
if (table) table.setRowSelection({});
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate({
|
||||
to: "/user/edit/$userName",
|
||||
params: { userName: row.original.userName },
|
||||
} as any);
|
||||
}}
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">Đổi thông tin</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate({
|
||||
to: "/user/change-password/$userName",
|
||||
params: { userName: row.original.userName },
|
||||
} as any);
|
||||
}}
|
||||
>
|
||||
<Edit2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">Đổi mật khẩu</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate({
|
||||
to: "/user/role/$roleId",
|
||||
params: { roleId: String(row.original.roleId) },
|
||||
} as any);
|
||||
}}
|
||||
>
|
||||
<Shield className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">Xem quyền</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
if (!confirm("Bạn có chắc muốn xóa người dùng này?")) return;
|
||||
// Placeholder delete - implement API call as needed
|
||||
toast.success("Xóa người dùng (chưa thực thi API)");
|
||||
if (table) table.setRowSelection({});
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-red-500" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">Xóa người dùng</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import type { PermissionOnRole } from "@/types/permission";
|
|||
|
||||
export const Route = createFileRoute("/_auth/user/role/$roleId/")({
|
||||
head: () => ({
|
||||
meta: [{ title: "Quyền của người dùng | AccessControl" }]
|
||||
meta: [{ title: "Quyền của người dùng" }]
|
||||
}),
|
||||
component: ViewRolePermissionsComponent,
|
||||
loader: async ({ context, params }) => {
|
||||
|
|
|
|||
|
|
@ -120,9 +120,12 @@ export async function deleteRequiredFile(data: { MsiFileIds: number[] }): Promis
|
|||
|
||||
/**
|
||||
* Xóa file từ server
|
||||
* @param fileId - ID file
|
||||
* @param data - DownloadMsiRequest { MsiFileIds: number[] }
|
||||
*/
|
||||
export async function deleteFile(fileId: number): Promise<{ message: string }> {
|
||||
const response = await axios.delete(API_ENDPOINTS.APP_VERSION.DELETE_FILES(fileId));
|
||||
export async function deleteFile(data: { MsiFileIds: number[] }): Promise<{ message: string }> {
|
||||
const response = await axios.delete(
|
||||
API_ENDPOINTS.APP_VERSION.DELETE_FILES,
|
||||
{ data }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,20 @@
|
|||
import axios from "@/config/axios";
|
||||
import { API_ENDPOINTS } from "@/config/api";
|
||||
import type { UserProfile } from "@/types/user-profile";
|
||||
import type {
|
||||
UserProfile,
|
||||
UpdateUserInfoRequest,
|
||||
UpdateUserRoleRequest,
|
||||
UpdateUserInfoResponse,
|
||||
UpdateUserRoleResponse,
|
||||
} from "@/types/user-profile";
|
||||
|
||||
// Helper to extract data from wrapped or unwrapped response
|
||||
function extractData<T>(responseData: any): T {
|
||||
if (responseData && typeof responseData === "object" && "success" in responseData && "data" in responseData) {
|
||||
return responseData.data as T;
|
||||
}
|
||||
return responseData as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lấy danh sách thông tin người dùng và chuyển sang camelCase keys
|
||||
|
|
@ -11,6 +25,7 @@ export async function getUsersInfo(): Promise<UserProfile[]> {
|
|||
const list = Array.isArray(response.data) ? response.data : [];
|
||||
|
||||
return list.map((u: any) => ({
|
||||
userId: u.id ?? u.Id ?? u.userId ?? u.UserId ?? undefined,
|
||||
userName: u.userName ?? u.UserName ?? "",
|
||||
name: u.name ?? u.Name ?? "",
|
||||
role: u.role ?? u.Role ?? "",
|
||||
|
|
@ -31,4 +46,32 @@ export async function getUsersInfo(): Promise<UserProfile[]> {
|
|||
}
|
||||
}
|
||||
|
||||
export default { getUsersInfo };
|
||||
/**
|
||||
* Cập nhật thông tin người dùng
|
||||
*/
|
||||
export async function updateUserInfo(
|
||||
userId: number,
|
||||
data: UpdateUserInfoRequest
|
||||
): Promise<UpdateUserInfoResponse> {
|
||||
const response = await axios.put(
|
||||
API_ENDPOINTS.USER.UPDATE_INFO(userId),
|
||||
data
|
||||
);
|
||||
return extractData<UpdateUserInfoResponse>(response.data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cập nhật role người dùng
|
||||
*/
|
||||
export async function updateUserRole(
|
||||
userId: number,
|
||||
data: UpdateUserRoleRequest
|
||||
): Promise<UpdateUserRoleResponse> {
|
||||
const response = await axios.put(
|
||||
API_ENDPOINTS.USER.UPDATE_ROLE(userId),
|
||||
data
|
||||
);
|
||||
return extractData<UpdateUserRoleResponse>(response.data);
|
||||
}
|
||||
|
||||
export default { getUsersInfo, updateUserInfo, updateUserRole };
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ export function DashboardTemplate({
|
|||
variant={usageRange === "weekly" ? "default" : "outline"}
|
||||
onClick={() => setUsageRange("weekly")}
|
||||
>
|
||||
7 ngay
|
||||
7 ngày
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
|
|
@ -176,7 +176,7 @@ export function DashboardTemplate({
|
|||
variant={usageRange === "monthly" ? "default" : "outline"}
|
||||
onClick={() => setUsageRange("monthly")}
|
||||
>
|
||||
30 ngay
|
||||
30 ngày
|
||||
</Button>
|
||||
</div>
|
||||
</CardAction>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export const appSidebarSection = {
|
|||
code: AppSidebarSectionCode.DASHBOARD,
|
||||
icon: Home,
|
||||
permissions: [PermissionEnum.ALLOW_ALL],
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -40,6 +40,13 @@ export const appSidebarSection = {
|
|||
icon: Building,
|
||||
permissions: [PermissionEnum.VIEW_ROOM],
|
||||
},
|
||||
{
|
||||
title: "Điều khiển trực tiếp",
|
||||
url: "/remote-control",
|
||||
code: AppSidebarSectionCode.REMOTE_LIVE_CONTROL,
|
||||
icon: Monitor,
|
||||
permissions: [PermissionEnum.VIEW_REMOTE_CONTROL],
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -95,18 +102,6 @@ export const appSidebarSection = {
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Điều khiển từ xa",
|
||||
items: [
|
||||
{
|
||||
title: "Điều khiển trực tiếp",
|
||||
url: "/remote-control",
|
||||
code: AppSidebarSectionCode.REMOTE_LIVE_CONTROL,
|
||||
icon: Monitor,
|
||||
permissions: [PermissionEnum.ALLOW_ALL],
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Audits",
|
||||
items: [
|
||||
|
|
|
|||
|
|
@ -101,6 +101,11 @@ export enum PermissionEnum {
|
|||
AUDIT_OPERATION = 190,
|
||||
VIEW_AUDIT_LOGS = 191,
|
||||
|
||||
//REMOTE CONTROL
|
||||
REMOTE_CONTROL_OPERATION = 200,
|
||||
VIEW_REMOTE_CONTROL = 201,
|
||||
CONTROL_REMOTE = 202,
|
||||
|
||||
//Undefined
|
||||
UNDEFINED = 9999,
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export type UserProfile = {
|
||||
userId?: number;
|
||||
userName: string;
|
||||
name: string;
|
||||
role: string;
|
||||
|
|
@ -8,4 +9,33 @@ export type UserProfile = {
|
|||
createdBy?: string | null;
|
||||
updatedAt?: string | null;
|
||||
updatedBy?: string | null;
|
||||
};
|
||||
|
||||
export type UpdateUserInfoRequest = {
|
||||
name: string;
|
||||
userName: string;
|
||||
accessRooms?: number[];
|
||||
};
|
||||
|
||||
export type UpdateUserRoleRequest = {
|
||||
roleId: number;
|
||||
};
|
||||
|
||||
export type UpdateUserInfoResponse = {
|
||||
userId: number;
|
||||
userName: string;
|
||||
name: string;
|
||||
roleId: number;
|
||||
accessRooms: number[];
|
||||
updatedAt?: string | null;
|
||||
updatedBy?: string | null;
|
||||
};
|
||||
|
||||
export type UpdateUserRoleResponse = {
|
||||
userId: number;
|
||||
userName: string;
|
||||
roleId: number;
|
||||
roleName?: string | null;
|
||||
updatedAt?: string | null;
|
||||
updatedBy?: string | null;
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user