fix bug in delete file and add user crud
This commit is contained in:
parent
dd3fd1403a
commit
0286e6e217
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">
|
<Card className="bg-white/80 backdrop-blur border-muted/60 shadow-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Quản lý phòng</CardTitle>
|
<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>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|
@ -47,7 +47,7 @@ export function RoomManagementCard({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">
|
<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">
|
<div className="mt-2 space-y-2">
|
||||||
{data?.roomsNeedAttention && data.roomsNeedAttention.length > 0 ? (
|
{data?.roomsNeedAttention && data.roomsNeedAttention.length > 0 ? (
|
||||||
data.roomsNeedAttention.map((r: RoomHealthStatus) => (
|
data.roomsNeedAttention.map((r: RoomHealthStatus) => (
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/u
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { useState, useMemo } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
export interface SelectItem {
|
export interface SelectItem {
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -16,6 +16,7 @@ interface SelectDialogProps {
|
||||||
description?: string;
|
description?: string;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
items: SelectItem[];
|
items: SelectItem[];
|
||||||
|
selectedValues?: string[];
|
||||||
onConfirm: (values: string[]) => Promise<void> | void;
|
onConfirm: (values: string[]) => Promise<void> | void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -26,11 +27,18 @@ export function SelectDialog({
|
||||||
description,
|
description,
|
||||||
icon,
|
icon,
|
||||||
items,
|
items,
|
||||||
|
selectedValues,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
}: SelectDialogProps) {
|
}: SelectDialogProps) {
|
||||||
const [selected, setSelected] = useState<string[]>([]);
|
const [selected, setSelected] = useState<string[]>([]);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
if (!selectedValues) return;
|
||||||
|
setSelected(selectedValues);
|
||||||
|
}, [open, selectedValues]);
|
||||||
|
|
||||||
const filteredItems = useMemo(() => {
|
const filteredItems = useMemo(() => {
|
||||||
return items.filter((item) =>
|
return items.filter((item) =>
|
||||||
item.label.toLowerCase().includes(search.toLowerCase())
|
item.label.toLowerCase().includes(search.toLowerCase())
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ export const API_ENDPOINTS = {
|
||||||
CREATE_ACCOUNT: `${BASE_URL}/auth/create-account`,
|
CREATE_ACCOUNT: `${BASE_URL}/auth/create-account`,
|
||||||
GET_USERS_LIST: `${BASE_URL}/users-info`,
|
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: {
|
APP_VERSION: {
|
||||||
//agent and app api
|
//agent and app api
|
||||||
GET_VERSION: `${BASE_URL}/AppVersion/version`,
|
GET_VERSION: `${BASE_URL}/AppVersion/version`,
|
||||||
|
|
@ -38,7 +42,7 @@ export const API_ENDPOINTS = {
|
||||||
GET_REQUIRED_FILES: `${BASE_URL}/AppVersion/requirefiles`,
|
GET_REQUIRED_FILES: `${BASE_URL}/AppVersion/requirefiles`,
|
||||||
ADD_REQUIRED_FILE: `${BASE_URL}/AppVersion/requirefile/add`,
|
ADD_REQUIRED_FILE: `${BASE_URL}/AppVersion/requirefile/add`,
|
||||||
DELETE_REQUIRED_FILE: `${BASE_URL}/AppVersion/requirefile/delete`,
|
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: {
|
DEVICE_COMM: {
|
||||||
DOWNLOAD_FILES: (roomName: string) => `${BASE_URL}/DeviceComm/downloadfile/${roomName}`,
|
DOWNLOAD_FILES: (roomName: string) => `${BASE_URL}/DeviceComm/downloadfile/${roomName}`,
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@ export function useDeleteFile() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (fileId: number) => appVersionService.deleteFile(fileId),
|
mutationFn: (data: { MsiFileIds: number[] }) => appVersionService.deleteFile(data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: APP_VERSION_QUERY_KEYS.softwareList(),
|
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 * 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 = {
|
const USER_QUERY_KEYS = {
|
||||||
all: ["users"] as const,
|
all: ["users"] as const,
|
||||||
|
|
@ -18,3 +22,47 @@ export function useGetUsersInfo(enabled = true) {
|
||||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
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 AuthProfileUserNameIndexRouteImport } from './routes/_auth/profile/$userName/index'
|
||||||
import { Route as authSsoCallbackIndexRouteImport } from './routes/(auth)/sso/callback/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 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 AuthUserChangePasswordUserNameIndexRouteImport } from './routes/_auth/user/change-password/$userName/index'
|
||||||
import { Route as AuthRoomsRoomNameFolderStatusIndexRouteImport } from './routes/_auth/rooms/$roomName/folder-status/index'
|
import { Route as AuthRoomsRoomNameFolderStatusIndexRouteImport } from './routes/_auth/rooms/$roomName/folder-status/index'
|
||||||
import { Route as AuthRoomsRoomNameConnectIndexRouteImport } from './routes/_auth/rooms/$roomName/connect/index'
|
import { Route as AuthRoomsRoomNameConnectIndexRouteImport } from './routes/_auth/rooms/$roomName/connect/index'
|
||||||
|
|
@ -141,6 +142,12 @@ const AuthUserRoleRoleIdIndexRoute = AuthUserRoleRoleIdIndexRouteImport.update({
|
||||||
path: '/user/role/$roleId/',
|
path: '/user/role/$roleId/',
|
||||||
getParentRoute: () => AuthRoute,
|
getParentRoute: () => AuthRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
const AuthUserEditUserNameIndexRoute =
|
||||||
|
AuthUserEditUserNameIndexRouteImport.update({
|
||||||
|
id: '/user/edit/$userName/',
|
||||||
|
path: '/user/edit/$userName/',
|
||||||
|
getParentRoute: () => AuthRoute,
|
||||||
|
} as any)
|
||||||
const AuthUserChangePasswordUserNameIndexRoute =
|
const AuthUserChangePasswordUserNameIndexRoute =
|
||||||
AuthUserChangePasswordUserNameIndexRouteImport.update({
|
AuthUserChangePasswordUserNameIndexRouteImport.update({
|
||||||
id: '/user/change-password/$userName/',
|
id: '/user/change-password/$userName/',
|
||||||
|
|
@ -189,6 +196,7 @@ export interface FileRoutesByFullPath {
|
||||||
'/rooms/$roomName/connect': typeof AuthRoomsRoomNameConnectIndexRoute
|
'/rooms/$roomName/connect': typeof AuthRoomsRoomNameConnectIndexRoute
|
||||||
'/rooms/$roomName/folder-status': typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
'/rooms/$roomName/folder-status': typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
||||||
'/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute
|
'/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute
|
||||||
|
'/user/edit/$userName': typeof AuthUserEditUserNameIndexRoute
|
||||||
'/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute
|
'/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
|
|
@ -215,6 +223,7 @@ export interface FileRoutesByTo {
|
||||||
'/rooms/$roomName/connect': typeof AuthRoomsRoomNameConnectIndexRoute
|
'/rooms/$roomName/connect': typeof AuthRoomsRoomNameConnectIndexRoute
|
||||||
'/rooms/$roomName/folder-status': typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
'/rooms/$roomName/folder-status': typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
||||||
'/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute
|
'/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute
|
||||||
|
'/user/edit/$userName': typeof AuthUserEditUserNameIndexRoute
|
||||||
'/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute
|
'/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
|
|
@ -243,6 +252,7 @@ export interface FileRoutesById {
|
||||||
'/_auth/rooms/$roomName/connect/': typeof AuthRoomsRoomNameConnectIndexRoute
|
'/_auth/rooms/$roomName/connect/': typeof AuthRoomsRoomNameConnectIndexRoute
|
||||||
'/_auth/rooms/$roomName/folder-status/': typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
'/_auth/rooms/$roomName/folder-status/': typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
||||||
'/_auth/user/change-password/$userName/': typeof AuthUserChangePasswordUserNameIndexRoute
|
'/_auth/user/change-password/$userName/': typeof AuthUserChangePasswordUserNameIndexRoute
|
||||||
|
'/_auth/user/edit/$userName/': typeof AuthUserEditUserNameIndexRoute
|
||||||
'/_auth/user/role/$roleId/': typeof AuthUserRoleRoleIdIndexRoute
|
'/_auth/user/role/$roleId/': typeof AuthUserRoleRoleIdIndexRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
|
|
@ -271,6 +281,7 @@ export interface FileRouteTypes {
|
||||||
| '/rooms/$roomName/connect'
|
| '/rooms/$roomName/connect'
|
||||||
| '/rooms/$roomName/folder-status'
|
| '/rooms/$roomName/folder-status'
|
||||||
| '/user/change-password/$userName'
|
| '/user/change-password/$userName'
|
||||||
|
| '/user/edit/$userName'
|
||||||
| '/user/role/$roleId'
|
| '/user/role/$roleId'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
|
|
@ -297,6 +308,7 @@ export interface FileRouteTypes {
|
||||||
| '/rooms/$roomName/connect'
|
| '/rooms/$roomName/connect'
|
||||||
| '/rooms/$roomName/folder-status'
|
| '/rooms/$roomName/folder-status'
|
||||||
| '/user/change-password/$userName'
|
| '/user/change-password/$userName'
|
||||||
|
| '/user/edit/$userName'
|
||||||
| '/user/role/$roleId'
|
| '/user/role/$roleId'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
|
|
@ -324,6 +336,7 @@ export interface FileRouteTypes {
|
||||||
| '/_auth/rooms/$roomName/connect/'
|
| '/_auth/rooms/$roomName/connect/'
|
||||||
| '/_auth/rooms/$roomName/folder-status/'
|
| '/_auth/rooms/$roomName/folder-status/'
|
||||||
| '/_auth/user/change-password/$userName/'
|
| '/_auth/user/change-password/$userName/'
|
||||||
|
| '/_auth/user/edit/$userName/'
|
||||||
| '/_auth/user/role/$roleId/'
|
| '/_auth/user/role/$roleId/'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
|
|
@ -483,6 +496,13 @@ declare module '@tanstack/react-router' {
|
||||||
preLoaderRoute: typeof AuthUserRoleRoleIdIndexRouteImport
|
preLoaderRoute: typeof AuthUserRoleRoleIdIndexRouteImport
|
||||||
parentRoute: typeof AuthRoute
|
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/': {
|
'/_auth/user/change-password/$userName/': {
|
||||||
id: '/_auth/user/change-password/$userName/'
|
id: '/_auth/user/change-password/$userName/'
|
||||||
path: '/user/change-password/$userName'
|
path: '/user/change-password/$userName'
|
||||||
|
|
@ -535,6 +555,7 @@ interface AuthRouteChildren {
|
||||||
AuthRoomsRoomNameConnectIndexRoute: typeof AuthRoomsRoomNameConnectIndexRoute
|
AuthRoomsRoomNameConnectIndexRoute: typeof AuthRoomsRoomNameConnectIndexRoute
|
||||||
AuthRoomsRoomNameFolderStatusIndexRoute: typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
AuthRoomsRoomNameFolderStatusIndexRoute: typeof AuthRoomsRoomNameFolderStatusIndexRoute
|
||||||
AuthUserChangePasswordUserNameIndexRoute: typeof AuthUserChangePasswordUserNameIndexRoute
|
AuthUserChangePasswordUserNameIndexRoute: typeof AuthUserChangePasswordUserNameIndexRoute
|
||||||
|
AuthUserEditUserNameIndexRoute: typeof AuthUserEditUserNameIndexRoute
|
||||||
AuthUserRoleRoleIdIndexRoute: typeof AuthUserRoleRoleIdIndexRoute
|
AuthUserRoleRoleIdIndexRoute: typeof AuthUserRoleRoleIdIndexRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -561,6 +582,7 @@ const AuthRouteChildren: AuthRouteChildren = {
|
||||||
AuthRoomsRoomNameFolderStatusIndexRoute,
|
AuthRoomsRoomNameFolderStatusIndexRoute,
|
||||||
AuthUserChangePasswordUserNameIndexRoute:
|
AuthUserChangePasswordUserNameIndexRoute:
|
||||||
AuthUserChangePasswordUserNameIndexRoute,
|
AuthUserChangePasswordUserNameIndexRoute,
|
||||||
|
AuthUserEditUserNameIndexRoute: AuthUserEditUserNameIndexRoute,
|
||||||
AuthUserRoleRoleIdIndexRoute: AuthUserRoleRoleIdIndexRoute,
|
AuthUserRoleRoleIdIndexRoute: AuthUserRoleRoleIdIndexRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -137,11 +137,10 @@ function AppsComponent() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MsiFileIds = selectedRows.map((row: any) => row.original.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const row of selectedRows) {
|
await deleteMutation.mutateAsync({ MsiFileIds });
|
||||||
const { id } = row.original;
|
|
||||||
await deleteMutation.mutateAsync(id);
|
|
||||||
}
|
|
||||||
toast.success("Xóa phần mềm thành công!");
|
toast.success("Xóa phần mềm thành công!");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error("Xóa phần mềm thất bại!");
|
toast.error("Xóa phần mềm thất bại!");
|
||||||
|
|
@ -175,12 +174,10 @@ function AppsComponent() {
|
||||||
if (!table) return;
|
if (!table) return;
|
||||||
|
|
||||||
const selectedRows = table.getSelectedRowModel().rows;
|
const selectedRows = table.getSelectedRowModel().rows;
|
||||||
|
const MsiFileIds = selectedRows.map((row: any) => row.original.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const row of selectedRows) {
|
await deleteMutation.mutateAsync({ MsiFileIds });
|
||||||
const { id } = row.original;
|
|
||||||
await deleteMutation.mutateAsync(id);
|
|
||||||
}
|
|
||||||
toast.success("Xóa phần mềm từ server thành công!");
|
toast.success("Xóa phần mềm từ server thành công!");
|
||||||
if (table) {
|
if (table) {
|
||||||
table.setRowSelection({});
|
table.setRowSelection({});
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,9 @@ import { LoaderCircle } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_auth/user/change-password/$userName/")({
|
export const Route = createFileRoute("/_auth/user/change-password/$userName/")({
|
||||||
|
head: () => ({
|
||||||
|
meta: [{ title: "Thay đổi mật khẩu" }],
|
||||||
|
}),
|
||||||
component: AdminChangePasswordComponent,
|
component: AdminChangePasswordComponent,
|
||||||
loader: async ({ context, params }) => {
|
loader: async ({ context, params }) => {
|
||||||
context.breadcrumbs = [
|
context.breadcrumbs = [
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,9 @@ import { UserPlus, ArrowLeft, Save, Loader2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_auth/user/create/")({
|
export const Route = createFileRoute("/_auth/user/create/")({
|
||||||
|
head: () => ({
|
||||||
|
meta: [{ title: "Tạo người dùng mới" }],
|
||||||
|
}),
|
||||||
component: CreateUserComponent,
|
component: CreateUserComponent,
|
||||||
loader: async ({ context }) => {
|
loader: async ({ context }) => {
|
||||||
context.breadcrumbs = [
|
context.breadcrumbs = [
|
||||||
|
|
@ -59,7 +62,8 @@ function CreateUserComponent() {
|
||||||
if (!formData.userName) {
|
if (!formData.userName) {
|
||||||
newErrors.userName = "Tên đăng nhập không được để trống";
|
newErrors.userName = "Tên đăng nhập không được để trống";
|
||||||
} else if (!validateUserName(formData.userName)) {
|
} 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
|
// Validate name
|
||||||
|
|
@ -106,7 +110,8 @@ function CreateUserComponent() {
|
||||||
toast.success("Tạo tài khoản thành công!");
|
toast.success("Tạo tài khoản thành công!");
|
||||||
navigate({ to: "/dashboard" }); // TODO: Navigate to user list page when it exists
|
navigate({ to: "/dashboard" }); // TODO: Navigate to user list page when it exists
|
||||||
} catch (error: any) {
|
} 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);
|
toast.error(errorMessage);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -128,15 +133,14 @@ function CreateUserComponent() {
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<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">
|
<p className="text-muted-foreground mt-1">
|
||||||
Thêm tài khoản người dùng mới vào hệ thống
|
Thêm tài khoản người dùng mới vào hệ thống
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button variant="outline" onClick={() => navigate({ to: "/user" })}>
|
||||||
variant="outline"
|
|
||||||
onClick={() => navigate({ to: "/user" })}
|
|
||||||
>
|
|
||||||
<ArrowLeft className="h-4 w-4 mr-2" />
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
Quay lại
|
Quay lại
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -164,7 +168,9 @@ function CreateUserComponent() {
|
||||||
<Input
|
<Input
|
||||||
id="userName"
|
id="userName"
|
||||||
value={formData.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à _)"
|
placeholder="Nhập tên đăng nhập (3-20 ký tự, chỉ chữ, số, . và _)"
|
||||||
disabled={createMutation.isPending}
|
disabled={createMutation.isPending}
|
||||||
className="h-10"
|
className="h-10"
|
||||||
|
|
@ -202,7 +208,9 @@ function CreateUserComponent() {
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
value={formData.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ự)"
|
placeholder="Nhập mật khẩu (tối thiểu 6 ký tự)"
|
||||||
disabled={createMutation.isPending}
|
disabled={createMutation.isPending}
|
||||||
className="h-10"
|
className="h-10"
|
||||||
|
|
@ -220,13 +228,17 @@ function CreateUserComponent() {
|
||||||
id="confirmPassword"
|
id="confirmPassword"
|
||||||
type="password"
|
type="password"
|
||||||
value={formData.confirmPassword}
|
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"
|
placeholder="Nhập lại mật khẩu"
|
||||||
disabled={createMutation.isPending}
|
disabled={createMutation.isPending}
|
||||||
className="h-10"
|
className="h-10"
|
||||||
/>
|
/>
|
||||||
{errors.confirmPassword && (
|
{errors.confirmPassword && (
|
||||||
<p className="text-sm text-destructive">{errors.confirmPassword}</p>
|
<p className="text-sm text-destructive">
|
||||||
|
{errors.confirmPassword}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
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";
|
} from "@/components/ui/tooltip";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import { VersionTable } from "@/components/tables/version-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";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_auth/user/")({
|
export const Route = createFileRoute("/_auth/user/")({
|
||||||
|
head: () => ({
|
||||||
|
meta: [{ title: "Danh sách người dùng" }],
|
||||||
|
}),
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
loader: async ({ context }) => {
|
loader: async ({ context }) => {
|
||||||
context.breadcrumbs = [
|
context.breadcrumbs = [
|
||||||
|
|
@ -65,21 +68,6 @@ function RouteComponent() {
|
||||||
<div className="flex justify-center">{Array.isArray(getValue()) ? (getValue() as number[]).join(", ") : "-"}</div>
|
<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",
|
id: "actions",
|
||||||
header: () => (
|
header: () => (
|
||||||
|
|
@ -87,42 +75,78 @@ function RouteComponent() {
|
||||||
),
|
),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="flex gap-2 justify-center items-center">
|
<div className="flex gap-2 justify-center items-center">
|
||||||
<Button
|
<Tooltip>
|
||||||
size="sm"
|
<TooltipTrigger asChild>
|
||||||
variant="ghost"
|
<Button
|
||||||
onClick={(e) => {
|
size="sm"
|
||||||
e.stopPropagation();
|
variant="ghost"
|
||||||
navigate({
|
onClick={(e) => {
|
||||||
to: "/user/change-password/$userName",
|
e.stopPropagation();
|
||||||
params: { userName: row.original.userName },
|
navigate({
|
||||||
} as any);
|
to: "/user/edit/$userName",
|
||||||
}}
|
params: { userName: row.original.userName },
|
||||||
>
|
} as any);
|
||||||
<Edit2 className="h-4 w-4" />
|
}}
|
||||||
</Button>
|
>
|
||||||
<Button
|
<Settings className="h-4 w-4" />
|
||||||
size="sm"
|
</Button>
|
||||||
variant="ghost"
|
</TooltipTrigger>
|
||||||
onClick={(e) => {
|
<TooltipContent side="top">Đổi thông tin</TooltipContent>
|
||||||
e.stopPropagation();
|
</Tooltip>
|
||||||
navigate({ to: "/user/role/$roleId", params: { roleId: String(row.original.roleId) } } as any);
|
<Tooltip>
|
||||||
}}
|
<TooltipTrigger asChild>
|
||||||
>
|
<Button
|
||||||
<Shield className="h-4 w-4" />
|
size="sm"
|
||||||
</Button>
|
variant="ghost"
|
||||||
<Button
|
onClick={(e) => {
|
||||||
size="sm"
|
e.stopPropagation();
|
||||||
variant="ghost"
|
navigate({
|
||||||
onClick={async (e) => {
|
to: "/user/change-password/$userName",
|
||||||
e.stopPropagation();
|
params: { userName: row.original.userName },
|
||||||
if (!confirm("Bạn có chắc muốn xóa người dùng này?")) return;
|
} as any);
|
||||||
// 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({});
|
<Edit2 className="h-4 w-4" />
|
||||||
}}
|
</Button>
|
||||||
>
|
</TooltipTrigger>
|
||||||
<Trash2 className="h-4 w-4 text-red-500" />
|
<TooltipContent side="top">Đổi mật khẩu</TooltipContent>
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
),
|
),
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import type { PermissionOnRole } from "@/types/permission";
|
||||||
|
|
||||||
export const Route = createFileRoute("/_auth/user/role/$roleId/")({
|
export const Route = createFileRoute("/_auth/user/role/$roleId/")({
|
||||||
head: () => ({
|
head: () => ({
|
||||||
meta: [{ title: "Quyền của người dùng | AccessControl" }]
|
meta: [{ title: "Quyền của người dùng" }]
|
||||||
}),
|
}),
|
||||||
component: ViewRolePermissionsComponent,
|
component: ViewRolePermissionsComponent,
|
||||||
loader: async ({ context, params }) => {
|
loader: async ({ context, params }) => {
|
||||||
|
|
|
||||||
|
|
@ -120,9 +120,12 @@ export async function deleteRequiredFile(data: { MsiFileIds: number[] }): Promis
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Xóa file từ server
|
* Xóa file từ server
|
||||||
* @param fileId - ID file
|
* @param data - DownloadMsiRequest { MsiFileIds: number[] }
|
||||||
*/
|
*/
|
||||||
export async function deleteFile(fileId: number): Promise<{ message: string }> {
|
export async function deleteFile(data: { MsiFileIds: number[] }): Promise<{ message: string }> {
|
||||||
const response = await axios.delete(API_ENDPOINTS.APP_VERSION.DELETE_FILES(fileId));
|
const response = await axios.delete(
|
||||||
|
API_ENDPOINTS.APP_VERSION.DELETE_FILES,
|
||||||
|
{ data }
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,20 @@
|
||||||
import axios from "@/config/axios";
|
import axios from "@/config/axios";
|
||||||
import { API_ENDPOINTS } from "@/config/api";
|
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
|
* 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 : [];
|
const list = Array.isArray(response.data) ? response.data : [];
|
||||||
|
|
||||||
return list.map((u: any) => ({
|
return list.map((u: any) => ({
|
||||||
|
userId: u.id ?? u.Id ?? u.userId ?? u.UserId ?? undefined,
|
||||||
userName: u.userName ?? u.UserName ?? "",
|
userName: u.userName ?? u.UserName ?? "",
|
||||||
name: u.name ?? u.Name ?? "",
|
name: u.name ?? u.Name ?? "",
|
||||||
role: u.role ?? u.Role ?? "",
|
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"}
|
variant={usageRange === "weekly" ? "default" : "outline"}
|
||||||
onClick={() => setUsageRange("weekly")}
|
onClick={() => setUsageRange("weekly")}
|
||||||
>
|
>
|
||||||
7 ngay
|
7 ngày
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -176,7 +176,7 @@ export function DashboardTemplate({
|
||||||
variant={usageRange === "monthly" ? "default" : "outline"}
|
variant={usageRange === "monthly" ? "default" : "outline"}
|
||||||
onClick={() => setUsageRange("monthly")}
|
onClick={() => setUsageRange("monthly")}
|
||||||
>
|
>
|
||||||
30 ngay
|
30 ngày
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardAction>
|
</CardAction>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export const appSidebarSection = {
|
||||||
code: AppSidebarSectionCode.DASHBOARD,
|
code: AppSidebarSectionCode.DASHBOARD,
|
||||||
icon: Home,
|
icon: Home,
|
||||||
permissions: [PermissionEnum.ALLOW_ALL],
|
permissions: [PermissionEnum.ALLOW_ALL],
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -40,6 +40,13 @@ export const appSidebarSection = {
|
||||||
icon: Building,
|
icon: Building,
|
||||||
permissions: [PermissionEnum.VIEW_ROOM],
|
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",
|
title: "Audits",
|
||||||
items: [
|
items: [
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,11 @@ export enum PermissionEnum {
|
||||||
AUDIT_OPERATION = 190,
|
AUDIT_OPERATION = 190,
|
||||||
VIEW_AUDIT_LOGS = 191,
|
VIEW_AUDIT_LOGS = 191,
|
||||||
|
|
||||||
|
//REMOTE CONTROL
|
||||||
|
REMOTE_CONTROL_OPERATION = 200,
|
||||||
|
VIEW_REMOTE_CONTROL = 201,
|
||||||
|
CONTROL_REMOTE = 202,
|
||||||
|
|
||||||
//Undefined
|
//Undefined
|
||||||
UNDEFINED = 9999,
|
UNDEFINED = 9999,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export type UserProfile = {
|
export type UserProfile = {
|
||||||
|
userId?: number;
|
||||||
userName: string;
|
userName: string;
|
||||||
name: string;
|
name: string;
|
||||||
role: string;
|
role: string;
|
||||||
|
|
@ -9,3 +10,32 @@ export type UserProfile = {
|
||||||
updatedAt?: string | null;
|
updatedAt?: string | null;
|
||||||
updatedBy?: 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